Vendor dependencies for writeas-climaster
@@ -1,7 +1,6 @@ | |||
module github.com/writeas/writeas-cli | |||
require ( | |||
code.as/core/socks v1.0.0 | |||
github.com/atotto/clipboard v0.1.1 | |||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 | |||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect | |||
@@ -24,3 +23,5 @@ require ( | |||
gopkg.in/ini.v1 v1.39.3 | |||
gopkg.in/urfave/cli.v1 v1.20.0 | |||
) | |||
go 1.13 |
@@ -0,0 +1,22 @@ | |||
# Compiled Object files, Static and Dynamic libs (Shared Objects) | |||
*.o | |||
*.a | |||
*.so | |||
# Folders | |||
_obj | |||
_test | |||
# Architecture specific extensions/prefixes | |||
*.[568vq] | |||
[568vq].out | |||
*.cgo1.go | |||
*.cgo2.c | |||
_cgo_defun.c | |||
_cgo_gotypes.go | |||
_cgo_export.* | |||
_testmain.go | |||
*.exe |
@@ -0,0 +1,22 @@ | |||
Copyright (c) 2012, Hailiang Wang. All rights reserved. | |||
Redistribution and use in source and binary forms, with or without modification, | |||
are permitted provided that the following conditions are met: | |||
* Redistributions of source code must retain the above copyright notice, this | |||
list of conditions and the following disclaimer. | |||
* Redistributions in binary form must reproduce the above copyright notice, | |||
this list of conditions and the following disclaimer in the documentation | |||
and/or other materials provided with the distribution. | |||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@@ -0,0 +1,58 @@ | |||
SOCKS | |||
===== | |||
[![GoDoc](https://godoc.org/code.as/core/socks?status.svg)](https://godoc.org/code.as/core/socks) | |||
SOCKS is a SOCKS4, SOCKS4A and SOCKS5 proxy package for Go, forked from [h12w/socks](https://github.com/h12w/socks) and patched so it's `go get`able. | |||
## Quick Start | |||
### Get the package | |||
go get -u "code.as/core/socks" | |||
### Import the package | |||
import "code.as/core/socks" | |||
### Create a SOCKS proxy dialing function | |||
dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:1080") | |||
tr := &http.Transport{Dial: dialSocksProxy} | |||
httpClient := &http.Client{Transport: tr} | |||
## Example | |||
```go | |||
package main | |||
import ( | |||
"fmt" | |||
"io/ioutil" | |||
"log" | |||
"net/http" | |||
"code.as/core/socks" | |||
) | |||
func main() { | |||
dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:1080") | |||
tr := &http.Transport{Dial: dialSocksProxy} | |||
httpClient := &http.Client{Transport: tr} | |||
resp, err := httpClient.Get("http://www.google.com") | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer resp.Body.Close() | |||
if resp.StatusCode != http.StatusOK { | |||
log.Fatal(resp.StatusCode) | |||
} | |||
buf, err := ioutil.ReadAll(resp.Body) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
fmt.Println(string(buf)) | |||
} | |||
``` | |||
## Alternatives | |||
http://godoc.org/golang.org/x/net/proxy |
@@ -0,0 +1,218 @@ | |||
// Copyright 2012, Hailiang Wang. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
/* | |||
Package socks implements a SOCKS (SOCKS4, SOCKS4A and SOCKS5) proxy client. | |||
A complete example using this package: | |||
package main | |||
import ( | |||
"code.as/core/socks" | |||
"fmt" | |||
"net/http" | |||
"io/ioutil" | |||
) | |||
func main() { | |||
dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:1080") | |||
tr := &http.Transport{Dial: dialSocksProxy} | |||
httpClient := &http.Client{Transport: tr} | |||
bodyText, err := TestHttpsGet(httpClient, "https://h12.io/about") | |||
if err != nil { | |||
fmt.Println(err.Error()) | |||
} | |||
fmt.Print(bodyText) | |||
} | |||
func TestHttpsGet(c *http.Client, url string) (bodyText string, err error) { | |||
resp, err := c.Get(url) | |||
if err != nil { return } | |||
defer resp.Body.Close() | |||
body, err := ioutil.ReadAll(resp.Body) | |||
if err != nil { return } | |||
bodyText = string(body) | |||
return | |||
} | |||
*/ | |||
package socks | |||
import ( | |||
"errors" | |||
"fmt" | |||
"net" | |||
"strconv" | |||
) | |||
// Constants to choose which version of SOCKS protocol to use. | |||
const ( | |||
SOCKS4 = iota | |||
SOCKS4A | |||
SOCKS5 | |||
) | |||
// DialSocksProxy returns the dial function to be used in http.Transport object. | |||
// Argument socksType should be one of SOCKS4, SOCKS4A and SOCKS5. | |||
// Argument proxy should be in this format "127.0.0.1:1080". | |||
func DialSocksProxy(socksType int, proxy string) func(string, string) (net.Conn, error) { | |||
if socksType == SOCKS5 { | |||
return func(_, targetAddr string) (conn net.Conn, err error) { | |||
return dialSocks5(proxy, targetAddr) | |||
} | |||
} | |||
// SOCKS4, SOCKS4A | |||
return func(_, targetAddr string) (conn net.Conn, err error) { | |||
return dialSocks4(socksType, proxy, targetAddr) | |||
} | |||
} | |||
func dialSocks5(proxy, targetAddr string) (conn net.Conn, err error) { | |||
// dial TCP | |||
conn, err = net.Dial("tcp", proxy) | |||
if err != nil { | |||
return | |||
} | |||
// version identifier/method selection request | |||
req := []byte{ | |||
5, // version number | |||
1, // number of methods | |||
0, // method 0: no authentication (only anonymous access supported for now) | |||
} | |||
resp, err := sendReceive(conn, req) | |||
if err != nil { | |||
return | |||
} else if len(resp) != 2 { | |||
err = errors.New("Server does not respond properly.") | |||
return | |||
} else if resp[0] != 5 { | |||
err = errors.New("Server does not support Socks 5.") | |||
return | |||
} else if resp[1] != 0 { // no auth | |||
err = errors.New("socks method negotiation failed.") | |||
return | |||
} | |||
// detail request | |||
host, port, err := splitHostPort(targetAddr) | |||
req = []byte{ | |||
5, // version number | |||
1, // connect command | |||
0, // reserved, must be zero | |||
3, // address type, 3 means domain name | |||
byte(len(host)), // address length | |||
} | |||
req = append(req, []byte(host)...) | |||
req = append(req, []byte{ | |||
byte(port >> 8), // higher byte of destination port | |||
byte(port), // lower byte of destination port (big endian) | |||
}...) | |||
resp, err = sendReceive(conn, req) | |||
if err != nil { | |||
return | |||
} else if len(resp) != 10 { | |||
err = errors.New("Server does not respond properly.") | |||
} else if resp[1] != 0 { | |||
err = errors.New("Can't complete SOCKS5 connection.") | |||
} | |||
return | |||
} | |||
func dialSocks4(socksType int, proxy, targetAddr string) (conn net.Conn, err error) { | |||
// dial TCP | |||
conn, err = net.Dial("tcp", proxy) | |||
if err != nil { | |||
return | |||
} | |||
// connection request | |||
host, port, err := splitHostPort(targetAddr) | |||
if err != nil { | |||
return | |||
} | |||
ip := net.IPv4(0, 0, 0, 1).To4() | |||
if socksType == SOCKS4 { | |||
ip, err = lookupIP(host) | |||
if err != nil { | |||
return | |||
} | |||
} | |||
req := []byte{ | |||
4, // version number | |||
1, // command CONNECT | |||
byte(port >> 8), // higher byte of destination port | |||
byte(port), // lower byte of destination port (big endian) | |||
ip[0], ip[1], ip[2], ip[3], // special invalid IP address to indicate the host name is provided | |||
0, // user id is empty, anonymous proxy only | |||
} | |||
if socksType == SOCKS4A { | |||
req = append(req, []byte(host+"\x00")...) | |||
} | |||
resp, err := sendReceive(conn, req) | |||
if err != nil { | |||
return | |||
} else if len(resp) != 8 { | |||
err = errors.New("Server does not respond properly.") | |||
return | |||
} | |||
switch resp[1] { | |||
case 90: | |||
// request granted | |||
case 91: | |||
err = errors.New("Socks connection request rejected or failed.") | |||
case 92: | |||
err = errors.New("Socks connection request rejected becasue SOCKS server cannot connect to identd on the client.") | |||
case 93: | |||
err = errors.New("Socks connection request rejected because the client program and identd report different user-ids.") | |||
default: | |||
err = errors.New("Socks connection request failed, unknown error.") | |||
} | |||
return | |||
} | |||
func sendReceive(conn net.Conn, req []byte) (resp []byte, err error) { | |||
_, err = conn.Write(req) | |||
if err != nil { | |||
return | |||
} | |||
resp, err = readAll(conn) | |||
return | |||
} | |||
func readAll(conn net.Conn) (resp []byte, err error) { | |||
resp = make([]byte, 1024) | |||
n, err := conn.Read(resp) | |||
resp = resp[:n] | |||
return | |||
} | |||
func lookupIP(host string) (ip net.IP, err error) { | |||
ips, err := net.LookupIP(host) | |||
if err != nil { | |||
return | |||
} | |||
if len(ips) == 0 { | |||
err = errors.New(fmt.Sprintf("Cannot resolve host: %s.", host)) | |||
return | |||
} | |||
ip = ips[0].To4() | |||
if len(ip) != net.IPv4len { | |||
fmt.Println(len(ip), ip) | |||
err = errors.New("IPv6 is not supported by SOCKS4.") | |||
return | |||
} | |||
return | |||
} | |||
func splitHostPort(addr string) (host string, port uint16, err error) { | |||
host, portStr, err := net.SplitHostPort(addr) | |||
portInt, err := strconv.ParseUint(portStr, 10, 16) | |||
port = uint16(portInt) | |||
return | |||
} |
@@ -0,0 +1,20 @@ | |||
language: go | |||
go: | |||
- go1.4.3 | |||
- go1.5.4 | |||
- go1.6.4 | |||
- go1.7.6 | |||
- go1.8.7 | |||
- go1.9.4 | |||
- go1.10 | |||
before_install: | |||
- export DISPLAY=:99.0 | |||
- sh -e /etc/init.d/xvfb start | |||
script: | |||
- sudo apt-get install xsel | |||
- go test -v . | |||
- sudo apt-get install xclip | |||
- go test -v . |
@@ -0,0 +1,27 @@ | |||
Copyright (c) 2013 Ato Araki. All rights reserved. | |||
Redistribution and use in source and binary forms, with or without | |||
modification, are permitted provided that the following conditions are | |||
met: | |||
* Redistributions of source code must retain the above copyright | |||
notice, this list of conditions and the following disclaimer. | |||
* Redistributions in binary form must reproduce the above | |||
copyright notice, this list of conditions and the following disclaimer | |||
in the documentation and/or other materials provided with the | |||
distribution. | |||
* Neither the name of @atotto. nor the names of its | |||
contributors may be used to endorse or promote products derived from | |||
this software without specific prior written permission. | |||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@@ -0,0 +1,48 @@ | |||
[![Build Status](https://travis-ci.org/atotto/clipboard.svg?branch=master)](https://travis-ci.org/atotto/clipboard) | |||
[![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](http://godoc.org/github.com/atotto/clipboard) | |||
# Clipboard for Go | |||
Provide copying and pasting to the Clipboard for Go. | |||
Build: | |||
$ go get github.com/atotto/clipboard | |||
Platforms: | |||
* OSX | |||
* Windows 7 (probably work on other Windows) | |||
* Linux, Unix (requires 'xclip' or 'xsel' command to be installed) | |||
Document: | |||
* http://godoc.org/github.com/atotto/clipboard | |||
Notes: | |||
* Text string only | |||
* UTF-8 text encoding only (no conversion) | |||
TODO: | |||
* Clipboard watcher(?) | |||
## Commands: | |||
paste shell command: | |||
$ go get github.com/atotto/clipboard/cmd/gopaste | |||
$ # example: | |||
$ gopaste > document.txt | |||
copy shell command: | |||
$ go get github.com/atotto/clipboard/cmd/gocopy | |||
$ # example: | |||
$ cat document.txt | gocopy | |||
@@ -0,0 +1,20 @@ | |||
// Copyright 2013 @atotto. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
// Package clipboard read/write on clipboard | |||
package clipboard | |||
// ReadAll read string from clipboard | |||
func ReadAll() (string, error) { | |||
return readAll() | |||
} | |||
// WriteAll write string to clipboard | |||
func WriteAll(text string) error { | |||
return writeAll(text) | |||
} | |||
// Unsupported might be set true during clipboard init, to help callers decide | |||
// whether or not to offer clipboard options. | |||
var Unsupported bool |
@@ -0,0 +1,52 @@ | |||
// Copyright 2013 @atotto. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
// +build darwin | |||
package clipboard | |||
import ( | |||
"os/exec" | |||
) | |||
var ( | |||
pasteCmdArgs = "pbpaste" | |||
copyCmdArgs = "pbcopy" | |||
) | |||
func getPasteCommand() *exec.Cmd { | |||
return exec.Command(pasteCmdArgs) | |||
} | |||
func getCopyCommand() *exec.Cmd { | |||
return exec.Command(copyCmdArgs) | |||
} | |||
func readAll() (string, error) { | |||
pasteCmd := getPasteCommand() | |||
out, err := pasteCmd.Output() | |||
if err != nil { | |||
return "", err | |||
} | |||
return string(out), nil | |||
} | |||
func writeAll(text string) error { | |||
copyCmd := getCopyCommand() | |||
in, err := copyCmd.StdinPipe() | |||
if err != nil { | |||
return err | |||
} | |||
if err := copyCmd.Start(); err != nil { | |||
return err | |||
} | |||
if _, err := in.Write([]byte(text)); err != nil { | |||
return err | |||
} | |||
if err := in.Close(); err != nil { | |||
return err | |||
} | |||
return copyCmd.Wait() | |||
} |
@@ -0,0 +1,112 @@ | |||
// Copyright 2013 @atotto. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
// +build freebsd linux netbsd openbsd solaris dragonfly | |||
package clipboard | |||
import ( | |||
"errors" | |||
"os/exec" | |||
) | |||
const ( | |||
xsel = "xsel" | |||
xclip = "xclip" | |||
termuxClipboardGet = "termux-clipboard-get" | |||
termuxClipboardSet = "termux-clipboard-set" | |||
) | |||
var ( | |||
Primary bool | |||
pasteCmdArgs []string | |||
copyCmdArgs []string | |||
xselPasteArgs = []string{xsel, "--output", "--clipboard"} | |||
xselCopyArgs = []string{xsel, "--input", "--clipboard"} | |||
xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"} | |||
xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"} | |||
termuxPasteArgs = []string{termuxClipboardGet} | |||
termuxCopyArgs = []string{termuxClipboardSet} | |||
missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, or Termux:API add-on for termux-clipboard-get/set.") | |||
) | |||
func init() { | |||
pasteCmdArgs = xclipPasteArgs | |||
copyCmdArgs = xclipCopyArgs | |||
if _, err := exec.LookPath(xclip); err == nil { | |||
return | |||
} | |||
pasteCmdArgs = xselPasteArgs | |||
copyCmdArgs = xselCopyArgs | |||
if _, err := exec.LookPath(xsel); err == nil { | |||
return | |||
} | |||
pasteCmdArgs = termuxPasteArgs | |||
copyCmdArgs = termuxCopyArgs | |||
if _, err := exec.LookPath(termuxClipboardSet); err == nil { | |||
if _, err := exec.LookPath(termuxClipboardGet); err == nil { | |||
return | |||
} | |||
} | |||
Unsupported = true | |||
} | |||
func getPasteCommand() *exec.Cmd { | |||
if Primary { | |||
pasteCmdArgs = pasteCmdArgs[:1] | |||
} | |||
return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...) | |||
} | |||
func getCopyCommand() *exec.Cmd { | |||
if Primary { | |||
copyCmdArgs = copyCmdArgs[:1] | |||
} | |||
return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...) | |||
} | |||
func readAll() (string, error) { | |||
if Unsupported { | |||
return "", missingCommands | |||
} | |||
pasteCmd := getPasteCommand() | |||
out, err := pasteCmd.Output() | |||
if err != nil { | |||
return "", err | |||
} | |||
return string(out), nil | |||
} | |||
func writeAll(text string) error { | |||
if Unsupported { | |||
return missingCommands | |||
} | |||
copyCmd := getCopyCommand() | |||
in, err := copyCmd.StdinPipe() | |||
if err != nil { | |||
return err | |||
} | |||
if err := copyCmd.Start(); err != nil { | |||
return err | |||
} | |||
if _, err := in.Write([]byte(text)); err != nil { | |||
return err | |||
} | |||
if err := in.Close(); err != nil { | |||
return err | |||
} | |||
return copyCmd.Wait() | |||
} |
@@ -0,0 +1,128 @@ | |||
// Copyright 2013 @atotto. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
// +build windows | |||
package clipboard | |||
import ( | |||
"syscall" | |||
"time" | |||
"unsafe" | |||
) | |||
const ( | |||
cfUnicodetext = 13 | |||
gmemMoveable = 0x0002 | |||
) | |||
var ( | |||
user32 = syscall.MustLoadDLL("user32") | |||
openClipboard = user32.MustFindProc("OpenClipboard") | |||
closeClipboard = user32.MustFindProc("CloseClipboard") | |||
emptyClipboard = user32.MustFindProc("EmptyClipboard") | |||
getClipboardData = user32.MustFindProc("GetClipboardData") | |||
setClipboardData = user32.MustFindProc("SetClipboardData") | |||
kernel32 = syscall.NewLazyDLL("kernel32") | |||
globalAlloc = kernel32.NewProc("GlobalAlloc") | |||
globalFree = kernel32.NewProc("GlobalFree") | |||
globalLock = kernel32.NewProc("GlobalLock") | |||
globalUnlock = kernel32.NewProc("GlobalUnlock") | |||
lstrcpy = kernel32.NewProc("lstrcpyW") | |||
) | |||
// waitOpenClipboard opens the clipboard, waiting for up to a second to do so. | |||
func waitOpenClipboard() error { | |||
started := time.Now() | |||
limit := started.Add(time.Second) | |||
var r uintptr | |||
var err error | |||
for time.Now().Before(limit) { | |||
r, _, err = openClipboard.Call(0) | |||
if r != 0 { | |||
return nil | |||
} | |||
time.Sleep(time.Millisecond) | |||
} | |||
return err | |||
} | |||
func readAll() (string, error) { | |||
err := waitOpenClipboard() | |||
if err != nil { | |||
return "", err | |||
} | |||
defer closeClipboard.Call() | |||
h, _, err := getClipboardData.Call(cfUnicodetext) | |||
if h == 0 { | |||
return "", err | |||
} | |||
l, _, err := globalLock.Call(h) | |||
if l == 0 { | |||
return "", err | |||
} | |||
text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) | |||
r, _, err := globalUnlock.Call(h) | |||
if r == 0 { | |||
return "", err | |||
} | |||
return text, nil | |||
} | |||
func writeAll(text string) error { | |||
err := waitOpenClipboard() | |||
if err != nil { | |||
return err | |||
} | |||
defer closeClipboard.Call() | |||
r, _, err := emptyClipboard.Call(0) | |||
if r == 0 { | |||
return err | |||
} | |||
data := syscall.StringToUTF16(text) | |||
// "If the hMem parameter identifies a memory object, the object must have | |||
// been allocated using the function with the GMEM_MOVEABLE flag." | |||
h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) | |||
if h == 0 { | |||
return err | |||
} | |||
defer func() { | |||
if h != 0 { | |||
globalFree.Call(h) | |||
} | |||
}() | |||
l, _, err := globalLock.Call(h) | |||
if l == 0 { | |||
return err | |||
} | |||
r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) | |||
if r == 0 { | |||
return err | |||
} | |||
r, _, err = globalUnlock.Call(h) | |||
if r == 0 { | |||
if err.(syscall.Errno) != 0 { | |||
return err | |||
} | |||
} | |||
r, _, err = setClipboardData.Call(cfUnicodetext, h) | |||
if r == 0 { | |||
return err | |||
} | |||
h = 0 // suppress deferred cleanup | |||
return nil | |||
} |
@@ -0,0 +1 @@ | |||
module github.com/atotto/clipboard |
@@ -0,0 +1,11 @@ | |||
language: go | |||
go: | |||
- 1.2 | |||
before_install: | |||
- go get github.com/onsi/ginkgo/... | |||
- go get github.com/onsi/gomega/... | |||
- go install github.com/onsi/ginkgo/ginkgo | |||
script: PATH=$PATH:$HOME/gopath/bin ginkgo -r . | |||
branches: | |||
only: | |||
- master |
@@ -0,0 +1,201 @@ | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS | |||
APPENDIX: How to apply the Apache License to your work. | |||
To apply the Apache License to your work, attach the following | |||
boilerplate notice, with the fields enclosed by brackets "[]" | |||
replaced with your own identifying information. (Don't include | |||
the brackets!) The text should be enclosed in the appropriate | |||
comment syntax for the file format. We also recommend that a | |||
file or class name and description of purpose be included on the | |||
same "printed page" as the copyright notice for easier | |||
identification within third-party archives. | |||
Copyright 2014 Pivotal | |||
Licensed under the Apache License, Version 2.0 (the "License"); | |||
you may not use this file except in compliance with the License. | |||
You may obtain a copy of the License at | |||
http://www.apache.org/licenses/LICENSE-2.0 | |||
Unless required by applicable law or agreed to in writing, software | |||
distributed under the License is distributed on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
See the License for the specific language governing permissions and | |||
limitations under the License. |
@@ -0,0 +1,44 @@ | |||
# Jibber Jabber [![Build Status](https://travis-ci.org/cloudfoundry/jibber_jabber.svg?branch=master)](https://travis-ci.org/cloudfoundry/jibber_jabber) | |||
Jibber Jabber is a GoLang Library that can be used to detect an operating system's current language. | |||
### OS Support | |||
OSX and Linux via the `LC_ALL` and `LANG` environment variables. These are standard variables that are used in ALL versions of UNIX for language detection. | |||
Windows via [GetUserDefaultLocaleName](http://msdn.microsoft.com/en-us/library/windows/desktop/dd318136.aspx) and [GetSystemDefaultLocaleName](http://msdn.microsoft.com/en-us/library/windows/desktop/dd318122.aspx) system calls. These calls are supported in Windows Vista and up. | |||
# Usage | |||
Add the following line to your go `import`: | |||
``` | |||
"github.com/cloudfoundry/jibber_jabber" | |||
``` | |||
### DetectIETF | |||
`DetectIETF` will return the current locale as a string. The format of the locale will be the [ISO 639](http://en.wikipedia.org/wiki/ISO_639) two-letter language code, a DASH, then an [ISO 3166](http://en.wikipedia.org/wiki/ISO_3166-1) two-letter country code. | |||
``` | |||
userLocale, err := jibber_jabber.DetectIETF() | |||
println("Locale:", userLocale) | |||
``` | |||
### DetectLanguage | |||
`DetectLanguage` will return the current languge as a string. The format will be the [ISO 639](http://en.wikipedia.org/wiki/ISO_639) two-letter language code. | |||
``` | |||
userLanguage, err := jibber_jabber.DetectLanguage() | |||
println("Language:", userLanguage) | |||
``` | |||
### DetectTerritory | |||
`DetectTerritory` will return the current locale territory as a string. The format will be the [ISO 3166](http://en.wikipedia.org/wiki/ISO_3166-1) two-letter country code. | |||
``` | |||
localeTerritory, err := jibber_jabber.DetectTerritory() | |||
println("Territory:", localeTerritory) | |||
``` | |||
### Errors | |||
All the Detect commands will return an error if they are unable to read the Locale from the system. | |||
For Windows, additional error information is provided due to the nature of the system call being used. |
@@ -0,0 +1,22 @@ | |||
package jibber_jabber | |||
import ( | |||
"strings" | |||
) | |||
const ( | |||
COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE = "Could not detect Language" | |||
) | |||
func splitLocale(locale string) (string, string) { | |||
formattedLocale := strings.Split(locale, ".")[0] | |||
formattedLocale = strings.Replace(formattedLocale, "-", "_", -1) | |||
pieces := strings.Split(formattedLocale, "_") | |||
language := pieces[0] | |||
territory := "" | |||
if len(pieces) > 1 { | |||
territory = strings.Split(formattedLocale, "_")[1] | |||
} | |||
return language, territory | |||
} |
@@ -0,0 +1,57 @@ | |||
// +build darwin freebsd linux netbsd openbsd | |||
package jibber_jabber | |||
import ( | |||
"errors" | |||
"os" | |||
"strings" | |||
) | |||
func getLangFromEnv() (locale string) { | |||
locale = os.Getenv("LC_ALL") | |||
if locale == "" { | |||
locale = os.Getenv("LANG") | |||
} | |||
return | |||
} | |||
func getUnixLocale() (unix_locale string, err error) { | |||
unix_locale = getLangFromEnv() | |||
if unix_locale == "" { | |||
err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE) | |||
} | |||
return | |||
} | |||
func DetectIETF() (locale string, err error) { | |||
unix_locale, err := getUnixLocale() | |||
if err == nil { | |||
language, territory := splitLocale(unix_locale) | |||
locale = language | |||
if territory != "" { | |||
locale = strings.Join([]string{language, territory}, "-") | |||
} | |||
} | |||
return | |||
} | |||
func DetectLanguage() (language string, err error) { | |||
unix_locale, err := getUnixLocale() | |||
if err == nil { | |||
language, _ = splitLocale(unix_locale) | |||
} | |||
return | |||
} | |||
func DetectTerritory() (territory string, err error) { | |||
unix_locale, err := getUnixLocale() | |||
if err == nil { | |||
_, territory = splitLocale(unix_locale) | |||
} | |||
return | |||
} |
@@ -0,0 +1,114 @@ | |||
// +build windows | |||
package jibber_jabber | |||
import ( | |||
"errors" | |||
"syscall" | |||
"unsafe" | |||
) | |||
const LOCALE_NAME_MAX_LENGTH uint32 = 85 | |||
var SUPPORTED_LOCALES = map[uintptr]string{ | |||
0x0407: "de-DE", | |||
0x0409: "en-US", | |||
0x0c0a: "es-ES", //or is it 0x040a | |||
0x040c: "fr-FR", | |||
0x0410: "it-IT", | |||
0x0411: "ja-JA", | |||
0x0412: "ko_KR", | |||
0x0416: "pt-BR", | |||
//0x0419: "ru_RU", - Will add support for Russian when nicksnyder/go-i18n supports Russian | |||
0x0804: "zh-CN", | |||
0x0c04: "zh-HK", | |||
0x0404: "zh-TW", | |||
} | |||
func getWindowsLocaleFrom(sysCall string) (locale string, err error) { | |||
buffer := make([]uint16, LOCALE_NAME_MAX_LENGTH) | |||
dll := syscall.MustLoadDLL("kernel32") | |||
proc := dll.MustFindProc(sysCall) | |||
r, _, dllError := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(LOCALE_NAME_MAX_LENGTH)) | |||
if r == 0 { | |||
err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error()) | |||
return | |||
} | |||
locale = syscall.UTF16ToString(buffer) | |||
return | |||
} | |||
func getAllWindowsLocaleFrom(sysCall string) (string, error) { | |||
dll, err := syscall.LoadDLL("kernel32") | |||
if err != nil { | |||
return "", errors.New("Could not find kernel32 dll") | |||
} | |||
proc, err := dll.FindProc(sysCall) | |||
if err != nil { | |||
return "", err | |||
} | |||
locale, _, dllError := proc.Call() | |||
if locale == 0 { | |||
return "", errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error()) | |||
} | |||
return SUPPORTED_LOCALES[locale], nil | |||
} | |||
func getWindowsLocale() (locale string, err error) { | |||
dll, err := syscall.LoadDLL("kernel32") | |||
if err != nil { | |||
return "", errors.New("Could not find kernel32 dll") | |||
} | |||
proc, err := dll.FindProc("GetVersion") | |||
if err != nil { | |||
return "", err | |||
} | |||
v, _, _ := proc.Call() | |||
windowsVersion := byte(v) | |||
isVistaOrGreater := (windowsVersion >= 6) | |||
if isVistaOrGreater { | |||
locale, err = getWindowsLocaleFrom("GetUserDefaultLocaleName") | |||
if err != nil { | |||
locale, err = getWindowsLocaleFrom("GetSystemDefaultLocaleName") | |||
} | |||
} else if !isVistaOrGreater { | |||
locale, err = getAllWindowsLocaleFrom("GetUserDefaultLCID") | |||
if err != nil { | |||
locale, err = getAllWindowsLocaleFrom("GetSystemDefaultLCID") | |||
} | |||
} else { | |||
panic(v) | |||
} | |||
return | |||
} | |||
func DetectIETF() (locale string, err error) { | |||
locale, err = getWindowsLocale() | |||
return | |||
} | |||
func DetectLanguage() (language string, err error) { | |||
windows_locale, err := getWindowsLocale() | |||
if err == nil { | |||
language, _ = splitLocale(windows_locale) | |||
} | |||
return | |||
} | |||
func DetectTerritory() (territory string, err error) { | |||
windows_locale, err := getWindowsLocale() | |||
if err == nil { | |||
_, territory = splitLocale(windows_locale) | |||
} | |||
return | |||
} |
@@ -0,0 +1,354 @@ | |||
Mozilla Public License, version 2.0 | |||
1. Definitions | |||
1.1. “Contributor” | |||
means each individual or legal entity that creates, contributes to the | |||
creation of, or owns Covered Software. | |||
1.2. “Contributor Version” | |||
means the combination of the Contributions of others (if any) used by a | |||
Contributor and that particular Contributor’s Contribution. | |||
1.3. “Contribution” | |||
means Covered Software of a particular Contributor. | |||
1.4. “Covered Software” | |||
means Source Code Form to which the initial Contributor has attached the | |||
notice in Exhibit A, the Executable Form of such Source Code Form, and | |||
Modifications of such Source Code Form, in each case including portions | |||
thereof. | |||
1.5. “Incompatible With Secondary Licenses” | |||
means | |||
a. that the initial Contributor has attached the notice described in | |||
Exhibit B to the Covered Software; or | |||
b. that the Covered Software was made available under the terms of version | |||
1.1 or earlier of the License, but not also under the terms of a | |||
Secondary License. | |||
1.6. “Executable Form” | |||
means any form of the work other than Source Code Form. | |||
1.7. “Larger Work” | |||
means a work that combines Covered Software with other material, in a separate | |||
file or files, that is not Covered Software. | |||
1.8. “License” | |||
means this document. | |||
1.9. “Licensable” | |||
means having the right to grant, to the maximum extent possible, whether at the | |||
time of the initial grant or subsequently, any and all of the rights conveyed by | |||
this License. | |||
1.10. “Modifications” | |||
means any of the following: | |||
a. any file in Source Code Form that results from an addition to, deletion | |||
from, or modification of the contents of Covered Software; or | |||
b. any new file in Source Code Form that contains any Covered Software. | |||
1.11. “Patent Claims” of a Contributor | |||
means any patent claim(s), including without limitation, method, process, | |||
and apparatus claims, in any patent Licensable by such Contributor that | |||
would be infringed, but for the grant of the License, by the making, | |||
using, selling, offering for sale, having made, import, or transfer of | |||
either its Contributions or its Contributor Version. | |||
1.12. “Secondary License” | |||
means either the GNU General Public License, Version 2.0, the GNU Lesser | |||
General Public License, Version 2.1, the GNU Affero General Public | |||
License, Version 3.0, or any later versions of those licenses. | |||
1.13. “Source Code Form” | |||
means the form of the work preferred for making modifications. | |||
1.14. “You” (or “Your”) | |||
means an individual or a legal entity exercising rights under this | |||
License. For legal entities, “You” includes any entity that controls, is | |||
controlled by, or is under common control with You. For purposes of this | |||
definition, “control” means (a) the power, direct or indirect, to cause | |||
the direction or management of such entity, whether by contract or | |||
otherwise, or (b) ownership of more than fifty percent (50%) of the | |||
outstanding shares or beneficial ownership of such entity. | |||
2. License Grants and Conditions | |||
2.1. Grants | |||
Each Contributor hereby grants You a world-wide, royalty-free, | |||
non-exclusive license: | |||
a. under intellectual property rights (other than patent or trademark) | |||
Licensable by such Contributor to use, reproduce, make available, | |||
modify, display, perform, distribute, and otherwise exploit its | |||
Contributions, either on an unmodified basis, with Modifications, or as | |||
part of a Larger Work; and | |||
b. under Patent Claims of such Contributor to make, use, sell, offer for | |||
sale, have made, import, and otherwise transfer either its Contributions | |||
or its Contributor Version. | |||
2.2. Effective Date | |||
The licenses granted in Section 2.1 with respect to any Contribution become | |||
effective for each Contribution on the date the Contributor first distributes | |||
such Contribution. | |||
2.3. Limitations on Grant Scope | |||
The licenses granted in this Section 2 are the only rights granted under this | |||
License. No additional rights or licenses will be implied from the distribution | |||
or licensing of Covered Software under this License. Notwithstanding Section | |||
2.1(b) above, no patent license is granted by a Contributor: | |||
a. for any code that a Contributor has removed from Covered Software; or | |||
b. for infringements caused by: (i) Your and any other third party’s | |||
modifications of Covered Software, or (ii) the combination of its | |||
Contributions with other software (except as part of its Contributor | |||
Version); or | |||
c. under Patent Claims infringed by Covered Software in the absence of its | |||
Contributions. | |||
This License does not grant any rights in the trademarks, service marks, or | |||
logos of any Contributor (except as may be necessary to comply with the | |||
notice requirements in Section 3.4). | |||
2.4. Subsequent Licenses | |||
No Contributor makes additional grants as a result of Your choice to | |||
distribute the Covered Software under a subsequent version of this License | |||
(see Section 10.2) or under the terms of a Secondary License (if permitted | |||
under the terms of Section 3.3). | |||
2.5. Representation | |||
Each Contributor represents that the Contributor believes its Contributions | |||
are its original creation(s) or it has sufficient rights to grant the | |||
rights to its Contributions conveyed by this License. | |||
2.6. Fair Use | |||
This License is not intended to limit any rights You have under applicable | |||
copyright doctrines of fair use, fair dealing, or other equivalents. | |||
2.7. Conditions | |||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in | |||
Section 2.1. | |||
3. Responsibilities | |||
3.1. Distribution of Source Form | |||
All distribution of Covered Software in Source Code Form, including any | |||
Modifications that You create or to which You contribute, must be under the | |||
terms of this License. You must inform recipients that the Source Code Form | |||
of the Covered Software is governed by the terms of this License, and how | |||
they can obtain a copy of this License. You may not attempt to alter or | |||
restrict the recipients’ rights in the Source Code Form. | |||
3.2. Distribution of Executable Form | |||
If You distribute Covered Software in Executable Form then: | |||
a. such Covered Software must also be made available in Source Code Form, | |||
as described in Section 3.1, and You must inform recipients of the | |||
Executable Form how they can obtain a copy of such Source Code Form by | |||
reasonable means in a timely manner, at a charge no more than the cost | |||
of distribution to the recipient; and | |||
b. You may distribute such Executable Form under the terms of this License, | |||
or sublicense it under different terms, provided that the license for | |||
the Executable Form does not attempt to limit or alter the recipients’ | |||
rights in the Source Code Form under this License. | |||
3.3. Distribution of a Larger Work | |||
You may create and distribute a Larger Work under terms of Your choice, | |||
provided that You also comply with the requirements of this License for the | |||
Covered Software. If the Larger Work is a combination of Covered Software | |||
with a work governed by one or more Secondary Licenses, and the Covered | |||
Software is not Incompatible With Secondary Licenses, this License permits | |||
You to additionally distribute such Covered Software under the terms of | |||
such Secondary License(s), so that the recipient of the Larger Work may, at | |||
their option, further distribute the Covered Software under the terms of | |||
either this License or such Secondary License(s). | |||
3.4. Notices | |||
You may not remove or alter the substance of any license notices (including | |||
copyright notices, patent notices, disclaimers of warranty, or limitations | |||
of liability) contained within the Source Code Form of the Covered | |||
Software, except that You may alter any license notices to the extent | |||
required to remedy known factual inaccuracies. | |||
3.5. Application of Additional Terms | |||
You may choose to offer, and to charge a fee for, warranty, support, | |||
indemnity or liability obligations to one or more recipients of Covered | |||
Software. However, You may do so only on Your own behalf, and not on behalf | |||
of any Contributor. You must make it absolutely clear that any such | |||
warranty, support, indemnity, or liability obligation is offered by You | |||
alone, and You hereby agree to indemnify every Contributor for any | |||
liability incurred by such Contributor as a result of warranty, support, | |||
indemnity or liability terms You offer. You may include additional | |||
disclaimers of warranty and limitations of liability specific to any | |||
jurisdiction. | |||
4. Inability to Comply Due to Statute or Regulation | |||
If it is impossible for You to comply with any of the terms of this License | |||
with respect to some or all of the Covered Software due to statute, judicial | |||
order, or regulation then You must: (a) comply with the terms of this License | |||
to the maximum extent possible; and (b) describe the limitations and the code | |||
they affect. Such description must be placed in a text file included with all | |||
distributions of the Covered Software under this License. Except to the | |||
extent prohibited by statute or regulation, such description must be | |||
sufficiently detailed for a recipient of ordinary skill to be able to | |||
understand it. | |||
5. Termination | |||
5.1. The rights granted under this License will terminate automatically if You | |||
fail to comply with any of its terms. However, if You become compliant, | |||
then the rights granted under this License from a particular Contributor | |||
are reinstated (a) provisionally, unless and until such Contributor | |||
explicitly and finally terminates Your grants, and (b) on an ongoing basis, | |||
if such Contributor fails to notify You of the non-compliance by some | |||
reasonable means prior to 60 days after You have come back into compliance. | |||
Moreover, Your grants from a particular Contributor are reinstated on an | |||
ongoing basis if such Contributor notifies You of the non-compliance by | |||
some reasonable means, this is the first time You have received notice of | |||
non-compliance with this License from such Contributor, and You become | |||
compliant prior to 30 days after Your receipt of the notice. | |||
5.2. If You initiate litigation against any entity by asserting a patent | |||
infringement claim (excluding declaratory judgment actions, counter-claims, | |||
and cross-claims) alleging that a Contributor Version directly or | |||
indirectly infringes any patent, then the rights granted to You by any and | |||
all Contributors for the Covered Software under Section 2.1 of this License | |||
shall terminate. | |||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user | |||
license agreements (excluding distributors and resellers) which have been | |||
validly granted by You or Your distributors under this License prior to | |||
termination shall survive termination. | |||
6. Disclaimer of Warranty | |||
Covered Software is provided under this License on an “as is” basis, without | |||
warranty of any kind, either expressed, implied, or statutory, including, | |||
without limitation, warranties that the Covered Software is free of defects, | |||
merchantable, fit for a particular purpose or non-infringing. The entire | |||
risk as to the quality and performance of the Covered Software is with You. | |||
Should any Covered Software prove defective in any respect, You (not any | |||
Contributor) assume the cost of any necessary servicing, repair, or | |||
correction. This disclaimer of warranty constitutes an essential part of this | |||
License. No use of any Covered Software is authorized under this License | |||
except under this disclaimer. | |||
7. Limitation of Liability | |||
Under no circumstances and under no legal theory, whether tort (including | |||
negligence), contract, or otherwise, shall any Contributor, or anyone who | |||
distributes Covered Software as permitted above, be liable to You for any | |||
direct, indirect, special, incidental, or consequential damages of any | |||
character including, without limitation, damages for lost profits, loss of | |||
goodwill, work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses, even if such party shall have been | |||
informed of the possibility of such damages. This limitation of liability | |||
shall not apply to liability for death or personal injury resulting from such | |||
party’s negligence to the extent applicable law prohibits such limitation. | |||
Some jurisdictions do not allow the exclusion or limitation of incidental or | |||
consequential damages, so this exclusion and limitation may not apply to You. | |||
8. Litigation | |||
Any litigation relating to this License may be brought only in the courts of | |||
a jurisdiction where the defendant maintains its principal place of business | |||
and such litigation shall be governed by laws of that jurisdiction, without | |||
reference to its conflict-of-law provisions. Nothing in this Section shall | |||
prevent a party’s ability to bring cross-claims or counter-claims. | |||
9. Miscellaneous | |||
This License represents the complete agreement concerning the subject matter | |||
hereof. If any provision of this License is held to be unenforceable, such | |||
provision shall be reformed only to the extent necessary to make it | |||
enforceable. Any law or regulation which provides that the language of a | |||
contract shall be construed against the drafter shall not be used to construe | |||
this License against a Contributor. | |||
10. Versions of the License | |||
10.1. New Versions | |||
Mozilla Foundation is the license steward. Except as provided in Section | |||
10.3, no one other than the license steward has the right to modify or | |||
publish new versions of this License. Each version will be given a | |||
distinguishing version number. | |||
10.2. Effect of New Versions | |||
You may distribute the Covered Software under the terms of the version of | |||
the License under which You originally received the Covered Software, or | |||
under the terms of any subsequent version published by the license | |||
steward. | |||
10.3. Modified Versions | |||
If you create software not governed by this License, and you want to | |||
create a new license for such software, you may create and use a modified | |||
version of this License if you rename the license and remove any | |||
references to the name of the license steward (except to note that such | |||
modified license differs from this License). | |||
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses | |||
If You choose to distribute Source Code Form that is Incompatible With | |||
Secondary Licenses under the terms of this version of the License, the | |||
notice described in Exhibit B of this License must be attached. | |||
Exhibit A - Source Code Form License Notice | |||
This Source Code Form is subject to the | |||
terms of the Mozilla Public License, v. | |||
2.0. If a copy of the MPL was not | |||
distributed with this file, You can | |||
obtain one at | |||
http://mozilla.org/MPL/2.0/. | |||
If it is not possible or desirable to put the notice in a particular file, then | |||
You may include the notice in a location (such as a LICENSE file in a relevant | |||
directory) where a recipient would be likely to look for such a notice. | |||
You may add additional accurate notices of copyright ownership. | |||
Exhibit B - “Incompatible With Secondary Licenses” Notice | |||
This Source Code Form is “Incompatible | |||
With Secondary Licenses”, as defined by | |||
the Mozilla Public License, v. 2.0. | |||
@@ -0,0 +1,89 @@ | |||
# errwrap | |||
`errwrap` is a package for Go that formalizes the pattern of wrapping errors | |||
and checking if an error contains another error. | |||
There is a common pattern in Go of taking a returned `error` value and | |||
then wrapping it (such as with `fmt.Errorf`) before returning it. The problem | |||
with this pattern is that you completely lose the original `error` structure. | |||
Arguably the _correct_ approach is that you should make a custom structure | |||
implementing the `error` interface, and have the original error as a field | |||
on that structure, such [as this example](http://golang.org/pkg/os/#PathError). | |||
This is a good approach, but you have to know the entire chain of possible | |||
rewrapping that happens, when you might just care about one. | |||
`errwrap` formalizes this pattern (it doesn't matter what approach you use | |||
above) by giving a single interface for wrapping errors, checking if a specific | |||
error is wrapped, and extracting that error. | |||
## Installation and Docs | |||
Install using `go get github.com/hashicorp/errwrap`. | |||
Full documentation is available at | |||
http://godoc.org/github.com/hashicorp/errwrap | |||
## Usage | |||
#### Basic Usage | |||
Below is a very basic example of its usage: | |||
```go | |||
// A function that always returns an error, but wraps it, like a real | |||
// function might. | |||
func tryOpen() error { | |||
_, err := os.Open("/i/dont/exist") | |||
if err != nil { | |||
return errwrap.Wrapf("Doesn't exist: {{err}}", err) | |||
} | |||
return nil | |||
} | |||
func main() { | |||
err := tryOpen() | |||
// We can use the Contains helpers to check if an error contains | |||
// another error. It is safe to do this with a nil error, or with | |||
// an error that doesn't even use the errwrap package. | |||
if errwrap.Contains(err, "does not exist") { | |||
// Do something | |||
} | |||
if errwrap.ContainsType(err, new(os.PathError)) { | |||
// Do something | |||
} | |||
// Or we can use the associated `Get` functions to just extract | |||
// a specific error. This would return nil if that specific error doesn't | |||
// exist. | |||
perr := errwrap.GetType(err, new(os.PathError)) | |||
} | |||
``` | |||
#### Custom Types | |||
If you're already making custom types that properly wrap errors, then | |||
you can get all the functionality of `errwraps.Contains` and such by | |||
implementing the `Wrapper` interface with just one function. Example: | |||
```go | |||
type AppError { | |||
Code ErrorCode | |||
Err error | |||
} | |||
func (e *AppError) WrappedErrors() []error { | |||
return []error{e.Err} | |||
} | |||
``` | |||
Now this works: | |||
```go | |||
err := &AppError{Err: fmt.Errorf("an error")} | |||
if errwrap.ContainsType(err, fmt.Errorf("")) { | |||
// This will work! | |||
} | |||
``` |
@@ -0,0 +1,169 @@ | |||
// Package errwrap implements methods to formalize error wrapping in Go. | |||
// | |||
// All of the top-level functions that take an `error` are built to be able | |||
// to take any error, not just wrapped errors. This allows you to use errwrap | |||
// without having to type-check and type-cast everywhere. | |||
package errwrap | |||
import ( | |||
"errors" | |||
"reflect" | |||
"strings" | |||
) | |||
// WalkFunc is the callback called for Walk. | |||
type WalkFunc func(error) | |||
// Wrapper is an interface that can be implemented by custom types to | |||
// have all the Contains, Get, etc. functions in errwrap work. | |||
// | |||
// When Walk reaches a Wrapper, it will call the callback for every | |||
// wrapped error in addition to the wrapper itself. Since all the top-level | |||
// functions in errwrap use Walk, this means that all those functions work | |||
// with your custom type. | |||
type Wrapper interface { | |||
WrappedErrors() []error | |||
} | |||
// Wrap defines that outer wraps inner, returning an error type that | |||
// can be cleanly used with the other methods in this package, such as | |||
// Contains, GetAll, etc. | |||
// | |||
// This function won't modify the error message at all (the outer message | |||
// will be used). | |||
func Wrap(outer, inner error) error { | |||
return &wrappedError{ | |||
Outer: outer, | |||
Inner: inner, | |||
} | |||
} | |||
// Wrapf wraps an error with a formatting message. This is similar to using | |||
// `fmt.Errorf` to wrap an error. If you're using `fmt.Errorf` to wrap | |||
// errors, you should replace it with this. | |||
// | |||
// format is the format of the error message. The string '{{err}}' will | |||
// be replaced with the original error message. | |||
func Wrapf(format string, err error) error { | |||
outerMsg := "<nil>" | |||
if err != nil { | |||
outerMsg = err.Error() | |||
} | |||
outer := errors.New(strings.Replace( | |||
format, "{{err}}", outerMsg, -1)) | |||
return Wrap(outer, err) | |||
} | |||
// Contains checks if the given error contains an error with the | |||
// message msg. If err is not a wrapped error, this will always return | |||
// false unless the error itself happens to match this msg. | |||
func Contains(err error, msg string) bool { | |||
return len(GetAll(err, msg)) > 0 | |||
} | |||
// ContainsType checks if the given error contains an error with | |||
// the same concrete type as v. If err is not a wrapped error, this will | |||
// check the err itself. | |||
func ContainsType(err error, v interface{}) bool { | |||
return len(GetAllType(err, v)) > 0 | |||
} | |||
// Get is the same as GetAll but returns the deepest matching error. | |||
func Get(err error, msg string) error { | |||
es := GetAll(err, msg) | |||
if len(es) > 0 { | |||
return es[len(es)-1] | |||
} | |||
return nil | |||
} | |||
// GetType is the same as GetAllType but returns the deepest matching error. | |||
func GetType(err error, v interface{}) error { | |||
es := GetAllType(err, v) | |||
if len(es) > 0 { | |||
return es[len(es)-1] | |||
} | |||
return nil | |||
} | |||
// GetAll gets all the errors that might be wrapped in err with the | |||
// given message. The order of the errors is such that the outermost | |||
// matching error (the most recent wrap) is index zero, and so on. | |||
func GetAll(err error, msg string) []error { | |||
var result []error | |||
Walk(err, func(err error) { | |||
if err.Error() == msg { | |||
result = append(result, err) | |||
} | |||
}) | |||
return result | |||
} | |||
// GetAllType gets all the errors that are the same type as v. | |||
// | |||
// The order of the return value is the same as described in GetAll. | |||
func GetAllType(err error, v interface{}) []error { | |||
var result []error | |||
var search string | |||
if v != nil { | |||
search = reflect.TypeOf(v).String() | |||
} | |||
Walk(err, func(err error) { | |||
var needle string | |||
if err != nil { | |||
needle = reflect.TypeOf(err).String() | |||
} | |||
if needle == search { | |||
result = append(result, err) | |||
} | |||
}) | |||
return result | |||
} | |||
// Walk walks all the wrapped errors in err and calls the callback. If | |||
// err isn't a wrapped error, this will be called once for err. If err | |||
// is a wrapped error, the callback will be called for both the wrapper | |||
// that implements error as well as the wrapped error itself. | |||
func Walk(err error, cb WalkFunc) { | |||
if err == nil { | |||
return | |||
} | |||
switch e := err.(type) { | |||
case *wrappedError: | |||
cb(e.Outer) | |||
Walk(e.Inner, cb) | |||
case Wrapper: | |||
cb(err) | |||
for _, err := range e.WrappedErrors() { | |||
Walk(err, cb) | |||
} | |||
default: | |||
cb(err) | |||
} | |||
} | |||
// wrappedError is an implementation of error that has both the | |||
// outer and inner errors. | |||
type wrappedError struct { | |||
Outer error | |||
Inner error | |||
} | |||
func (w *wrappedError) Error() string { | |||
return w.Outer.Error() | |||
} | |||
func (w *wrappedError) WrappedErrors() []error { | |||
return []error{w.Outer, w.Inner} | |||
} |
@@ -0,0 +1 @@ | |||
module github.com/hashicorp/errwrap |
@@ -0,0 +1,12 @@ | |||
sudo: false | |||
language: go | |||
go: | |||
- 1.x | |||
branches: | |||
only: | |||
- master | |||
script: make test testrace |
@@ -0,0 +1,353 @@ | |||
Mozilla Public License, version 2.0 | |||
1. Definitions | |||
1.1. “Contributor” | |||
means each individual or legal entity that creates, contributes to the | |||
creation of, or owns Covered Software. | |||
1.2. “Contributor Version” | |||
means the combination of the Contributions of others (if any) used by a | |||
Contributor and that particular Contributor’s Contribution. | |||
1.3. “Contribution” | |||
means Covered Software of a particular Contributor. | |||
1.4. “Covered Software” | |||
means Source Code Form to which the initial Contributor has attached the | |||
notice in Exhibit A, the Executable Form of such Source Code Form, and | |||
Modifications of such Source Code Form, in each case including portions | |||
thereof. | |||
1.5. “Incompatible With Secondary Licenses” | |||
means | |||
a. that the initial Contributor has attached the notice described in | |||
Exhibit B to the Covered Software; or | |||
b. that the Covered Software was made available under the terms of version | |||
1.1 or earlier of the License, but not also under the terms of a | |||
Secondary License. | |||
1.6. “Executable Form” | |||
means any form of the work other than Source Code Form. | |||
1.7. “Larger Work” | |||
means a work that combines Covered Software with other material, in a separate | |||
file or files, that is not Covered Software. | |||
1.8. “License” | |||
means this document. | |||
1.9. “Licensable” | |||
means having the right to grant, to the maximum extent possible, whether at the | |||
time of the initial grant or subsequently, any and all of the rights conveyed by | |||
this License. | |||
1.10. “Modifications” | |||
means any of the following: | |||
a. any file in Source Code Form that results from an addition to, deletion | |||
from, or modification of the contents of Covered Software; or | |||
b. any new file in Source Code Form that contains any Covered Software. | |||
1.11. “Patent Claims” of a Contributor | |||
means any patent claim(s), including without limitation, method, process, | |||
and apparatus claims, in any patent Licensable by such Contributor that | |||
would be infringed, but for the grant of the License, by the making, | |||
using, selling, offering for sale, having made, import, or transfer of | |||
either its Contributions or its Contributor Version. | |||
1.12. “Secondary License” | |||
means either the GNU General Public License, Version 2.0, the GNU Lesser | |||
General Public License, Version 2.1, the GNU Affero General Public | |||
License, Version 3.0, or any later versions of those licenses. | |||
1.13. “Source Code Form” | |||
means the form of the work preferred for making modifications. | |||
1.14. “You” (or “Your”) | |||
means an individual or a legal entity exercising rights under this | |||
License. For legal entities, “You” includes any entity that controls, is | |||
controlled by, or is under common control with You. For purposes of this | |||
definition, “control” means (a) the power, direct or indirect, to cause | |||
the direction or management of such entity, whether by contract or | |||
otherwise, or (b) ownership of more than fifty percent (50%) of the | |||
outstanding shares or beneficial ownership of such entity. | |||
2. License Grants and Conditions | |||
2.1. Grants | |||
Each Contributor hereby grants You a world-wide, royalty-free, | |||
non-exclusive license: | |||
a. under intellectual property rights (other than patent or trademark) | |||
Licensable by such Contributor to use, reproduce, make available, | |||
modify, display, perform, distribute, and otherwise exploit its | |||
Contributions, either on an unmodified basis, with Modifications, or as | |||
part of a Larger Work; and | |||
b. under Patent Claims of such Contributor to make, use, sell, offer for | |||
sale, have made, import, and otherwise transfer either its Contributions | |||
or its Contributor Version. | |||
2.2. Effective Date | |||
The licenses granted in Section 2.1 with respect to any Contribution become | |||
effective for each Contribution on the date the Contributor first distributes | |||
such Contribution. | |||
2.3. Limitations on Grant Scope | |||
The licenses granted in this Section 2 are the only rights granted under this | |||
License. No additional rights or licenses will be implied from the distribution | |||
or licensing of Covered Software under this License. Notwithstanding Section | |||
2.1(b) above, no patent license is granted by a Contributor: | |||
a. for any code that a Contributor has removed from Covered Software; or | |||
b. for infringements caused by: (i) Your and any other third party’s | |||
modifications of Covered Software, or (ii) the combination of its | |||
Contributions with other software (except as part of its Contributor | |||
Version); or | |||
c. under Patent Claims infringed by Covered Software in the absence of its | |||
Contributions. | |||
This License does not grant any rights in the trademarks, service marks, or | |||
logos of any Contributor (except as may be necessary to comply with the | |||
notice requirements in Section 3.4). | |||
2.4. Subsequent Licenses | |||
No Contributor makes additional grants as a result of Your choice to | |||
distribute the Covered Software under a subsequent version of this License | |||
(see Section 10.2) or under the terms of a Secondary License (if permitted | |||
under the terms of Section 3.3). | |||
2.5. Representation | |||
Each Contributor represents that the Contributor believes its Contributions | |||
are its original creation(s) or it has sufficient rights to grant the | |||
rights to its Contributions conveyed by this License. | |||
2.6. Fair Use | |||
This License is not intended to limit any rights You have under applicable | |||
copyright doctrines of fair use, fair dealing, or other equivalents. | |||
2.7. Conditions | |||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in | |||
Section 2.1. | |||
3. Responsibilities | |||
3.1. Distribution of Source Form | |||
All distribution of Covered Software in Source Code Form, including any | |||
Modifications that You create or to which You contribute, must be under the | |||
terms of this License. You must inform recipients that the Source Code Form | |||
of the Covered Software is governed by the terms of this License, and how | |||
they can obtain a copy of this License. You may not attempt to alter or | |||
restrict the recipients’ rights in the Source Code Form. | |||
3.2. Distribution of Executable Form | |||
If You distribute Covered Software in Executable Form then: | |||
a. such Covered Software must also be made available in Source Code Form, | |||
as described in Section 3.1, and You must inform recipients of the | |||
Executable Form how they can obtain a copy of such Source Code Form by | |||
reasonable means in a timely manner, at a charge no more than the cost | |||
of distribution to the recipient; and | |||
b. You may distribute such Executable Form under the terms of this License, | |||
or sublicense it under different terms, provided that the license for | |||
the Executable Form does not attempt to limit or alter the recipients’ | |||
rights in the Source Code Form under this License. | |||
3.3. Distribution of a Larger Work | |||
You may create and distribute a Larger Work under terms of Your choice, | |||
provided that You also comply with the requirements of this License for the | |||
Covered Software. If the Larger Work is a combination of Covered Software | |||
with a work governed by one or more Secondary Licenses, and the Covered | |||
Software is not Incompatible With Secondary Licenses, this License permits | |||
You to additionally distribute such Covered Software under the terms of | |||
such Secondary License(s), so that the recipient of the Larger Work may, at | |||
their option, further distribute the Covered Software under the terms of | |||
either this License or such Secondary License(s). | |||
3.4. Notices | |||
You may not remove or alter the substance of any license notices (including | |||
copyright notices, patent notices, disclaimers of warranty, or limitations | |||
of liability) contained within the Source Code Form of the Covered | |||
Software, except that You may alter any license notices to the extent | |||
required to remedy known factual inaccuracies. | |||
3.5. Application of Additional Terms | |||
You may choose to offer, and to charge a fee for, warranty, support, | |||
indemnity or liability obligations to one or more recipients of Covered | |||
Software. However, You may do so only on Your own behalf, and not on behalf | |||
of any Contributor. You must make it absolutely clear that any such | |||
warranty, support, indemnity, or liability obligation is offered by You | |||
alone, and You hereby agree to indemnify every Contributor for any | |||
liability incurred by such Contributor as a result of warranty, support, | |||
indemnity or liability terms You offer. You may include additional | |||
disclaimers of warranty and limitations of liability specific to any | |||
jurisdiction. | |||
4. Inability to Comply Due to Statute or Regulation | |||
If it is impossible for You to comply with any of the terms of this License | |||
with respect to some or all of the Covered Software due to statute, judicial | |||
order, or regulation then You must: (a) comply with the terms of this License | |||
to the maximum extent possible; and (b) describe the limitations and the code | |||
they affect. Such description must be placed in a text file included with all | |||
distributions of the Covered Software under this License. Except to the | |||
extent prohibited by statute or regulation, such description must be | |||
sufficiently detailed for a recipient of ordinary skill to be able to | |||
understand it. | |||
5. Termination | |||
5.1. The rights granted under this License will terminate automatically if You | |||
fail to comply with any of its terms. However, if You become compliant, | |||
then the rights granted under this License from a particular Contributor | |||
are reinstated (a) provisionally, unless and until such Contributor | |||
explicitly and finally terminates Your grants, and (b) on an ongoing basis, | |||
if such Contributor fails to notify You of the non-compliance by some | |||
reasonable means prior to 60 days after You have come back into compliance. | |||
Moreover, Your grants from a particular Contributor are reinstated on an | |||
ongoing basis if such Contributor notifies You of the non-compliance by | |||
some reasonable means, this is the first time You have received notice of | |||
non-compliance with this License from such Contributor, and You become | |||
compliant prior to 30 days after Your receipt of the notice. | |||
5.2. If You initiate litigation against any entity by asserting a patent | |||
infringement claim (excluding declaratory judgment actions, counter-claims, | |||
and cross-claims) alleging that a Contributor Version directly or | |||
indirectly infringes any patent, then the rights granted to You by any and | |||
all Contributors for the Covered Software under Section 2.1 of this License | |||
shall terminate. | |||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user | |||
license agreements (excluding distributors and resellers) which have been | |||
validly granted by You or Your distributors under this License prior to | |||
termination shall survive termination. | |||
6. Disclaimer of Warranty | |||
Covered Software is provided under this License on an “as is” basis, without | |||
warranty of any kind, either expressed, implied, or statutory, including, | |||
without limitation, warranties that the Covered Software is free of defects, | |||
merchantable, fit for a particular purpose or non-infringing. The entire | |||
risk as to the quality and performance of the Covered Software is with You. | |||
Should any Covered Software prove defective in any respect, You (not any | |||
Contributor) assume the cost of any necessary servicing, repair, or | |||
correction. This disclaimer of warranty constitutes an essential part of this | |||
License. No use of any Covered Software is authorized under this License | |||
except under this disclaimer. | |||
7. Limitation of Liability | |||
Under no circumstances and under no legal theory, whether tort (including | |||
negligence), contract, or otherwise, shall any Contributor, or anyone who | |||
distributes Covered Software as permitted above, be liable to You for any | |||
direct, indirect, special, incidental, or consequential damages of any | |||
character including, without limitation, damages for lost profits, loss of | |||
goodwill, work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses, even if such party shall have been | |||
informed of the possibility of such damages. This limitation of liability | |||
shall not apply to liability for death or personal injury resulting from such | |||
party’s negligence to the extent applicable law prohibits such limitation. | |||
Some jurisdictions do not allow the exclusion or limitation of incidental or | |||
consequential damages, so this exclusion and limitation may not apply to You. | |||
8. Litigation | |||
Any litigation relating to this License may be brought only in the courts of | |||
a jurisdiction where the defendant maintains its principal place of business | |||
and such litigation shall be governed by laws of that jurisdiction, without | |||
reference to its conflict-of-law provisions. Nothing in this Section shall | |||
prevent a party’s ability to bring cross-claims or counter-claims. | |||
9. Miscellaneous | |||
This License represents the complete agreement concerning the subject matter | |||
hereof. If any provision of this License is held to be unenforceable, such | |||
provision shall be reformed only to the extent necessary to make it | |||
enforceable. Any law or regulation which provides that the language of a | |||
contract shall be construed against the drafter shall not be used to construe | |||
this License against a Contributor. | |||
10. Versions of the License | |||
10.1. New Versions | |||
Mozilla Foundation is the license steward. Except as provided in Section | |||
10.3, no one other than the license steward has the right to modify or | |||
publish new versions of this License. Each version will be given a | |||
distinguishing version number. | |||
10.2. Effect of New Versions | |||
You may distribute the Covered Software under the terms of the version of | |||
the License under which You originally received the Covered Software, or | |||
under the terms of any subsequent version published by the license | |||
steward. | |||
10.3. Modified Versions | |||
If you create software not governed by this License, and you want to | |||
create a new license for such software, you may create and use a modified | |||
version of this License if you rename the license and remove any | |||
references to the name of the license steward (except to note that such | |||
modified license differs from this License). | |||
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses | |||
If You choose to distribute Source Code Form that is Incompatible With | |||
Secondary Licenses under the terms of this version of the License, the | |||
notice described in Exhibit B of this License must be attached. | |||
Exhibit A - Source Code Form License Notice | |||
This Source Code Form is subject to the | |||
terms of the Mozilla Public License, v. | |||
2.0. If a copy of the MPL was not | |||
distributed with this file, You can | |||
obtain one at | |||
http://mozilla.org/MPL/2.0/. | |||
If it is not possible or desirable to put the notice in a particular file, then | |||
You may include the notice in a location (such as a LICENSE file in a relevant | |||
directory) where a recipient would be likely to look for such a notice. | |||
You may add additional accurate notices of copyright ownership. | |||
Exhibit B - “Incompatible With Secondary Licenses” Notice | |||
This Source Code Form is “Incompatible | |||
With Secondary Licenses”, as defined by | |||
the Mozilla Public License, v. 2.0. |
@@ -0,0 +1,31 @@ | |||
TEST?=./... | |||
default: test | |||
# test runs the test suite and vets the code. | |||
test: generate | |||
@echo "==> Running tests..." | |||
@go list $(TEST) \ | |||
| grep -v "/vendor/" \ | |||
| xargs -n1 go test -timeout=60s -parallel=10 ${TESTARGS} | |||
# testrace runs the race checker | |||
testrace: generate | |||
@echo "==> Running tests (race)..." | |||
@go list $(TEST) \ | |||
| grep -v "/vendor/" \ | |||
| xargs -n1 go test -timeout=60s -race ${TESTARGS} | |||
# updatedeps installs all the dependencies needed to run and build. | |||
updatedeps: | |||
@sh -c "'${CURDIR}/scripts/deps.sh' '${NAME}'" | |||
# generate runs `go generate` to build the dynamically generated source files. | |||
generate: | |||
@echo "==> Generating..." | |||
@find . -type f -name '.DS_Store' -delete | |||
@go list ./... \ | |||
| grep -v "/vendor/" \ | |||
| xargs -n1 go generate | |||
.PHONY: default test testrace updatedeps generate |
@@ -0,0 +1,97 @@ | |||
# go-multierror | |||
[![Build Status](http://img.shields.io/travis/hashicorp/go-multierror.svg?style=flat-square)][travis] | |||
[![Go Documentation](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godocs] | |||
[travis]: https://travis-ci.org/hashicorp/go-multierror | |||
[godocs]: https://godoc.org/github.com/hashicorp/go-multierror | |||
`go-multierror` is a package for Go that provides a mechanism for | |||
representing a list of `error` values as a single `error`. | |||
This allows a function in Go to return an `error` that might actually | |||
be a list of errors. If the caller knows this, they can unwrap the | |||
list and access the errors. If the caller doesn't know, the error | |||
formats to a nice human-readable format. | |||
`go-multierror` implements the | |||
[errwrap](https://github.com/hashicorp/errwrap) interface so that it can | |||
be used with that library, as well. | |||
## Installation and Docs | |||
Install using `go get github.com/hashicorp/go-multierror`. | |||
Full documentation is available at | |||
http://godoc.org/github.com/hashicorp/go-multierror | |||
## Usage | |||
go-multierror is easy to use and purposely built to be unobtrusive in | |||
existing Go applications/libraries that may not be aware of it. | |||
**Building a list of errors** | |||
The `Append` function is used to create a list of errors. This function | |||
behaves a lot like the Go built-in `append` function: it doesn't matter | |||
if the first argument is nil, a `multierror.Error`, or any other `error`, | |||
the function behaves as you would expect. | |||
```go | |||
var result error | |||
if err := step1(); err != nil { | |||
result = multierror.Append(result, err) | |||
} | |||
if err := step2(); err != nil { | |||
result = multierror.Append(result, err) | |||
} | |||
return result | |||
``` | |||
**Customizing the formatting of the errors** | |||
By specifying a custom `ErrorFormat`, you can customize the format | |||
of the `Error() string` function: | |||
```go | |||
var result *multierror.Error | |||
// ... accumulate errors here, maybe using Append | |||
if result != nil { | |||
result.ErrorFormat = func([]error) string { | |||
return "errors!" | |||
} | |||
} | |||
``` | |||
**Accessing the list of errors** | |||
`multierror.Error` implements `error` so if the caller doesn't know about | |||
multierror, it will work just fine. But if you're aware a multierror might | |||
be returned, you can use type switches to access the list of errors: | |||
```go | |||
if err := something(); err != nil { | |||
if merr, ok := err.(*multierror.Error); ok { | |||
// Use merr.Errors | |||
} | |||
} | |||
``` | |||
**Returning a multierror only if there are errors** | |||
If you build a `multierror.Error`, you can use the `ErrorOrNil` function | |||
to return an `error` implementation only if there are errors to return: | |||
```go | |||
var result *multierror.Error | |||
// ... accumulate errors here | |||
// Return the `error` only if errors were added to the multierror, otherwise | |||
// return nil since there are no errors. | |||
return result.ErrorOrNil() | |||
``` |
@@ -0,0 +1,41 @@ | |||
package multierror | |||
// Append is a helper function that will append more errors | |||
// onto an Error in order to create a larger multi-error. | |||
// | |||
// If err is not a multierror.Error, then it will be turned into | |||
// one. If any of the errs are multierr.Error, they will be flattened | |||
// one level into err. | |||
func Append(err error, errs ...error) *Error { | |||
switch err := err.(type) { | |||
case *Error: | |||
// Typed nils can reach here, so initialize if we are nil | |||
if err == nil { | |||
err = new(Error) | |||
} | |||
// Go through each error and flatten | |||
for _, e := range errs { | |||
switch e := e.(type) { | |||
case *Error: | |||
if e != nil { | |||
err.Errors = append(err.Errors, e.Errors...) | |||
} | |||
default: | |||
if e != nil { | |||
err.Errors = append(err.Errors, e) | |||
} | |||
} | |||
} | |||
return err | |||
default: | |||
newErrs := make([]error, 0, len(errs)+1) | |||
if err != nil { | |||
newErrs = append(newErrs, err) | |||
} | |||
newErrs = append(newErrs, errs...) | |||
return Append(&Error{}, newErrs...) | |||
} | |||
} |
@@ -0,0 +1,26 @@ | |||
package multierror | |||
// Flatten flattens the given error, merging any *Errors together into | |||
// a single *Error. | |||
func Flatten(err error) error { | |||
// If it isn't an *Error, just return the error as-is | |||
if _, ok := err.(*Error); !ok { | |||
return err | |||
} | |||
// Otherwise, make the result and flatten away! | |||
flatErr := new(Error) | |||
flatten(err, flatErr) | |||
return flatErr | |||
} | |||
func flatten(err error, flatErr *Error) { | |||
switch err := err.(type) { | |||
case *Error: | |||
for _, e := range err.Errors { | |||
flatten(e, flatErr) | |||
} | |||
default: | |||
flatErr.Errors = append(flatErr.Errors, err) | |||
} | |||
} |
@@ -0,0 +1,27 @@ | |||
package multierror | |||
import ( | |||
"fmt" | |||
"strings" | |||
) | |||
// ErrorFormatFunc is a function callback that is called by Error to | |||
// turn the list of errors into a string. | |||
type ErrorFormatFunc func([]error) string | |||
// ListFormatFunc is a basic formatter that outputs the number of errors | |||
// that occurred along with a bullet point list of the errors. | |||
func ListFormatFunc(es []error) string { | |||
if len(es) == 1 { | |||
return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0]) | |||
} | |||
points := make([]string, len(es)) | |||
for i, err := range es { | |||
points[i] = fmt.Sprintf("* %s", err) | |||
} | |||
return fmt.Sprintf( | |||
"%d errors occurred:\n\t%s\n\n", | |||
len(es), strings.Join(points, "\n\t")) | |||
} |
@@ -0,0 +1,3 @@ | |||
module github.com/hashicorp/go-multierror | |||
require github.com/hashicorp/errwrap v1.0.0 |
@@ -0,0 +1,4 @@ | |||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce h1:prjrVgOk2Yg6w+PflHoszQNLTUh4kaByUcEWM/9uin4= | |||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | |||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= | |||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= |
@@ -0,0 +1,51 @@ | |||
package multierror | |||
import ( | |||
"fmt" | |||
) | |||
// Error is an error type to track multiple errors. This is used to | |||
// accumulate errors in cases and return them as a single "error". | |||
type Error struct { | |||
Errors []error | |||
ErrorFormat ErrorFormatFunc | |||
} | |||
func (e *Error) Error() string { | |||
fn := e.ErrorFormat | |||
if fn == nil { | |||
fn = ListFormatFunc | |||
} | |||
return fn(e.Errors) | |||
} | |||
// ErrorOrNil returns an error interface if this Error represents | |||
// a list of errors, or returns nil if the list of errors is empty. This | |||
// function is useful at the end of accumulation to make sure that the value | |||
// returned represents the existence of errors. | |||
func (e *Error) ErrorOrNil() error { | |||
if e == nil { | |||
return nil | |||
} | |||
if len(e.Errors) == 0 { | |||
return nil | |||
} | |||
return e | |||
} | |||
func (e *Error) GoString() string { | |||
return fmt.Sprintf("*%#v", *e) | |||
} | |||
// WrappedErrors returns the list of errors that this Error is wrapping. | |||
// It is an implementation of the errwrap.Wrapper interface so that | |||
// multierror.Error can be used with that library. | |||
// | |||
// This method is not safe to be called concurrently and is no different | |||
// than accessing the Errors field directly. It is implemented only to | |||
// satisfy the errwrap.Wrapper interface. | |||
func (e *Error) WrappedErrors() []error { | |||
return e.Errors | |||
} |
@@ -0,0 +1,37 @@ | |||
package multierror | |||
import ( | |||
"fmt" | |||
"github.com/hashicorp/errwrap" | |||
) | |||
// Prefix is a helper function that will prefix some text | |||
// to the given error. If the error is a multierror.Error, then | |||
// it will be prefixed to each wrapped error. | |||
// | |||
// This is useful to use when appending multiple multierrors | |||
// together in order to give better scoping. | |||
func Prefix(err error, prefix string) error { | |||
if err == nil { | |||
return nil | |||
} | |||
format := fmt.Sprintf("%s {{err}}", prefix) | |||
switch err := err.(type) { | |||
case *Error: | |||
// Typed nils can reach here, so initialize if we are nil | |||
if err == nil { | |||
err = new(Error) | |||
} | |||
// Wrap each of the errors | |||
for i, e := range err.Errors { | |||
err.Errors[i] = errwrap.Wrapf(format, e) | |||
} | |||
return err | |||
default: | |||
return errwrap.Wrapf(format, err) | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
package multierror | |||
// Len implements sort.Interface function for length | |||
func (err Error) Len() int { | |||
return len(err.Errors) | |||
} | |||
// Swap implements sort.Interface function for swapping elements | |||
func (err Error) Swap(i, j int) { | |||
err.Errors[i], err.Errors[j] = err.Errors[j], err.Errors[i] | |||
} | |||
// Less implements sort.Interface function for determining order | |||
func (err Error) Less(i, j int) bool { | |||
return err.Errors[i].Error() < err.Errors[j].Error() | |||
} |
@@ -0,0 +1,11 @@ | |||
language: go | |||
os: | |||
- linux | |||
- osx | |||
go: | |||
- 1.3 | |||
- 1.4 | |||
- 1.5 | |||
- tip |
@@ -0,0 +1,15 @@ | |||
ISC License | |||
Copyright (c) 2012 Chris Howey | |||
Permission to use, copy, modify, and distribute this software for any | |||
purpose with or without fee is hereby granted, provided that the above | |||
copyright notice and this permission notice appear in all copies. | |||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
@@ -0,0 +1,384 @@ | |||
Unless otherwise noted, all files in this distribution are released | |||
under the Common Development and Distribution License (CDDL). | |||
Exceptions are noted within the associated source files. | |||
-------------------------------------------------------------------- | |||
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0 | |||
1. Definitions. | |||
1.1. "Contributor" means each individual or entity that creates | |||
or contributes to the creation of Modifications. | |||
1.2. "Contributor Version" means the combination of the Original | |||
Software, prior Modifications used by a Contributor (if any), | |||
and the Modifications made by that particular Contributor. | |||
1.3. "Covered Software" means (a) the Original Software, or (b) | |||
Modifications, or (c) the combination of files containing | |||
Original Software with files containing Modifications, in | |||
each case including portions thereof. | |||
1.4. "Executable" means the Covered Software in any form other | |||
than Source Code. | |||
1.5. "Initial Developer" means the individual or entity that first | |||
makes Original Software available under this License. | |||
1.6. "Larger Work" means a work which combines Covered Software or | |||
portions thereof with code not governed by the terms of this | |||
License. | |||
1.7. "License" means this document. | |||
1.8. "Licensable" means having the right to grant, to the maximum | |||
extent possible, whether at the time of the initial grant or | |||
subsequently acquired, any and all of the rights conveyed | |||
herein. | |||
1.9. "Modifications" means the Source Code and Executable form of | |||
any of the following: | |||
A. Any file that results from an addition to, deletion from or | |||
modification of the contents of a file containing Original | |||
Software or previous Modifications; | |||
B. Any new file that contains any part of the Original | |||
Software or previous Modifications; or | |||
C. Any new file that is contributed or otherwise made | |||
available under the terms of this License. | |||
1.10. "Original Software" means the Source Code and Executable | |||
form of computer software code that is originally released | |||
under this License. | |||
1.11. "Patent Claims" means any patent claim(s), now owned or | |||
hereafter acquired, including without limitation, method, | |||
process, and apparatus claims, in any patent Licensable by | |||
grantor. | |||
1.12. "Source Code" means (a) the common form of computer software | |||
code in which modifications are made and (b) associated | |||
documentation included in or with such code. | |||
1.13. "You" (or "Your") means an individual or a legal entity | |||
exercising rights under, and complying with all of the terms | |||
of, this License. For legal entities, "You" includes any | |||
entity which controls, is controlled by, or is under common | |||
control with You. For purposes of this definition, | |||
"control" means (a) the power, direct or indirect, to cause | |||
the direction or management of such entity, whether by | |||
contract or otherwise, or (b) ownership of more than fifty | |||
percent (50%) of the outstanding shares or beneficial | |||
ownership of such entity. | |||
2. License Grants. | |||
2.1. The Initial Developer Grant. | |||
Conditioned upon Your compliance with Section 3.1 below and | |||
subject to third party intellectual property claims, the Initial | |||
Developer hereby grants You a world-wide, royalty-free, | |||
non-exclusive license: | |||
(a) under intellectual property rights (other than patent or | |||
trademark) Licensable by Initial Developer, to use, | |||
reproduce, modify, display, perform, sublicense and | |||
distribute the Original Software (or portions thereof), | |||
with or without Modifications, and/or as part of a Larger | |||
Work; and | |||
(b) under Patent Claims infringed by the making, using or | |||
selling of Original Software, to make, have made, use, | |||
practice, sell, and offer for sale, and/or otherwise | |||
dispose of the Original Software (or portions thereof). | |||
(c) The licenses granted in Sections 2.1(a) and (b) are | |||
effective on the date Initial Developer first distributes | |||
or otherwise makes the Original Software available to a | |||
third party under the terms of this License. | |||
(d) Notwithstanding Section 2.1(b) above, no patent license is | |||
granted: (1) for code that You delete from the Original | |||
Software, or (2) for infringements caused by: (i) the | |||
modification of the Original Software, or (ii) the | |||
combination of the Original Software with other software | |||
or devices. | |||
2.2. Contributor Grant. | |||
Conditioned upon Your compliance with Section 3.1 below and | |||
subject to third party intellectual property claims, each | |||
Contributor hereby grants You a world-wide, royalty-free, | |||
non-exclusive license: | |||
(a) under intellectual property rights (other than patent or | |||
trademark) Licensable by Contributor to use, reproduce, | |||
modify, display, perform, sublicense and distribute the | |||
Modifications created by such Contributor (or portions | |||
thereof), either on an unmodified basis, with other | |||
Modifications, as Covered Software and/or as part of a | |||
Larger Work; and | |||
(b) under Patent Claims infringed by the making, using, or | |||
selling of Modifications made by that Contributor either | |||
alone and/or in combination with its Contributor Version | |||
(or portions of such combination), to make, use, sell, | |||
offer for sale, have made, and/or otherwise dispose of: | |||
(1) Modifications made by that Contributor (or portions | |||
thereof); and (2) the combination of Modifications made by | |||
that Contributor with its Contributor Version (or portions | |||
of such combination). | |||
(c) The licenses granted in Sections 2.2(a) and 2.2(b) are | |||
effective on the date Contributor first distributes or | |||
otherwise makes the Modifications available to a third | |||
party. | |||
(d) Notwithstanding Section 2.2(b) above, no patent license is | |||
granted: (1) for any code that Contributor has deleted | |||
from the Contributor Version; (2) for infringements caused | |||
by: (i) third party modifications of Contributor Version, | |||
or (ii) the combination of Modifications made by that | |||
Contributor with other software (except as part of the | |||
Contributor Version) or other devices; or (3) under Patent | |||
Claims infringed by Covered Software in the absence of | |||
Modifications made by that Contributor. | |||
3. Distribution Obligations. | |||
3.1. Availability of Source Code. | |||
Any Covered Software that You distribute or otherwise make | |||
available in Executable form must also be made available in Source | |||
Code form and that Source Code form must be distributed only under | |||
the terms of this License. You must include a copy of this | |||
License with every copy of the Source Code form of the Covered | |||
Software You distribute or otherwise make available. You must | |||
inform recipients of any such Covered Software in Executable form | |||
as to how they can obtain such Covered Software in Source Code | |||
form in a reasonable manner on or through a medium customarily | |||
used for software exchange. | |||
3.2. Modifications. | |||
The Modifications that You create or to which You contribute are | |||
governed by the terms of this License. You represent that You | |||
believe Your Modifications are Your original creation(s) and/or | |||
You have sufficient rights to grant the rights conveyed by this | |||
License. | |||
3.3. Required Notices. | |||
You must include a notice in each of Your Modifications that | |||
identifies You as the Contributor of the Modification. You may | |||
not remove or alter any copyright, patent or trademark notices | |||
contained within the Covered Software, or any notices of licensing | |||
or any descriptive text giving attribution to any Contributor or | |||
the Initial Developer. | |||
3.4. Application of Additional Terms. | |||
You may not offer or impose any terms on any Covered Software in | |||
Source Code form that alters or restricts the applicable version | |||
of this License or the recipients' rights hereunder. You may | |||
choose to offer, and to charge a fee for, warranty, support, | |||
indemnity or liability obligations to one or more recipients of | |||
Covered Software. However, you may do so only on Your own behalf, | |||
and not on behalf of the Initial Developer or any Contributor. | |||
You must make it absolutely clear that any such warranty, support, | |||
indemnity or liability obligation is offered by You alone, and You | |||
hereby agree to indemnify the Initial Developer and every | |||
Contributor for any liability incurred by the Initial Developer or | |||
such Contributor as a result of warranty, support, indemnity or | |||
liability terms You offer. | |||
3.5. Distribution of Executable Versions. | |||
You may distribute the Executable form of the Covered Software | |||
under the terms of this License or under the terms of a license of | |||
Your choice, which may contain terms different from this License, | |||
provided that You are in compliance with the terms of this License | |||
and that the license for the Executable form does not attempt to | |||
limit or alter the recipient's rights in the Source Code form from | |||
the rights set forth in this License. If You distribute the | |||
Covered Software in Executable form under a different license, You | |||
must make it absolutely clear that any terms which differ from | |||
this License are offered by You alone, not by the Initial | |||
Developer or Contributor. You hereby agree to indemnify the | |||
Initial Developer and every Contributor for any liability incurred | |||
by the Initial Developer or such Contributor as a result of any | |||
such terms You offer. | |||
3.6. Larger Works. | |||
You may create a Larger Work by combining Covered Software with | |||
other code not governed by the terms of this License and | |||
distribute the Larger Work as a single product. In such a case, | |||
You must make sure the requirements of this License are fulfilled | |||
for the Covered Software. | |||
4. Versions of the License. | |||
4.1. New Versions. | |||
Sun Microsystems, Inc. is the initial license steward and may | |||
publish revised and/or new versions of this License from time to | |||
time. Each version will be given a distinguishing version number. | |||
Except as provided in Section 4.3, no one other than the license | |||
steward has the right to modify this License. | |||
4.2. Effect of New Versions. | |||
You may always continue to use, distribute or otherwise make the | |||
Covered Software available under the terms of the version of the | |||
License under which You originally received the Covered Software. | |||
If the Initial Developer includes a notice in the Original | |||
Software prohibiting it from being distributed or otherwise made | |||
available under any subsequent version of the License, You must | |||
distribute and make the Covered Software available under the terms | |||
of the version of the License under which You originally received | |||
the Covered Software. Otherwise, You may also choose to use, | |||
distribute or otherwise make the Covered Software available under | |||
the terms of any subsequent version of the License published by | |||
the license steward. | |||
4.3. Modified Versions. | |||
When You are an Initial Developer and You want to create a new | |||
license for Your Original Software, You may create and use a | |||
modified version of this License if You: (a) rename the license | |||
and remove any references to the name of the license steward | |||
(except to note that the license differs from this License); and | |||
(b) otherwise make it clear that the license contains terms which | |||
differ from this License. | |||
5. DISCLAIMER OF WARRANTY. | |||
COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" | |||
BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, | |||
INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED | |||
SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR | |||
PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND | |||
PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY | |||
COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE | |||
INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY | |||
NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF | |||
WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF | |||
ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS | |||
DISCLAIMER. | |||
6. TERMINATION. | |||
6.1. This License and the rights granted hereunder will terminate | |||
automatically if You fail to comply with terms herein and fail to | |||
cure such breach within 30 days of becoming aware of the breach. | |||
Provisions which, by their nature, must remain in effect beyond | |||
the termination of this License shall survive. | |||
6.2. If You assert a patent infringement claim (excluding | |||
declaratory judgment actions) against Initial Developer or a | |||
Contributor (the Initial Developer or Contributor against whom You | |||
assert such claim is referred to as "Participant") alleging that | |||
the Participant Software (meaning the Contributor Version where | |||
the Participant is a Contributor or the Original Software where | |||
the Participant is the Initial Developer) directly or indirectly | |||
infringes any patent, then any and all rights granted directly or | |||
indirectly to You by such Participant, the Initial Developer (if | |||
the Initial Developer is not the Participant) and all Contributors | |||
under Sections 2.1 and/or 2.2 of this License shall, upon 60 days | |||
notice from Participant terminate prospectively and automatically | |||
at the expiration of such 60 day notice period, unless if within | |||
such 60 day period You withdraw Your claim with respect to the | |||
Participant Software against such Participant either unilaterally | |||
or pursuant to a written agreement with Participant. | |||
6.3. In the event of termination under Sections 6.1 or 6.2 above, | |||
all end user licenses that have been validly granted by You or any | |||
distributor hereunder prior to termination (excluding licenses | |||
granted to You by any distributor) shall survive termination. | |||
7. LIMITATION OF LIABILITY. | |||
UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT | |||
(INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE | |||
INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF | |||
COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE | |||
LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR | |||
CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT | |||
LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK | |||
STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER | |||
COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN | |||
INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF | |||
LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL | |||
INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT | |||
APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO | |||
NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR | |||
CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT | |||
APPLY TO YOU. | |||
8. U.S. GOVERNMENT END USERS. | |||
The Covered Software is a "commercial item," as that term is | |||
defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial | |||
computer software" (as that term is defined at 48 | |||
C.F.R. 252.227-7014(a)(1)) and "commercial computer software | |||
documentation" as such terms are used in 48 C.F.R. 12.212 | |||
(Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 | |||
C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all | |||
U.S. Government End Users acquire Covered Software with only those | |||
rights set forth herein. This U.S. Government Rights clause is in | |||
lieu of, and supersedes, any other FAR, DFAR, or other clause or | |||
provision that addresses Government rights in computer software | |||
under this License. | |||
9. MISCELLANEOUS. | |||
This License represents the complete agreement concerning subject | |||
matter hereof. If any provision of this License is held to be | |||
unenforceable, such provision shall be reformed only to the extent | |||
necessary to make it enforceable. This License shall be governed | |||
by the law of the jurisdiction specified in a notice contained | |||
within the Original Software (except to the extent applicable law, | |||
if any, provides otherwise), excluding such jurisdiction's | |||
conflict-of-law provisions. Any litigation relating to this | |||
License shall be subject to the jurisdiction of the courts located | |||
in the jurisdiction and venue specified in a notice contained | |||
within the Original Software, with the losing party responsible | |||
for costs, including, without limitation, court costs and | |||
reasonable attorneys' fees and expenses. The application of the | |||
United Nations Convention on Contracts for the International Sale | |||
of Goods is expressly excluded. Any law or regulation which | |||
provides that the language of a contract shall be construed | |||
against the drafter shall not apply to this License. You agree | |||
that You alone are responsible for compliance with the United | |||
States export administration regulations (and the export control | |||
laws and regulation of any other countries) when You use, | |||
distribute or otherwise make available any Covered Software. | |||
10. RESPONSIBILITY FOR CLAIMS. | |||
As between Initial Developer and the Contributors, each party is | |||
responsible for claims and damages arising, directly or | |||
indirectly, out of its utilization of rights under this License | |||
and You agree to work with Initial Developer and Contributors to | |||
distribute such responsibility on an equitable basis. Nothing | |||
herein is intended or shall be deemed to constitute any admission | |||
of liability. | |||
-------------------------------------------------------------------- | |||
NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND | |||
DISTRIBUTION LICENSE (CDDL) | |||
For Covered Software in this distribution, this License shall | |||
be governed by the laws of the State of California (excluding | |||
conflict-of-law provisions). | |||
Any litigation relating to this License shall be subject to the | |||
jurisdiction of the Federal Courts of the Northern District of | |||
California and the state courts of the State of California, with | |||
venue lying in Santa Clara County, California. |
@@ -0,0 +1,27 @@ | |||
# getpasswd in Go [![GoDoc](https://godoc.org/github.com/howeyc/gopass?status.svg)](https://godoc.org/github.com/howeyc/gopass) [![Build Status](https://secure.travis-ci.org/howeyc/gopass.png?branch=master)](http://travis-ci.org/howeyc/gopass) | |||
Retrieve password from user terminal or piped input without echo. | |||
Verified on BSD, Linux, and Windows. | |||
Example: | |||
```go | |||
package main | |||
import "fmt" | |||
import "github.com/howeyc/gopass" | |||
func main() { | |||
fmt.Printf("Password: ") | |||
// Silent. For printing *'s use gopass.GetPasswdMasked() | |||
pass, err := gopass.GetPasswd() | |||
if err != nil { | |||
// Handle gopass.ErrInterrupted or getch() read error | |||
} | |||
// Do something with pass | |||
} | |||
``` | |||
Caution: Multi-byte characters not supported! |
@@ -0,0 +1,110 @@ | |||
package gopass | |||
import ( | |||
"errors" | |||
"fmt" | |||
"io" | |||
"os" | |||
) | |||
type FdReader interface { | |||
io.Reader | |||
Fd() uintptr | |||
} | |||
var defaultGetCh = func(r io.Reader) (byte, error) { | |||
buf := make([]byte, 1) | |||
if n, err := r.Read(buf); n == 0 || err != nil { | |||
if err != nil { | |||
return 0, err | |||
} | |||
return 0, io.EOF | |||
} | |||
return buf[0], nil | |||
} | |||
var ( | |||
maxLength = 512 | |||
ErrInterrupted = errors.New("interrupted") | |||
ErrMaxLengthExceeded = fmt.Errorf("maximum byte limit (%v) exceeded", maxLength) | |||
// Provide variable so that tests can provide a mock implementation. | |||
getch = defaultGetCh | |||
) | |||
// getPasswd returns the input read from terminal. | |||
// If prompt is not empty, it will be output as a prompt to the user | |||
// If masked is true, typing will be matched by asterisks on the screen. | |||
// Otherwise, typing will echo nothing. | |||
func getPasswd(prompt string, masked bool, r FdReader, w io.Writer) ([]byte, error) { | |||
var err error | |||
var pass, bs, mask []byte | |||
if masked { | |||
bs = []byte("\b \b") | |||
mask = []byte("*") | |||
} | |||
if isTerminal(r.Fd()) { | |||
if oldState, err := makeRaw(r.Fd()); err != nil { | |||
return pass, err | |||
} else { | |||
defer func() { | |||
restore(r.Fd(), oldState) | |||
fmt.Fprintln(w) | |||
}() | |||
} | |||
} | |||
if prompt != "" { | |||
fmt.Fprint(w, prompt) | |||
} | |||
// Track total bytes read, not just bytes in the password. This ensures any | |||
// errors that might flood the console with nil or -1 bytes infinitely are | |||
// capped. | |||
var counter int | |||
for counter = 0; counter <= maxLength; counter++ { | |||
if v, e := getch(r); e != nil { | |||
err = e | |||
break | |||
} else if v == 127 || v == 8 { | |||
if l := len(pass); l > 0 { | |||
pass = pass[:l-1] | |||
fmt.Fprint(w, string(bs)) | |||
} | |||
} else if v == 13 || v == 10 { | |||
break | |||
} else if v == 3 { | |||
err = ErrInterrupted | |||
break | |||
} else if v != 0 { | |||
pass = append(pass, v) | |||
fmt.Fprint(w, string(mask)) | |||
} | |||
} | |||
if counter > maxLength { | |||
err = ErrMaxLengthExceeded | |||
} | |||
return pass, err | |||
} | |||
// GetPasswd returns the password read from the terminal without echoing input. | |||
// The returned byte array does not include end-of-line characters. | |||
func GetPasswd() ([]byte, error) { | |||
return getPasswd("", false, os.Stdin, os.Stdout) | |||
} | |||
// GetPasswdMasked returns the password read from the terminal, echoing asterisks. | |||
// The returned byte array does not include end-of-line characters. | |||
func GetPasswdMasked() ([]byte, error) { | |||
return getPasswd("", true, os.Stdin, os.Stdout) | |||
} | |||
// GetPasswdPrompt prompts the user and returns the password read from the terminal. | |||
// If mask is true, then asterisks are echoed. | |||
// The returned byte array does not include end-of-line characters. | |||
func GetPasswdPrompt(prompt string, mask bool, r FdReader, w io.Writer) ([]byte, error) { | |||
return getPasswd(prompt, mask, r, w) | |||
} |
@@ -0,0 +1,25 @@ | |||
// +build !solaris | |||
package gopass | |||
import "golang.org/x/crypto/ssh/terminal" | |||
type terminalState struct { | |||
state *terminal.State | |||
} | |||
func isTerminal(fd uintptr) bool { | |||
return terminal.IsTerminal(int(fd)) | |||
} | |||
func makeRaw(fd uintptr) (*terminalState, error) { | |||
state, err := terminal.MakeRaw(int(fd)) | |||
return &terminalState{ | |||
state: state, | |||
}, err | |||
} | |||
func restore(fd uintptr, oldState *terminalState) error { | |||
return terminal.Restore(int(fd), oldState.state) | |||
} |
@@ -0,0 +1,69 @@ | |||
/* | |||
* CDDL HEADER START | |||
* | |||
* The contents of this file are subject to the terms of the | |||
* Common Development and Distribution License, Version 1.0 only | |||
* (the "License"). You may not use this file except in compliance | |||
* with the License. | |||
* | |||
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE | |||
* or http://www.opensolaris.org/os/licensing. | |||
* See the License for the specific language governing permissions | |||
* and limitations under the License. | |||
* | |||
* When distributing Covered Code, include this CDDL HEADER in each | |||
* file and include the License file at usr/src/OPENSOLARIS.LICENSE. | |||
* If applicable, add the following below this CDDL HEADER, with the | |||
* fields enclosed by brackets "[]" replaced with your own identifying | |||
* information: Portions Copyright [yyyy] [name of copyright owner] | |||
* | |||
* CDDL HEADER END | |||
*/ | |||
// Below is derived from Solaris source, so CDDL license is included. | |||
package gopass | |||
import ( | |||
"syscall" | |||
"golang.org/x/sys/unix" | |||
) | |||
type terminalState struct { | |||
state *unix.Termios | |||
} | |||
// isTerminal returns true if there is a terminal attached to the given | |||
// file descriptor. | |||
// Source: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c | |||
func isTerminal(fd uintptr) bool { | |||
var termio unix.Termio | |||
err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) | |||
return err == nil | |||
} | |||
// makeRaw puts the terminal connected to the given file descriptor into raw | |||
// mode and returns the previous state of the terminal so that it can be | |||
// restored. | |||
// Source: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c | |||
func makeRaw(fd uintptr) (*terminalState, error) { | |||
oldTermiosPtr, err := unix.IoctlGetTermios(int(fd), unix.TCGETS) | |||
if err != nil { | |||
return nil, err | |||
} | |||
oldTermios := *oldTermiosPtr | |||
newTermios := oldTermios | |||
newTermios.Lflag &^= syscall.ECHO | syscall.ECHOE | syscall.ECHOK | syscall.ECHONL | |||
if err := unix.IoctlSetTermios(int(fd), unix.TCSETS, &newTermios); err != nil { | |||
return nil, err | |||
} | |||
return &terminalState{ | |||
state: oldTermiosPtr, | |||
}, nil | |||
} | |||
func restore(fd uintptr, oldState *terminalState) error { | |||
return unix.IoctlSetTermios(int(fd), unix.TCSETS, oldState.state) | |||
} |
@@ -0,0 +1 @@ | |||
repo_token: x2wlA1x0X8CK45ybWpZRCVRB4g7vtkhaw |
@@ -0,0 +1,21 @@ | |||
language: go | |||
go: | |||
- 1.1 | |||
- 1.2 | |||
- 1.3 | |||
- 1.4 | |||
- 1.5 | |||
- 1.6 | |||
- 1.7 | |||
- 1.8 | |||
- 1.9 | |||
- 1.10 | |||
- tip | |||
matrix: | |||
allow_failures: | |||
- go: tip | |||
fast_finish: true | |||
install: | |||
- go get golang.org/x/net/html | |||
script: | |||
- go test -v ./... |
@@ -0,0 +1,51 @@ | |||
# Contributing to bluemonday | |||
Third-party patches are essential for keeping bluemonday secure and offering the features developers want. However there are a few guidelines that we need contributors to follow so that we can maintain the quality of work that developers who use bluemonday expect. | |||
## Getting Started | |||
* Make sure you have a [Github account](https://github.com/signup/free) | |||
## Guidelines | |||
1. Do not vendor dependencies. As a security package, were we to vendor dependencies the projects that then vendor bluemonday may not receive the latest security updates to the dependencies. By not vendoring dependencies the project that implements bluemonday will vendor the latest version of any dependent packages. Vendoring is a project problem, not a package problem. bluemonday will be tested against the latest version of dependencies periodically and during any PR/merge. | |||
## Submitting an Issue | |||
* Submit a ticket for your issue, assuming one does not already exist | |||
* Clearly describe the issue including the steps to reproduce (with sample input and output) if it is a bug | |||
If you are reporting a security flaw, you may expect that we will provide the code to fix it for you. Otherwise you may want to submit a pull request to ensure the resolution is applied sooner rather than later: | |||
* Fork the repository on Github | |||
* Issue a pull request containing code to resolve the issue | |||
## Submitting a Pull Request | |||
* Submit a ticket for your issue, assuming one does not already exist | |||
* Describe the reason for the pull request and if applicable show some example inputs and outputs to demonstrate what the patch does | |||
* Fork the repository on Github | |||
* Before submitting the pull request you should | |||
1. Include tests for your patch, 1 test should encapsulate the entire patch and should refer to the Github issue | |||
1. If you have added new exposed/public functionality, you should ensure it is documented appropriately | |||
1. If you have added new exposed/public functionality, you should consider demonstrating how to use it within one of the helpers or shipped policies if appropriate or within a test if modifying a helper or policy is not appropriate | |||
1. Run all of the tests `go test -v ./...` or `make test` and ensure all tests pass | |||
1. Run gofmt `gofmt -w ./$*` or `make fmt` | |||
1. Run vet `go tool vet *.go` or `make vet` and resolve any issues | |||
1. Install golint using `go get -u github.com/golang/lint/golint` and run vet `golint *.go` or `make lint` and resolve every warning | |||
* When submitting the pull request you should | |||
1. Note the issue(s) it resolves, i.e. `Closes #6` in the pull request comment to close issue #6 when the pull request is accepted | |||
Once you have submitted a pull request, we *may* merge it without changes. If we have any comments or feedback, or need you to make changes to your pull request we will update the Github pull request or the associated issue. We expect responses from you within two weeks, and we may close the pull request is there is no activity. | |||
### Contributor Licence Agreement | |||
We haven't gone for the formal "Sign a Contributor Licence Agreement" thing that projects like [puppet](https://cla.puppetlabs.com/), [Mojito](https://developer.yahoo.com/cocktails/mojito/cla/) and companies like [Google](http://code.google.com/legal/individual-cla-v1.0.html) are using. | |||
But we do need to know that we can accept and merge your contributions, so for now the act of contributing a pull request should be considered equivalent to agreeing to a contributor licence agreement, specifically: | |||
You accept that the act of submitting code to the bluemonday project is to grant a copyright licence to the project that is perpetual, worldwide, non-exclusive, no-charge, royalty free and irrevocable. | |||
You accept that all who comply with the licence of the project (BSD 3-clause) are permitted to use your contributions to the project. | |||
You accept, and by submitting code do declare, that you have the legal right to grant such a licence to the project and that each of the contributions is your own original creation. |
@@ -0,0 +1,6 @@ | |||
1. Andrew Krasichkov @buglloc https://github.com/buglloc | |||
1. John Graham-Cumming http://jgc.org/ | |||
1. Mike Samuel mikesamuel@gmail.com | |||
1. Dmitri Shuralyov shurcooL@gmail.com | |||
1. https://github.com/opennota | |||
1. https://github.com/Gufran |
@@ -0,0 +1,28 @@ | |||
Copyright (c) 2014, David Kitchen <david@buro9.com> | |||
All rights reserved. | |||
Redistribution and use in source and binary forms, with or without | |||
modification, are permitted provided that the following conditions are met: | |||
* Redistributions of source code must retain the above copyright notice, this | |||
list of conditions and the following disclaimer. | |||
* Redistributions in binary form must reproduce the above copyright notice, | |||
this list of conditions and the following disclaimer in the documentation | |||
and/or other materials provided with the distribution. | |||
* Neither the name of the organisation (Microcosm) nor the names of its | |||
contributors may be used to endorse or promote products derived from | |||
this software without specific prior written permission. | |||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@@ -0,0 +1,42 @@ | |||
# Targets: | |||
# | |||
# all: Builds the code locally after testing | |||
# | |||
# fmt: Formats the source files | |||
# build: Builds the code locally | |||
# vet: Vets the code | |||
# lint: Runs lint over the code (you do not need to fix everything) | |||
# test: Runs the tests | |||
# cover: Gives you the URL to a nice test coverage report | |||
# | |||
# install: Builds, tests and installs the code locally | |||
.PHONY: all fmt build vet lint test cover install | |||
# The first target is always the default action if `make` is called without | |||
# args we build and install into $GOPATH so that it can just be run | |||
all: fmt vet test install | |||
fmt: | |||
@gofmt -s -w ./$* | |||
build: | |||
@go build | |||
vet: | |||
@go vet *.go | |||
lint: | |||
@golint *.go | |||
test: | |||
@go test -v ./... | |||
cover: COVERAGE_FILE := coverage.out | |||
cover: | |||
@go test -coverprofile=$(COVERAGE_FILE) && \ | |||
cover -html=$(COVERAGE_FILE) && rm $(COVERAGE_FILE) | |||
install: | |||
@go install ./... |
@@ -0,0 +1,350 @@ | |||
# bluemonday [![Build Status](https://travis-ci.org/microcosm-cc/bluemonday.svg?branch=master)](https://travis-ci.org/microcosm-cc/bluemonday) [![GoDoc](https://godoc.org/github.com/microcosm-cc/bluemonday?status.png)](https://godoc.org/github.com/microcosm-cc/bluemonday) [![Sourcegraph](https://sourcegraph.com/github.com/microcosm-cc/bluemonday/-/badge.svg)](https://sourcegraph.com/github.com/microcosm-cc/bluemonday?badge) | |||
bluemonday is a HTML sanitizer implemented in Go. It is fast and highly configurable. | |||
bluemonday takes untrusted user generated content as an input, and will return HTML that has been sanitised against a whitelist of approved HTML elements and attributes so that you can safely include the content in your web page. | |||
If you accept user generated content, and your server uses Go, you **need** bluemonday. | |||
The default policy for user generated content (`bluemonday.UGCPolicy().Sanitize()`) turns this: | |||
```html | |||
Hello <STYLE>.XSS{background-image:url("javascript:alert('XSS')");}</STYLE><A CLASS=XSS></A>World | |||
``` | |||
Into a harmless: | |||
```html | |||
Hello World | |||
``` | |||
And it turns this: | |||
```html | |||
<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a> | |||
``` | |||
Into this: | |||
```html | |||
XSS | |||
``` | |||
Whilst still allowing this: | |||
```html | |||
<a href="http://www.google.com/"> | |||
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/> | |||
</a> | |||
``` | |||
To pass through mostly unaltered (it gained a rel="nofollow" which is a good thing for user generated content): | |||
```html | |||
<a href="http://www.google.com/" rel="nofollow"> | |||
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/> | |||
</a> | |||
``` | |||
It protects sites from [XSS](http://en.wikipedia.org/wiki/Cross-site_scripting) attacks. There are many [vectors for an XSS attack](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) and the best way to mitigate the risk is to sanitize user input against a known safe list of HTML elements and attributes. | |||
You should **always** run bluemonday **after** any other processing. | |||
If you use [blackfriday](https://github.com/russross/blackfriday) or [Pandoc](http://johnmacfarlane.net/pandoc/) then bluemonday should be run after these steps. This ensures that no insecure HTML is introduced later in your process. | |||
bluemonday is heavily inspired by both the [OWASP Java HTML Sanitizer](https://code.google.com/p/owasp-java-html-sanitizer/) and the [HTML Purifier](http://htmlpurifier.org/). | |||
## Technical Summary | |||
Whitelist based, you need to either build a policy describing the HTML elements and attributes to permit (and the `regexp` patterns of attributes), or use one of the supplied policies representing good defaults. | |||
The policy containing the whitelist is applied using a fast non-validating, forward only, token-based parser implemented in the [Go net/html library](https://godoc.org/golang.org/x/net/html) by the core Go team. | |||
We expect to be supplied with well-formatted HTML (closing elements for every applicable open element, nested correctly) and so we do not focus on repairing badly nested or incomplete HTML. We focus on simply ensuring that whatever elements do exist are described in the policy whitelist and that attributes and links are safe for use on your web page. [GIGO](http://en.wikipedia.org/wiki/Garbage_in,_garbage_out) does apply and if you feed it bad HTML bluemonday is not tasked with figuring out how to make it good again. | |||
### Supported Go Versions | |||
bluemonday is tested against Go 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, and tip. | |||
We do not support Go 1.0 as we depend on `golang.org/x/net/html` which includes a reference to `io.ErrNoProgress` which did not exist in Go 1.0. | |||
## Is it production ready? | |||
*Yes* | |||
We are using bluemonday in production having migrated from the widely used and heavily field tested OWASP Java HTML Sanitizer. | |||
We are passing our extensive test suite (including AntiSamy tests as well as tests for any issues raised). Check for any [unresolved issues](https://github.com/microcosm-cc/bluemonday/issues?page=1&state=open) to see whether anything may be a blocker for you. | |||
We invite pull requests and issues to help us ensure we are offering comprehensive protection against various attacks via user generated content. | |||
## Usage | |||
Install in your `${GOPATH}` using `go get -u github.com/microcosm-cc/bluemonday` | |||
Then call it: | |||
```go | |||
package main | |||
import ( | |||
"fmt" | |||
"github.com/microcosm-cc/bluemonday" | |||
) | |||
func main() { | |||
// Do this once for each unique policy, and use the policy for the life of the program | |||
// Policy creation/editing is not safe to use in multiple goroutines | |||
p := bluemonday.UGCPolicy() | |||
// The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines | |||
html := p.Sanitize( | |||
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, | |||
) | |||
// Output: | |||
// <a href="http://www.google.com" rel="nofollow">Google</a> | |||
fmt.Println(html) | |||
} | |||
``` | |||
We offer three ways to call Sanitize: | |||
```go | |||
p.Sanitize(string) string | |||
p.SanitizeBytes([]byte) []byte | |||
p.SanitizeReader(io.Reader) bytes.Buffer | |||
``` | |||
If you are obsessed about performance, `p.SanitizeReader(r).Bytes()` will return a `[]byte` without performing any unnecessary casting of the inputs or outputs. Though the difference is so negligible you should never need to care. | |||
You can build your own policies: | |||
```go | |||
package main | |||
import ( | |||
"fmt" | |||
"github.com/microcosm-cc/bluemonday" | |||
) | |||
func main() { | |||
p := bluemonday.NewPolicy() | |||
// Require URLs to be parseable by net/url.Parse and either: | |||
// mailto: http:// or https:// | |||
p.AllowStandardURLs() | |||
// We only allow <p> and <a href=""> | |||
p.AllowAttrs("href").OnElements("a") | |||
p.AllowElements("p") | |||
html := p.Sanitize( | |||
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, | |||
) | |||
// Output: | |||
// <a href="http://www.google.com">Google</a> | |||
fmt.Println(html) | |||
} | |||
``` | |||
We ship two default policies: | |||
1. `bluemonday.StrictPolicy()` which can be thought of as equivalent to stripping all HTML elements and their attributes as it has nothing on its whitelist. An example usage scenario would be blog post titles where HTML tags are not expected at all and if they are then the elements *and* the content of the elements should be stripped. This is a *very* strict policy. | |||
2. `bluemonday.UGCPolicy()` which allows a broad selection of HTML elements and attributes that are safe for user generated content. Note that this policy does *not* whitelist iframes, object, embed, styles, script, etc. An example usage scenario would be blog post bodies where a variety of formatting is expected along with the potential for TABLEs and IMGs. | |||
## Policy Building | |||
The essence of building a policy is to determine which HTML elements and attributes are considered safe for your scenario. OWASP provide an [XSS prevention cheat sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) to help explain the risks, but essentially: | |||
1. Avoid anything other than the standard HTML elements | |||
1. Avoid `script`, `style`, `iframe`, `object`, `embed`, `base` elements that allow code to be executed by the client or third party content to be included that can execute code | |||
1. Avoid anything other than plain HTML attributes with values matched to a regexp | |||
Basically, you should be able to describe what HTML is fine for your scenario. If you do not have confidence that you can describe your policy please consider using one of the shipped policies such as `bluemonday.UGCPolicy()`. | |||
To create a new policy: | |||
```go | |||
p := bluemonday.NewPolicy() | |||
``` | |||
To add elements to a policy either add just the elements: | |||
```go | |||
p.AllowElements("b", "strong") | |||
``` | |||
Or add elements as a virtue of adding an attribute: | |||
```go | |||
// Not the recommended pattern, see the recommendation on using .Matching() below | |||
p.AllowAttrs("nowrap").OnElements("td", "th") | |||
``` | |||
Attributes can either be added to all elements: | |||
```go | |||
p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally() | |||
``` | |||
Or attributes can be added to specific elements: | |||
```go | |||
// Not the recommended pattern, see the recommendation on using .Matching() below | |||
p.AllowAttrs("value").OnElements("li") | |||
``` | |||
It is **always** recommended that an attribute be made to match a pattern. XSS in HTML attributes is very easy otherwise: | |||
```go | |||
// \p{L} matches unicode letters, \p{N} matches unicode numbers | |||
p.AllowAttrs("title").Matching(regexp.MustCompile(`[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*`)).Globally() | |||
``` | |||
You can stop at any time and call .Sanitize(): | |||
```go | |||
// string htmlIn passed in from a HTTP POST | |||
htmlOut := p.Sanitize(htmlIn) | |||
``` | |||
And you can take any existing policy and extend it: | |||
```go | |||
p := bluemonday.UGCPolicy() | |||
p.AllowElements("fieldset", "select", "option") | |||
``` | |||
### Links | |||
Links are difficult beasts to sanitise safely and also one of the biggest attack vectors for malicious content. | |||
It is possible to do this: | |||
```go | |||
p.AllowAttrs("href").Matching(regexp.MustCompile(`(?i)mailto|https?`)).OnElements("a") | |||
``` | |||
But that will not protect you as the regular expression is insufficient in this case to have prevented a malformed value doing something unexpected. | |||
We provide some additional global options for safely working with links. | |||
`RequireParseableURLs` will ensure that URLs are parseable by Go's `net/url` package: | |||
```go | |||
p.RequireParseableURLs(true) | |||
``` | |||
If you have enabled parseable URLs then the following option will `AllowRelativeURLs`. By default this is disabled (bluemonday is a whitelist tool... you need to explicitly tell us to permit things) and when disabled it will prevent all local and scheme relative URLs (i.e. `href="localpage.html"`, `href="../home.html"` and even `href="//www.google.com"` are relative): | |||
```go | |||
p.AllowRelativeURLs(true) | |||
``` | |||
If you have enabled parseable URLs then you can whitelist the schemes (commonly called protocol when thinking of `http` and `https`) that are permitted. Bear in mind that allowing relative URLs in the above option will allow for a blank scheme: | |||
```go | |||
p.AllowURLSchemes("mailto", "http", "https") | |||
``` | |||
Regardless of whether you have enabled parseable URLs, you can force all URLs to have a rel="nofollow" attribute. This will be added if it does not exist, but only when the `href` is valid: | |||
```go | |||
// This applies to "a" "area" "link" elements that have a "href" attribute | |||
p.RequireNoFollowOnLinks(true) | |||
``` | |||
We provide a convenience method that applies all of the above, but you will still need to whitelist the linkable elements for the URL rules to be applied to: | |||
```go | |||
p.AllowStandardURLs() | |||
p.AllowAttrs("cite").OnElements("blockquote", "q") | |||
p.AllowAttrs("href").OnElements("a", "area") | |||
p.AllowAttrs("src").OnElements("img") | |||
``` | |||
An additional complexity regarding links is the data URI as defined in [RFC2397](http://tools.ietf.org/html/rfc2397). The data URI allows for images to be served inline using this format: | |||
```html | |||
<img src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="> | |||
``` | |||
We have provided a helper to verify the mimetype followed by base64 content of data URIs links: | |||
```go | |||
p.AllowDataURIImages() | |||
``` | |||
That helper will enable GIF, JPEG, PNG and WEBP images. | |||
It should be noted that there is a potential [security](http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/) [risk](https://capec.mitre.org/data/definitions/244.html) with the use of data URI links. You should only enable data URI links if you already trust the content. | |||
We also have some features to help deal with user generated content: | |||
```go | |||
p.AddTargetBlankToFullyQualifiedLinks(true) | |||
``` | |||
This will ensure that anchor `<a href="" />` links that are fully qualified (the href destination includes a host name) will get `target="_blank"` added to them. | |||
Additionally any link that has `target="_blank"` after the policy has been applied will also have the `rel` attribute adjusted to add `noopener`. This means a link may start like `<a href="//host/path"/>` and will end up as `<a href="//host/path" rel="noopener" target="_blank">`. It is important to note that the addition of `noopener` is a security feature and not an issue. There is an unfortunate feature to browsers that a browser window opened as a result of `target="_blank"` can still control the opener (your web page) and this protects against that. The background to this can be found here: [https://dev.to/ben/the-targetblank-vulnerability-by-example](https://dev.to/ben/the-targetblank-vulnerability-by-example) | |||
### Policy Building Helpers | |||
We also bundle some helpers to simplify policy building: | |||
```go | |||
// Permits the "dir", "id", "lang", "title" attributes globally | |||
p.AllowStandardAttributes() | |||
// Permits the "img" element and its standard attributes | |||
p.AllowImages() | |||
// Permits ordered and unordered lists, and also definition lists | |||
p.AllowLists() | |||
// Permits HTML tables and all applicable elements and non-styling attributes | |||
p.AllowTables() | |||
``` | |||
### Invalid Instructions | |||
The following are invalid: | |||
```go | |||
// This does not say where the attributes are allowed, you need to add | |||
// .Globally() or .OnElements(...) | |||
// This will be ignored without error. | |||
p.AllowAttrs("value") | |||
// This does not say where the attributes are allowed, you need to add | |||
// .Globally() or .OnElements(...) | |||
// This will be ignored without error. | |||
p.AllowAttrs( | |||
"type", | |||
).Matching( | |||
regexp.MustCompile("(?i)^(circle|disc|square|a|A|i|I|1)$"), | |||
) | |||
``` | |||
Both examples exhibit the same issue, they declare attributes but do not then specify whether they are whitelisted globally or only on specific elements (and which elements). Attributes belong to one or more elements, and the policy needs to declare this. | |||
## Limitations | |||
We are not yet including any tools to help whitelist and sanitize CSS. Which means that unless you wish to do the heavy lifting in a single regular expression (inadvisable), **you should not allow the "style" attribute anywhere**. | |||
It is not the job of bluemonday to fix your bad HTML, it is merely the job of bluemonday to prevent malicious HTML getting through. If you have mismatched HTML elements, or non-conforming nesting of elements, those will remain. But if you have well-structured HTML bluemonday will not break it. | |||
## TODO | |||
* Add support for CSS sanitisation to allow some CSS properties based on a whitelist, possibly using the [Gorilla CSS3 scanner](http://www.gorillatoolkit.org/pkg/css/scanner) - PRs welcome so long as testing covers XSS and demonstrates safety first | |||
* Investigate whether devs want to blacklist elements and attributes. This would allow devs to take an existing policy (such as the `bluemonday.UGCPolicy()` ) that encapsulates 90% of what they're looking for but does more than they need, and to remove the extra things they do not want to make it 100% what they want | |||
* Investigate whether devs want a validating HTML mode, in which the HTML elements are not just transformed into a balanced tree (every start tag has a closing tag at the correct depth) but also that elements and character data appear only in their allowed context (i.e. that a `table` element isn't a descendent of a `caption`, that `colgroup`, `thead`, `tbody`, `tfoot` and `tr` are permitted, and that character data is not permitted) | |||
## Development | |||
If you have cloned this repo you will probably need the dependency: | |||
`go get golang.org/x/net/html` | |||
Gophers can use their familiar tools: | |||
`go build` | |||
`go test` | |||
I personally use a Makefile as it spares typing the same args over and over whilst providing consistency for those of us who jump from language to language and enjoy just typing `make` in a project directory and watch magic happen. | |||
`make` will build, vet, test and install the library. | |||
`make clean` will remove the library from a *single* `${GOPATH}/pkg` directory tree | |||
`make test` will run the tests | |||
`make cover` will run the tests and *open a browser window* with the coverage report | |||
`make lint` will run golint (install via `go get github.com/golang/lint/golint`) | |||
## Long term goals | |||
1. Open the code to adversarial peer review similar to the [Attack Review Ground Rules](https://code.google.com/p/owasp-java-html-sanitizer/wiki/AttackReviewGroundRules) | |||
1. Raise funds and pay for an external security review |
@@ -0,0 +1,104 @@ | |||
// Copyright (c) 2014, David Kitchen <david@buro9.com> | |||
// | |||
// All rights reserved. | |||
// | |||
// Redistribution and use in source and binary forms, with or without | |||
// modification, are permitted provided that the following conditions are met: | |||
// | |||
// * Redistributions of source code must retain the above copyright notice, this | |||
// list of conditions and the following disclaimer. | |||
// | |||
// * Redistributions in binary form must reproduce the above copyright notice, | |||
// this list of conditions and the following disclaimer in the documentation | |||
// and/or other materials provided with the distribution. | |||
// | |||
// * Neither the name of the organisation (Microcosm) nor the names of its | |||
// contributors may be used to endorse or promote products derived from | |||
// this software without specific prior written permission. | |||
// | |||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
/* | |||
Package bluemonday provides a way of describing a whitelist of HTML elements | |||
and attributes as a policy, and for that policy to be applied to untrusted | |||
strings from users that may contain markup. All elements and attributes not on | |||
the whitelist will be stripped. | |||
The default bluemonday.UGCPolicy().Sanitize() turns this: | |||
Hello <STYLE>.XSS{background-image:url("javascript:alert('XSS')");}</STYLE><A CLASS=XSS></A>World | |||
Into the more harmless: | |||
Hello World | |||
And it turns this: | |||
<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a> | |||
Into this: | |||
XSS | |||
Whilst still allowing this: | |||
<a href="http://www.google.com/"> | |||
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/> | |||
</a> | |||
To pass through mostly unaltered (it gained a rel="nofollow"): | |||
<a href="http://www.google.com/" rel="nofollow"> | |||
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/> | |||
</a> | |||
The primary purpose of bluemonday is to take potentially unsafe user generated | |||
content (from things like Markdown, HTML WYSIWYG tools, etc) and make it safe | |||
for you to put on your website. | |||
It protects sites against XSS (http://en.wikipedia.org/wiki/Cross-site_scripting) | |||
and other malicious content that a user interface may deliver. There are many | |||
vectors for an XSS attack (https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) | |||
and the safest thing to do is to sanitize user input against a known safe list | |||
of HTML elements and attributes. | |||
Note: You should always run bluemonday after any other processing. | |||
If you use blackfriday (https://github.com/russross/blackfriday) or | |||
Pandoc (http://johnmacfarlane.net/pandoc/) then bluemonday should be run after | |||
these steps. This ensures that no insecure HTML is introduced later in your | |||
process. | |||
bluemonday is heavily inspired by both the OWASP Java HTML Sanitizer | |||
(https://code.google.com/p/owasp-java-html-sanitizer/) and the HTML Purifier | |||
(http://htmlpurifier.org/). | |||
We ship two default policies, one is bluemonday.StrictPolicy() and can be | |||
thought of as equivalent to stripping all HTML elements and their attributes as | |||
it has nothing on its whitelist. | |||
The other is bluemonday.UGCPolicy() and allows a broad selection of HTML | |||
elements and attributes that are safe for user generated content. Note that | |||
this policy does not whitelist iframes, object, embed, styles, script, etc. | |||
The essence of building a policy is to determine which HTML elements and | |||
attributes are considered safe for your scenario. OWASP provide an XSS | |||
prevention cheat sheet ( https://www.google.com/search?q=xss+prevention+cheat+sheet ) | |||
to help explain the risks, but essentially: | |||
1. Avoid whitelisting anything other than plain HTML elements | |||
2. Avoid whitelisting `script`, `style`, `iframe`, `object`, `embed`, `base` | |||
elements | |||
3. Avoid whitelisting anything other than plain HTML elements with simple | |||
values that you can match to a regexp | |||
*/ | |||
package bluemonday |
@@ -0,0 +1,297 @@ | |||
// Copyright (c) 2014, David Kitchen <david@buro9.com> | |||
// | |||
// All rights reserved. | |||
// | |||
// Redistribution and use in source and binary forms, with or without | |||
// modification, are permitted provided that the following conditions are met: | |||
// | |||
// * Redistributions of source code must retain the above copyright notice, this | |||
// list of conditions and the following disclaimer. | |||
// | |||
// * Redistributions in binary form must reproduce the above copyright notice, | |||
// this list of conditions and the following disclaimer in the documentation | |||
// and/or other materials provided with the distribution. | |||
// | |||
// * Neither the name of the organisation (Microcosm) nor the names of its | |||
// contributors may be used to endorse or promote products derived from | |||
// this software without specific prior written permission. | |||
// | |||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
package bluemonday | |||
import ( | |||
"encoding/base64" | |||
"net/url" | |||
"regexp" | |||
) | |||
// A selection of regular expressions that can be used as .Matching() rules on | |||
// HTML attributes. | |||
var ( | |||
// CellAlign handles the `align` attribute | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-align | |||
CellAlign = regexp.MustCompile(`(?i)^(center|justify|left|right|char)$`) | |||
// CellVerticalAlign handles the `valign` attribute | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-valign | |||
CellVerticalAlign = regexp.MustCompile(`(?i)^(baseline|bottom|middle|top)$`) | |||
// Direction handles the `dir` attribute | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdo#attr-dir | |||
Direction = regexp.MustCompile(`(?i)^(rtl|ltr)$`) | |||
// ImageAlign handles the `align` attribute on the `image` tag | |||
// http://www.w3.org/MarkUp/Test/Img/imgtest.html | |||
ImageAlign = regexp.MustCompile( | |||
`(?i)^(left|right|top|texttop|middle|absmiddle|baseline|bottom|absbottom)$`, | |||
) | |||
// Integer describes whole positive integers (including 0) used in places | |||
// like td.colspan | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-colspan | |||
Integer = regexp.MustCompile(`^[0-9]+$`) | |||
// ISO8601 according to the W3 group is only a subset of the ISO8601 | |||
// standard: http://www.w3.org/TR/NOTE-datetime | |||
// | |||
// Used in places like time.datetime | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time#attr-datetime | |||
// | |||
// Matches patterns: | |||
// Year: | |||
// YYYY (eg 1997) | |||
// Year and month: | |||
// YYYY-MM (eg 1997-07) | |||
// Complete date: | |||
// YYYY-MM-DD (eg 1997-07-16) | |||
// Complete date plus hours and minutes: | |||
// YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00) | |||
// Complete date plus hours, minutes and seconds: | |||
// YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) | |||
// Complete date plus hours, minutes, seconds and a decimal fraction of a | |||
// second | |||
// YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) | |||
ISO8601 = regexp.MustCompile( | |||
`^[0-9]{4}(-[0-9]{2}(-[0-9]{2}([ T][0-9]{2}(:[0-9]{2}){1,2}(.[0-9]{1,6})` + | |||
`?Z?([\+-][0-9]{2}:[0-9]{2})?)?)?)?$`, | |||
) | |||
// ListType encapsulates the common value as well as the latest spec | |||
// values for lists | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#attr-type | |||
ListType = regexp.MustCompile(`(?i)^(circle|disc|square|a|A|i|I|1)$`) | |||
// SpaceSeparatedTokens is used in places like `a.rel` and the common attribute | |||
// `class` which both contain space delimited lists of data tokens | |||
// http://www.w3.org/TR/html-markup/datatypes.html#common.data.tokens-def | |||
// Regexp: \p{L} matches unicode letters, \p{N} matches unicode numbers | |||
SpaceSeparatedTokens = regexp.MustCompile(`^([\s\p{L}\p{N}_-]+)$`) | |||
// Number is a double value used on HTML5 meter and progress elements | |||
// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-button-element.html#the-meter-element | |||
Number = regexp.MustCompile(`^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$`) | |||
// NumberOrPercent is used predominantly as units of measurement in width | |||
// and height attributes | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-height | |||
NumberOrPercent = regexp.MustCompile(`^[0-9]+[%]?$`) | |||
// Paragraph of text in an attribute such as *.'title', img.alt, etc | |||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-title | |||
// Note that we are not allowing chars that could close tags like '>' | |||
Paragraph = regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$`) | |||
// dataURIImagePrefix is used by AllowDataURIImages to define the acceptable | |||
// prefix of data URIs that contain common web image formats. | |||
// | |||
// This is not exported as it's not useful by itself, and only has value | |||
// within the AllowDataURIImages func | |||
dataURIImagePrefix = regexp.MustCompile( | |||
`^image/(gif|jpeg|png|webp);base64,`, | |||
) | |||
) | |||
// AllowStandardURLs is a convenience function that will enable rel="nofollow" | |||
// on "a", "area" and "link" (if you have allowed those elements) and will | |||
// ensure that the URL values are parseable and either relative or belong to the | |||
// "mailto", "http", or "https" schemes | |||
func (p *Policy) AllowStandardURLs() { | |||
// URLs must be parseable by net/url.Parse() | |||
p.RequireParseableURLs(true) | |||
// !url.IsAbs() is permitted | |||
p.AllowRelativeURLs(true) | |||
// Most common URL schemes only | |||
p.AllowURLSchemes("mailto", "http", "https") | |||
// For all anchors we will add rel="nofollow" if it does not already exist | |||
// This applies to "a" "area" "link" | |||
p.RequireNoFollowOnLinks(true) | |||
} | |||
// AllowStandardAttributes will enable "id", "title" and the language specific | |||
// attributes "dir" and "lang" on all elements that are whitelisted | |||
func (p *Policy) AllowStandardAttributes() { | |||
// "dir" "lang" are permitted as both language attributes affect charsets | |||
// and direction of text. | |||
p.AllowAttrs("dir").Matching(Direction).Globally() | |||
p.AllowAttrs( | |||
"lang", | |||
).Matching(regexp.MustCompile(`[a-zA-Z]{2,20}`)).Globally() | |||
// "id" is permitted. This is pretty much as some HTML elements require this | |||
// to work well ("dfn" is an example of a "id" being value) | |||
// This does create a risk that JavaScript and CSS within your web page | |||
// might identify the wrong elements. Ensure that you select things | |||
// accurately | |||
p.AllowAttrs("id").Matching( | |||
regexp.MustCompile(`[a-zA-Z0-9\:\-_\.]+`), | |||
).Globally() | |||
// "title" is permitted as it improves accessibility. | |||
p.AllowAttrs("title").Matching(Paragraph).Globally() | |||
} | |||
// AllowStyling presently enables the class attribute globally. | |||
// | |||
// Note: When bluemonday ships a CSS parser and we can safely sanitise that, | |||
// this will also allow sanitized styling of elements via the style attribute. | |||
func (p *Policy) AllowStyling() { | |||
// "class" is permitted globally | |||
p.AllowAttrs("class").Matching(SpaceSeparatedTokens).Globally() | |||
} | |||
// AllowImages enables the img element and some popular attributes. It will also | |||
// ensure that URL values are parseable. This helper does not enable data URI | |||
// images, for that you should also use the AllowDataURIImages() helper. | |||
func (p *Policy) AllowImages() { | |||
// "img" is permitted | |||
p.AllowAttrs("align").Matching(ImageAlign).OnElements("img") | |||
p.AllowAttrs("alt").Matching(Paragraph).OnElements("img") | |||
p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("img") | |||
// Standard URLs enabled | |||
p.AllowStandardURLs() | |||
p.AllowAttrs("src").OnElements("img") | |||
} | |||
// AllowDataURIImages permits the use of inline images defined in RFC2397 | |||
// http://tools.ietf.org/html/rfc2397 | |||
// http://en.wikipedia.org/wiki/Data_URI_scheme | |||
// | |||
// Images must have a mimetype matching: | |||
// image/gif | |||
// image/jpeg | |||
// image/png | |||
// image/webp | |||
// | |||
// NOTE: There is a potential security risk to allowing data URIs and you should | |||
// only permit them on content you already trust. | |||
// http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/ | |||
// https://capec.mitre.org/data/definitions/244.html | |||
func (p *Policy) AllowDataURIImages() { | |||
// URLs must be parseable by net/url.Parse() | |||
p.RequireParseableURLs(true) | |||
// Supply a function to validate images contained within data URI | |||
p.AllowURLSchemeWithCustomPolicy( | |||
"data", | |||
func(url *url.URL) (allowUrl bool) { | |||
if url.RawQuery != "" || url.Fragment != "" { | |||
return false | |||
} | |||
matched := dataURIImagePrefix.FindString(url.Opaque) | |||
if matched == "" { | |||
return false | |||
} | |||
_, err := base64.StdEncoding.DecodeString(url.Opaque[len(matched):]) | |||
if err != nil { | |||
return false | |||
} | |||
return true | |||
}, | |||
) | |||
} | |||
// AllowLists will enabled ordered and unordered lists, as well as definition | |||
// lists | |||
func (p *Policy) AllowLists() { | |||
// "ol" "ul" are permitted | |||
p.AllowAttrs("type").Matching(ListType).OnElements("ol", "ul") | |||
// "li" is permitted | |||
p.AllowAttrs("type").Matching(ListType).OnElements("li") | |||
p.AllowAttrs("value").Matching(Integer).OnElements("li") | |||
// "dl" "dt" "dd" are permitted | |||
p.AllowElements("dl", "dt", "dd") | |||
} | |||
// AllowTables will enable a rich set of elements and attributes to describe | |||
// HTML tables | |||
func (p *Policy) AllowTables() { | |||
// "table" is permitted | |||
p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("table") | |||
p.AllowAttrs("summary").Matching(Paragraph).OnElements("table") | |||
// "caption" is permitted | |||
p.AllowElements("caption") | |||
// "col" "colgroup" are permitted | |||
p.AllowAttrs("align").Matching(CellAlign).OnElements("col", "colgroup") | |||
p.AllowAttrs("height", "width").Matching( | |||
NumberOrPercent, | |||
).OnElements("col", "colgroup") | |||
p.AllowAttrs("span").Matching(Integer).OnElements("colgroup", "col") | |||
p.AllowAttrs("valign").Matching( | |||
CellVerticalAlign, | |||
).OnElements("col", "colgroup") | |||
// "thead" "tr" are permitted | |||
p.AllowAttrs("align").Matching(CellAlign).OnElements("thead", "tr") | |||
p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("thead", "tr") | |||
// "td" "th" are permitted | |||
p.AllowAttrs("abbr").Matching(Paragraph).OnElements("td", "th") | |||
p.AllowAttrs("align").Matching(CellAlign).OnElements("td", "th") | |||
p.AllowAttrs("colspan", "rowspan").Matching(Integer).OnElements("td", "th") | |||
p.AllowAttrs("headers").Matching( | |||
SpaceSeparatedTokens, | |||
).OnElements("td", "th") | |||
p.AllowAttrs("height", "width").Matching( | |||
NumberOrPercent, | |||
).OnElements("td", "th") | |||
p.AllowAttrs( | |||
"scope", | |||
).Matching( | |||
regexp.MustCompile(`(?i)(?:row|col)(?:group)?`), | |||
).OnElements("td", "th") | |||
p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("td", "th") | |||
p.AllowAttrs("nowrap").Matching( | |||
regexp.MustCompile(`(?i)|nowrap`), | |||
).OnElements("td", "th") | |||
// "tbody" "tfoot" | |||
p.AllowAttrs("align").Matching(CellAlign).OnElements("tbody", "tfoot") | |||
p.AllowAttrs("valign").Matching( | |||
CellVerticalAlign, | |||
).OnElements("tbody", "tfoot") | |||
} |
@@ -0,0 +1,253 @@ | |||
// Copyright (c) 2014, David Kitchen <david@buro9.com> | |||
// | |||
// All rights reserved. | |||
// | |||
// Redistribution and use in source and binary forms, with or without | |||
// modification, are permitted provided that the following conditions are met: | |||
// | |||
// * Redistributions of source code must retain the above copyright notice, this | |||
// list of conditions and the following disclaimer. | |||
// | |||
// * Redistributions in binary form must reproduce the above copyright notice, | |||
// this list of conditions and the following disclaimer in the documentation | |||
// and/or other materials provided with the distribution. | |||
// | |||
// * Neither the name of the organisation (Microcosm) nor the names of its | |||
// contributors may be used to endorse or promote products derived from | |||
// this software without specific prior written permission. | |||
// | |||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
package bluemonday | |||
import ( | |||
"regexp" | |||
) | |||
// StrictPolicy returns an empty policy, which will effectively strip all HTML | |||
// elements and their attributes from a document. | |||
func StrictPolicy() *Policy { | |||
return NewPolicy() | |||
} | |||
// StripTagsPolicy is DEPRECATED. Use StrictPolicy instead. | |||
func StripTagsPolicy() *Policy { | |||
return StrictPolicy() | |||
} | |||
// UGCPolicy returns a policy aimed at user generated content that is a result | |||
// of HTML WYSIWYG tools and Markdown conversions. | |||
// | |||
// This is expected to be a fairly rich document where as much markup as | |||
// possible should be retained. Markdown permits raw HTML so we are basically | |||
// providing a policy to sanitise HTML5 documents safely but with the | |||
// least intrusion on the formatting expectations of the user. | |||
func UGCPolicy() *Policy { | |||
p := NewPolicy() | |||
/////////////////////// | |||
// Global attributes // | |||
/////////////////////// | |||
// "class" is not permitted as we are not allowing users to style their own | |||
// content | |||
p.AllowStandardAttributes() | |||
////////////////////////////// | |||
// Global URL format policy // | |||
////////////////////////////// | |||
p.AllowStandardURLs() | |||
//////////////////////////////// | |||
// Declarations and structure // | |||
//////////////////////////////// | |||
// "xml" "xslt" "DOCTYPE" "html" "head" are not permitted as we are | |||
// expecting user generated content to be a fragment of HTML and not a full | |||
// document. | |||
////////////////////////// | |||
// Sectioning root tags // | |||
////////////////////////// | |||
// "article" and "aside" are permitted and takes no attributes | |||
p.AllowElements("article", "aside") | |||
// "body" is not permitted as we are expecting user generated content to be a fragment | |||
// of HTML and not a full document. | |||
// "details" is permitted, including the "open" attribute which can either | |||
// be blank or the value "open". | |||
p.AllowAttrs( | |||
"open", | |||
).Matching(regexp.MustCompile(`(?i)^(|open)$`)).OnElements("details") | |||
// "fieldset" is not permitted as we are not allowing forms to be created. | |||
// "figure" is permitted and takes no attributes | |||
p.AllowElements("figure") | |||
// "nav" is not permitted as it is assumed that the site (and not the user) | |||
// has defined navigation elements | |||
// "section" is permitted and takes no attributes | |||
p.AllowElements("section") | |||
// "summary" is permitted and takes no attributes | |||
p.AllowElements("summary") | |||
////////////////////////// | |||
// Headings and footers // | |||
////////////////////////// | |||
// "footer" is not permitted as we expect user content to be a fragment and | |||
// not structural to this extent | |||
// "h1" through "h6" are permitted and take no attributes | |||
p.AllowElements("h1", "h2", "h3", "h4", "h5", "h6") | |||
// "header" is not permitted as we expect user content to be a fragment and | |||
// not structural to this extent | |||
// "hgroup" is permitted and takes no attributes | |||
p.AllowElements("hgroup") | |||
///////////////////////////////////// | |||
// Content grouping and separating // | |||
///////////////////////////////////// | |||
// "blockquote" is permitted, including the "cite" attribute which must be | |||
// a standard URL. | |||
p.AllowAttrs("cite").OnElements("blockquote") | |||
// "br" "div" "hr" "p" "span" "wbr" are permitted and take no attributes | |||
p.AllowElements("br", "div", "hr", "p", "span", "wbr") | |||
/////////// | |||
// Links // | |||
/////////// | |||
// "a" is permitted | |||
p.AllowAttrs("href").OnElements("a") | |||
// "area" is permitted along with the attributes that map image maps work | |||
p.AllowAttrs("name").Matching( | |||
regexp.MustCompile(`^([\p{L}\p{N}_-]+)$`), | |||
).OnElements("map") | |||
p.AllowAttrs("alt").Matching(Paragraph).OnElements("area") | |||
p.AllowAttrs("coords").Matching( | |||
regexp.MustCompile(`^([0-9]+,)+[0-9]+$`), | |||
).OnElements("area") | |||
p.AllowAttrs("href").OnElements("area") | |||
p.AllowAttrs("rel").Matching(SpaceSeparatedTokens).OnElements("area") | |||
p.AllowAttrs("shape").Matching( | |||
regexp.MustCompile(`(?i)^(default|circle|rect|poly)$`), | |||
).OnElements("area") | |||
p.AllowAttrs("usemap").Matching( | |||
regexp.MustCompile(`(?i)^#[\p{L}\p{N}_-]+$`), | |||
).OnElements("img") | |||
// "link" is not permitted | |||
///////////////////// | |||
// Phrase elements // | |||
///////////////////// | |||
// The following are all inline phrasing elements | |||
p.AllowElements("abbr", "acronym", "cite", "code", "dfn", "em", | |||
"figcaption", "mark", "s", "samp", "strong", "sub", "sup", "var") | |||
// "q" is permitted and "cite" is a URL and handled by URL policies | |||
p.AllowAttrs("cite").OnElements("q") | |||
// "time" is permitted | |||
p.AllowAttrs("datetime").Matching(ISO8601).OnElements("time") | |||
//////////////////// | |||
// Style elements // | |||
//////////////////// | |||
// block and inline elements that impart no semantic meaning but style the | |||
// document | |||
p.AllowElements("b", "i", "pre", "small", "strike", "tt", "u") | |||
// "style" is not permitted as we are not yet sanitising CSS and it is an | |||
// XSS attack vector | |||
////////////////////// | |||
// HTML5 Formatting // | |||
////////////////////// | |||
// "bdi" "bdo" are permitted | |||
p.AllowAttrs("dir").Matching(Direction).OnElements("bdi", "bdo") | |||
// "rp" "rt" "ruby" are permitted | |||
p.AllowElements("rp", "rt", "ruby") | |||
/////////////////////////// | |||
// HTML5 Change tracking // | |||
/////////////////////////// | |||
// "del" "ins" are permitted | |||
p.AllowAttrs("cite").Matching(Paragraph).OnElements("del", "ins") | |||
p.AllowAttrs("datetime").Matching(ISO8601).OnElements("del", "ins") | |||
/////////// | |||
// Lists // | |||
/////////// | |||
p.AllowLists() | |||
//////////// | |||
// Tables // | |||
//////////// | |||
p.AllowTables() | |||
/////////// | |||
// Forms // | |||
/////////// | |||
// By and large, forms are not permitted. However there are some form | |||
// elements that can be used to present data, and we do permit those | |||
// | |||
// "button" "fieldset" "input" "keygen" "label" "output" "select" "datalist" | |||
// "textarea" "optgroup" "option" are all not permitted | |||
// "meter" is permitted | |||
p.AllowAttrs( | |||
"value", | |||
"min", | |||
"max", | |||
"low", | |||
"high", | |||
"optimum", | |||
).Matching(Number).OnElements("meter") | |||
// "progress" is permitted | |||
p.AllowAttrs("value", "max").Matching(Number).OnElements("progress") | |||
////////////////////// | |||
// Embedded content // | |||
////////////////////// | |||
// Vast majority not permitted | |||
// "audio" "canvas" "embed" "iframe" "object" "param" "source" "svg" "track" | |||
// "video" are all not permitted | |||
p.AllowImages() | |||
return p | |||
} |
@@ -0,0 +1,552 @@ | |||
// Copyright (c) 2014, David Kitchen <david@buro9.com> | |||
// | |||
// All rights reserved. | |||
// | |||
// Redistribution and use in source and binary forms, with or without | |||
// modification, are permitted provided that the following conditions are met: | |||
// | |||
// * Redistributions of source code must retain the above copyright notice, this | |||
// list of conditions and the following disclaimer. | |||
// | |||
// * Redistributions in binary form must reproduce the above copyright notice, | |||
// this list of conditions and the following disclaimer in the documentation | |||
// and/or other materials provided with the distribution. | |||
// | |||
// * Neither the name of the organisation (Microcosm) nor the names of its | |||
// contributors may be used to endorse or promote products derived from | |||
// this software without specific prior written permission. | |||
// | |||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
package bluemonday | |||
import ( | |||
"net/url" | |||
"regexp" | |||
"strings" | |||
) | |||
// Policy encapsulates the whitelist of HTML elements and attributes that will | |||
// be applied to the sanitised HTML. | |||
// | |||
// You should use bluemonday.NewPolicy() to create a blank policy as the | |||
// unexported fields contain maps that need to be initialized. | |||
type Policy struct { | |||
// Declares whether the maps have been initialized, used as a cheap check to | |||
// ensure that those using Policy{} directly won't cause nil pointer | |||
// exceptions | |||
initialized bool | |||
// If true then we add spaces when stripping tags, specifically the closing | |||
// tag is replaced by a space character. | |||
addSpaces bool | |||
// When true, add rel="nofollow" to HTML anchors | |||
requireNoFollow bool | |||
// When true, add rel="nofollow" to HTML anchors | |||
// Will add for href="http://foo" | |||
// Will skip for href="/foo" or href="foo" | |||
requireNoFollowFullyQualifiedLinks bool | |||
// When true add target="_blank" to fully qualified links | |||
// Will add for href="http://foo" | |||
// Will skip for href="/foo" or href="foo" | |||
addTargetBlankToFullyQualifiedLinks bool | |||
// When true, URLs must be parseable by "net/url" url.Parse() | |||
requireParseableURLs bool | |||
// When true, u, _ := url.Parse("url"); !u.IsAbs() is permitted | |||
allowRelativeURLs bool | |||
// When true, allow data attributes. | |||
allowDataAttributes bool | |||
// map[htmlElementName]map[htmlAttributeName]attrPolicy | |||
elsAndAttrs map[string]map[string]attrPolicy | |||
// map[htmlAttributeName]attrPolicy | |||
globalAttrs map[string]attrPolicy | |||
// If urlPolicy is nil, all URLs with matching schema are allowed. | |||
// Otherwise, only the URLs with matching schema and urlPolicy(url) | |||
// returning true are allowed. | |||
allowURLSchemes map[string]urlPolicy | |||
// If an element has had all attributes removed as a result of a policy | |||
// being applied, then the element would be removed from the output. | |||
// | |||
// However some elements are valid and have strong layout meaning without | |||
// any attributes, i.e. <table>. To prevent those being removed we maintain | |||
// a list of elements that are allowed to have no attributes and that will | |||
// be maintained in the output HTML. | |||
setOfElementsAllowedWithoutAttrs map[string]struct{} | |||
setOfElementsToSkipContent map[string]struct{} | |||
} | |||
type attrPolicy struct { | |||
// optional pattern to match, when not nil the regexp needs to match | |||
// otherwise the attribute is removed | |||
regexp *regexp.Regexp | |||
} | |||
type attrPolicyBuilder struct { | |||
p *Policy | |||
attrNames []string | |||
regexp *regexp.Regexp | |||
allowEmpty bool | |||
} | |||
type urlPolicy func(url *url.URL) (allowUrl bool) | |||
// init initializes the maps if this has not been done already | |||
func (p *Policy) init() { | |||
if !p.initialized { | |||
p.elsAndAttrs = make(map[string]map[string]attrPolicy) | |||
p.globalAttrs = make(map[string]attrPolicy) | |||
p.allowURLSchemes = make(map[string]urlPolicy) | |||
p.setOfElementsAllowedWithoutAttrs = make(map[string]struct{}) | |||
p.setOfElementsToSkipContent = make(map[string]struct{}) | |||
p.initialized = true | |||
} | |||
} | |||
// NewPolicy returns a blank policy with nothing whitelisted or permitted. This | |||
// is the recommended way to start building a policy and you should now use | |||
// AllowAttrs() and/or AllowElements() to construct the whitelist of HTML | |||
// elements and attributes. | |||
func NewPolicy() *Policy { | |||
p := Policy{} | |||
p.addDefaultElementsWithoutAttrs() | |||
p.addDefaultSkipElementContent() | |||
return &p | |||
} | |||
// AllowAttrs takes a range of HTML attribute names and returns an | |||
// attribute policy builder that allows you to specify the pattern and scope of | |||
// the whitelisted attribute. | |||
// | |||
// The attribute policy is only added to the core policy when either Globally() | |||
// or OnElements(...) are called. | |||
func (p *Policy) AllowAttrs(attrNames ...string) *attrPolicyBuilder { | |||
p.init() | |||
abp := attrPolicyBuilder{ | |||
p: p, | |||
allowEmpty: false, | |||
} | |||
for _, attrName := range attrNames { | |||
abp.attrNames = append(abp.attrNames, strings.ToLower(attrName)) | |||
} | |||
return &abp | |||
} | |||
// AllowDataAttributes whitelists all data attributes. We can't specify the name | |||
// of each attribute exactly as they are customized. | |||
// | |||
// NOTE: These values are not sanitized and applications that evaluate or process | |||
// them without checking and verification of the input may be at risk if this option | |||
// is enabled. This is a 'caveat emptor' option and the person enabling this option | |||
// needs to fully understand the potential impact with regards to whatever application | |||
// will be consuming the sanitized HTML afterwards, i.e. if you know you put a link in a | |||
// data attribute and use that to automatically load some new window then you're giving | |||
// the author of a HTML fragment the means to open a malicious destination automatically. | |||
// Use with care! | |||
func (p *Policy) AllowDataAttributes() { | |||
p.allowDataAttributes = true | |||
} | |||
// AllowNoAttrs says that attributes on element are optional. | |||
// | |||
// The attribute policy is only added to the core policy when OnElements(...) | |||
// are called. | |||
func (p *Policy) AllowNoAttrs() *attrPolicyBuilder { | |||
p.init() | |||
abp := attrPolicyBuilder{ | |||
p: p, | |||
allowEmpty: true, | |||
} | |||
return &abp | |||
} | |||
// AllowNoAttrs says that attributes on element are optional. | |||
// | |||
// The attribute policy is only added to the core policy when OnElements(...) | |||
// are called. | |||
func (abp *attrPolicyBuilder) AllowNoAttrs() *attrPolicyBuilder { | |||
abp.allowEmpty = true | |||
return abp | |||
} | |||
// Matching allows a regular expression to be applied to a nascent attribute | |||
// policy, and returns the attribute policy. Calling this more than once will | |||
// replace the existing regexp. | |||
func (abp *attrPolicyBuilder) Matching(regex *regexp.Regexp) *attrPolicyBuilder { | |||
abp.regexp = regex | |||
return abp | |||
} | |||
// OnElements will bind an attribute policy to a given range of HTML elements | |||
// and return the updated policy | |||
func (abp *attrPolicyBuilder) OnElements(elements ...string) *Policy { | |||
for _, element := range elements { | |||
element = strings.ToLower(element) | |||
for _, attr := range abp.attrNames { | |||
if _, ok := abp.p.elsAndAttrs[element]; !ok { | |||
abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) | |||
} | |||
ap := attrPolicy{} | |||
if abp.regexp != nil { | |||
ap.regexp = abp.regexp | |||
} | |||
abp.p.elsAndAttrs[element][attr] = ap | |||
} | |||
if abp.allowEmpty { | |||
abp.p.setOfElementsAllowedWithoutAttrs[element] = struct{}{} | |||
if _, ok := abp.p.elsAndAttrs[element]; !ok { | |||
abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) | |||
} | |||
} | |||
} | |||
return abp.p | |||
} | |||
// Globally will bind an attribute policy to all HTML elements and return the | |||
// updated policy | |||
func (abp *attrPolicyBuilder) Globally() *Policy { | |||
for _, attr := range abp.attrNames { | |||
if _, ok := abp.p.globalAttrs[attr]; !ok { | |||
abp.p.globalAttrs[attr] = attrPolicy{} | |||
} | |||
ap := attrPolicy{} | |||
if abp.regexp != nil { | |||
ap.regexp = abp.regexp | |||
} | |||
abp.p.globalAttrs[attr] = ap | |||
} | |||
return abp.p | |||
} | |||
// AllowElements will append HTML elements to the whitelist without applying an | |||
// attribute policy to those elements (the elements are permitted | |||
// sans-attributes) | |||
func (p *Policy) AllowElements(names ...string) *Policy { | |||
p.init() | |||
for _, element := range names { | |||
element = strings.ToLower(element) | |||
if _, ok := p.elsAndAttrs[element]; !ok { | |||
p.elsAndAttrs[element] = make(map[string]attrPolicy) | |||
} | |||
} | |||
return p | |||
} | |||
// RequireNoFollowOnLinks will result in all <a> tags having a rel="nofollow" | |||
// added to them if one does not already exist | |||
// | |||
// Note: This requires p.RequireParseableURLs(true) and will enable it. | |||
func (p *Policy) RequireNoFollowOnLinks(require bool) *Policy { | |||
p.requireNoFollow = require | |||
p.requireParseableURLs = true | |||
return p | |||
} | |||
// RequireNoFollowOnFullyQualifiedLinks will result in all <a> tags that point | |||
// to a non-local destination (i.e. starts with a protocol and has a host) | |||
// having a rel="nofollow" added to them if one does not already exist | |||
// | |||
// Note: This requires p.RequireParseableURLs(true) and will enable it. | |||
func (p *Policy) RequireNoFollowOnFullyQualifiedLinks(require bool) *Policy { | |||
p.requireNoFollowFullyQualifiedLinks = require | |||
p.requireParseableURLs = true | |||
return p | |||
} | |||
// AddTargetBlankToFullyQualifiedLinks will result in all <a> tags that point | |||
// to a non-local destination (i.e. starts with a protocol and has a host) | |||
// having a target="_blank" added to them if one does not already exist | |||
// | |||
// Note: This requires p.RequireParseableURLs(true) and will enable it. | |||
func (p *Policy) AddTargetBlankToFullyQualifiedLinks(require bool) *Policy { | |||
p.addTargetBlankToFullyQualifiedLinks = require | |||
p.requireParseableURLs = true | |||
return p | |||
} | |||
// RequireParseableURLs will result in all URLs requiring that they be parseable | |||
// by "net/url" url.Parse() | |||
// This applies to: | |||
// - a.href | |||
// - area.href | |||
// - blockquote.cite | |||
// - img.src | |||
// - link.href | |||
// - script.src | |||
func (p *Policy) RequireParseableURLs(require bool) *Policy { | |||
p.requireParseableURLs = require | |||
return p | |||
} | |||
// AllowRelativeURLs enables RequireParseableURLs and then permits URLs that | |||
// are parseable, have no schema information and url.IsAbs() returns false | |||
// This permits local URLs | |||
func (p *Policy) AllowRelativeURLs(require bool) *Policy { | |||
p.RequireParseableURLs(true) | |||
p.allowRelativeURLs = require | |||
return p | |||
} | |||
// AllowURLSchemes will append URL schemes to the whitelist | |||
// Example: p.AllowURLSchemes("mailto", "http", "https") | |||
func (p *Policy) AllowURLSchemes(schemes ...string) *Policy { | |||
p.init() | |||
p.RequireParseableURLs(true) | |||
for _, scheme := range schemes { | |||
scheme = strings.ToLower(scheme) | |||
// Allow all URLs with matching scheme. | |||
p.allowURLSchemes[scheme] = nil | |||
} | |||
return p | |||
} | |||
// AllowURLSchemeWithCustomPolicy will append URL schemes with | |||
// a custom URL policy to the whitelist. | |||
// Only the URLs with matching schema and urlPolicy(url) | |||
// returning true will be allowed. | |||
func (p *Policy) AllowURLSchemeWithCustomPolicy( | |||
scheme string, | |||
urlPolicy func(url *url.URL) (allowUrl bool), | |||
) *Policy { | |||
p.init() | |||
p.RequireParseableURLs(true) | |||
scheme = strings.ToLower(scheme) | |||
p.allowURLSchemes[scheme] = urlPolicy | |||
return p | |||
} | |||
// AddSpaceWhenStrippingTag states whether to add a single space " " when | |||
// removing tags that are not whitelisted by the policy. | |||
// | |||
// This is useful if you expect to strip tags in dense markup and may lose the | |||
// value of whitespace. | |||
// | |||
// For example: "<p>Hello</p><p>World</p>"" would be sanitized to "HelloWorld" | |||
// with the default value of false, but you may wish to sanitize this to | |||
// " Hello World " by setting AddSpaceWhenStrippingTag to true as this would | |||
// retain the intent of the text. | |||
func (p *Policy) AddSpaceWhenStrippingTag(allow bool) *Policy { | |||
p.addSpaces = allow | |||
return p | |||
} | |||
// SkipElementsContent adds the HTML elements whose tags is needed to be removed | |||
// with its content. | |||
func (p *Policy) SkipElementsContent(names ...string) *Policy { | |||
p.init() | |||
for _, element := range names { | |||
element = strings.ToLower(element) | |||
if _, ok := p.setOfElementsToSkipContent[element]; !ok { | |||
p.setOfElementsToSkipContent[element] = struct{}{} | |||
} | |||
} | |||
return p | |||
} | |||
// AllowElementsContent marks the HTML elements whose content should be | |||
// retained after removing the tag. | |||
func (p *Policy) AllowElementsContent(names ...string) *Policy { | |||
p.init() | |||
for _, element := range names { | |||
delete(p.setOfElementsToSkipContent, strings.ToLower(element)) | |||
} | |||
return p | |||
} | |||
// addDefaultElementsWithoutAttrs adds the HTML elements that we know are valid | |||
// without any attributes to an internal map. | |||
// i.e. we know that <table> is valid, but <bdo> isn't valid as the "dir" attr | |||
// is mandatory | |||
func (p *Policy) addDefaultElementsWithoutAttrs() { | |||
p.init() | |||
p.setOfElementsAllowedWithoutAttrs["abbr"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["acronym"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["address"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["article"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["aside"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["audio"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["b"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["bdi"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["blockquote"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["body"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["br"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["button"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["canvas"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["caption"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["center"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["cite"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["code"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["col"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["colgroup"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["datalist"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["dd"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["del"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["details"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["dfn"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["div"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["dl"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["dt"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["em"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["fieldset"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["figcaption"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["figure"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["footer"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["h1"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["h2"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["h3"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["h4"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["h5"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["h6"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["head"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["header"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["hgroup"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["hr"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["html"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["i"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["ins"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["kbd"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["li"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["mark"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["marquee"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["nav"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["ol"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["optgroup"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["option"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["p"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["pre"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["q"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["rp"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["rt"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["ruby"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["s"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["samp"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["script"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["section"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["select"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["small"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["span"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["strike"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["strong"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["style"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["sub"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["summary"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["sup"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["svg"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["table"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["tbody"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["td"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["textarea"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["tfoot"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["th"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["thead"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["title"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["time"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["tr"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["tt"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["u"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["ul"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["var"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["video"] = struct{}{} | |||
p.setOfElementsAllowedWithoutAttrs["wbr"] = struct{}{} | |||
} | |||
// addDefaultSkipElementContent adds the HTML elements that we should skip | |||
// rendering the character content of, if the element itself is not allowed. | |||
// This is all character data that the end user would not normally see. | |||
// i.e. if we exclude a <script> tag then we shouldn't render the JavaScript or | |||
// anything else until we encounter the closing </script> tag. | |||
func (p *Policy) addDefaultSkipElementContent() { | |||
p.init() | |||
p.setOfElementsToSkipContent["frame"] = struct{}{} | |||
p.setOfElementsToSkipContent["frameset"] = struct{}{} | |||
p.setOfElementsToSkipContent["iframe"] = struct{}{} | |||
p.setOfElementsToSkipContent["noembed"] = struct{}{} | |||
p.setOfElementsToSkipContent["noframes"] = struct{}{} | |||
p.setOfElementsToSkipContent["noscript"] = struct{}{} | |||
p.setOfElementsToSkipContent["nostyle"] = struct{}{} | |||
p.setOfElementsToSkipContent["object"] = struct{}{} | |||
p.setOfElementsToSkipContent["script"] = struct{}{} | |||
p.setOfElementsToSkipContent["style"] = struct{}{} | |||
p.setOfElementsToSkipContent["title"] = struct{}{} | |||
} |
@@ -0,0 +1,581 @@ | |||
// Copyright (c) 2014, David Kitchen <david@buro9.com> | |||
// | |||
// All rights reserved. | |||
// | |||
// Redistribution and use in source and binary forms, with or without | |||
// modification, are permitted provided that the following conditions are met: | |||
// | |||
// * Redistributions of source code must retain the above copyright notice, this | |||
// list of conditions and the following disclaimer. | |||
// | |||
// * Redistributions in binary form must reproduce the above copyright notice, | |||
// this list of conditions and the following disclaimer in the documentation | |||
// and/or other materials provided with the distribution. | |||
// | |||
// * Neither the name of the organisation (Microcosm) nor the names of its | |||
// contributors may be used to endorse or promote products derived from | |||
// this software without specific prior written permission. | |||
// | |||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
package bluemonday | |||
import ( | |||
"bytes" | |||
"io" | |||
"net/url" | |||
"regexp" | |||
"strings" | |||
"golang.org/x/net/html" | |||
) | |||
var ( | |||
dataAttribute = regexp.MustCompile("^data-.+") | |||
dataAttributeXMLPrefix = regexp.MustCompile("^xml.+") | |||
dataAttributeInvalidChars = regexp.MustCompile("[A-Z;]+") | |||
) | |||
// Sanitize takes a string that contains a HTML fragment or document and applies | |||
// the given policy whitelist. | |||
// | |||
// It returns a HTML string that has been sanitized by the policy or an empty | |||
// string if an error has occurred (most likely as a consequence of extremely | |||
// malformed input) | |||
func (p *Policy) Sanitize(s string) string { | |||
if strings.TrimSpace(s) == "" { | |||
return s | |||
} | |||
return p.sanitize(strings.NewReader(s)).String() | |||
} | |||
// SanitizeBytes takes a []byte that contains a HTML fragment or document and applies | |||
// the given policy whitelist. | |||
// | |||
// It returns a []byte containing the HTML that has been sanitized by the policy | |||
// or an empty []byte if an error has occurred (most likely as a consequence of | |||
// extremely malformed input) | |||
func (p *Policy) SanitizeBytes(b []byte) []byte { | |||
if len(bytes.TrimSpace(b)) == 0 { | |||
return b | |||
} | |||
return p.sanitize(bytes.NewReader(b)).Bytes() | |||
} | |||
// SanitizeReader takes an io.Reader that contains a HTML fragment or document | |||
// and applies the given policy whitelist. | |||
// | |||
// It returns a bytes.Buffer containing the HTML that has been sanitized by the | |||
// policy. Errors during sanitization will merely return an empty result. | |||
func (p *Policy) SanitizeReader(r io.Reader) *bytes.Buffer { | |||
return p.sanitize(r) | |||
} | |||
// Performs the actual sanitization process. | |||
func (p *Policy) sanitize(r io.Reader) *bytes.Buffer { | |||
// It is possible that the developer has created the policy via: | |||
// p := bluemonday.Policy{} | |||
// rather than: | |||
// p := bluemonday.NewPolicy() | |||
// If this is the case, and if they haven't yet triggered an action that | |||
// would initiliaze the maps, then we need to do that. | |||
p.init() | |||
var ( | |||
buff bytes.Buffer | |||
skipElementContent bool | |||
skippingElementsCount int64 | |||
skipClosingTag bool | |||
closingTagToSkipStack []string | |||
mostRecentlyStartedToken string | |||
) | |||
tokenizer := html.NewTokenizer(r) | |||
for { | |||
if tokenizer.Next() == html.ErrorToken { | |||
err := tokenizer.Err() | |||
if err == io.EOF { | |||
// End of input means end of processing | |||
return &buff | |||
} | |||
// Raw tokenizer error | |||
return &bytes.Buffer{} | |||
} | |||
token := tokenizer.Token() | |||
switch token.Type { | |||
case html.DoctypeToken: | |||
// DocType is not handled as there is no safe parsing mechanism | |||
// provided by golang.org/x/net/html for the content, and this can | |||
// be misused to insert HTML tags that are not then sanitized | |||
// | |||
// One might wish to recursively sanitize here using the same policy | |||
// but I will need to do some further testing before considering | |||
// this. | |||
case html.CommentToken: | |||
// Comments are ignored by default | |||
case html.StartTagToken: | |||
mostRecentlyStartedToken = token.Data | |||
aps, ok := p.elsAndAttrs[token.Data] | |||
if !ok { | |||
if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { | |||
skipElementContent = true | |||
skippingElementsCount++ | |||
} | |||
if p.addSpaces { | |||
buff.WriteString(" ") | |||
} | |||
break | |||
} | |||
if len(token.Attr) != 0 { | |||
token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) | |||
} | |||
if len(token.Attr) == 0 { | |||
if !p.allowNoAttrs(token.Data) { | |||
skipClosingTag = true | |||
closingTagToSkipStack = append(closingTagToSkipStack, token.Data) | |||
if p.addSpaces { | |||
buff.WriteString(" ") | |||
} | |||
break | |||
} | |||
} | |||
if !skipElementContent { | |||
buff.WriteString(token.String()) | |||
} | |||
case html.EndTagToken: | |||
if mostRecentlyStartedToken == token.Data { | |||
mostRecentlyStartedToken = "" | |||
} | |||
if skipClosingTag && closingTagToSkipStack[len(closingTagToSkipStack)-1] == token.Data { | |||
closingTagToSkipStack = closingTagToSkipStack[:len(closingTagToSkipStack)-1] | |||
if len(closingTagToSkipStack) == 0 { | |||
skipClosingTag = false | |||
} | |||
if p.addSpaces { | |||
buff.WriteString(" ") | |||
} | |||
break | |||
} | |||
if _, ok := p.elsAndAttrs[token.Data]; !ok { | |||
if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { | |||
skippingElementsCount-- | |||
if skippingElementsCount == 0 { | |||
skipElementContent = false | |||
} | |||
} | |||
if p.addSpaces { | |||
buff.WriteString(" ") | |||
} | |||
break | |||
} | |||
if !skipElementContent { | |||
buff.WriteString(token.String()) | |||
} | |||
case html.SelfClosingTagToken: | |||
aps, ok := p.elsAndAttrs[token.Data] | |||
if !ok { | |||
if p.addSpaces { | |||
buff.WriteString(" ") | |||
} | |||
break | |||
} | |||
if len(token.Attr) != 0 { | |||
token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) | |||
} | |||
if len(token.Attr) == 0 && !p.allowNoAttrs(token.Data) { | |||
if p.addSpaces { | |||
buff.WriteString(" ") | |||
} | |||
break | |||
} | |||
if !skipElementContent { | |||
buff.WriteString(token.String()) | |||
} | |||
case html.TextToken: | |||
if !skipElementContent { | |||
switch mostRecentlyStartedToken { | |||
case "script": | |||
// not encouraged, but if a policy allows JavaScript we | |||
// should not HTML escape it as that would break the output | |||
buff.WriteString(token.Data) | |||
case "style": | |||
// not encouraged, but if a policy allows CSS styles we | |||
// should not HTML escape it as that would break the output | |||
buff.WriteString(token.Data) | |||
default: | |||
// HTML escape the text | |||
buff.WriteString(token.String()) | |||
} | |||
} | |||
default: | |||
// A token that didn't exist in the html package when we wrote this | |||
return &bytes.Buffer{} | |||
} | |||
} | |||
} | |||
// sanitizeAttrs takes a set of element attribute policies and the global | |||
// attribute policies and applies them to the []html.Attribute returning a set | |||
// of html.Attributes that match the policies | |||
func (p *Policy) sanitizeAttrs( | |||
elementName string, | |||
attrs []html.Attribute, | |||
aps map[string]attrPolicy, | |||
) []html.Attribute { | |||
if len(attrs) == 0 { | |||
return attrs | |||
} | |||
// Builds a new attribute slice based on the whether the attribute has been | |||
// whitelisted explicitly or globally. | |||
cleanAttrs := []html.Attribute{} | |||
for _, htmlAttr := range attrs { | |||
if p.allowDataAttributes { | |||
// If we see a data attribute, let it through. | |||
if isDataAttribute(htmlAttr.Key) { | |||
cleanAttrs = append(cleanAttrs, htmlAttr) | |||
continue | |||
} | |||
} | |||
// Is there an element specific attribute policy that applies? | |||
if ap, ok := aps[htmlAttr.Key]; ok { | |||
if ap.regexp != nil { | |||
if ap.regexp.MatchString(htmlAttr.Val) { | |||
cleanAttrs = append(cleanAttrs, htmlAttr) | |||
continue | |||
} | |||
} else { | |||
cleanAttrs = append(cleanAttrs, htmlAttr) | |||
continue | |||
} | |||
} | |||
// Is there a global attribute policy that applies? | |||
if ap, ok := p.globalAttrs[htmlAttr.Key]; ok { | |||
if ap.regexp != nil { | |||
if ap.regexp.MatchString(htmlAttr.Val) { | |||
cleanAttrs = append(cleanAttrs, htmlAttr) | |||
} | |||
} else { | |||
cleanAttrs = append(cleanAttrs, htmlAttr) | |||
} | |||
} | |||
} | |||
if len(cleanAttrs) == 0 { | |||
// If nothing was allowed, let's get out of here | |||
return cleanAttrs | |||
} | |||
// cleanAttrs now contains the attributes that are permitted | |||
if linkable(elementName) { | |||
if p.requireParseableURLs { | |||
// Ensure URLs are parseable: | |||
// - a.href | |||
// - area.href | |||
// - link.href | |||
// - blockquote.cite | |||
// - q.cite | |||
// - img.src | |||
// - script.src | |||
tmpAttrs := []html.Attribute{} | |||
for _, htmlAttr := range cleanAttrs { | |||
switch elementName { | |||
case "a", "area", "link": | |||
if htmlAttr.Key == "href" { | |||
if u, ok := p.validURL(htmlAttr.Val); ok { | |||
htmlAttr.Val = u | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} | |||
break | |||
} | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
case "blockquote", "q": | |||
if htmlAttr.Key == "cite" { | |||
if u, ok := p.validURL(htmlAttr.Val); ok { | |||
htmlAttr.Val = u | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} | |||
break | |||
} | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
case "img", "script": | |||
if htmlAttr.Key == "src" { | |||
if u, ok := p.validURL(htmlAttr.Val); ok { | |||
htmlAttr.Val = u | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} | |||
break | |||
} | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
default: | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} | |||
} | |||
cleanAttrs = tmpAttrs | |||
} | |||
if (p.requireNoFollow || | |||
p.requireNoFollowFullyQualifiedLinks || | |||
p.addTargetBlankToFullyQualifiedLinks) && | |||
len(cleanAttrs) > 0 { | |||
// Add rel="nofollow" if a "href" exists | |||
switch elementName { | |||
case "a", "area", "link": | |||
var hrefFound bool | |||
var externalLink bool | |||
for _, htmlAttr := range cleanAttrs { | |||
if htmlAttr.Key == "href" { | |||
hrefFound = true | |||
u, err := url.Parse(htmlAttr.Val) | |||
if err != nil { | |||
continue | |||
} | |||
if u.Host != "" { | |||
externalLink = true | |||
} | |||
continue | |||
} | |||
} | |||
if hrefFound { | |||
var ( | |||
noFollowFound bool | |||
targetBlankFound bool | |||
) | |||
addNoFollow := (p.requireNoFollow || | |||
externalLink && p.requireNoFollowFullyQualifiedLinks) | |||
addTargetBlank := (externalLink && | |||
p.addTargetBlankToFullyQualifiedLinks) | |||
tmpAttrs := []html.Attribute{} | |||
for _, htmlAttr := range cleanAttrs { | |||
var appended bool | |||
if htmlAttr.Key == "rel" && addNoFollow { | |||
if strings.Contains(htmlAttr.Val, "nofollow") { | |||
noFollowFound = true | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
appended = true | |||
} else { | |||
htmlAttr.Val += " nofollow" | |||
noFollowFound = true | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
appended = true | |||
} | |||
} | |||
if elementName == "a" && htmlAttr.Key == "target" { | |||
if htmlAttr.Val == "_blank" { | |||
targetBlankFound = true | |||
} | |||
if addTargetBlank && !targetBlankFound { | |||
htmlAttr.Val = "_blank" | |||
targetBlankFound = true | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
appended = true | |||
} | |||
} | |||
if !appended { | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} | |||
} | |||
if noFollowFound || targetBlankFound { | |||
cleanAttrs = tmpAttrs | |||
} | |||
if addNoFollow && !noFollowFound { | |||
rel := html.Attribute{} | |||
rel.Key = "rel" | |||
rel.Val = "nofollow" | |||
cleanAttrs = append(cleanAttrs, rel) | |||
} | |||
if elementName == "a" && addTargetBlank && !targetBlankFound { | |||
rel := html.Attribute{} | |||
rel.Key = "target" | |||
rel.Val = "_blank" | |||
targetBlankFound = true | |||
cleanAttrs = append(cleanAttrs, rel) | |||
} | |||
if targetBlankFound { | |||
// target="_blank" has a security risk that allows the | |||
// opened window/tab to issue JavaScript calls against | |||
// window.opener, which in effect allow the destination | |||
// of the link to control the source: | |||
// https://dev.to/ben/the-targetblank-vulnerability-by-example | |||
// | |||
// To mitigate this risk, we need to add a specific rel | |||
// attribute if it is not already present. | |||
// rel="noopener" | |||
// | |||
// Unfortunately this is processing the rel twice (we | |||
// already looked at it earlier ^^) as we cannot be sure | |||
// of the ordering of the href and rel, and whether we | |||
// have fully satisfied that we need to do this. This | |||
// double processing only happens *if* target="_blank" | |||
// is true. | |||
var noOpenerAdded bool | |||
tmpAttrs := []html.Attribute{} | |||
for _, htmlAttr := range cleanAttrs { | |||
var appended bool | |||
if htmlAttr.Key == "rel" { | |||
if strings.Contains(htmlAttr.Val, "noopener") { | |||
noOpenerAdded = true | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} else { | |||
htmlAttr.Val += " noopener" | |||
noOpenerAdded = true | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} | |||
appended = true | |||
} | |||
if !appended { | |||
tmpAttrs = append(tmpAttrs, htmlAttr) | |||
} | |||
} | |||
if noOpenerAdded { | |||
cleanAttrs = tmpAttrs | |||
} else { | |||
// rel attr was not found, or else noopener would | |||
// have been added already | |||
rel := html.Attribute{} | |||
rel.Key = "rel" | |||
rel.Val = "noopener" | |||
cleanAttrs = append(cleanAttrs, rel) | |||
} | |||
} | |||
} | |||
default: | |||
} | |||
} | |||
} | |||
return cleanAttrs | |||
} | |||
func (p *Policy) allowNoAttrs(elementName string) bool { | |||
_, ok := p.setOfElementsAllowedWithoutAttrs[elementName] | |||
return ok | |||
} | |||
func (p *Policy) validURL(rawurl string) (string, bool) { | |||
if p.requireParseableURLs { | |||
// URLs are valid if when space is trimmed the URL is valid | |||
rawurl = strings.TrimSpace(rawurl) | |||
// URLs cannot contain whitespace, unless it is a data-uri | |||
if (strings.Contains(rawurl, " ") || | |||
strings.Contains(rawurl, "\t") || | |||
strings.Contains(rawurl, "\n")) && | |||
!strings.HasPrefix(rawurl, `data:`) { | |||
return "", false | |||
} | |||
// URLs are valid if they parse | |||
u, err := url.Parse(rawurl) | |||
if err != nil { | |||
return "", false | |||
} | |||
if u.Scheme != "" { | |||
urlPolicy, ok := p.allowURLSchemes[u.Scheme] | |||
if !ok { | |||
return "", false | |||
} | |||
if urlPolicy == nil || urlPolicy(u) == true { | |||
return u.String(), true | |||
} | |||
return "", false | |||
} | |||
if p.allowRelativeURLs { | |||
if u.String() != "" { | |||
return u.String(), true | |||
} | |||
} | |||
return "", false | |||
} | |||
return rawurl, true | |||
} | |||
func linkable(elementName string) bool { | |||
switch elementName { | |||
case "a", "area", "blockquote", "img", "link", "script": | |||
return true | |||
default: | |||
return false | |||
} | |||
} | |||
func isDataAttribute(val string) bool { | |||
if !dataAttribute.MatchString(val) { | |||
return false | |||
} | |||
rest := strings.Split(val, "data-") | |||
if len(rest) == 1 { | |||
return false | |||
} | |||
// data-xml* is invalid. | |||
if dataAttributeXMLPrefix.MatchString(rest[1]) { | |||
return false | |||
} | |||
// no uppercase or semi-colons allowed. | |||
if dataAttributeInvalidChars.MatchString(rest[1]) { | |||
return false | |||
} | |||
return true | |||
} |
@@ -0,0 +1,21 @@ | |||
The MIT License (MIT) | |||
Copyright (c) 2013 Mitchell Hashimoto | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in | |||
all copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
THE SOFTWARE. |
@@ -0,0 +1,14 @@ | |||
# go-homedir | |||
This is a Go library for detecting the user's home directory without | |||
the use of cgo, so the library can be used in cross-compilation environments. | |||
Usage is incredibly simple, just call `homedir.Dir()` to get the home directory | |||
for a user, and `homedir.Expand()` to expand the `~` in a path to the home | |||
directory. | |||
**Why not just use `os/user`?** The built-in `os/user` package requires | |||
cgo on Darwin systems. This means that any Go code that uses that package | |||
cannot cross compile. But 99% of the time the use for `os/user` is just to | |||
retrieve the home directory, which we can do for the current user without | |||
cgo. This library does that, enabling cross-compilation. |
@@ -0,0 +1 @@ | |||
module github.com/mitchellh/go-homedir |
@@ -0,0 +1,157 @@ | |||
package homedir | |||
import ( | |||
"bytes" | |||
"errors" | |||
"os" | |||
"os/exec" | |||
"path/filepath" | |||
"runtime" | |||
"strconv" | |||
"strings" | |||
"sync" | |||
) | |||
// DisableCache will disable caching of the home directory. Caching is enabled | |||
// by default. | |||
var DisableCache bool | |||
var homedirCache string | |||
var cacheLock sync.RWMutex | |||
// Dir returns the home directory for the executing user. | |||
// | |||
// This uses an OS-specific method for discovering the home directory. | |||
// An error is returned if a home directory cannot be detected. | |||
func Dir() (string, error) { | |||
if !DisableCache { | |||
cacheLock.RLock() | |||
cached := homedirCache | |||
cacheLock.RUnlock() | |||
if cached != "" { | |||
return cached, nil | |||
} | |||
} | |||
cacheLock.Lock() | |||
defer cacheLock.Unlock() | |||
var result string | |||
var err error | |||
if runtime.GOOS == "windows" { | |||
result, err = dirWindows() | |||
} else { | |||
// Unix-like system, so just assume Unix | |||
result, err = dirUnix() | |||
} | |||
if err != nil { | |||
return "", err | |||
} | |||
homedirCache = result | |||
return result, nil | |||
} | |||
// Expand expands the path to include the home directory if the path | |||
// is prefixed with `~`. If it isn't prefixed with `~`, the path is | |||
// returned as-is. | |||
func Expand(path string) (string, error) { | |||
if len(path) == 0 { | |||
return path, nil | |||
} | |||
if path[0] != '~' { | |||
return path, nil | |||
} | |||
if len(path) > 1 && path[1] != '/' && path[1] != '\\' { | |||
return "", errors.New("cannot expand user-specific home dir") | |||
} | |||
dir, err := Dir() | |||
if err != nil { | |||
return "", err | |||
} | |||
return filepath.Join(dir, path[1:]), nil | |||
} | |||
func dirUnix() (string, error) { | |||
homeEnv := "HOME" | |||
if runtime.GOOS == "plan9" { | |||
// On plan9, env vars are lowercase. | |||
homeEnv = "home" | |||
} | |||
// First prefer the HOME environmental variable | |||
if home := os.Getenv(homeEnv); home != "" { | |||
return home, nil | |||
} | |||
var stdout bytes.Buffer | |||
// If that fails, try OS specific commands | |||
if runtime.GOOS == "darwin" { | |||
cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) | |||
cmd.Stdout = &stdout | |||
if err := cmd.Run(); err == nil { | |||
result := strings.TrimSpace(stdout.String()) | |||
if result != "" { | |||
return result, nil | |||
} | |||
} | |||
} else { | |||
cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) | |||
cmd.Stdout = &stdout | |||
if err := cmd.Run(); err != nil { | |||
// If the error is ErrNotFound, we ignore it. Otherwise, return it. | |||
if err != exec.ErrNotFound { | |||
return "", err | |||
} | |||
} else { | |||
if passwd := strings.TrimSpace(stdout.String()); passwd != "" { | |||
// username:password:uid:gid:gecos:home:shell | |||
passwdParts := strings.SplitN(passwd, ":", 7) | |||
if len(passwdParts) > 5 { | |||
return passwdParts[5], nil | |||
} | |||
} | |||
} | |||
} | |||
// If all else fails, try the shell | |||
stdout.Reset() | |||
cmd := exec.Command("sh", "-c", "cd && pwd") | |||
cmd.Stdout = &stdout | |||
if err := cmd.Run(); err != nil { | |||
return "", err | |||
} | |||
result := strings.TrimSpace(stdout.String()) | |||
if result == "" { | |||
return "", errors.New("blank output when reading home directory") | |||
} | |||
return result, nil | |||
} | |||
func dirWindows() (string, error) { | |||
// First prefer the HOME environmental variable | |||
if home := os.Getenv("HOME"); home != "" { | |||
return home, nil | |||
} | |||
// Prefer standard environment variable USERPROFILE | |||
if home := os.Getenv("USERPROFILE"); home != "" { | |||
return home, nil | |||
} | |||
drive := os.Getenv("HOMEDRIVE") | |||
path := os.Getenv("HOMEPATH") | |||
home := drive + path | |||
if drive == "" || path == "" { | |||
return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank") | |||
} | |||
return home, nil | |||
} |
@@ -0,0 +1,16 @@ | |||
sudo: false | |||
language: go | |||
go: | |||
- 1.x | |||
- master | |||
matrix: | |||
allow_failures: | |||
- go: master | |||
fast_finish: true | |||
install: | |||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). | |||
script: | |||
- go get -t -v ./... | |||
- diff -u <(echo -n) <(gofmt -d -s .) | |||
- go tool vet . | |||
- go test -v -race ./... |
@@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2015 Dmitri Shuralyov | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@@ -0,0 +1,36 @@ | |||
sanitized_anchor_name | |||
===================== | |||
[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name) | |||
Package sanitized_anchor_name provides a func to create sanitized anchor names. | |||
Its logic can be reused by multiple packages to create interoperable anchor names | |||
and links to those anchors. | |||
At this time, it does not try to ensure that generated anchor names | |||
are unique, that responsibility falls on the caller. | |||
Installation | |||
------------ | |||
```bash | |||
go get -u github.com/shurcooL/sanitized_anchor_name | |||
``` | |||
Example | |||
------- | |||
```Go | |||
anchorName := sanitized_anchor_name.Create("This is a header") | |||
fmt.Println(anchorName) | |||
// Output: | |||
// this-is-a-header | |||
``` | |||
License | |||
------- | |||
- [MIT License](LICENSE) |
@@ -0,0 +1,29 @@ | |||
// Package sanitized_anchor_name provides a func to create sanitized anchor names. | |||
// | |||
// Its logic can be reused by multiple packages to create interoperable anchor names | |||
// and links to those anchors. | |||
// | |||
// At this time, it does not try to ensure that generated anchor names | |||
// are unique, that responsibility falls on the caller. | |||
package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name" | |||
import "unicode" | |||
// Create returns a sanitized anchor name for the given text. | |||
func Create(text string) string { | |||
var anchorName []rune | |||
var futureDash = false | |||
for _, r := range text { | |||
switch { | |||
case unicode.IsLetter(r) || unicode.IsNumber(r): | |||
if futureDash && len(anchorName) > 0 { | |||
anchorName = append(anchorName, '-') | |||
} | |||
futureDash = false | |||
anchorName = append(anchorName, unicode.ToLower(r)) | |||
default: | |||
futureDash = true | |||
} | |||
} | |||
return string(anchorName) | |||
} |
@@ -0,0 +1,3 @@ | |||
*~ | |||
*.swp | |||
writeas |
@@ -0,0 +1,21 @@ | |||
The MIT License (MIT) | |||
Copyright (c) 2016 Write.as | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@@ -0,0 +1,71 @@ | |||
# go-writeas | |||
[![godoc](https://godoc.org/go.code.as/writeas.v2?status.svg)](https://godoc.org/go.code.as/writeas.v2) | |||
Official Write.as Go client library. | |||
## Installation | |||
**Warning**: the `v2` branch is under heavy development and its API will change without notice. | |||
For a stable API, use `go.code.as/writeas.v1` and upgrade to `v2` once everything is merged into `master`. | |||
```bash | |||
go get go.code.as/writeas.v2 | |||
``` | |||
## Documentation | |||
See all functionality and usages in the [API documentation](https://developer.write.as/docs/api/). | |||
### Example usage | |||
```go | |||
import "go.code.as/writeas.v2" | |||
func main() { | |||
// Create the client | |||
c := writeas.NewClient() | |||
// Publish a post | |||
p, err := c.CreatePost(&writeas.PostParams{ | |||
Title: "Title!", | |||
Content: "This is a post.", | |||
Font: "sans", | |||
}) | |||
if err != nil { | |||
// Perhaps show err.Error() | |||
} | |||
// Save token for later, since it won't ever be returned again | |||
token := p.Token | |||
// Update a published post | |||
p, err = c.UpdatePost(p.ID, token, &writeas.PostParams{ | |||
Content: "Now it's been updated!", | |||
}) | |||
if err != nil { | |||
// handle | |||
} | |||
// Get a published post | |||
p, err = c.GetPost(p.ID) | |||
if err != nil { | |||
// handle | |||
} | |||
// Delete a post | |||
err = c.DeletePost(p.ID, token) | |||
} | |||
``` | |||
## Contributing | |||
The library covers our usage, but might not be comprehensive of the API. So we always welcome contributions and improvements from the community. Before sending pull requests, make sure you've done the following: | |||
* Run `goimports` on all updated .go files. | |||
* Document all exported structs and funcs. | |||
## License | |||
MIT |
@@ -0,0 +1,75 @@ | |||
package writeas | |||
import ( | |||
"fmt" | |||
"net/http" | |||
) | |||
// LogIn authenticates a user with Write.as. | |||
// See https://developer.write.as/docs/api/#authenticate-a-user | |||
func (c *Client) LogIn(username, pass string) (*AuthUser, error) { | |||
u := &AuthUser{} | |||
up := struct { | |||
Alias string `json:"alias"` | |||
Pass string `json:"pass"` | |||
}{ | |||
Alias: username, | |||
Pass: pass, | |||
} | |||
env, err := c.post("/auth/login", up, u) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if u, ok = env.Data.(*AuthUser); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status != http.StatusOK { | |||
if status == http.StatusBadRequest { | |||
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) | |||
} else if status == http.StatusUnauthorized { | |||
return nil, fmt.Errorf("Incorrect password.") | |||
} else if status == http.StatusNotFound { | |||
return nil, fmt.Errorf("User does not exist.") | |||
} else if status == http.StatusTooManyRequests { | |||
return nil, fmt.Errorf("Too many log in attempts in a short period of time.") | |||
} | |||
return nil, fmt.Errorf("Problem authenticating: %d. %v\n", status, err) | |||
} | |||
c.SetToken(u.AccessToken) | |||
return u, nil | |||
} | |||
// LogOut logs the current user out, making the Client's current access token | |||
// invalid. | |||
func (c *Client) LogOut() error { | |||
env, err := c.delete("/auth/me", nil) | |||
if err != nil { | |||
return err | |||
} | |||
status := env.Code | |||
if status != http.StatusNoContent { | |||
if status == http.StatusNotFound { | |||
return fmt.Errorf("Access token is invalid or doesn't exist") | |||
} | |||
return fmt.Errorf("Unable to log out: %v", env.ErrorMessage) | |||
} | |||
// Logout successful, so update the Client | |||
c.token = "" | |||
return nil | |||
} | |||
func (c *Client) isNotLoggedIn(code int) bool { | |||
if c.token == "" { | |||
return false | |||
} | |||
return code == http.StatusUnauthorized | |||
} |
@@ -0,0 +1,186 @@ | |||
package writeas | |||
import ( | |||
"fmt" | |||
"net/http" | |||
) | |||
type ( | |||
// Collection represents a collection of posts. Blogs are a type of collection | |||
// on Write.as. | |||
Collection struct { | |||
Alias string `json:"alias"` | |||
Title string `json:"title"` | |||
Description string `json:"description"` | |||
StyleSheet string `json:"style_sheet"` | |||
Private bool `json:"private"` | |||
Views int64 `json:"views"` | |||
Domain string `json:"domain,omitempty"` | |||
Email string `json:"email,omitempty"` | |||
URL string `json:"url,omitempty"` | |||
TotalPosts int `json:"total_posts"` | |||
Posts *[]Post `json:"posts,omitempty"` | |||
} | |||
// CollectionParams holds values for creating a collection. | |||
CollectionParams struct { | |||
Alias string `json:"alias"` | |||
Title string `json:"title"` | |||
Description string `json:"description,omitempty"` | |||
} | |||
) | |||
// CreateCollection creates a new collection, returning a user-friendly error | |||
// if one comes up. Requires a Write.as subscription. See | |||
// https://developer.write.as/docs/api/#create-a-collection | |||
func (c *Client) CreateCollection(sp *CollectionParams) (*Collection, error) { | |||
p := &Collection{} | |||
env, err := c.post("/collections", sp, p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if p, ok = env.Data.(*Collection); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status != http.StatusCreated { | |||
if status == http.StatusBadRequest { | |||
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) | |||
} else if status == http.StatusForbidden { | |||
return nil, fmt.Errorf("Casual or Pro user required.") | |||
} else if status == http.StatusConflict { | |||
return nil, fmt.Errorf("Collection name is already taken.") | |||
} else if status == http.StatusPreconditionFailed { | |||
return nil, fmt.Errorf("Reached max collection quota.") | |||
} | |||
return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err) | |||
} | |||
return p, nil | |||
} | |||
// GetCollection retrieves a collection, returning the Collection and any error | |||
// (in user-friendly form) that occurs. See | |||
// https://developer.write.as/docs/api/#retrieve-a-collection | |||
func (c *Client) GetCollection(alias string) (*Collection, error) { | |||
coll := &Collection{} | |||
env, err := c.get(fmt.Sprintf("/collections/%s", alias), coll) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if coll, ok = env.Data.(*Collection); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status == http.StatusOK { | |||
return coll, nil | |||
} else if status == http.StatusNotFound { | |||
return nil, fmt.Errorf("Collection not found.") | |||
} else { | |||
return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) | |||
} | |||
} | |||
// GetCollectionPosts retrieves a collection's posts, returning the Posts | |||
// and any error (in user-friendly form) that occurs. See | |||
// https://developer.write.as/docs/api/#retrieve-collection-posts | |||
func (c *Client) GetCollectionPosts(alias string) (*[]Post, error) { | |||
coll := &Collection{} | |||
env, err := c.get(fmt.Sprintf("/collections/%s/posts", alias), coll) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if coll, ok = env.Data.(*Collection); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status == http.StatusOK { | |||
return coll.Posts, nil | |||
} else if status == http.StatusNotFound { | |||
return nil, fmt.Errorf("Collection not found.") | |||
} else { | |||
return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) | |||
} | |||
} | |||
// GetCollectionPost retrieves a post from a collection | |||
// and any error (in user-friendly form) that occurs). See | |||
// https://developers.write.as/docs/api/#retrieve-a-collection-post | |||
func (c *Client) GetCollectionPost(alias, slug string) (*Post, error) { | |||
post := Post{} | |||
env, err := c.get(fmt.Sprintf("/collections/%s/posts/%s", alias, slug), &post) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if _, ok := env.Data.(*Post); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
if env.Code == http.StatusOK { | |||
return &post, nil | |||
} else if env.Code == http.StatusNotFound { | |||
return nil, fmt.Errorf("Post %s not found in collection %s", slug, alias) | |||
} | |||
return nil, fmt.Errorf("Problem getting post %s from collection %s: %d. %v\n", slug, alias, env.Code, err) | |||
} | |||
// GetUserCollections retrieves the authenticated user's collections. | |||
// See https://developers.write.as/docs/api/#retrieve-user-39-s-collections | |||
func (c *Client) GetUserCollections() (*[]Collection, error) { | |||
colls := &[]Collection{} | |||
env, err := c.get("/me/collections", colls) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if colls, ok = env.Data.(*[]Collection); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status != http.StatusOK { | |||
if c.isNotLoggedIn(status) { | |||
return nil, fmt.Errorf("Not authenticated.") | |||
} | |||
return nil, fmt.Errorf("Problem getting collections: %d. %v\n", status, err) | |||
} | |||
return colls, nil | |||
} | |||
// DeleteCollection permanently deletes a collection and makes any posts on it | |||
// anonymous. | |||
// | |||
// See https://developers.write.as/docs/api/#delete-a-collection. | |||
func (c *Client) DeleteCollection(alias string) error { | |||
endpoint := "/collections/" + alias | |||
env, err := c.delete(endpoint, nil /* data */) | |||
if err != nil { | |||
return err | |||
} | |||
status := env.Code | |||
switch status { | |||
case http.StatusNoContent: | |||
return nil | |||
case http.StatusUnauthorized: | |||
return fmt.Errorf("Not authenticated.") | |||
case http.StatusBadRequest: | |||
return fmt.Errorf("Bad request: %s", env.ErrorMessage) | |||
default: | |||
return fmt.Errorf("Problem deleting collection: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
module github.com/writeas/go-writeas/v2 | |||
go 1.9 | |||
require ( | |||
code.as/core/socks v1.0.0 | |||
github.com/writeas/impart v1.1.0 | |||
) |
@@ -0,0 +1,4 @@ | |||
code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= | |||
code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= | |||
github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= | |||
github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= |
@@ -0,0 +1,330 @@ | |||
package writeas | |||
import ( | |||
"fmt" | |||
"net/http" | |||
"time" | |||
) | |||
type ( | |||
// Post represents a published Write.as post, whether anonymous, owned by a | |||
// user, or part of a collection. | |||
Post struct { | |||
ID string `json:"id"` | |||
Slug string `json:"slug"` | |||
Token string `json:"token"` | |||
Font string `json:"appearance"` | |||
Language *string `json:"language"` | |||
RTL *bool `json:"rtl"` | |||
Listed bool `json:"listed"` | |||
Created time.Time `json:"created"` | |||
Updated time.Time `json:"updated"` | |||
Title string `json:"title"` | |||
Content string `json:"body"` | |||
Views int64 `json:"views"` | |||
Tags []string `json:"tags"` | |||
Images []string `json:"images"` | |||
OwnerName string `json:"owner,omitempty"` | |||
Collection *Collection `json:"collection,omitempty"` | |||
} | |||
// OwnedPostParams are, together, fields only the original post author knows. | |||
OwnedPostParams struct { | |||
ID string `json:"id"` | |||
Token string `json:"token,omitempty"` | |||
} | |||
// PostParams holds values for creating or updating a post. | |||
PostParams struct { | |||
// Parameters only for updating | |||
ID string `json:"-"` | |||
Token string `json:"token,omitempty"` | |||
// Parameters for creating or updating | |||
Slug string `json:"slug"` | |||
Created *time.Time `json:"created,omitempty"` | |||
Updated *time.Time `json:"updated,omitempty"` | |||
Title string `json:"title,omitempty"` | |||
Content string `json:"body,omitempty"` | |||
Font string `json:"font,omitempty"` | |||
IsRTL *bool `json:"rtl,omitempty"` | |||
Language *string `json:"lang,omitempty"` | |||
// Parameters only for creating | |||
Crosspost []map[string]string `json:"crosspost,omitempty"` | |||
// Parameters for collection posts | |||
Collection string `json:"-"` | |||
} | |||
// PinnedPostParams holds values for pinning a post | |||
PinnedPostParams struct { | |||
ID string `json:"id"` | |||
Position int `json:"position"` | |||
} | |||
// BatchPostResult contains the post-specific result as part of a larger | |||
// batch operation. | |||
BatchPostResult struct { | |||
ID string `json:"id,omitempty"` | |||
Code int `json:"code,omitempty"` | |||
ErrorMessage string `json:"error_msg,omitempty"` | |||
} | |||
// ClaimPostResult contains the post-specific result for a request to | |||
// associate a post to an account. | |||
ClaimPostResult struct { | |||
ID string `json:"id,omitempty"` | |||
Code int `json:"code,omitempty"` | |||
ErrorMessage string `json:"error_msg,omitempty"` | |||
Post *Post `json:"post,omitempty"` | |||
} | |||
) | |||
// GetPost retrieves a published post, returning the Post and any error (in | |||
// user-friendly form) that occurs. See | |||
// https://developer.write.as/docs/api/#retrieve-a-post. | |||
func (c *Client) GetPost(id string) (*Post, error) { | |||
p := &Post{} | |||
env, err := c.get(fmt.Sprintf("/posts/%s", id), p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if p, ok = env.Data.(*Post); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status == http.StatusOK { | |||
return p, nil | |||
} else if status == http.StatusNotFound { | |||
return nil, fmt.Errorf("Post not found.") | |||
} else if status == http.StatusGone { | |||
return nil, fmt.Errorf("Post unpublished.") | |||
} | |||
return nil, fmt.Errorf("Problem getting post: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
// CreatePost publishes a new post, returning a user-friendly error if one comes | |||
// up. See https://developer.write.as/docs/api/#publish-a-post. | |||
func (c *Client) CreatePost(sp *PostParams) (*Post, error) { | |||
p := &Post{} | |||
endPre := "" | |||
if sp.Collection != "" { | |||
endPre = "/collections/" + sp.Collection | |||
} | |||
env, err := c.post(endPre+"/posts", sp, p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if p, ok = env.Data.(*Post); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status != http.StatusCreated { | |||
if status == http.StatusBadRequest { | |||
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) | |||
} | |||
return nil, fmt.Errorf("Problem creating post: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
return p, nil | |||
} | |||
// UpdatePost updates a published post with the given PostParams. See | |||
// https://developer.write.as/docs/api/#update-a-post. | |||
func (c *Client) UpdatePost(id, token string, sp *PostParams) (*Post, error) { | |||
return c.updatePost("", id, token, sp) | |||
} | |||
func (c *Client) updatePost(collection, identifier, token string, sp *PostParams) (*Post, error) { | |||
p := &Post{} | |||
endpoint := "/posts/" + identifier | |||
/* | |||
if collection != "" { | |||
endpoint = "/collections/" + collection + endpoint | |||
} else { | |||
sp.Token = token | |||
} | |||
*/ | |||
sp.Token = token | |||
env, err := c.put(endpoint, sp, p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if p, ok = env.Data.(*Post); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status != http.StatusOK { | |||
if c.isNotLoggedIn(status) { | |||
return nil, fmt.Errorf("Not authenticated.") | |||
} else if status == http.StatusBadRequest { | |||
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) | |||
} | |||
return nil, fmt.Errorf("Problem updating post: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
return p, nil | |||
} | |||
// DeletePost permanently deletes a published post. See | |||
// https://developer.write.as/docs/api/#delete-a-post. | |||
func (c *Client) DeletePost(id, token string) error { | |||
return c.deletePost("", id, token) | |||
} | |||
func (c *Client) deletePost(collection, identifier, token string) error { | |||
p := map[string]string{} | |||
endpoint := "/posts/" + identifier | |||
/* | |||
if collection != "" { | |||
endpoint = "/collections/" + collection + endpoint | |||
} else { | |||
p["token"] = token | |||
} | |||
*/ | |||
p["token"] = token | |||
env, err := c.delete(endpoint, p) | |||
if err != nil { | |||
return err | |||
} | |||
status := env.Code | |||
if status == http.StatusNoContent { | |||
return nil | |||
} else if c.isNotLoggedIn(status) { | |||
return fmt.Errorf("Not authenticated.") | |||
} else if status == http.StatusBadRequest { | |||
return fmt.Errorf("Bad request: %s", env.ErrorMessage) | |||
} | |||
return fmt.Errorf("Problem deleting post: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
// ClaimPosts associates anonymous posts with a user / account. | |||
// https://developer.write.as/docs/api/#claim-posts. | |||
func (c *Client) ClaimPosts(sp *[]OwnedPostParams) (*[]ClaimPostResult, error) { | |||
p := &[]ClaimPostResult{} | |||
env, err := c.post("/posts/claim", sp, p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if p, ok = env.Data.(*[]ClaimPostResult); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status == http.StatusOK { | |||
return p, nil | |||
} else if c.isNotLoggedIn(status) { | |||
return nil, fmt.Errorf("Not authenticated.") | |||
} else if status == http.StatusBadRequest { | |||
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) | |||
} else { | |||
return nil, fmt.Errorf("Problem claiming post: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
// TODO: does this also happen with moving posts? | |||
} | |||
// GetUserPosts retrieves the authenticated user's posts. | |||
// See https://developers.write.as/docs/api/#retrieve-user-39-s-posts | |||
func (c *Client) GetUserPosts() (*[]Post, error) { | |||
p := &[]Post{} | |||
env, err := c.get("/me/posts", p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var ok bool | |||
if p, ok = env.Data.(*[]Post); !ok { | |||
return nil, fmt.Errorf("Wrong data returned from API.") | |||
} | |||
status := env.Code | |||
if status != http.StatusOK { | |||
if c.isNotLoggedIn(status) { | |||
return nil, fmt.Errorf("Not authenticated.") | |||
} | |||
return nil, fmt.Errorf("Problem getting user posts: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
return p, nil | |||
} | |||
// PinPost pins a post in the given collection. | |||
// See https://developers.write.as/docs/api/#pin-a-post-to-a-collection | |||
func (c *Client) PinPost(alias string, pp *PinnedPostParams) error { | |||
res := &[]BatchPostResult{} | |||
env, err := c.post(fmt.Sprintf("/collections/%s/pin", alias), []*PinnedPostParams{pp}, res) | |||
if err != nil { | |||
return err | |||
} | |||
var ok bool | |||
if res, ok = env.Data.(*[]BatchPostResult); !ok { | |||
return fmt.Errorf("Wrong data returned from API.") | |||
} | |||
// Check for basic request errors on top level response | |||
status := env.Code | |||
if status != http.StatusOK { | |||
if c.isNotLoggedIn(status) { | |||
return fmt.Errorf("Not authenticated.") | |||
} | |||
return fmt.Errorf("Problem pinning post: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
// Check the individual post result | |||
if len(*res) == 0 || len(*res) > 1 { | |||
return fmt.Errorf("Wrong data returned from API.") | |||
} | |||
if (*res)[0].Code != http.StatusOK { | |||
return fmt.Errorf("Problem pinning post: %d", (*res)[0].Code) | |||
// TODO: return ErrorMessage (right now it'll be empty) | |||
// return fmt.Errorf("Problem pinning post: %s", res[0].ErrorMessage) | |||
} | |||
return nil | |||
} | |||
// UnpinPost unpins a post from the given collection. | |||
// See https://developers.write.as/docs/api/#unpin-a-post-from-a-collection | |||
func (c *Client) UnpinPost(alias string, pp *PinnedPostParams) error { | |||
res := &[]BatchPostResult{} | |||
env, err := c.post(fmt.Sprintf("/collections/%s/unpin", alias), []*PinnedPostParams{pp}, res) | |||
if err != nil { | |||
return err | |||
} | |||
var ok bool | |||
if res, ok = env.Data.(*[]BatchPostResult); !ok { | |||
return fmt.Errorf("Wrong data returned from API.") | |||
} | |||
// Check for basic request errors on top level response | |||
status := env.Code | |||
if status != http.StatusOK { | |||
if c.isNotLoggedIn(status) { | |||
return fmt.Errorf("Not authenticated.") | |||
} | |||
return fmt.Errorf("Problem unpinning post: %d. %s\n", status, env.ErrorMessage) | |||
} | |||
// Check the individual post result | |||
if len(*res) == 0 || len(*res) > 1 { | |||
return fmt.Errorf("Wrong data returned from API.") | |||
} | |||
if (*res)[0].Code != http.StatusOK { | |||
return fmt.Errorf("Problem unpinning post: %d", (*res)[0].Code) | |||
// TODO: return ErrorMessage (right now it'll be empty) | |||
// return fmt.Errorf("Problem unpinning post: %s", res[0].ErrorMessage) | |||
} | |||
return nil | |||
} |
@@ -0,0 +1,34 @@ | |||
package writeas | |||
import "time" | |||
type ( | |||
// AuthUser represents a just-authenticated user. It contains information | |||
// that'll only be returned once (now) per user session. | |||
AuthUser struct { | |||
AccessToken string `json:"access_token,omitempty"` | |||
Password string `json:"password,omitempty"` | |||
User *User `json:"user"` | |||
} | |||
// User represents a registered Write.as user. | |||
User struct { | |||
Username string `json:"username"` | |||
Email string `json:"email"` | |||
Created time.Time `json:"created"` | |||
// Optional properties | |||
Subscription *UserSubscription `json:"subscription"` | |||
} | |||
// UserSubscription contains information about a user's Write.as | |||
// subscription. | |||
UserSubscription struct { | |||
Name string `json:"name"` | |||
Begin time.Time `json:"begin"` | |||
End time.Time `json:"end"` | |||
AutoRenew bool `json:"auto_renew"` | |||
Active bool `json:"is_active"` | |||
Delinquent bool `json:"is_delinquent"` | |||
} | |||
) |
@@ -0,0 +1,199 @@ | |||
// Package writeas provides the binding for the Write.as API | |||
package writeas | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"fmt" | |||
"io" | |||
"net/http" | |||
"time" | |||
"code.as/core/socks" | |||
"github.com/writeas/impart" | |||
) | |||
const ( | |||
apiURL = "https://write.as/api" | |||
devAPIURL = "https://development.write.as/api" | |||
torAPIURL = "http://writeas7pm7rcdqg.onion/api" | |||
// Current go-writeas version | |||
Version = "2-dev" | |||
) | |||
// Client is used to interact with the Write.as API. It can be used to make | |||
// authenticated or unauthenticated calls. | |||
type Client struct { | |||
baseURL string | |||
// Access token for the user making requests. | |||
token string | |||
// Client making requests to the API | |||
client *http.Client | |||
// UserAgent overrides the default User-Agent header | |||
UserAgent string | |||
} | |||
// defaultHTTPTimeout is the default http.Client timeout. | |||
const defaultHTTPTimeout = 10 * time.Second | |||
// NewClient creates a new API client. By default, all requests are made | |||
// unauthenticated. To optionally make authenticated requests, call `SetToken`. | |||
// | |||
// c := writeas.NewClient() | |||
// c.SetToken("00000000-0000-0000-0000-000000000000") | |||
func NewClient() *Client { | |||
return NewClientWith(Config{URL: apiURL}) | |||
} | |||
// NewTorClient creates a new API client for communicating with the Write.as | |||
// Tor hidden service, using the given port to connect to the local SOCKS | |||
// proxy. | |||
func NewTorClient(port int) *Client { | |||
return NewClientWith(Config{URL: torAPIURL, TorPort: port}) | |||
} | |||
// NewDevClient creates a new API client for development and testing. It'll | |||
// communicate with our development servers, and SHOULD NOT be used in | |||
// production. | |||
func NewDevClient() *Client { | |||
return NewClientWith(Config{URL: devAPIURL}) | |||
} | |||
// Config configures a Write.as client. | |||
type Config struct { | |||
// URL of the Write.as API service. Defaults to https://write.as/api. | |||
URL string | |||
// If specified, the API client will communicate with the Write.as Tor | |||
// hidden service using the provided port to connect to the local SOCKS | |||
// proxy. | |||
TorPort int | |||
// If specified, requests will be authenticated using this user token. | |||
// This may be provided after making a few anonymous requests with | |||
// SetToken. | |||
Token string | |||
} | |||
// NewClientWith builds a new API client with the provided configuration. | |||
func NewClientWith(c Config) *Client { | |||
if c.URL == "" { | |||
c.URL = apiURL | |||
} | |||
httpClient := &http.Client{Timeout: defaultHTTPTimeout} | |||
if c.TorPort > 0 { | |||
dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", c.TorPort)) | |||
httpClient.Transport = &http.Transport{Dial: dialSocksProxy} | |||
} | |||
return &Client{ | |||
client: httpClient, | |||
baseURL: c.URL, | |||
token: c.Token, | |||
} | |||
} | |||
// SetToken sets the user token for all future Client requests. Setting this to | |||
// an empty string will change back to unauthenticated requests. | |||
func (c *Client) SetToken(token string) { | |||
c.token = token | |||
} | |||
// Token returns the user token currently set to the Client. | |||
func (c *Client) Token() string { | |||
return c.token | |||
} | |||
func (c *Client) get(path string, r interface{}) (*impart.Envelope, error) { | |||
method := "GET" | |||
if method != "GET" && method != "HEAD" { | |||
return nil, fmt.Errorf("Method %s not currently supported by library (only HEAD and GET).\n", method) | |||
} | |||
return c.request(method, path, nil, r) | |||
} | |||
func (c *Client) post(path string, data, r interface{}) (*impart.Envelope, error) { | |||
b := new(bytes.Buffer) | |||
json.NewEncoder(b).Encode(data) | |||
return c.request("POST", path, b, r) | |||
} | |||
func (c *Client) put(path string, data, r interface{}) (*impart.Envelope, error) { | |||
b := new(bytes.Buffer) | |||
json.NewEncoder(b).Encode(data) | |||
return c.request("PUT", path, b, r) | |||
} | |||
func (c *Client) delete(path string, data map[string]string) (*impart.Envelope, error) { | |||
r, err := c.buildRequest("DELETE", path, nil) | |||
if err != nil { | |||
return nil, err | |||
} | |||
q := r.URL.Query() | |||
for k, v := range data { | |||
q.Add(k, v) | |||
} | |||
r.URL.RawQuery = q.Encode() | |||
return c.doRequest(r, nil) | |||
} | |||
func (c *Client) request(method, path string, data io.Reader, result interface{}) (*impart.Envelope, error) { | |||
r, err := c.buildRequest(method, path, data) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return c.doRequest(r, result) | |||
} | |||
func (c *Client) buildRequest(method, path string, data io.Reader) (*http.Request, error) { | |||
url := fmt.Sprintf("%s%s", c.baseURL, path) | |||
r, err := http.NewRequest(method, url, data) | |||
if err != nil { | |||
return nil, fmt.Errorf("Create request: %v", err) | |||
} | |||
c.prepareRequest(r) | |||
return r, nil | |||
} | |||
func (c *Client) doRequest(r *http.Request, result interface{}) (*impart.Envelope, error) { | |||
resp, err := c.client.Do(r) | |||
if err != nil { | |||
return nil, fmt.Errorf("Request: %v", err) | |||
} | |||
defer resp.Body.Close() | |||
env := &impart.Envelope{ | |||
Code: resp.StatusCode, | |||
} | |||
if result != nil { | |||
env.Data = result | |||
err = json.NewDecoder(resp.Body).Decode(&env) | |||
if err != nil { | |||
return nil, err | |||
} | |||
} | |||
return env, nil | |||
} | |||
func (c *Client) prepareRequest(r *http.Request) { | |||
ua := c.UserAgent | |||
if ua == "" { | |||
ua = "go-writeas v" + Version | |||
} | |||
r.Header.Set("User-Agent", ua) | |||
r.Header.Add("Content-Type", "application/json") | |||
if c.token != "" { | |||
r.Header.Add("Authorization", "Token "+c.token) | |||
} | |||
} |
@@ -0,0 +1,27 @@ | |||
*~ | |||
*.swp | |||
# Compiled Object files, Static and Dynamic libs (Shared Objects) | |||
*.o | |||
*.a | |||
*.so | |||
# Folders | |||
_obj | |||
_test | |||
# Architecture specific extensions/prefixes | |||
*.[568vq] | |||
[568vq].out | |||
*.cgo1.go | |||
*.cgo2.c | |||
_cgo_defun.c | |||
_cgo_gotypes.go | |||
_cgo_export.* | |||
_testmain.go | |||
*.exe | |||
*.test | |||
*.prof |
@@ -0,0 +1,22 @@ | |||
The MIT License (MIT) | |||
Copyright (c) 2015 Write.as | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. | |||
@@ -0,0 +1,61 @@ | |||
impart | |||
====== | |||
![MIT license](https://img.shields.io/github/license/writeas/impart.svg) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) | |||
**impart** is a library for the final layer between the API and the consumer. It's used in the latest [Write.as](https://write.as) and [HTMLhouse](https://html.house) APIs. | |||
We're still in the early stages of development, so there may be breaking changes. | |||
## Example use | |||
```go | |||
package main | |||
import ( | |||
"fmt" | |||
"github.com/writeas/impart" | |||
"net/http" | |||
) | |||
type handlerFunc func(w http.ResponseWriter, r *http.Request) error | |||
func main() { | |||
http.HandleFunc("/", handle(index)) | |||
http.ListenAndServe("127.0.0.1:8080", nil) | |||
} | |||
func index(w http.ResponseWriter, r *http.Request) error { | |||
fmt.Fprintf(w, "Hello world!") | |||
return nil | |||
} | |||
func handle(f handlerFunc) http.HandlerFunc { | |||
return func(w http.ResponseWriter, r *http.Request) { | |||
handleError(w, r, func() error { | |||
// Do authentication... | |||
// Handle the request | |||
err := f(w, r) | |||
// Log the request and result... | |||
return err | |||
}()) | |||
} | |||
} | |||
func handleError(w http.ResponseWriter, r *http.Request, err error) { | |||
if err == nil { | |||
return | |||
} | |||
if err, ok := err.(impart.HTTPError); ok { | |||
impart.WriteError(w, err) | |||
return | |||
} | |||
impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Internal server error :("}) | |||
} | |||
``` |
@@ -0,0 +1,4 @@ | |||
// Package impart provides a simple interface for a JSON-based API. It is | |||
// designed for passing errors around a web application, sending back a status | |||
// code and error message if needed, or a status code and some data on success. | |||
package impart |
@@ -0,0 +1,20 @@ | |||
package impart | |||
import ( | |||
"net/http" | |||
) | |||
// HTTPError holds an HTTP status code and an error message. | |||
type HTTPError struct { | |||
Status int | |||
Message string | |||
} | |||
// Error displays the HTTPError's error message and satisfies the error | |||
// interface. | |||
func (h HTTPError) Error() string { | |||
if h.Message == "" { | |||
return http.StatusText(h.Status) | |||
} | |||
return h.Message | |||
} |
@@ -0,0 +1,3 @@ | |||
module github.com/writeas/impart | |||
go 1.9 |
@@ -0,0 +1,13 @@ | |||
package impart | |||
import ( | |||
"mime" | |||
"net/http" | |||
) | |||
// ReqJSON returns whether or not the given Request is sending JSON, based on | |||
// the Content-Type header being application/json. | |||
func ReqJSON(r *http.Request) bool { | |||
ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) | |||
return ct == "application/json" | |||
} |
@@ -0,0 +1,76 @@ | |||
package impart | |||
import ( | |||
"encoding/json" | |||
"net/http" | |||
"strconv" | |||
) | |||
type ( | |||
// Envelope contains metadata and optional data for a response object. | |||
// Responses will always contain a status code and either: | |||
// - response Data on a 2xx response, or | |||
// - an ErrorMessage on non-2xx responses | |||
// | |||
// ErrorType is not currently used. | |||
Envelope struct { | |||
Code int `json:"code"` | |||
ErrorType string `json:"error_type,omitempty"` | |||
ErrorMessage string `json:"error_msg,omitempty"` | |||
Data interface{} `json:"data,omitempty"` | |||
} | |||
) | |||
func writeBody(w http.ResponseWriter, body []byte, status int, contentType string) error { | |||
w.Header().Set("Content-Type", contentType+"; charset=UTF-8") | |||
w.Header().Set("Content-Length", strconv.Itoa(len(body))) | |||
w.WriteHeader(status) | |||
_, err := w.Write(body) | |||
return err | |||
} | |||
func RenderActivityJSON(w http.ResponseWriter, value interface{}, status int) error { | |||
body, err := json.Marshal(value) | |||
if err != nil { | |||
return err | |||
} | |||
return writeBody(w, body, status, "application/activity+json") | |||
} | |||
func renderJSON(w http.ResponseWriter, value interface{}, status int) error { | |||
body, err := json.Marshal(value) | |||
if err != nil { | |||
return err | |||
} | |||
return writeBody(w, body, status, "application/json") | |||
} | |||
func renderString(w http.ResponseWriter, status int, msg string) error { | |||
return writeBody(w, []byte(msg), status, "text/plain") | |||
} | |||
// WriteSuccess writes the successful data and metadata to the ResponseWriter as | |||
// JSON. | |||
func WriteSuccess(w http.ResponseWriter, data interface{}, status int) error { | |||
env := &Envelope{ | |||
Code: status, | |||
Data: data, | |||
} | |||
return renderJSON(w, env, status) | |||
} | |||
// WriteError writes the error to the ResponseWriter as JSON. | |||
func WriteError(w http.ResponseWriter, e HTTPError) error { | |||
env := &Envelope{ | |||
Code: e.Status, | |||
ErrorMessage: e.Message, | |||
} | |||
return renderJSON(w, env, e.Status) | |||
} | |||
// WriteRedirect sends a redirect | |||
func WriteRedirect(w http.ResponseWriter, e HTTPError) int { | |||
w.Header().Set("Location", e.Message) | |||
w.WriteHeader(e.Status) | |||
return e.Status | |||
} |
@@ -0,0 +1,8 @@ | |||
*.out | |||
*.swp | |||
*.8 | |||
*.6 | |||
_obj | |||
_test* | |||
markdown | |||
tags |
@@ -0,0 +1,30 @@ | |||
sudo: false | |||
language: go | |||
go: | |||
- 1.5.4 | |||
- 1.6.2 | |||
- tip | |||
matrix: | |||
include: | |||
- go: 1.2.2 | |||
script: | |||
- go get -t -v ./... | |||
- go test -v -race ./... | |||
- go: 1.3.3 | |||
script: | |||
- go get -t -v ./... | |||
- go test -v -race ./... | |||
- go: 1.4.3 | |||
script: | |||
- go get -t -v ./... | |||
- go test -v -race ./... | |||
allow_failures: | |||
- go: tip | |||
fast_finish: true | |||
install: | |||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). | |||
script: | |||
- go get -t -v ./... | |||
- diff -u <(echo -n) <(gofmt -d -s .) | |||
- go tool vet . | |||
- go test -v -race ./... |
@@ -0,0 +1,29 @@ | |||
Blackfriday is distributed under the Simplified BSD License: | |||
> Copyright © 2011 Russ Ross | |||
> All rights reserved. | |||
> | |||
> Redistribution and use in source and binary forms, with or without | |||
> modification, are permitted provided that the following conditions | |||
> are met: | |||
> | |||
> 1. Redistributions of source code must retain the above copyright | |||
> notice, this list of conditions and the following disclaimer. | |||
> | |||
> 2. Redistributions in binary form must reproduce the above | |||
> copyright notice, this list of conditions and the following | |||
> disclaimer in the documentation and/or other materials provided with | |||
> the distribution. | |||
> | |||
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |||
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |||
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | |||
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | |||
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | |||
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | |||
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |||
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |||
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | |||
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |||
> POSSIBILITY OF SUCH DAMAGE. |
@@ -0,0 +1,284 @@ | |||
Saturday | |||
======== | |||
Saturday is a fork of [Blackfriday](https://github.com/russross/blackfriday) used on [Write.as](https://write.as). | |||
We love Markdown, but aren't a Markdown-only platform. So we've stripped out and modified redundant or potentially frustrating syntax in this library. | |||
## Changes | |||
* Made images and links behave like standard Markdown (now they won't render when there are spaces between label/alt-text and URL) 12db6e2f7ebcc5d6d88e5b330e4c6d88b577bc95 | |||
* Only support atx-style headings 32843b3dfc510153e76d8f535a9084fc8e22245a | |||
* Removed smart periods, quotes, angles & backticks 72080d757965efc04255fd25ad97c76ef6f03ea9 | |||
* Only support horizontal rules made of hyphens f75e5c8d41435593b7f24243e5c22b50f2b399b4 | |||
* Only support fenced code blocks, not indented blocks 8223c01e430de7fd35f3c38ef75f802734cc0cfc | |||
* Keep leading spaces in paragraphs 24845d212205e789fe24ec27ebc1c4cd121523c9 | |||
Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) [![GoDoc](https://godoc.org/github.com/russross/blackfriday?status.svg)](https://godoc.org/github.com/russross/blackfriday) | |||
----------- | |||
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It | |||
is paranoid about its input (so you can safely feed it user-supplied | |||
data), it is fast, it supports common extensions (tables, smart | |||
punctuation substitutions, etc.), and it is safe for all utf-8 | |||
(unicode) input. | |||
HTML output is currently supported, along with Smartypants | |||
extensions. An experimental LaTeX output engine is also included. | |||
It started as a translation from C of [Sundown][3]. | |||
### Installation | |||
Blackfriday is compatible with Go 1. If you are using an older | |||
release of Go, consider using v1.1 of blackfriday, which was based | |||
on the last stable release of Go prior to Go 1. You can find it as a | |||
tagged commit on github. | |||
With Go 1 and git installed: | |||
go get github.com/russross/blackfriday | |||
will download, compile, and install the package into your `$GOPATH` | |||
directory hierarchy. Alternatively, you can achieve the same if you | |||
import it into a project: | |||
import "github.com/russross/blackfriday" | |||
and `go get` without parameters. | |||
### Usage | |||
For basic usage, it is as simple as getting your input into a byte | |||
slice and calling: | |||
output := blackfriday.MarkdownBasic(input) | |||
This renders it with no extensions enabled. To get a more useful | |||
feature set, use this instead: | |||
output := blackfriday.MarkdownCommon(input) | |||
#### Sanitize untrusted content | |||
Blackfriday itself does nothing to protect against malicious content. If you are | |||
dealing with user-supplied markdown, we recommend running blackfriday's output | |||
through HTML sanitizer such as | |||
[Bluemonday](https://github.com/microcosm-cc/bluemonday). | |||
Here's an example of simple usage of blackfriday together with bluemonday: | |||
``` go | |||
import ( | |||
"github.com/microcosm-cc/bluemonday" | |||
"github.com/russross/blackfriday" | |||
) | |||
// ... | |||
unsafe := blackfriday.MarkdownCommon(input) | |||
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) | |||
``` | |||
#### Custom options | |||
If you want to customize the set of options, first get a renderer | |||
(currently either the HTML or LaTeX output engines), then use it to | |||
call the more general `Markdown` function. For examples, see the | |||
implementations of `MarkdownBasic` and `MarkdownCommon` in | |||
`markdown.go`. | |||
You can also check out `blackfriday-tool` for a more complete example | |||
of how to use it. Download and install it using: | |||
go get github.com/russross/blackfriday-tool | |||
This is a simple command-line tool that allows you to process a | |||
markdown file using a standalone program. You can also browse the | |||
source directly on github if you are just looking for some example | |||
code: | |||
* <http://github.com/russross/blackfriday-tool> | |||
Note that if you have not already done so, installing | |||
`blackfriday-tool` will be sufficient to download and install | |||
blackfriday in addition to the tool itself. The tool binary will be | |||
installed in `$GOPATH/bin`. This is a statically-linked binary that | |||
can be copied to wherever you need it without worrying about | |||
dependencies and library versions. | |||
### Features | |||
All features of Sundown are supported, including: | |||
* **Compatibility**. The Markdown v1.0.3 test suite passes with | |||
the `--tidy` option. Without `--tidy`, the differences are | |||
mostly in whitespace and entity escaping, where blackfriday is | |||
more consistent and cleaner. | |||
* **Common extensions**, including table support, fenced code | |||
blocks, autolinks, strikethroughs, non-strict emphasis, etc. | |||
* **Safety**. Blackfriday is paranoid when parsing, making it safe | |||
to feed untrusted user input without fear of bad things | |||
happening. The test suite stress tests this and there are no | |||
known inputs that make it crash. If you find one, please let me | |||
know and send me the input that does it. | |||
NOTE: "safety" in this context means *runtime safety only*. In order to | |||
protect yourself against JavaScript injection in untrusted content, see | |||
[this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). | |||
* **Fast processing**. It is fast enough to render on-demand in | |||
most web applications without having to cache the output. | |||
* **Thread safety**. You can run multiple parsers in different | |||
goroutines without ill effect. There is no dependence on global | |||
shared state. | |||
* **Minimal dependencies**. Blackfriday only depends on standard | |||
library packages in Go. The source code is pretty | |||
self-contained, so it is easy to add to any project, including | |||
Google App Engine projects. | |||
* **Standards compliant**. Output successfully validates using the | |||
W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. | |||
### Extensions | |||
In addition to the standard markdown syntax, this package | |||
implements the following extensions: | |||
* **Intra-word emphasis supression**. The `_` character is | |||
commonly used inside words when discussing code, so having | |||
markdown interpret it as an emphasis command is usually the | |||
wrong thing. Blackfriday lets you treat all emphasis markers as | |||
normal characters when they occur inside a word. | |||
* **Tables**. Tables can be created by drawing them in the input | |||
using a simple syntax: | |||
``` | |||
Name | Age | |||
--------|------ | |||
Bob | 27 | |||
Alice | 23 | |||
``` | |||
* **Fenced code blocks**. In addition to the normal 4-space | |||
indentation to mark code blocks, you can explicitly mark them | |||
and supply a language (to make syntax highlighting simple). Just | |||
mark it like this: | |||
``` go | |||
func getTrue() bool { | |||
return true | |||
} | |||
``` | |||
You can use 3 or more backticks to mark the beginning of the | |||
block, and the same number to mark the end of the block. | |||
To preserve classes of fenced code blocks while using the bluemonday | |||
HTML sanitizer, use the following policy: | |||
``` go | |||
p := bluemonday.UGCPolicy() | |||
p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") | |||
html := p.SanitizeBytes(unsafe) | |||
``` | |||
* **Definition lists**. A simple definition list is made of a single-line | |||
term followed by a colon and the definition for that term. | |||
Cat | |||
: Fluffy animal everyone likes | |||
Internet | |||
: Vector of transmission for pictures of cats | |||
Terms must be separated from the previous definition by a blank line. | |||
* **Footnotes**. A marker in the text that will become a superscript number; | |||
a footnote definition that will be placed in a list of footnotes at the | |||
end of the document. A footnote looks like this: | |||
This is a footnote.[^1] | |||
[^1]: the footnote text. | |||
* **Autolinking**. Blackfriday can find URLs that have not been | |||
explicitly marked as links and turn them into links. | |||
* **Strikethrough**. Use two tildes (`~~`) to mark text that | |||
should be crossed out. | |||
* **Hard line breaks**. With this extension enabled (it is off by | |||
default in the `MarkdownBasic` and `MarkdownCommon` convenience | |||
functions), newlines in the input translate into line breaks in | |||
the output. | |||
* **Smart quotes**. Smartypants-style punctuation substitution is | |||
supported, turning normal double- and single-quote marks into | |||
curly quotes, etc. | |||
* **LaTeX-style dash parsing** is an additional option, where `--` | |||
is translated into `–`, and `---` is translated into | |||
`—`. This differs from most smartypants processors, which | |||
turn a single hyphen into an ndash and a double hyphen into an | |||
mdash. | |||
* **Smart fractions**, where anything that looks like a fraction | |||
is translated into suitable HTML (instead of just a few special | |||
cases like most smartypant processors). For example, `4/5` | |||
becomes `<sup>4</sup>⁄<sub>5</sub>`, which renders as | |||
<sup>4</sup>⁄<sub>5</sub>. | |||
### Other renderers | |||
Blackfriday is structured to allow alternative rendering engines. Here | |||
are a few of note: | |||
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): | |||
provides a GitHub Flavored Markdown renderer with fenced code block | |||
highlighting, clickable header anchor links. | |||
It's not customizable, and its goal is to produce HTML output | |||
equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), | |||
except the rendering is performed locally. | |||
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, | |||
but for markdown. | |||
* LaTeX output: renders output as LaTeX. This is currently part of the | |||
main Blackfriday repository, but may be split into its own project | |||
in the future. If you are interested in owning and maintaining the | |||
LaTeX output component, please be in touch. | |||
It renders some basic documents, but is only experimental at this | |||
point. In particular, it does not do any inline escaping, so input | |||
that happens to look like LaTeX code will be passed through without | |||
modification. | |||
* [Md2Vim](https://github.com/FooSoft/md2vim): transforms markdown files into vimdoc format. | |||
### Todo | |||
* More unit testing | |||
* Improve unicode support. It does not understand all unicode | |||
rules (about what constitutes a letter, a punctuation symbol, | |||
etc.), so it may fail to detect word boundaries correctly in | |||
some instances. It is safe on all utf-8 input. | |||
### License | |||
[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) | |||
[1]: http://daringfireball.net/projects/markdown/ "Markdown" | |||
[2]: http://golang.org/ "Go Language" | |||
[3]: https://github.com/vmg/sundown "Sundown" |
@@ -0,0 +1,949 @@ | |||
// | |||
// Blackfriday Markdown Processor | |||
// Available at http://github.com/russross/blackfriday | |||
// | |||
// Copyright © 2011 Russ Ross <russ@russross.com>. | |||
// Distributed under the Simplified BSD License. | |||
// See README.md for details. | |||
// | |||
// | |||
// | |||
// HTML rendering backend | |||
// | |||
// | |||
package blackfriday | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"regexp" | |||
"strconv" | |||
"strings" | |||
) | |||
// Html renderer configuration options. | |||
const ( | |||
HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks | |||
HTML_SKIP_STYLE // skip embedded <style> elements | |||
HTML_SKIP_IMAGES // skip embedded images | |||
HTML_SKIP_LINKS // skip all links | |||
HTML_SAFELINK // only link to trusted protocols | |||
HTML_NOFOLLOW_LINKS // only link with rel="nofollow" | |||
HTML_NOREFERRER_LINKS // only link with rel="noreferrer" | |||
HTML_HREF_TARGET_BLANK // add a blank target | |||
HTML_TOC // generate a table of contents | |||
HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents) | |||
HTML_COMPLETE_PAGE // generate a complete HTML page | |||
HTML_USE_XHTML // generate XHTML output instead of HTML | |||
HTML_USE_SMARTYPANTS // enable smart punctuation substitutions | |||
HTML_SMARTYPANTS_FRACTIONS // enable smart fractions (with HTML_USE_SMARTYPANTS) | |||
HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS) | |||
HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES) | |||
HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering | |||
HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source | |||
) | |||
var ( | |||
alignments = []string{ | |||
"left", | |||
"right", | |||
"center", | |||
} | |||
// TODO: improve this regexp to catch all possible entities: | |||
htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`) | |||
) | |||
type HtmlRendererParameters struct { | |||
// Prepend this text to each relative URL. | |||
AbsolutePrefix string | |||
// Add this text to each footnote anchor, to ensure uniqueness. | |||
FootnoteAnchorPrefix string | |||
// Show this text inside the <a> tag for a footnote return link, if the | |||
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string | |||
// <sup>[return]</sup> is used. | |||
FootnoteReturnLinkContents string | |||
// If set, add this text to the front of each Header ID, to ensure | |||
// uniqueness. | |||
HeaderIDPrefix string | |||
// If set, add this text to the back of each Header ID, to ensure uniqueness. | |||
HeaderIDSuffix string | |||
} | |||
// Html is a type that implements the Renderer interface for HTML output. | |||
// | |||
// Do not create this directly, instead use the HtmlRenderer function. | |||
type Html struct { | |||
flags int // HTML_* options | |||
closeTag string // how to end singleton tags: either " />" or ">" | |||
title string // document title | |||
css string // optional css file url (used with HTML_COMPLETE_PAGE) | |||
parameters HtmlRendererParameters | |||
// table of contents data | |||
tocMarker int | |||
headerCount int | |||
currentLevel int | |||
toc *bytes.Buffer | |||
// Track header IDs to prevent ID collision in a single generation. | |||
headerIDs map[string]int | |||
smartypants *smartypantsRenderer | |||
} | |||
const ( | |||
xhtmlClose = " />" | |||
htmlClose = ">" | |||
) | |||
// HtmlRenderer creates and configures an Html object, which | |||
// satisfies the Renderer interface. | |||
// | |||
// flags is a set of HTML_* options ORed together. | |||
// title is the title of the document, and css is a URL for the document's | |||
// stylesheet. | |||
// title and css are only used when HTML_COMPLETE_PAGE is selected. | |||
func HtmlRenderer(flags int, title string, css string) Renderer { | |||
return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{}) | |||
} | |||
func HtmlRendererWithParameters(flags int, title string, | |||
css string, renderParameters HtmlRendererParameters) Renderer { | |||
// configure the rendering engine | |||
closeTag := htmlClose | |||
if flags&HTML_USE_XHTML != 0 { | |||
closeTag = xhtmlClose | |||
} | |||
if renderParameters.FootnoteReturnLinkContents == "" { | |||
renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>` | |||
} | |||
return &Html{ | |||
flags: flags, | |||
closeTag: closeTag, | |||
title: title, | |||
css: css, | |||
parameters: renderParameters, | |||
headerCount: 0, | |||
currentLevel: 0, | |||
toc: new(bytes.Buffer), | |||
headerIDs: make(map[string]int), | |||
smartypants: smartypants(flags), | |||
} | |||
} | |||
// Using if statements is a bit faster than a switch statement. As the compiler | |||
// improves, this should be unnecessary this is only worthwhile because | |||
// attrEscape is the single largest CPU user in normal use. | |||
// Also tried using map, but that gave a ~3x slowdown. | |||
func escapeSingleChar(char byte) (string, bool) { | |||
if char == '"' { | |||
return """, true | |||
} | |||
if char == '&' { | |||
return "&", true | |||
} | |||
if char == '<' { | |||
return "<", true | |||
} | |||
if char == '>' { | |||
return ">", true | |||
} | |||
return "", false | |||
} | |||
func attrEscape(out *bytes.Buffer, src []byte) { | |||
org := 0 | |||
for i, ch := range src { | |||
if entity, ok := escapeSingleChar(ch); ok { | |||
if i > org { | |||
// copy all the normal characters since the last escape | |||
out.Write(src[org:i]) | |||
} | |||
org = i + 1 | |||
out.WriteString(entity) | |||
} | |||
} | |||
if org < len(src) { | |||
out.Write(src[org:]) | |||
} | |||
} | |||
func entityEscapeWithSkip(out *bytes.Buffer, src []byte, skipRanges [][]int) { | |||
end := 0 | |||
for _, rang := range skipRanges { | |||
attrEscape(out, src[end:rang[0]]) | |||
out.Write(src[rang[0]:rang[1]]) | |||
end = rang[1] | |||
} | |||
attrEscape(out, src[end:]) | |||
} | |||
func (options *Html) GetFlags() int { | |||
return options.flags | |||
} | |||
func (options *Html) TitleBlock(out *bytes.Buffer, text []byte) { | |||
text = bytes.TrimPrefix(text, []byte("% ")) | |||
text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1) | |||
out.WriteString("<h1 class=\"title\">") | |||
out.Write(text) | |||
out.WriteString("\n</h1>") | |||
} | |||
func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id string) { | |||
marker := out.Len() | |||
doubleSpace(out) | |||
if id == "" && options.flags&HTML_TOC != 0 { | |||
id = fmt.Sprintf("toc_%d", options.headerCount) | |||
} | |||
if id != "" { | |||
id = options.ensureUniqueHeaderID(id) | |||
if options.parameters.HeaderIDPrefix != "" { | |||
id = options.parameters.HeaderIDPrefix + id | |||
} | |||
if options.parameters.HeaderIDSuffix != "" { | |||
id = id + options.parameters.HeaderIDSuffix | |||
} | |||
out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id)) | |||
} else { | |||
out.WriteString(fmt.Sprintf("<h%d>", level)) | |||
} | |||
tocMarker := out.Len() | |||
if !text() { | |||
out.Truncate(marker) | |||
return | |||
} | |||
// are we building a table of contents? | |||
if options.flags&HTML_TOC != 0 { | |||
options.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id) | |||
} | |||
out.WriteString(fmt.Sprintf("</h%d>\n", level)) | |||
} | |||
func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) { | |||
if options.flags&HTML_SKIP_HTML != 0 { | |||
return | |||
} | |||
doubleSpace(out) | |||
out.Write(text) | |||
out.WriteByte('\n') | |||
} | |||
func (options *Html) HRule(out *bytes.Buffer) { | |||
doubleSpace(out) | |||
out.WriteString("<hr") | |||
out.WriteString(options.closeTag) | |||
out.WriteByte('\n') | |||
} | |||
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, lang string) { | |||
doubleSpace(out) | |||
// parse out the language names/classes | |||
count := 0 | |||
for _, elt := range strings.Fields(lang) { | |||
if elt[0] == '.' { | |||
elt = elt[1:] | |||
} | |||
if len(elt) == 0 { | |||
continue | |||
} | |||
if count == 0 { | |||
out.WriteString("<pre><code class=\"language-") | |||
} else { | |||
out.WriteByte(' ') | |||
} | |||
attrEscape(out, []byte(elt)) | |||
count++ | |||
} | |||
if count == 0 { | |||
out.WriteString("<pre><code>") | |||
} else { | |||
out.WriteString("\">") | |||
} | |||
attrEscape(out, text) | |||
out.WriteString("</code></pre>\n") | |||
} | |||
func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) { | |||
doubleSpace(out) | |||
out.WriteString("<blockquote>\n") | |||
out.Write(text) | |||
out.WriteString("</blockquote>\n") | |||
} | |||
func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { | |||
doubleSpace(out) | |||
out.WriteString("<table>\n<thead>\n") | |||
out.Write(header) | |||
out.WriteString("</thead>\n\n<tbody>\n") | |||
out.Write(body) | |||
out.WriteString("</tbody>\n</table>\n") | |||
} | |||
func (options *Html) TableRow(out *bytes.Buffer, text []byte) { | |||
doubleSpace(out) | |||
out.WriteString("<tr>\n") | |||
out.Write(text) | |||
out.WriteString("\n</tr>\n") | |||
} | |||
func (options *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { | |||
doubleSpace(out) | |||
switch align { | |||
case TABLE_ALIGNMENT_LEFT: | |||
out.WriteString("<th align=\"left\">") | |||
case TABLE_ALIGNMENT_RIGHT: | |||
out.WriteString("<th align=\"right\">") | |||
case TABLE_ALIGNMENT_CENTER: | |||
out.WriteString("<th align=\"center\">") | |||
default: | |||
out.WriteString("<th>") | |||
} | |||
out.Write(text) | |||
out.WriteString("</th>") | |||
} | |||
func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) { | |||
doubleSpace(out) | |||
switch align { | |||
case TABLE_ALIGNMENT_LEFT: | |||
out.WriteString("<td align=\"left\">") | |||
case TABLE_ALIGNMENT_RIGHT: | |||
out.WriteString("<td align=\"right\">") | |||
case TABLE_ALIGNMENT_CENTER: | |||
out.WriteString("<td align=\"center\">") | |||
default: | |||
out.WriteString("<td>") | |||
} | |||
out.Write(text) | |||
out.WriteString("</td>") | |||
} | |||
func (options *Html) Footnotes(out *bytes.Buffer, text func() bool) { | |||
out.WriteString("<div class=\"footnotes\">\n") | |||
options.HRule(out) | |||
options.List(out, text, LIST_TYPE_ORDERED) | |||
out.WriteString("</div>\n") | |||
} | |||
func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { | |||
if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { | |||
doubleSpace(out) | |||
} | |||
slug := slugify(name) | |||
out.WriteString(`<li id="`) | |||
out.WriteString(`fn:`) | |||
out.WriteString(options.parameters.FootnoteAnchorPrefix) | |||
out.Write(slug) | |||
out.WriteString(`">`) | |||
out.Write(text) | |||
if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 { | |||
out.WriteString(` <a class="footnote-return" href="#`) | |||
out.WriteString(`fnref:`) | |||
out.WriteString(options.parameters.FootnoteAnchorPrefix) | |||
out.Write(slug) | |||
out.WriteString(`">`) | |||
out.WriteString(options.parameters.FootnoteReturnLinkContents) | |||
out.WriteString(`</a>`) | |||
} | |||
out.WriteString("</li>\n") | |||
} | |||
func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) { | |||
marker := out.Len() | |||
doubleSpace(out) | |||
if flags&LIST_TYPE_DEFINITION != 0 { | |||
out.WriteString("<dl>") | |||
} else if flags&LIST_TYPE_ORDERED != 0 { | |||
out.WriteString("<ol>") | |||
} else { | |||
out.WriteString("<ul>") | |||
} | |||
if !text() { | |||
out.Truncate(marker) | |||
return | |||
} | |||
if flags&LIST_TYPE_DEFINITION != 0 { | |||
out.WriteString("</dl>\n") | |||
} else if flags&LIST_TYPE_ORDERED != 0 { | |||
out.WriteString("</ol>\n") | |||
} else { | |||
out.WriteString("</ul>\n") | |||
} | |||
} | |||
func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) { | |||
if (flags&LIST_ITEM_CONTAINS_BLOCK != 0 && flags&LIST_TYPE_DEFINITION == 0) || | |||
flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { | |||
doubleSpace(out) | |||
} | |||
if flags&LIST_TYPE_TERM != 0 { | |||
out.WriteString("<dt>") | |||
} else if flags&LIST_TYPE_DEFINITION != 0 { | |||
out.WriteString("<dd>") | |||
} else { | |||
out.WriteString("<li>") | |||
} | |||
out.Write(text) | |||
if flags&LIST_TYPE_TERM != 0 { | |||
out.WriteString("</dt>\n") | |||
} else if flags&LIST_TYPE_DEFINITION != 0 { | |||
out.WriteString("</dd>\n") | |||
} else { | |||
out.WriteString("</li>\n") | |||
} | |||
} | |||
func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) { | |||
marker := out.Len() | |||
doubleSpace(out) | |||
out.WriteString("<p>") | |||
if !text() { | |||
out.Truncate(marker) | |||
return | |||
} | |||
out.WriteString("</p>\n") | |||
} | |||
func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) { | |||
skipRanges := htmlEntity.FindAllIndex(link, -1) | |||
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL { | |||
// mark it but don't link it if it is not a safe link: no smartypants | |||
out.WriteString("<tt>") | |||
entityEscapeWithSkip(out, link, skipRanges) | |||
out.WriteString("</tt>") | |||
return | |||
} | |||
out.WriteString("<a href=\"") | |||
if kind == LINK_TYPE_EMAIL { | |||
out.WriteString("mailto:") | |||
} else { | |||
options.maybeWriteAbsolutePrefix(out, link) | |||
} | |||
entityEscapeWithSkip(out, link, skipRanges) | |||
var relAttrs []string | |||
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) { | |||
relAttrs = append(relAttrs, "nofollow") | |||
} | |||
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) { | |||
relAttrs = append(relAttrs, "noreferrer") | |||
} | |||
if len(relAttrs) > 0 { | |||
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " "))) | |||
} | |||
// blank target only add to external link | |||
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) { | |||
out.WriteString("\" target=\"_blank") | |||
} | |||
out.WriteString("\">") | |||
// Pretty print: if we get an email address as | |||
// an actual URI, e.g. `mailto:foo@bar.com`, we don't | |||
// want to print the `mailto:` prefix | |||
switch { | |||
case bytes.HasPrefix(link, []byte("mailto://")): | |||
attrEscape(out, link[len("mailto://"):]) | |||
case bytes.HasPrefix(link, []byte("mailto:")): | |||
attrEscape(out, link[len("mailto:"):]) | |||
default: | |||
entityEscapeWithSkip(out, link, skipRanges) | |||
} | |||
out.WriteString("</a>") | |||
} | |||
func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) { | |||
out.WriteString("<code>") | |||
attrEscape(out, text) | |||
out.WriteString("</code>") | |||
} | |||
func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) { | |||
out.WriteString("<strong>") | |||
out.Write(text) | |||
out.WriteString("</strong>") | |||
} | |||
func (options *Html) Emphasis(out *bytes.Buffer, text []byte) { | |||
if len(text) == 0 { | |||
return | |||
} | |||
out.WriteString("<em>") | |||
out.Write(text) | |||
out.WriteString("</em>") | |||
} | |||
func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) { | |||
if options.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { | |||
out.WriteString(options.parameters.AbsolutePrefix) | |||
if link[0] != '/' { | |||
out.WriteByte('/') | |||
} | |||
} | |||
} | |||
func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { | |||
if options.flags&HTML_SKIP_IMAGES != 0 { | |||
return | |||
} | |||
out.WriteString("<img src=\"") | |||
options.maybeWriteAbsolutePrefix(out, link) | |||
attrEscape(out, link) | |||
out.WriteString("\" alt=\"") | |||
if len(alt) > 0 { | |||
attrEscape(out, alt) | |||
} | |||
if len(title) > 0 { | |||
out.WriteString("\" title=\"") | |||
attrEscape(out, title) | |||
} | |||
out.WriteByte('"') | |||
out.WriteString(options.closeTag) | |||
} | |||
func (options *Html) LineBreak(out *bytes.Buffer) { | |||
out.WriteString("<br") | |||
out.WriteString(options.closeTag) | |||
out.WriteByte('\n') | |||
} | |||
func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { | |||
if options.flags&HTML_SKIP_LINKS != 0 { | |||
// write the link text out but don't link it, just mark it with typewriter font | |||
out.WriteString("<tt>") | |||
attrEscape(out, content) | |||
out.WriteString("</tt>") | |||
return | |||
} | |||
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) { | |||
// write the link text out but don't link it, just mark it with typewriter font | |||
out.WriteString("<tt>") | |||
attrEscape(out, content) | |||
out.WriteString("</tt>") | |||
return | |||
} | |||
out.WriteString("<a href=\"") | |||
options.maybeWriteAbsolutePrefix(out, link) | |||
attrEscape(out, link) | |||
if len(title) > 0 { | |||
out.WriteString("\" title=\"") | |||
attrEscape(out, title) | |||
} | |||
var relAttrs []string | |||
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) { | |||
relAttrs = append(relAttrs, "nofollow") | |||
} | |||
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) { | |||
relAttrs = append(relAttrs, "noreferrer") | |||
} | |||
if len(relAttrs) > 0 { | |||
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " "))) | |||
} | |||
// blank target only add to external link | |||
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) { | |||
out.WriteString("\" target=\"_blank") | |||
} | |||
out.WriteString("\">") | |||
out.Write(content) | |||
out.WriteString("</a>") | |||
return | |||
} | |||
func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) { | |||
if options.flags&HTML_SKIP_HTML != 0 { | |||
return | |||
} | |||
if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") { | |||
return | |||
} | |||
if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") { | |||
return | |||
} | |||
if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") { | |||
return | |||
} | |||
out.Write(text) | |||
} | |||
func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) { | |||
out.WriteString("<strong><em>") | |||
out.Write(text) | |||
out.WriteString("</em></strong>") | |||
} | |||
func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) { | |||
out.WriteString("<del>") | |||
out.Write(text) | |||
out.WriteString("</del>") | |||
} | |||
func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { | |||
slug := slugify(ref) | |||
out.WriteString(`<sup class="footnote-ref" id="`) | |||
out.WriteString(`fnref:`) | |||
out.WriteString(options.parameters.FootnoteAnchorPrefix) | |||
out.Write(slug) | |||
out.WriteString(`"><a rel="footnote" href="#`) | |||
out.WriteString(`fn:`) | |||
out.WriteString(options.parameters.FootnoteAnchorPrefix) | |||
out.Write(slug) | |||
out.WriteString(`">`) | |||
out.WriteString(strconv.Itoa(id)) | |||
out.WriteString(`</a></sup>`) | |||
} | |||
func (options *Html) Entity(out *bytes.Buffer, entity []byte) { | |||
out.Write(entity) | |||
} | |||
func (options *Html) NormalText(out *bytes.Buffer, text []byte) { | |||
if options.flags&HTML_USE_SMARTYPANTS != 0 { | |||
options.Smartypants(out, text) | |||
} else { | |||
attrEscape(out, text) | |||
} | |||
} | |||
func (options *Html) Smartypants(out *bytes.Buffer, text []byte) { | |||
smrt := smartypantsData{false, false} | |||
// first do normal entity escaping | |||
var escaped bytes.Buffer | |||
attrEscape(&escaped, text) | |||
text = escaped.Bytes() | |||
mark := 0 | |||
for i := 0; i < len(text); i++ { | |||
if action := options.smartypants[text[i]]; action != nil { | |||
if i > mark { | |||
out.Write(text[mark:i]) | |||
} | |||
previousChar := byte(0) | |||
if i > 0 { | |||
previousChar = text[i-1] | |||
} | |||
i += action(out, &smrt, previousChar, text[i:]) | |||
mark = i + 1 | |||
} | |||
} | |||
if mark < len(text) { | |||
out.Write(text[mark:]) | |||
} | |||
} | |||
func (options *Html) DocumentHeader(out *bytes.Buffer) { | |||
if options.flags&HTML_COMPLETE_PAGE == 0 { | |||
return | |||
} | |||
ending := "" | |||
if options.flags&HTML_USE_XHTML != 0 { | |||
out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") | |||
out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") | |||
out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") | |||
ending = " /" | |||
} else { | |||
out.WriteString("<!DOCTYPE html>\n") | |||
out.WriteString("<html>\n") | |||
} | |||
out.WriteString("<head>\n") | |||
out.WriteString(" <title>") | |||
options.NormalText(out, []byte(options.title)) | |||
out.WriteString("</title>\n") | |||
out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") | |||
out.WriteString(VERSION) | |||
out.WriteString("\"") | |||
out.WriteString(ending) | |||
out.WriteString(">\n") | |||
out.WriteString(" <meta charset=\"utf-8\"") | |||
out.WriteString(ending) | |||
out.WriteString(">\n") | |||
if options.css != "" { | |||
out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"") | |||
attrEscape(out, []byte(options.css)) | |||
out.WriteString("\"") | |||
out.WriteString(ending) | |||
out.WriteString(">\n") | |||
} | |||
out.WriteString("</head>\n") | |||
out.WriteString("<body>\n") | |||
options.tocMarker = out.Len() | |||
} | |||
func (options *Html) DocumentFooter(out *bytes.Buffer) { | |||
// finalize and insert the table of contents | |||
if options.flags&HTML_TOC != 0 { | |||
options.TocFinalize() | |||
// now we have to insert the table of contents into the document | |||
var temp bytes.Buffer | |||
// start by making a copy of everything after the document header | |||
temp.Write(out.Bytes()[options.tocMarker:]) | |||
// now clear the copied material from the main output buffer | |||
out.Truncate(options.tocMarker) | |||
// corner case spacing issue | |||
if options.flags&HTML_COMPLETE_PAGE != 0 { | |||
out.WriteByte('\n') | |||
} | |||
// insert the table of contents | |||
out.WriteString("<nav>\n") | |||
out.Write(options.toc.Bytes()) | |||
out.WriteString("</nav>\n") | |||
// corner case spacing issue | |||
if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 { | |||
out.WriteByte('\n') | |||
} | |||
// write out everything that came after it | |||
if options.flags&HTML_OMIT_CONTENTS == 0 { | |||
out.Write(temp.Bytes()) | |||
} | |||
} | |||
if options.flags&HTML_COMPLETE_PAGE != 0 { | |||
out.WriteString("\n</body>\n") | |||
out.WriteString("</html>\n") | |||
} | |||
} | |||
func (options *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) { | |||
for level > options.currentLevel { | |||
switch { | |||
case bytes.HasSuffix(options.toc.Bytes(), []byte("</li>\n")): | |||
// this sublist can nest underneath a header | |||
size := options.toc.Len() | |||
options.toc.Truncate(size - len("</li>\n")) | |||
case options.currentLevel > 0: | |||
options.toc.WriteString("<li>") | |||
} | |||
if options.toc.Len() > 0 { | |||
options.toc.WriteByte('\n') | |||
} | |||
options.toc.WriteString("<ul>\n") | |||
options.currentLevel++ | |||
} | |||
for level < options.currentLevel { | |||
options.toc.WriteString("</ul>") | |||
if options.currentLevel > 1 { | |||
options.toc.WriteString("</li>\n") | |||
} | |||
options.currentLevel-- | |||
} | |||
options.toc.WriteString("<li><a href=\"#") | |||
if anchor != "" { | |||
options.toc.WriteString(anchor) | |||
} else { | |||
options.toc.WriteString("toc_") | |||
options.toc.WriteString(strconv.Itoa(options.headerCount)) | |||
} | |||
options.toc.WriteString("\">") | |||
options.headerCount++ | |||
options.toc.Write(text) | |||
options.toc.WriteString("</a></li>\n") | |||
} | |||
func (options *Html) TocHeader(text []byte, level int) { | |||
options.TocHeaderWithAnchor(text, level, "") | |||
} | |||
func (options *Html) TocFinalize() { | |||
for options.currentLevel > 1 { | |||
options.toc.WriteString("</ul></li>\n") | |||
options.currentLevel-- | |||
} | |||
if options.currentLevel > 0 { | |||
options.toc.WriteString("</ul>\n") | |||
} | |||
} | |||
func isHtmlTag(tag []byte, tagname string) bool { | |||
found, _ := findHtmlTagPos(tag, tagname) | |||
return found | |||
} | |||
// Look for a character, but ignore it when it's in any kind of quotes, it | |||
// might be JavaScript | |||
func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { | |||
inSingleQuote := false | |||
inDoubleQuote := false | |||
inGraveQuote := false | |||
i := start | |||
for i < len(html) { | |||
switch { | |||
case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: | |||
return i | |||
case html[i] == '\'': | |||
inSingleQuote = !inSingleQuote | |||
case html[i] == '"': | |||
inDoubleQuote = !inDoubleQuote | |||
case html[i] == '`': | |||
inGraveQuote = !inGraveQuote | |||
} | |||
i++ | |||
} | |||
return start | |||
} | |||
func findHtmlTagPos(tag []byte, tagname string) (bool, int) { | |||
i := 0 | |||
if i < len(tag) && tag[0] != '<' { | |||
return false, -1 | |||
} | |||
i++ | |||
i = skipSpace(tag, i) | |||
if i < len(tag) && tag[i] == '/' { | |||
i++ | |||
} | |||
i = skipSpace(tag, i) | |||
j := 0 | |||
for ; i < len(tag); i, j = i+1, j+1 { | |||
if j >= len(tagname) { | |||
break | |||
} | |||
if strings.ToLower(string(tag[i]))[0] != tagname[j] { | |||
return false, -1 | |||
} | |||
} | |||
if i == len(tag) { | |||
return false, -1 | |||
} | |||
rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') | |||
if rightAngle > i { | |||
return true, rightAngle | |||
} | |||
return false, -1 | |||
} | |||
func skipUntilChar(text []byte, start int, char byte) int { | |||
i := start | |||
for i < len(text) && text[i] != char { | |||
i++ | |||
} | |||
return i | |||
} | |||
func skipSpace(tag []byte, i int) int { | |||
for i < len(tag) && isspace(tag[i]) { | |||
i++ | |||
} | |||
return i | |||
} | |||
func skipChar(data []byte, start int, char byte) int { | |||
i := start | |||
for i < len(data) && data[i] == char { | |||
i++ | |||
} | |||
return i | |||
} | |||
func doubleSpace(out *bytes.Buffer) { | |||
if out.Len() > 0 { | |||
out.WriteByte('\n') | |||
} | |||
} | |||
func isRelativeLink(link []byte) (yes bool) { | |||
// a tag begin with '#' | |||
if link[0] == '#' { | |||
return true | |||
} | |||
// link begin with '/' but not '//', the second maybe a protocol relative link | |||
if len(link) >= 2 && link[0] == '/' && link[1] != '/' { | |||
return true | |||
} | |||
// only the root '/' | |||
if len(link) == 1 && link[0] == '/' { | |||
return true | |||
} | |||
// current directory : begin with "./" | |||
if bytes.HasPrefix(link, []byte("./")) { | |||
return true | |||
} | |||
// parent directory : begin with "../" | |||
if bytes.HasPrefix(link, []byte("../")) { | |||
return true | |||
} | |||
return false | |||
} | |||
func (options *Html) ensureUniqueHeaderID(id string) string { | |||
for count, found := options.headerIDs[id]; found; count, found = options.headerIDs[id] { | |||
tmp := fmt.Sprintf("%s-%d", id, count+1) | |||
if _, tmpFound := options.headerIDs[tmp]; !tmpFound { | |||
options.headerIDs[id] = count + 1 | |||
id = tmp | |||
} else { | |||
id = id + "-1" | |||
} | |||
} | |||
if _, found := options.headerIDs[id]; !found { | |||
options.headerIDs[id] = 0 | |||
} | |||
return id | |||
} |
@@ -0,0 +1,332 @@ | |||
// | |||
// Blackfriday Markdown Processor | |||
// Available at http://github.com/russross/blackfriday | |||
// | |||
// Copyright © 2011 Russ Ross <russ@russross.com>. | |||
// Distributed under the Simplified BSD License. | |||
// See README.md for details. | |||
// | |||
// | |||
// | |||
// LaTeX rendering backend | |||
// | |||
// | |||
package blackfriday | |||
import ( | |||
"bytes" | |||
) | |||
// Latex is a type that implements the Renderer interface for LaTeX output. | |||
// | |||
// Do not create this directly, instead use the LatexRenderer function. | |||
type Latex struct { | |||
} | |||
// LatexRenderer creates and configures a Latex object, which | |||
// satisfies the Renderer interface. | |||
// | |||
// flags is a set of LATEX_* options ORed together (currently no such options | |||
// are defined). | |||
func LatexRenderer(flags int) Renderer { | |||
return &Latex{} | |||
} | |||
func (options *Latex) GetFlags() int { | |||
return 0 | |||
} | |||
// render code chunks using verbatim, or listings if we have a language | |||
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, lang string) { | |||
if lang == "" { | |||
out.WriteString("\n\\begin{verbatim}\n") | |||
} else { | |||
out.WriteString("\n\\begin{lstlisting}[language=") | |||
out.WriteString(lang) | |||
out.WriteString("]\n") | |||
} | |||
out.Write(text) | |||
if lang == "" { | |||
out.WriteString("\n\\end{verbatim}\n") | |||
} else { | |||
out.WriteString("\n\\end{lstlisting}\n") | |||
} | |||
} | |||
func (options *Latex) TitleBlock(out *bytes.Buffer, text []byte) { | |||
} | |||
func (options *Latex) BlockQuote(out *bytes.Buffer, text []byte) { | |||
out.WriteString("\n\\begin{quotation}\n") | |||
out.Write(text) | |||
out.WriteString("\n\\end{quotation}\n") | |||
} | |||
func (options *Latex) BlockHtml(out *bytes.Buffer, text []byte) { | |||
// a pretty lame thing to do... | |||
out.WriteString("\n\\begin{verbatim}\n") | |||
out.Write(text) | |||
out.WriteString("\n\\end{verbatim}\n") | |||
} | |||
func (options *Latex) Header(out *bytes.Buffer, text func() bool, level int, id string) { | |||
marker := out.Len() | |||
switch level { | |||
case 1: | |||
out.WriteString("\n\\section{") | |||
case 2: | |||
out.WriteString("\n\\subsection{") | |||
case 3: | |||
out.WriteString("\n\\subsubsection{") | |||
case 4: | |||
out.WriteString("\n\\paragraph{") | |||
case 5: | |||
out.WriteString("\n\\subparagraph{") | |||
case 6: | |||
out.WriteString("\n\\textbf{") | |||
} | |||
if !text() { | |||
out.Truncate(marker) | |||
return | |||
} | |||
out.WriteString("}\n") | |||
} | |||
func (options *Latex) HRule(out *bytes.Buffer) { | |||
out.WriteString("\n\\HRule\n") | |||
} | |||
func (options *Latex) List(out *bytes.Buffer, text func() bool, flags int) { | |||
marker := out.Len() | |||
if flags&LIST_TYPE_ORDERED != 0 { | |||
out.WriteString("\n\\begin{enumerate}\n") | |||
} else { | |||
out.WriteString("\n\\begin{itemize}\n") | |||
} | |||
if !text() { | |||
out.Truncate(marker) | |||
return | |||
} | |||
if flags&LIST_TYPE_ORDERED != 0 { | |||
out.WriteString("\n\\end{enumerate}\n") | |||
} else { | |||
out.WriteString("\n\\end{itemize}\n") | |||
} | |||
} | |||
func (options *Latex) ListItem(out *bytes.Buffer, text []byte, flags int) { | |||
out.WriteString("\n\\item ") | |||
out.Write(text) | |||
} | |||
func (options *Latex) Paragraph(out *bytes.Buffer, text func() bool) { | |||
marker := out.Len() | |||
out.WriteString("\n") | |||
if !text() { | |||
out.Truncate(marker) | |||
return | |||
} | |||
out.WriteString("\n") | |||
} | |||
func (options *Latex) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { | |||
out.WriteString("\n\\begin{tabular}{") | |||
for _, elt := range columnData { | |||
switch elt { | |||
case TABLE_ALIGNMENT_LEFT: | |||
out.WriteByte('l') | |||
case TABLE_ALIGNMENT_RIGHT: | |||
out.WriteByte('r') | |||
default: | |||
out.WriteByte('c') | |||
} | |||
} | |||
out.WriteString("}\n") | |||
out.Write(header) | |||
out.WriteString(" \\\\\n\\hline\n") | |||
out.Write(body) | |||
out.WriteString("\n\\end{tabular}\n") | |||
} | |||
func (options *Latex) TableRow(out *bytes.Buffer, text []byte) { | |||
if out.Len() > 0 { | |||
out.WriteString(" \\\\\n") | |||
} | |||
out.Write(text) | |||
} | |||
func (options *Latex) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { | |||
if out.Len() > 0 { | |||
out.WriteString(" & ") | |||
} | |||
out.Write(text) | |||
} | |||
func (options *Latex) TableCell(out *bytes.Buffer, text []byte, align int) { | |||
if out.Len() > 0 { | |||
out.WriteString(" & ") | |||
} | |||
out.Write(text) | |||
} | |||
// TODO: this | |||
func (options *Latex) Footnotes(out *bytes.Buffer, text func() bool) { | |||
} | |||
func (options *Latex) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { | |||
} | |||
func (options *Latex) AutoLink(out *bytes.Buffer, link []byte, kind int) { | |||
out.WriteString("\\href{") | |||
if kind == LINK_TYPE_EMAIL { | |||
out.WriteString("mailto:") | |||
} | |||
out.Write(link) | |||
out.WriteString("}{") | |||
out.Write(link) | |||
out.WriteString("}") | |||
} | |||
func (options *Latex) CodeSpan(out *bytes.Buffer, text []byte) { | |||
out.WriteString("\\texttt{") | |||
escapeSpecialChars(out, text) | |||
out.WriteString("}") | |||
} | |||
func (options *Latex) DoubleEmphasis(out *bytes.Buffer, text []byte) { | |||
out.WriteString("\\textbf{") | |||
out.Write(text) | |||
out.WriteString("}") | |||
} | |||
func (options *Latex) Emphasis(out *bytes.Buffer, text []byte) { | |||
out.WriteString("\\textit{") | |||
out.Write(text) | |||
out.WriteString("}") | |||
} | |||
func (options *Latex) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { | |||
if bytes.HasPrefix(link, []byte("http://")) || bytes.HasPrefix(link, []byte("https://")) { | |||
// treat it like a link | |||
out.WriteString("\\href{") | |||
out.Write(link) | |||
out.WriteString("}{") | |||
out.Write(alt) | |||
out.WriteString("}") | |||
} else { | |||
out.WriteString("\\includegraphics{") | |||
out.Write(link) | |||
out.WriteString("}") | |||
} | |||
} | |||
func (options *Latex) LineBreak(out *bytes.Buffer) { | |||
out.WriteString(" \\\\\n") | |||
} | |||
func (options *Latex) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { | |||
out.WriteString("\\href{") | |||
out.Write(link) | |||
out.WriteString("}{") | |||
out.Write(content) | |||
out.WriteString("}") | |||
} | |||
func (options *Latex) RawHtmlTag(out *bytes.Buffer, tag []byte) { | |||
} | |||
func (options *Latex) TripleEmphasis(out *bytes.Buffer, text []byte) { | |||
out.WriteString("\\textbf{\\textit{") | |||
out.Write(text) | |||
out.WriteString("}}") | |||
} | |||
func (options *Latex) StrikeThrough(out *bytes.Buffer, text []byte) { | |||
out.WriteString("\\sout{") | |||
out.Write(text) | |||
out.WriteString("}") | |||
} | |||
// TODO: this | |||
func (options *Latex) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { | |||
} | |||
func needsBackslash(c byte) bool { | |||
for _, r := range []byte("_{}%$&\\~#") { | |||
if c == r { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
func escapeSpecialChars(out *bytes.Buffer, text []byte) { | |||
for i := 0; i < len(text); i++ { | |||
// directly copy normal characters | |||
org := i | |||
for i < len(text) && !needsBackslash(text[i]) { | |||
i++ | |||
} | |||
if i > org { | |||
out.Write(text[org:i]) | |||
} | |||
// escape a character | |||
if i >= len(text) { | |||
break | |||
} | |||
out.WriteByte('\\') | |||
out.WriteByte(text[i]) | |||
} | |||
} | |||
func (options *Latex) Entity(out *bytes.Buffer, entity []byte) { | |||
// TODO: convert this into a unicode character or something | |||
out.Write(entity) | |||
} | |||
func (options *Latex) NormalText(out *bytes.Buffer, text []byte) { | |||
escapeSpecialChars(out, text) | |||
} | |||
// header and footer | |||
func (options *Latex) DocumentHeader(out *bytes.Buffer) { | |||
out.WriteString("\\documentclass{article}\n") | |||
out.WriteString("\n") | |||
out.WriteString("\\usepackage{graphicx}\n") | |||
out.WriteString("\\usepackage{listings}\n") | |||
out.WriteString("\\usepackage[margin=1in]{geometry}\n") | |||
out.WriteString("\\usepackage[utf8]{inputenc}\n") | |||
out.WriteString("\\usepackage{verbatim}\n") | |||
out.WriteString("\\usepackage[normalem]{ulem}\n") | |||
out.WriteString("\\usepackage{hyperref}\n") | |||
out.WriteString("\n") | |||
out.WriteString("\\hypersetup{colorlinks,%\n") | |||
out.WriteString(" citecolor=black,%\n") | |||
out.WriteString(" filecolor=black,%\n") | |||
out.WriteString(" linkcolor=black,%\n") | |||
out.WriteString(" urlcolor=black,%\n") | |||
out.WriteString(" pdfstartview=FitH,%\n") | |||
out.WriteString(" breaklinks=true,%\n") | |||
out.WriteString(" pdfauthor={Blackfriday Markdown Processor v") | |||
out.WriteString(VERSION) | |||
out.WriteString("}}\n") | |||
out.WriteString("\n") | |||
out.WriteString("\\newcommand{\\HRule}{\\rule{\\linewidth}{0.5mm}}\n") | |||
out.WriteString("\\addtolength{\\parskip}{0.5\\baselineskip}\n") | |||
out.WriteString("\\parindent=0pt\n") | |||
out.WriteString("\n") | |||
out.WriteString("\\begin{document}\n") | |||
} | |||
func (options *Latex) DocumentFooter(out *bytes.Buffer) { | |||
out.WriteString("\n\\end{document}\n") | |||
} |
@@ -0,0 +1,926 @@ | |||
// | |||
// Blackfriday Markdown Processor | |||
// Available at http://github.com/russross/blackfriday | |||
// | |||
// Copyright © 2011 Russ Ross <russ@russross.com>. | |||
// Distributed under the Simplified BSD License. | |||
// See README.md for details. | |||
// | |||
// | |||
// | |||
// Markdown parsing and processing | |||
// | |||
// | |||
// Blackfriday markdown processor. | |||
// | |||
// Translates plain text with simple formatting rules into HTML or LaTeX. | |||
package blackfriday | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"strings" | |||
"unicode/utf8" | |||
) | |||
const VERSION = "1.5" | |||
// These are the supported markdown parsing extensions. | |||
// OR these values together to select multiple extensions. | |||
const ( | |||
EXTENSION_NO_INTRA_EMPHASIS = 1 << iota // ignore emphasis markers inside words | |||
EXTENSION_TABLES // render tables | |||
EXTENSION_FENCED_CODE // render fenced code blocks | |||
EXTENSION_AUTOLINK // detect embedded URLs that are not explicitly marked | |||
EXTENSION_STRIKETHROUGH // strikethrough text using ~~test~~ | |||
EXTENSION_LAX_HTML_BLOCKS // loosen up HTML block parsing rules | |||
EXTENSION_SPACE_HEADERS // be strict about prefix header rules | |||
EXTENSION_HARD_LINE_BREAK // translate newlines into line breaks | |||
EXTENSION_TAB_SIZE_EIGHT // expand tabs to eight spaces instead of four | |||
EXTENSION_FOOTNOTES // Pandoc-style footnotes | |||
EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block | |||
EXTENSION_HEADER_IDS // specify header IDs with {#id} | |||
EXTENSION_TITLEBLOCK // Titleblock ala pandoc | |||
EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text | |||
EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks | |||
EXTENSION_DEFINITION_LISTS // render definition lists | |||
commonHtmlFlags = 0 | | |||
HTML_USE_XHTML | | |||
HTML_USE_SMARTYPANTS | | |||
HTML_SMARTYPANTS_FRACTIONS | | |||
HTML_SMARTYPANTS_DASHES | | |||
HTML_SMARTYPANTS_LATEX_DASHES | |||
commonExtensions = 0 | | |||
EXTENSION_NO_INTRA_EMPHASIS | | |||
EXTENSION_TABLES | | |||
EXTENSION_FENCED_CODE | | |||
EXTENSION_AUTOLINK | | |||
EXTENSION_STRIKETHROUGH | | |||
EXTENSION_SPACE_HEADERS | | |||
EXTENSION_HEADER_IDS | | |||
EXTENSION_BACKSLASH_LINE_BREAK | | |||
EXTENSION_DEFINITION_LISTS | |||
) | |||
// These are the possible flag values for the link renderer. | |||
// Only a single one of these values will be used; they are not ORed together. | |||
// These are mostly of interest if you are writing a new output format. | |||
const ( | |||
LINK_TYPE_NOT_AUTOLINK = iota | |||
LINK_TYPE_NORMAL | |||
LINK_TYPE_EMAIL | |||
) | |||
// These are the possible flag values for the ListItem renderer. | |||
// Multiple flag values may be ORed together. | |||
// These are mostly of interest if you are writing a new output format. | |||
const ( | |||
LIST_TYPE_ORDERED = 1 << iota | |||
LIST_TYPE_DEFINITION | |||
LIST_TYPE_TERM | |||
LIST_ITEM_CONTAINS_BLOCK | |||
LIST_ITEM_BEGINNING_OF_LIST | |||
LIST_ITEM_END_OF_LIST | |||
) | |||
// These are the possible flag values for the table cell renderer. | |||
// Only a single one of these values will be used; they are not ORed together. | |||
// These are mostly of interest if you are writing a new output format. | |||
const ( | |||
TABLE_ALIGNMENT_LEFT = 1 << iota | |||
TABLE_ALIGNMENT_RIGHT | |||
TABLE_ALIGNMENT_CENTER = (TABLE_ALIGNMENT_LEFT | TABLE_ALIGNMENT_RIGHT) | |||
) | |||
// The size of a tab stop. | |||
const ( | |||
TAB_SIZE_DEFAULT = 4 | |||
TAB_SIZE_EIGHT = 8 | |||
) | |||
// blockTags is a set of tags that are recognized as HTML block tags. | |||
// Any of these can be included in markdown text without special escaping. | |||
var blockTags = map[string]struct{}{ | |||
"blockquote": {}, | |||
"del": {}, | |||
"div": {}, | |||
"dl": {}, | |||
"fieldset": {}, | |||
"form": {}, | |||
"h1": {}, | |||
"h2": {}, | |||
"h3": {}, | |||
"h4": {}, | |||
"h5": {}, | |||
"h6": {}, | |||
"iframe": {}, | |||
"ins": {}, | |||
"math": {}, | |||
"noscript": {}, | |||
"ol": {}, | |||
"pre": {}, | |||
"p": {}, | |||
"script": {}, | |||
"style": {}, | |||
"table": {}, | |||
"ul": {}, | |||
// HTML5 | |||
"address": {}, | |||
"article": {}, | |||
"aside": {}, | |||
"canvas": {}, | |||
"figcaption": {}, | |||
"figure": {}, | |||
"footer": {}, | |||
"header": {}, | |||
"hgroup": {}, | |||
"main": {}, | |||
"nav": {}, | |||
"output": {}, | |||
"progress": {}, | |||
"section": {}, | |||
"video": {}, | |||
} | |||
// Renderer is the rendering interface. | |||
// This is mostly of interest if you are implementing a new rendering format. | |||
// | |||
// When a byte slice is provided, it contains the (rendered) contents of the | |||
// element. | |||
// | |||
// When a callback is provided instead, it will write the contents of the | |||
// respective element directly to the output buffer and return true on success. | |||
// If the callback returns false, the rendering function should reset the | |||
// output buffer as though it had never been called. | |||
// | |||
// Currently Html and Latex implementations are provided | |||
type Renderer interface { | |||
// block-level callbacks | |||
BlockCode(out *bytes.Buffer, text []byte, lang string) | |||
BlockQuote(out *bytes.Buffer, text []byte) | |||
BlockHtml(out *bytes.Buffer, text []byte) | |||
Header(out *bytes.Buffer, text func() bool, level int, id string) | |||
HRule(out *bytes.Buffer) | |||
List(out *bytes.Buffer, text func() bool, flags int) | |||
ListItem(out *bytes.Buffer, text []byte, flags int) | |||
Paragraph(out *bytes.Buffer, text func() bool) | |||
Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) | |||
TableRow(out *bytes.Buffer, text []byte) | |||
TableHeaderCell(out *bytes.Buffer, text []byte, flags int) | |||
TableCell(out *bytes.Buffer, text []byte, flags int) | |||
Footnotes(out *bytes.Buffer, text func() bool) | |||
FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) | |||
TitleBlock(out *bytes.Buffer, text []byte) | |||
// Span-level callbacks | |||
AutoLink(out *bytes.Buffer, link []byte, kind int) | |||
CodeSpan(out *bytes.Buffer, text []byte) | |||
DoubleEmphasis(out *bytes.Buffer, text []byte) | |||
Emphasis(out *bytes.Buffer, text []byte) | |||
Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) | |||
LineBreak(out *bytes.Buffer) | |||
Link(out *bytes.Buffer, link []byte, title []byte, content []byte) | |||
RawHtmlTag(out *bytes.Buffer, tag []byte) | |||
TripleEmphasis(out *bytes.Buffer, text []byte) | |||
StrikeThrough(out *bytes.Buffer, text []byte) | |||
FootnoteRef(out *bytes.Buffer, ref []byte, id int) | |||
// Low-level callbacks | |||
Entity(out *bytes.Buffer, entity []byte) | |||
NormalText(out *bytes.Buffer, text []byte) | |||
// Header and footer | |||
DocumentHeader(out *bytes.Buffer) | |||
DocumentFooter(out *bytes.Buffer) | |||
GetFlags() int | |||
} | |||
// Callback functions for inline parsing. One such function is defined | |||
// for each character that triggers a response when parsing inline data. | |||
type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int | |||
// Parser holds runtime state used by the parser. | |||
// This is constructed by the Markdown function. | |||
type parser struct { | |||
r Renderer | |||
refOverride ReferenceOverrideFunc | |||
refs map[string]*reference | |||
inlineCallback [256]inlineParser | |||
flags int | |||
nesting int | |||
maxNesting int | |||
insideLink bool | |||
// Footnotes need to be ordered as well as available to quickly check for | |||
// presence. If a ref is also a footnote, it's stored both in refs and here | |||
// in notes. Slice is nil if footnotes not enabled. | |||
notes []*reference | |||
} | |||
func (p *parser) getRef(refid string) (ref *reference, found bool) { | |||
if p.refOverride != nil { | |||
r, overridden := p.refOverride(refid) | |||
if overridden { | |||
if r == nil { | |||
return nil, false | |||
} | |||
return &reference{ | |||
link: []byte(r.Link), | |||
title: []byte(r.Title), | |||
noteId: 0, | |||
hasBlock: false, | |||
text: []byte(r.Text)}, true | |||
} | |||
} | |||
// refs are case insensitive | |||
ref, found = p.refs[strings.ToLower(refid)] | |||
return ref, found | |||
} | |||
// | |||
// | |||
// Public interface | |||
// | |||
// | |||
// Reference represents the details of a link. | |||
// See the documentation in Options for more details on use-case. | |||
type Reference struct { | |||
// Link is usually the URL the reference points to. | |||
Link string | |||
// Title is the alternate text describing the link in more detail. | |||
Title string | |||
// Text is the optional text to override the ref with if the syntax used was | |||
// [refid][] | |||
Text string | |||
} | |||
// ReferenceOverrideFunc is expected to be called with a reference string and | |||
// return either a valid Reference type that the reference string maps to or | |||
// nil. If overridden is false, the default reference logic will be executed. | |||
// See the documentation in Options for more details on use-case. | |||
type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) | |||
// Options represents configurable overrides and callbacks (in addition to the | |||
// extension flag set) for configuring a Markdown parse. | |||
type Options struct { | |||
// Extensions is a flag set of bit-wise ORed extension bits. See the | |||
// EXTENSION_* flags defined in this package. | |||
Extensions int | |||
// ReferenceOverride is an optional function callback that is called every | |||
// time a reference is resolved. | |||
// | |||
// In Markdown, the link reference syntax can be made to resolve a link to | |||
// a reference instead of an inline URL, in one of the following ways: | |||
// | |||
// * [link text][refid] | |||
// * [refid][] | |||
// | |||
// Usually, the refid is defined at the bottom of the Markdown document. If | |||
// this override function is provided, the refid is passed to the override | |||
// function first, before consulting the defined refids at the bottom. If | |||
// the override function indicates an override did not occur, the refids at | |||
// the bottom will be used to fill in the link details. | |||
ReferenceOverride ReferenceOverrideFunc | |||
} | |||
// MarkdownBasic is a convenience function for simple rendering. | |||
// It processes markdown input with no extensions enabled. | |||
func MarkdownBasic(input []byte) []byte { | |||
// set up the HTML renderer | |||
htmlFlags := HTML_USE_XHTML | |||
renderer := HtmlRenderer(htmlFlags, "", "") | |||
// set up the parser | |||
return MarkdownOptions(input, renderer, Options{Extensions: 0}) | |||
} | |||
// Call Markdown with most useful extensions enabled | |||
// MarkdownCommon is a convenience function for simple rendering. | |||
// It processes markdown input with common extensions enabled, including: | |||
// | |||
// * Smartypants processing with smart fractions and LaTeX dashes | |||
// | |||
// * Intra-word emphasis suppression | |||
// | |||
// * Tables | |||
// | |||
// * Fenced code blocks | |||
// | |||
// * Autolinking | |||
// | |||
// * Strikethrough support | |||
// | |||
// * Strict header parsing | |||
// | |||
// * Custom Header IDs | |||
func MarkdownCommon(input []byte) []byte { | |||
// set up the HTML renderer | |||
renderer := HtmlRenderer(commonHtmlFlags, "", "") | |||
return MarkdownOptions(input, renderer, Options{ | |||
Extensions: commonExtensions}) | |||
} | |||
// Markdown is the main rendering function. | |||
// It parses and renders a block of markdown-encoded text. | |||
// The supplied Renderer is used to format the output, and extensions dictates | |||
// which non-standard extensions are enabled. | |||
// | |||
// To use the supplied Html or LaTeX renderers, see HtmlRenderer and | |||
// LatexRenderer, respectively. | |||
func Markdown(input []byte, renderer Renderer, extensions int) []byte { | |||
return MarkdownOptions(input, renderer, Options{ | |||
Extensions: extensions}) | |||
} | |||
// MarkdownOptions is just like Markdown but takes additional options through | |||
// the Options struct. | |||
func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte { | |||
// no point in parsing if we can't render | |||
if renderer == nil { | |||
return nil | |||
} | |||
extensions := opts.Extensions | |||
// fill in the render structure | |||
p := new(parser) | |||
p.r = renderer | |||
p.flags = extensions | |||
p.refOverride = opts.ReferenceOverride | |||
p.refs = make(map[string]*reference) | |||
p.maxNesting = 16 | |||
p.insideLink = false | |||
// register inline parsers | |||
p.inlineCallback['*'] = emphasis | |||
p.inlineCallback['_'] = emphasis | |||
if extensions&EXTENSION_STRIKETHROUGH != 0 { | |||
p.inlineCallback['~'] = emphasis | |||
} | |||
p.inlineCallback['`'] = codeSpan | |||
p.inlineCallback['\n'] = lineBreak | |||
p.inlineCallback['['] = link | |||
p.inlineCallback['<'] = leftAngle | |||
p.inlineCallback['\\'] = escape | |||
p.inlineCallback['&'] = entity | |||
if extensions&EXTENSION_AUTOLINK != 0 { | |||
p.inlineCallback[':'] = autoLink | |||
} | |||
if extensions&EXTENSION_FOOTNOTES != 0 { | |||
p.notes = make([]*reference, 0) | |||
} | |||
first := firstPass(p, input) | |||
second := secondPass(p, first) | |||
return second | |||
} | |||
// first pass: | |||
// - normalize newlines | |||
// - extract references (outside of fenced code blocks) | |||
// - expand tabs (outside of fenced code blocks) | |||
// - copy everything else | |||
func firstPass(p *parser, input []byte) []byte { | |||
var out bytes.Buffer | |||
tabSize := TAB_SIZE_DEFAULT | |||
if p.flags&EXTENSION_TAB_SIZE_EIGHT != 0 { | |||
tabSize = TAB_SIZE_EIGHT | |||
} | |||
beg := 0 | |||
lastFencedCodeBlockEnd := 0 | |||
for beg < len(input) { | |||
// Find end of this line, then process the line. | |||
end := beg | |||
for end < len(input) && input[end] != '\n' && input[end] != '\r' { | |||
end++ | |||
} | |||
if p.flags&EXTENSION_FENCED_CODE != 0 { | |||
// track fenced code block boundaries to suppress tab expansion | |||
// and reference extraction inside them: | |||
if beg >= lastFencedCodeBlockEnd { | |||
if i := p.fencedCodeBlock(&out, input[beg:], false); i > 0 { | |||
lastFencedCodeBlockEnd = beg + i | |||
} | |||
} | |||
} | |||
// add the line body if present | |||
if end > beg { | |||
if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks. | |||
out.Write(input[beg:end]) | |||
} else if refEnd := isReference(p, input[beg:], tabSize); refEnd > 0 { | |||
beg += refEnd | |||
continue | |||
} else { | |||
expandTabs(&out, input[beg:end], tabSize) | |||
} | |||
} | |||
if end < len(input) && input[end] == '\r' { | |||
end++ | |||
} | |||
if end < len(input) && input[end] == '\n' { | |||
end++ | |||
} | |||
out.WriteByte('\n') | |||
beg = end | |||
} | |||
// empty input? | |||
if out.Len() == 0 { | |||
out.WriteByte('\n') | |||
} | |||
return out.Bytes() | |||
} | |||
// second pass: actual rendering | |||
func secondPass(p *parser, input []byte) []byte { | |||
var output bytes.Buffer | |||
p.r.DocumentHeader(&output) | |||
p.block(&output, input) | |||
if p.flags&EXTENSION_FOOTNOTES != 0 && len(p.notes) > 0 { | |||
p.r.Footnotes(&output, func() bool { | |||
flags := LIST_ITEM_BEGINNING_OF_LIST | |||
for i := 0; i < len(p.notes); i += 1 { | |||
ref := p.notes[i] | |||
var buf bytes.Buffer | |||
if ref.hasBlock { | |||
flags |= LIST_ITEM_CONTAINS_BLOCK | |||
p.block(&buf, ref.title) | |||
} else { | |||
p.inline(&buf, ref.title) | |||
} | |||
p.r.FootnoteItem(&output, ref.link, buf.Bytes(), flags) | |||
flags &^= LIST_ITEM_BEGINNING_OF_LIST | LIST_ITEM_CONTAINS_BLOCK | |||
} | |||
return true | |||
}) | |||
} | |||
p.r.DocumentFooter(&output) | |||
if p.nesting != 0 { | |||
panic("Nesting level did not end at zero") | |||
} | |||
return output.Bytes() | |||
} | |||
// | |||
// Link references | |||
// | |||
// This section implements support for references that (usually) appear | |||
// as footnotes in a document, and can be referenced anywhere in the document. | |||
// The basic format is: | |||
// | |||
// [1]: http://www.google.com/ "Google" | |||
// [2]: http://www.github.com/ "Github" | |||
// | |||
// Anywhere in the document, the reference can be linked by referring to its | |||
// label, i.e., 1 and 2 in this example, as in: | |||
// | |||
// This library is hosted on [Github][2], a git hosting site. | |||
// | |||
// Actual footnotes as specified in Pandoc and supported by some other Markdown | |||
// libraries such as php-markdown are also taken care of. They look like this: | |||
// | |||
// This sentence needs a bit of further explanation.[^note] | |||
// | |||
// [^note]: This is the explanation. | |||
// | |||
// Footnotes should be placed at the end of the document in an ordered list. | |||
// Inline footnotes such as: | |||
// | |||
// Inline footnotes^[Not supported.] also exist. | |||
// | |||
// are not yet supported. | |||
// References are parsed and stored in this struct. | |||
type reference struct { | |||
link []byte | |||
title []byte | |||
noteId int // 0 if not a footnote ref | |||
hasBlock bool | |||
text []byte | |||
} | |||
func (r *reference) String() string { | |||
return fmt.Sprintf("{link: %q, title: %q, text: %q, noteId: %d, hasBlock: %v}", | |||
r.link, r.title, r.text, r.noteId, r.hasBlock) | |||
} | |||
// Check whether or not data starts with a reference link. | |||
// If so, it is parsed and stored in the list of references | |||
// (in the render struct). | |||
// Returns the number of bytes to skip to move past it, | |||
// or zero if the first line is not a reference. | |||
func isReference(p *parser, data []byte, tabSize int) int { | |||
// up to 3 optional leading spaces | |||
if len(data) < 4 { | |||
return 0 | |||
} | |||
i := 0 | |||
for i < 3 && data[i] == ' ' { | |||
i++ | |||
} | |||
noteId := 0 | |||
// id part: anything but a newline between brackets | |||
if data[i] != '[' { | |||
return 0 | |||
} | |||
i++ | |||
if p.flags&EXTENSION_FOOTNOTES != 0 { | |||
if i < len(data) && data[i] == '^' { | |||
// we can set it to anything here because the proper noteIds will | |||
// be assigned later during the second pass. It just has to be != 0 | |||
noteId = 1 | |||
i++ | |||
} | |||
} | |||
idOffset := i | |||
for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' { | |||
i++ | |||
} | |||
if i >= len(data) || data[i] != ']' { | |||
return 0 | |||
} | |||
idEnd := i | |||
// spacer: colon (space | tab)* newline? (space | tab)* | |||
i++ | |||
if i >= len(data) || data[i] != ':' { | |||
return 0 | |||
} | |||
i++ | |||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') { | |||
i++ | |||
} | |||
if i < len(data) && (data[i] == '\n' || data[i] == '\r') { | |||
i++ | |||
if i < len(data) && data[i] == '\n' && data[i-1] == '\r' { | |||
i++ | |||
} | |||
} | |||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') { | |||
i++ | |||
} | |||
if i >= len(data) { | |||
return 0 | |||
} | |||
var ( | |||
linkOffset, linkEnd int | |||
titleOffset, titleEnd int | |||
lineEnd int | |||
raw []byte | |||
hasBlock bool | |||
) | |||
if p.flags&EXTENSION_FOOTNOTES != 0 && noteId != 0 { | |||
linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize) | |||
lineEnd = linkEnd | |||
} else { | |||
linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i) | |||
} | |||
if lineEnd == 0 { | |||
return 0 | |||
} | |||
// a valid ref has been found | |||
ref := &reference{ | |||
noteId: noteId, | |||
hasBlock: hasBlock, | |||
} | |||
if noteId > 0 { | |||
// reusing the link field for the id since footnotes don't have links | |||
ref.link = data[idOffset:idEnd] | |||
// if footnote, it's not really a title, it's the contained text | |||
ref.title = raw | |||
} else { | |||
ref.link = data[linkOffset:linkEnd] | |||
ref.title = data[titleOffset:titleEnd] | |||
} | |||
// id matches are case-insensitive | |||
id := string(bytes.ToLower(data[idOffset:idEnd])) | |||
p.refs[id] = ref | |||
return lineEnd | |||
} | |||
func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { | |||
// link: whitespace-free sequence, optionally between angle brackets | |||
if data[i] == '<' { | |||
i++ | |||
} | |||
linkOffset = i | |||
if i == len(data) { | |||
return | |||
} | |||
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { | |||
i++ | |||
} | |||
linkEnd = i | |||
if data[linkOffset] == '<' && data[linkEnd-1] == '>' { | |||
linkOffset++ | |||
linkEnd-- | |||
} | |||
// optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) | |||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') { | |||
i++ | |||
} | |||
if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' { | |||
return | |||
} | |||
// compute end-of-line | |||
if i >= len(data) || data[i] == '\r' || data[i] == '\n' { | |||
lineEnd = i | |||
} | |||
if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' { | |||
lineEnd++ | |||
} | |||
// optional (space|tab)* spacer after a newline | |||
if lineEnd > 0 { | |||
i = lineEnd + 1 | |||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') { | |||
i++ | |||
} | |||
} | |||
// optional title: any non-newline sequence enclosed in '"() alone on its line | |||
if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') { | |||
i++ | |||
titleOffset = i | |||
// look for EOL | |||
for i < len(data) && data[i] != '\n' && data[i] != '\r' { | |||
i++ | |||
} | |||
if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' { | |||
titleEnd = i + 1 | |||
} else { | |||
titleEnd = i | |||
} | |||
// step back | |||
i-- | |||
for i > titleOffset && (data[i] == ' ' || data[i] == '\t') { | |||
i-- | |||
} | |||
if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') { | |||
lineEnd = titleEnd | |||
titleEnd = i | |||
} | |||
} | |||
return | |||
} | |||
// The first bit of this logic is the same as (*parser).listItem, but the rest | |||
// is much simpler. This function simply finds the entire block and shifts it | |||
// over by one tab if it is indeed a block (just returns the line if it's not). | |||
// blockEnd is the end of the section in the input buffer, and contents is the | |||
// extracted text that was shifted over one tab. It will need to be rendered at | |||
// the end of the document. | |||
func scanFootnote(p *parser, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { | |||
if i == 0 || len(data) == 0 { | |||
return | |||
} | |||
// skip leading whitespace on first line | |||
for i < len(data) && data[i] == ' ' { | |||
i++ | |||
} | |||
blockStart = i | |||
// find the end of the line | |||
blockEnd = i | |||
for i < len(data) && data[i-1] != '\n' { | |||
i++ | |||
} | |||
// get working buffer | |||
var raw bytes.Buffer | |||
// put the first line into the working buffer | |||
raw.Write(data[blockEnd:i]) | |||
blockEnd = i | |||
// process the following lines | |||
containsBlankLine := false | |||
gatherLines: | |||
for blockEnd < len(data) { | |||
i++ | |||
// find the end of this line | |||
for i < len(data) && data[i-1] != '\n' { | |||
i++ | |||
} | |||
// if it is an empty line, guess that it is part of this item | |||
// and move on to the next line | |||
if p.isEmpty(data[blockEnd:i]) > 0 { | |||
containsBlankLine = true | |||
blockEnd = i | |||
continue | |||
} | |||
n := 0 | |||
if n = isIndented(data[blockEnd:i], indentSize); n == 0 { | |||
// this is the end of the block. | |||
// we don't want to include this last line in the index. | |||
break gatherLines | |||
} | |||
// if there were blank lines before this one, insert a new one now | |||
if containsBlankLine { | |||
raw.WriteByte('\n') | |||
containsBlankLine = false | |||
} | |||
// get rid of that first tab, write to buffer | |||
raw.Write(data[blockEnd+n : i]) | |||
hasBlock = true | |||
blockEnd = i | |||
} | |||
if data[blockEnd-1] != '\n' { | |||
raw.WriteByte('\n') | |||
} | |||
contents = raw.Bytes() | |||
return | |||
} | |||
// | |||
// | |||
// Miscellaneous helper functions | |||
// | |||
// | |||
// Test if a character is a punctuation symbol. | |||
// Taken from a private function in regexp in the stdlib. | |||
func ispunct(c byte) bool { | |||
for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") { | |||
if c == r { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
// Test if a character is a whitespace character. | |||
func isspace(c byte) bool { | |||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' | |||
} | |||
// Test if a character is letter. | |||
func isletter(c byte) bool { | |||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') | |||
} | |||
// Test if a character is a letter or a digit. | |||
// TODO: check when this is looking for ASCII alnum and when it should use unicode | |||
func isalnum(c byte) bool { | |||
return (c >= '0' && c <= '9') || isletter(c) | |||
} | |||
// Replace tab characters with spaces, aligning to the next TAB_SIZE column. | |||
// always ends output with a newline | |||
func expandTabs(out *bytes.Buffer, line []byte, tabSize int) { | |||
// first, check for common cases: no tabs, or only tabs at beginning of line | |||
i, prefix := 0, 0 | |||
slowcase := false | |||
for i = 0; i < len(line); i++ { | |||
if line[i] == '\t' { | |||
if prefix == i { | |||
prefix++ | |||
} else { | |||
slowcase = true | |||
break | |||
} | |||
} | |||
} | |||
// no need to decode runes if all tabs are at the beginning of the line | |||
if !slowcase { | |||
for i = 0; i < prefix*tabSize; i++ { | |||
out.WriteByte(' ') | |||
} | |||
out.Write(line[prefix:]) | |||
return | |||
} | |||
// the slow case: we need to count runes to figure out how | |||
// many spaces to insert for each tab | |||
column := 0 | |||
i = 0 | |||
for i < len(line) { | |||
start := i | |||
for i < len(line) && line[i] != '\t' { | |||
_, size := utf8.DecodeRune(line[i:]) | |||
i += size | |||
column++ | |||
} | |||
if i > start { | |||
out.Write(line[start:i]) | |||
} | |||
if i >= len(line) { | |||
break | |||
} | |||
for { | |||
out.WriteByte(' ') | |||
column++ | |||
if column%tabSize == 0 { | |||
break | |||
} | |||
} | |||
i++ | |||
} | |||
} | |||
// Find if a line counts as indented or not. | |||
// Returns number of characters the indent is (0 = not indented). | |||
func isIndented(data []byte, indentSize int) int { | |||
if len(data) == 0 { | |||
return 0 | |||
} | |||
if data[0] == '\t' { | |||
return 1 | |||
} | |||
if len(data) < indentSize { | |||
return 0 | |||
} | |||
for i := 0; i < indentSize; i++ { | |||
if data[i] != ' ' { | |||
return 0 | |||
} | |||
} | |||
return indentSize | |||
} | |||
// Create a url-safe slug for fragments | |||
func slugify(in []byte) []byte { | |||
if len(in) == 0 { | |||
return in | |||
} | |||
out := make([]byte, 0, len(in)) | |||
sym := false | |||
for _, ch := range in { | |||
if isalnum(ch) { | |||
sym = false | |||
out = append(out, ch) | |||
} else if sym { | |||
continue | |||
} else { | |||
out = append(out, '-') | |||
sym = true | |||
} | |||
} | |||
var a, b int | |||
var ch byte | |||
for a, ch = range out { | |||
if ch != '-' { | |||
break | |||
} | |||
} | |||
for b = len(out) - 1; b > 0; b-- { | |||
if out[b] != '-' { | |||
break | |||
} | |||
} | |||
return out[a : b+1] | |||
} |
@@ -0,0 +1,396 @@ | |||
// | |||
// Blackfriday Markdown Processor | |||
// Available at http://github.com/russross/blackfriday | |||
// | |||
// Copyright © 2011 Russ Ross <russ@russross.com>. | |||
// Distributed under the Simplified BSD License. | |||
// See README.md for details. | |||
// | |||
// | |||
// | |||
// SmartyPants rendering | |||
// | |||
// | |||
package blackfriday | |||
import ( | |||
"bytes" | |||
) | |||
type smartypantsData struct { | |||
inSingleQuote bool | |||
inDoubleQuote bool | |||
} | |||
func wordBoundary(c byte) bool { | |||
return c == 0 || isspace(c) || ispunct(c) | |||
} | |||
func tolower(c byte) byte { | |||
if c >= 'A' && c <= 'Z' { | |||
return c - 'A' + 'a' | |||
} | |||
return c | |||
} | |||
func isdigit(c byte) bool { | |||
return c >= '0' && c <= '9' | |||
} | |||
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool) bool { | |||
// edge of the buffer is likely to be a tag that we don't get to see, | |||
// so we treat it like text sometimes | |||
// enumerate all sixteen possibilities for (previousChar, nextChar) | |||
// each can be one of {0, space, punct, other} | |||
switch { | |||
case previousChar == 0 && nextChar == 0: | |||
// context is not any help here, so toggle | |||
*isOpen = !*isOpen | |||
case isspace(previousChar) && nextChar == 0: | |||
// [ "] might be [ "<code>foo...] | |||
*isOpen = true | |||
case ispunct(previousChar) && nextChar == 0: | |||
// [!"] hmm... could be [Run!"] or [("<code>...] | |||
*isOpen = false | |||
case /* isnormal(previousChar) && */ nextChar == 0: | |||
// [a"] is probably a close | |||
*isOpen = false | |||
case previousChar == 0 && isspace(nextChar): | |||
// [" ] might be [...foo</code>" ] | |||
*isOpen = false | |||
case isspace(previousChar) && isspace(nextChar): | |||
// [ " ] context is not any help here, so toggle | |||
*isOpen = !*isOpen | |||
case ispunct(previousChar) && isspace(nextChar): | |||
// [!" ] is probably a close | |||
*isOpen = false | |||
case /* isnormal(previousChar) && */ isspace(nextChar): | |||
// [a" ] this is one of the easy cases | |||
*isOpen = false | |||
case previousChar == 0 && ispunct(nextChar): | |||
// ["!] hmm... could be ["$1.95] or [</code>"!...] | |||
*isOpen = false | |||
case isspace(previousChar) && ispunct(nextChar): | |||
// [ "!] looks more like [ "$1.95] | |||
*isOpen = true | |||
case ispunct(previousChar) && ispunct(nextChar): | |||
// [!"!] context is not any help here, so toggle | |||
*isOpen = !*isOpen | |||
case /* isnormal(previousChar) && */ ispunct(nextChar): | |||
// [a"!] is probably a close | |||
*isOpen = false | |||
case previousChar == 0 /* && isnormal(nextChar) */ : | |||
// ["a] is probably an open | |||
*isOpen = true | |||
case isspace(previousChar) /* && isnormal(nextChar) */ : | |||
// [ "a] this is one of the easy cases | |||
*isOpen = true | |||
case ispunct(previousChar) /* && isnormal(nextChar) */ : | |||
// [!"a] is probably an open | |||
*isOpen = true | |||
default: | |||
// [a'b] maybe a contraction? | |||
*isOpen = false | |||
} | |||
out.WriteByte('&') | |||
if *isOpen { | |||
out.WriteByte('l') | |||
} else { | |||
out.WriteByte('r') | |||
} | |||
out.WriteByte(quote) | |||
out.WriteString("quo;") | |||
return true | |||
} | |||
func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if len(text) >= 2 { | |||
t1 := tolower(text[1]) | |||
if t1 == '\'' { | |||
nextChar := byte(0) | |||
if len(text) >= 3 { | |||
nextChar = text[2] | |||
} | |||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) { | |||
return 1 | |||
} | |||
} | |||
if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) { | |||
out.WriteString("’") | |||
return 0 | |||
} | |||
if len(text) >= 3 { | |||
t2 := tolower(text[2]) | |||
if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && | |||
(len(text) < 4 || wordBoundary(text[3])) { | |||
out.WriteString("’") | |||
return 0 | |||
} | |||
} | |||
} | |||
nextChar := byte(0) | |||
if len(text) > 1 { | |||
nextChar = text[1] | |||
} | |||
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote) { | |||
return 0 | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartParens(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if len(text) >= 3 { | |||
t1 := tolower(text[1]) | |||
t2 := tolower(text[2]) | |||
if t1 == 'c' && t2 == ')' { | |||
out.WriteString("©") | |||
return 2 | |||
} | |||
if t1 == 'r' && t2 == ')' { | |||
out.WriteString("®") | |||
return 2 | |||
} | |||
if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' { | |||
out.WriteString("™") | |||
return 3 | |||
} | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartDash(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if len(text) >= 2 { | |||
if text[1] == '-' { | |||
out.WriteString("—") | |||
return 1 | |||
} | |||
if wordBoundary(previousChar) && wordBoundary(text[1]) { | |||
out.WriteString("–") | |||
return 0 | |||
} | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if len(text) >= 3 && text[1] == '-' && text[2] == '-' { | |||
out.WriteString("—") | |||
return 2 | |||
} | |||
if len(text) >= 2 && text[1] == '-' { | |||
out.WriteString("–") | |||
return 1 | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int { | |||
if bytes.HasPrefix(text, []byte(""")) { | |||
nextChar := byte(0) | |||
if len(text) >= 7 { | |||
nextChar = text[6] | |||
} | |||
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) { | |||
return 5 | |||
} | |||
} | |||
if bytes.HasPrefix(text, []byte("�")) { | |||
return 3 | |||
} | |||
out.WriteByte('&') | |||
return 0 | |||
} | |||
func smartAmp(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
return smartAmpVariant(out, smrt, previousChar, text, 'd') | |||
} | |||
func smartAmpAngledQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
return smartAmpVariant(out, smrt, previousChar, text, 'a') | |||
} | |||
func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if len(text) >= 3 && text[1] == '.' && text[2] == '.' { | |||
out.WriteString("…") | |||
return 2 | |||
} | |||
if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' { | |||
out.WriteString("…") | |||
return 4 | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if len(text) >= 2 && text[1] == '`' { | |||
nextChar := byte(0) | |||
if len(text) >= 3 { | |||
nextChar = text[2] | |||
} | |||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) { | |||
return 1 | |||
} | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartNumberGeneric(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { | |||
// is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b | |||
// note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8) | |||
// and avoid changing dates like 1/23/2005 into fractions. | |||
numEnd := 0 | |||
for len(text) > numEnd && isdigit(text[numEnd]) { | |||
numEnd++ | |||
} | |||
if numEnd == 0 { | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
denStart := numEnd + 1 | |||
if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 { | |||
denStart = numEnd + 3 | |||
} else if len(text) < numEnd+2 || text[numEnd] != '/' { | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
denEnd := denStart | |||
for len(text) > denEnd && isdigit(text[denEnd]) { | |||
denEnd++ | |||
} | |||
if denEnd == denStart { | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' { | |||
out.WriteString("<sup>") | |||
out.Write(text[:numEnd]) | |||
out.WriteString("</sup>⁄<sub>") | |||
out.Write(text[denStart:denEnd]) | |||
out.WriteString("</sub>") | |||
return denEnd - 1 | |||
} | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartNumber(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { | |||
if text[0] == '1' && text[1] == '/' && text[2] == '2' { | |||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' { | |||
out.WriteString("½") | |||
return 2 | |||
} | |||
} | |||
if text[0] == '1' && text[1] == '/' && text[2] == '4' { | |||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') { | |||
out.WriteString("¼") | |||
return 2 | |||
} | |||
} | |||
if text[0] == '3' && text[1] == '/' && text[2] == '4' { | |||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') { | |||
out.WriteString("¾") | |||
return 2 | |||
} | |||
} | |||
} | |||
out.WriteByte(text[0]) | |||
return 0 | |||
} | |||
func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int { | |||
nextChar := byte(0) | |||
if len(text) > 1 { | |||
nextChar = text[1] | |||
} | |||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) { | |||
out.WriteString(""") | |||
} | |||
return 0 | |||
} | |||
func smartDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'd') | |||
} | |||
func smartAngledDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'a') | |||
} | |||
func smartLeftAngle(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||
i := 0 | |||
for i < len(text) && text[i] != '>' { | |||
i++ | |||
} | |||
out.Write(text[:i+1]) | |||
return i | |||
} | |||
type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int | |||
type smartypantsRenderer [256]smartCallback | |||
func smartypants(flags int) *smartypantsRenderer { | |||
r := new(smartypantsRenderer) | |||
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 { | |||
r['"'] = smartDoubleQuote | |||
r['&'] = smartAmp | |||
} else { | |||
r['"'] = smartAngledDoubleQuote | |||
r['&'] = smartAmpAngledQuote | |||
} | |||
r['('] = smartParens | |||
if flags&HTML_SMARTYPANTS_DASHES != 0 { | |||
if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 { | |||
r['-'] = smartDash | |||
} else { | |||
r['-'] = smartDashLatex | |||
} | |||
} | |||
if flags&HTML_SMARTYPANTS_FRACTIONS == 0 { | |||
r['1'] = smartNumber | |||
r['3'] = smartNumber | |||
} else { | |||
for ch := '1'; ch <= '9'; ch++ { | |||
r[ch] = smartNumberGeneric | |||
} | |||
} | |||
return r | |||
} |
@@ -0,0 +1,373 @@ | |||
Mozilla Public License Version 2.0 | |||
================================== | |||
1. Definitions | |||
-------------- | |||
1.1. "Contributor" | |||
means each individual or legal entity that creates, contributes to | |||
the creation of, or owns Covered Software. | |||
1.2. "Contributor Version" | |||
means the combination of the Contributions of others (if any) used | |||
by a Contributor and that particular Contributor's Contribution. | |||
1.3. "Contribution" | |||
means Covered Software of a particular Contributor. | |||
1.4. "Covered Software" | |||
means Source Code Form to which the initial Contributor has attached | |||
the notice in Exhibit A, the Executable Form of such Source Code | |||
Form, and Modifications of such Source Code Form, in each case | |||
including portions thereof. | |||
1.5. "Incompatible With Secondary Licenses" | |||
means | |||
(a) that the initial Contributor has attached the notice described | |||
in Exhibit B to the Covered Software; or | |||
(b) that the Covered Software was made available under the terms of | |||
version 1.1 or earlier of the License, but not also under the | |||
terms of a Secondary License. | |||
1.6. "Executable Form" | |||
means any form of the work other than Source Code Form. | |||
1.7. "Larger Work" | |||
means a work that combines Covered Software with other material, in | |||
a separate file or files, that is not Covered Software. | |||
1.8. "License" | |||
means this document. | |||
1.9. "Licensable" | |||
means having the right to grant, to the maximum extent possible, | |||
whether at the time of the initial grant or subsequently, any and | |||
all of the rights conveyed by this License. | |||
1.10. "Modifications" | |||
means any of the following: | |||
(a) any file in Source Code Form that results from an addition to, | |||
deletion from, or modification of the contents of Covered | |||
Software; or | |||
(b) any new file in Source Code Form that contains any Covered | |||
Software. | |||
1.11. "Patent Claims" of a Contributor | |||
means any patent claim(s), including without limitation, method, | |||
process, and apparatus claims, in any patent Licensable by such | |||
Contributor that would be infringed, but for the grant of the | |||
License, by the making, using, selling, offering for sale, having | |||
made, import, or transfer of either its Contributions or its | |||
Contributor Version. | |||
1.12. "Secondary License" | |||
means either the GNU General Public License, Version 2.0, the GNU | |||
Lesser General Public License, Version 2.1, the GNU Affero General | |||
Public License, Version 3.0, or any later versions of those | |||
licenses. | |||
1.13. "Source Code Form" | |||
means the form of the work preferred for making modifications. | |||
1.14. "You" (or "Your") | |||
means an individual or a legal entity exercising rights under this | |||
License. For legal entities, "You" includes any entity that | |||
controls, is controlled by, or is under common control with You. For | |||
purposes of this definition, "control" means (a) the power, direct | |||
or indirect, to cause the direction or management of such entity, | |||
whether by contract or otherwise, or (b) ownership of more than | |||
fifty percent (50%) of the outstanding shares or beneficial | |||
ownership of such entity. | |||
2. License Grants and Conditions | |||
-------------------------------- | |||
2.1. Grants | |||
Each Contributor hereby grants You a world-wide, royalty-free, | |||
non-exclusive license: | |||
(a) under intellectual property rights (other than patent or trademark) | |||
Licensable by such Contributor to use, reproduce, make available, | |||
modify, display, perform, distribute, and otherwise exploit its | |||
Contributions, either on an unmodified basis, with Modifications, or | |||
as part of a Larger Work; and | |||
(b) under Patent Claims of such Contributor to make, use, sell, offer | |||
for sale, have made, import, and otherwise transfer either its | |||
Contributions or its Contributor Version. | |||
2.2. Effective Date | |||
The licenses granted in Section 2.1 with respect to any Contribution | |||
become effective for each Contribution on the date the Contributor first | |||
distributes such Contribution. | |||
2.3. Limitations on Grant Scope | |||
The licenses granted in this Section 2 are the only rights granted under | |||
this License. No additional rights or licenses will be implied from the | |||
distribution or licensing of Covered Software under this License. | |||
Notwithstanding Section 2.1(b) above, no patent license is granted by a | |||
Contributor: | |||
(a) for any code that a Contributor has removed from Covered Software; | |||
or | |||
(b) for infringements caused by: (i) Your and any other third party's | |||
modifications of Covered Software, or (ii) the combination of its | |||
Contributions with other software (except as part of its Contributor | |||
Version); or | |||
(c) under Patent Claims infringed by Covered Software in the absence of | |||
its Contributions. | |||
This License does not grant any rights in the trademarks, service marks, | |||
or logos of any Contributor (except as may be necessary to comply with | |||
the notice requirements in Section 3.4). | |||
2.4. Subsequent Licenses | |||
No Contributor makes additional grants as a result of Your choice to | |||
distribute the Covered Software under a subsequent version of this | |||
License (see Section 10.2) or under the terms of a Secondary License (if | |||
permitted under the terms of Section 3.3). | |||
2.5. Representation | |||
Each Contributor represents that the Contributor believes its | |||
Contributions are its original creation(s) or it has sufficient rights | |||
to grant the rights to its Contributions conveyed by this License. | |||
2.6. Fair Use | |||
This License is not intended to limit any rights You have under | |||
applicable copyright doctrines of fair use, fair dealing, or other | |||
equivalents. | |||
2.7. Conditions | |||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted | |||
in Section 2.1. | |||
3. Responsibilities | |||
------------------- | |||
3.1. Distribution of Source Form | |||
All distribution of Covered Software in Source Code Form, including any | |||
Modifications that You create or to which You contribute, must be under | |||
the terms of this License. You must inform recipients that the Source | |||
Code Form of the Covered Software is governed by the terms of this | |||
License, and how they can obtain a copy of this License. You may not | |||
attempt to alter or restrict the recipients' rights in the Source Code | |||
Form. | |||
3.2. Distribution of Executable Form | |||
If You distribute Covered Software in Executable Form then: | |||
(a) such Covered Software must also be made available in Source Code | |||
Form, as described in Section 3.1, and You must inform recipients of | |||
the Executable Form how they can obtain a copy of such Source Code | |||
Form by reasonable means in a timely manner, at a charge no more | |||
than the cost of distribution to the recipient; and | |||
(b) You may distribute such Executable Form under the terms of this | |||
License, or sublicense it under different terms, provided that the | |||
license for the Executable Form does not attempt to limit or alter | |||
the recipients' rights in the Source Code Form under this License. | |||
3.3. Distribution of a Larger Work | |||
You may create and distribute a Larger Work under terms of Your choice, | |||
provided that You also comply with the requirements of this License for | |||
the Covered Software. If the Larger Work is a combination of Covered | |||
Software with a work governed by one or more Secondary Licenses, and the | |||
Covered Software is not Incompatible With Secondary Licenses, this | |||
License permits You to additionally distribute such Covered Software | |||
under the terms of such Secondary License(s), so that the recipient of | |||
the Larger Work may, at their option, further distribute the Covered | |||
Software under the terms of either this License or such Secondary | |||
License(s). | |||
3.4. Notices | |||
You may not remove or alter the substance of any license notices | |||
(including copyright notices, patent notices, disclaimers of warranty, | |||
or limitations of liability) contained within the Source Code Form of | |||
the Covered Software, except that You may alter any license notices to | |||
the extent required to remedy known factual inaccuracies. | |||
3.5. Application of Additional Terms | |||
You may choose to offer, and to charge a fee for, warranty, support, | |||
indemnity or liability obligations to one or more recipients of Covered | |||
Software. However, You may do so only on Your own behalf, and not on | |||
behalf of any Contributor. You must make it absolutely clear that any | |||
such warranty, support, indemnity, or liability obligation is offered by | |||
You alone, and You hereby agree to indemnify every Contributor for any | |||
liability incurred by such Contributor as a result of warranty, support, | |||
indemnity or liability terms You offer. You may include additional | |||
disclaimers of warranty and limitations of liability specific to any | |||
jurisdiction. | |||
4. Inability to Comply Due to Statute or Regulation | |||
--------------------------------------------------- | |||
If it is impossible for You to comply with any of the terms of this | |||
License with respect to some or all of the Covered Software due to | |||
statute, judicial order, or regulation then You must: (a) comply with | |||
the terms of this License to the maximum extent possible; and (b) | |||
describe the limitations and the code they affect. Such description must | |||
be placed in a text file included with all distributions of the Covered | |||
Software under this License. Except to the extent prohibited by statute | |||
or regulation, such description must be sufficiently detailed for a | |||
recipient of ordinary skill to be able to understand it. | |||
5. Termination | |||
-------------- | |||
5.1. The rights granted under this License will terminate automatically | |||
if You fail to comply with any of its terms. However, if You become | |||
compliant, then the rights granted under this License from a particular | |||
Contributor are reinstated (a) provisionally, unless and until such | |||
Contributor explicitly and finally terminates Your grants, and (b) on an | |||
ongoing basis, if such Contributor fails to notify You of the | |||
non-compliance by some reasonable means prior to 60 days after You have | |||
come back into compliance. Moreover, Your grants from a particular | |||
Contributor are reinstated on an ongoing basis if such Contributor | |||
notifies You of the non-compliance by some reasonable means, this is the | |||
first time You have received notice of non-compliance with this License | |||
from such Contributor, and You become compliant prior to 30 days after | |||
Your receipt of the notice. | |||
5.2. If You initiate litigation against any entity by asserting a patent | |||
infringement claim (excluding declaratory judgment actions, | |||
counter-claims, and cross-claims) alleging that a Contributor Version | |||
directly or indirectly infringes any patent, then the rights granted to | |||
You by any and all Contributors for the Covered Software under Section | |||
2.1 of this License shall terminate. | |||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all | |||
end user license agreements (excluding distributors and resellers) which | |||
have been validly granted by You or Your distributors under this License | |||
prior to termination shall survive termination. | |||
************************************************************************ | |||
* * | |||
* 6. Disclaimer of Warranty * | |||
* ------------------------- * | |||
* * | |||
* Covered Software is provided under this License on an "as is" * | |||
* basis, without warranty of any kind, either expressed, implied, or * | |||
* statutory, including, without limitation, warranties that the * | |||
* Covered Software is free of defects, merchantable, fit for a * | |||
* particular purpose or non-infringing. The entire risk as to the * | |||
* quality and performance of the Covered Software is with You. * | |||
* Should any Covered Software prove defective in any respect, You * | |||
* (not any Contributor) assume the cost of any necessary servicing, * | |||
* repair, or correction. This disclaimer of warranty constitutes an * | |||
* essential part of this License. No use of any Covered Software is * | |||
* authorized under this License except under this disclaimer. * | |||
* * | |||
************************************************************************ | |||
************************************************************************ | |||
* * | |||
* 7. Limitation of Liability * | |||
* -------------------------- * | |||
* * | |||
* Under no circumstances and under no legal theory, whether tort * | |||
* (including negligence), contract, or otherwise, shall any * | |||
* Contributor, or anyone who distributes Covered Software as * | |||
* permitted above, be liable to You for any direct, indirect, * | |||
* special, incidental, or consequential damages of any character * | |||
* including, without limitation, damages for lost profits, loss of * | |||
* goodwill, work stoppage, computer failure or malfunction, or any * | |||
* and all other commercial damages or losses, even if such party * | |||
* shall have been informed of the possibility of such damages. This * | |||
* limitation of liability shall not apply to liability for death or * | |||
* personal injury resulting from such party's negligence to the * | |||
* extent applicable law prohibits such limitation. Some * | |||
* jurisdictions do not allow the exclusion or limitation of * | |||
* incidental or consequential damages, so this exclusion and * | |||
* limitation may not apply to You. * | |||
* * | |||
************************************************************************ | |||
8. Litigation | |||
------------- | |||
Any litigation relating to this License may be brought only in the | |||
courts of a jurisdiction where the defendant maintains its principal | |||
place of business and such litigation shall be governed by laws of that | |||
jurisdiction, without reference to its conflict-of-law provisions. | |||
Nothing in this Section shall prevent a party's ability to bring | |||
cross-claims or counter-claims. | |||
9. Miscellaneous | |||
---------------- | |||
This License represents the complete agreement concerning the subject | |||
matter hereof. If any provision of this License is held to be | |||
unenforceable, such provision shall be reformed only to the extent | |||
necessary to make it enforceable. Any law or regulation which provides | |||
that the language of a contract shall be construed against the drafter | |||
shall not be used to construe this License against a Contributor. | |||
10. Versions of the License | |||
--------------------------- | |||
10.1. New Versions | |||
Mozilla Foundation is the license steward. Except as provided in Section | |||
10.3, no one other than the license steward has the right to modify or | |||
publish new versions of this License. Each version will be given a | |||
distinguishing version number. | |||
10.2. Effect of New Versions | |||
You may distribute the Covered Software under the terms of the version | |||
of the License under which You originally received the Covered Software, | |||
or under the terms of any subsequent version published by the license | |||
steward. | |||
10.3. Modified Versions | |||
If you create software not governed by this License, and you want to | |||
create a new license for such software, you may create and use a | |||
modified version of this License if you rename the license and remove | |||
any references to the name of the license steward (except to note that | |||
such modified license differs from this License). | |||
10.4. Distributing Source Code Form that is Incompatible With Secondary | |||
Licenses | |||
If You choose to distribute Source Code Form that is Incompatible With | |||
Secondary Licenses under the terms of this version of the License, the | |||
notice described in Exhibit B of this License must be attached. | |||
Exhibit A - Source Code Form License Notice | |||
------------------------------------------- | |||
This Source Code Form is subject to the terms of the Mozilla Public | |||
License, v. 2.0. If a copy of the MPL was not distributed with this | |||
file, You can obtain one at http://mozilla.org/MPL/2.0/. | |||
If it is not possible or desirable to put the notice in a particular | |||
file, then You may include the notice in a location (such as a LICENSE | |||
file in a relevant directory) where a recipient would be likely to look | |||
for such a notice. | |||
You may add additional accurate notices of copyright ownership. | |||
Exhibit B - "Incompatible With Secondary Licenses" Notice | |||
--------------------------------------------------------- | |||
This Source Code Form is "Incompatible With Secondary Licenses", as | |||
defined by the Mozilla Public License, v. 2.0. |
@@ -0,0 +1,19 @@ | |||
package posts | |||
import ( | |||
"strings" | |||
) | |||
func ExtractTitle(content string) (title string, body string) { | |||
if hashIndex := strings.Index(content, "# "); hashIndex == 0 { | |||
eol := strings.IndexRune(content, '\n') | |||
// First line should start with # and end with \n | |||
if eol != -1 { | |||
body = strings.TrimLeft(content[eol:], " \t\n\r") | |||
title = content[len("# "):eol] | |||
return | |||
} | |||
} | |||
body = content | |||
return | |||
} |
@@ -0,0 +1,63 @@ | |||
package posts | |||
import ( | |||
"github.com/microcosm-cc/bluemonday" | |||
"github.com/writeas/saturday" | |||
"regexp" | |||
"strings" | |||
"unicode" | |||
) | |||
var ( | |||
blockReg = regexp.MustCompile("<(ul|ol|blockquote)>\n") | |||
endBlockReg = regexp.MustCompile("</([a-z]+)>\n</(ul|ol|blockquote)>") | |||
markeddownReg = regexp.MustCompile("<p>(.+)</p>") | |||
) | |||
func ApplyMarkdown(data []byte) string { | |||
mdExtensions := 0 | | |||
blackfriday.EXTENSION_TABLES | | |||
blackfriday.EXTENSION_FENCED_CODE | | |||
blackfriday.EXTENSION_AUTOLINK | | |||
blackfriday.EXTENSION_STRIKETHROUGH | | |||
blackfriday.EXTENSION_SPACE_HEADERS | | |||
blackfriday.EXTENSION_HEADER_IDS | |||
htmlFlags := 0 | | |||
blackfriday.HTML_USE_SMARTYPANTS | | |||
blackfriday.HTML_SMARTYPANTS_DASHES | |||
// Generate Markdown | |||
md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) | |||
// Strip out bad HTML | |||
policy := bluemonday.UGCPolicy() | |||
policy.AllowAttrs("class", "id").Globally() | |||
outHTML := string(policy.SanitizeBytes(md)) | |||
// Strip newlines on certain block elements that render with them | |||
outHTML = blockReg.ReplaceAllString(outHTML, "<$1>") | |||
outHTML = endBlockReg.ReplaceAllString(outHTML, "</$1></$2>") | |||
return outHTML | |||
} | |||
func ApplyBasicMarkdown(data []byte) string { | |||
mdExtensions := 0 | | |||
blackfriday.EXTENSION_STRIKETHROUGH | | |||
blackfriday.EXTENSION_SPACE_HEADERS | | |||
blackfriday.EXTENSION_HEADER_IDS | |||
htmlFlags := 0 | | |||
blackfriday.HTML_SKIP_HTML | | |||
blackfriday.HTML_USE_SMARTYPANTS | | |||
blackfriday.HTML_SMARTYPANTS_DASHES | |||
// Generate Markdown | |||
md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) | |||
// Strip out bad HTML | |||
policy := bluemonday.UGCPolicy() | |||
policy.AllowAttrs("class", "id").Globally() | |||
outHTML := string(policy.SanitizeBytes(md)) | |||
outHTML = markeddownReg.ReplaceAllString(outHTML, "$1") | |||
outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace) | |||
return outHTML | |||
} |
@@ -0,0 +1,3 @@ | |||
# This source code refers to The Go Authors for copyright purposes. | |||
# The master list of authors is in the main Go distribution, | |||
# visible at https://tip.golang.org/AUTHORS. |
@@ -0,0 +1,3 @@ | |||
# This source code was written by the Go contributors. | |||
# The master list of contributors is in the main Go distribution, | |||
# visible at https://tip.golang.org/CONTRIBUTORS. |
@@ -0,0 +1,27 @@ | |||
Copyright (c) 2009 The Go Authors. All rights reserved. | |||
Redistribution and use in source and binary forms, with or without | |||
modification, are permitted provided that the following conditions are | |||
met: | |||
* Redistributions of source code must retain the above copyright | |||
notice, this list of conditions and the following disclaimer. | |||
* Redistributions in binary form must reproduce the above | |||
copyright notice, this list of conditions and the following disclaimer | |||
in the documentation and/or other materials provided with the | |||
distribution. | |||
* Neither the name of Google Inc. nor the names of its | |||
contributors may be used to endorse or promote products derived from | |||
this software without specific prior written permission. | |||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@@ -0,0 +1,22 @@ | |||
Additional IP Rights Grant (Patents) | |||
"This implementation" means the copyrightable works distributed by | |||
Google as part of the Go project. | |||
Google hereby grants to You a perpetual, worldwide, non-exclusive, | |||
no-charge, royalty-free, irrevocable (except as stated in this section) | |||
patent license to make, have made, use, offer to sell, sell, import, | |||
transfer and otherwise run, modify and propagate the contents of this | |||
implementation of Go, where such license applies only to those patent | |||
claims, both currently owned or controlled by Google and acquired in | |||
the future, licensable by Google that are necessarily infringed by this | |||
implementation of Go. This grant does not include claims that would be | |||
infringed only as a consequence of further modification of this | |||
implementation. If you or your agent or exclusive licensee institute or | |||
order or agree to the institution of patent litigation against any | |||
entity (including a cross-claim or counterclaim in a lawsuit) alleging | |||
that this implementation of Go or any code incorporated within this | |||
implementation of Go constitutes direct or contributory patent | |||
infringement, or inducement of patent infringement, then any patent | |||
rights granted to you under this License for this implementation of Go | |||
shall terminate as of the date such litigation is filed. |
@@ -0,0 +1,951 @@ | |||
// Copyright 2011 The Go Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
package terminal | |||
import ( | |||
"bytes" | |||
"io" | |||
"sync" | |||
"unicode/utf8" | |||
) | |||
// EscapeCodes contains escape sequences that can be written to the terminal in | |||
// order to achieve different styles of text. | |||
type EscapeCodes struct { | |||
// Foreground colors | |||
Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte | |||
// Reset all attributes | |||
Reset []byte | |||
} | |||
var vt100EscapeCodes = EscapeCodes{ | |||
Black: []byte{keyEscape, '[', '3', '0', 'm'}, | |||
Red: []byte{keyEscape, '[', '3', '1', 'm'}, | |||
Green: []byte{keyEscape, '[', '3', '2', 'm'}, | |||
Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, | |||
Blue: []byte{keyEscape, '[', '3', '4', 'm'}, | |||
Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, | |||
Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, | |||
White: []byte{keyEscape, '[', '3', '7', 'm'}, | |||
Reset: []byte{keyEscape, '[', '0', 'm'}, | |||
} | |||
// Terminal contains the state for running a VT100 terminal that is capable of | |||
// reading lines of input. | |||
type Terminal struct { | |||
// AutoCompleteCallback, if non-null, is called for each keypress with | |||
// the full input line and the current position of the cursor (in | |||
// bytes, as an index into |line|). If it returns ok=false, the key | |||
// press is processed normally. Otherwise it returns a replacement line | |||
// and the new cursor position. | |||
AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) | |||
// Escape contains a pointer to the escape codes for this terminal. | |||
// It's always a valid pointer, although the escape codes themselves | |||
// may be empty if the terminal doesn't support them. | |||
Escape *EscapeCodes | |||
// lock protects the terminal and the state in this object from | |||
// concurrent processing of a key press and a Write() call. | |||
lock sync.Mutex | |||
c io.ReadWriter | |||
prompt []rune | |||
// line is the current line being entered. | |||
line []rune | |||
// pos is the logical position of the cursor in line | |||
pos int | |||
// echo is true if local echo is enabled | |||
echo bool | |||
// pasteActive is true iff there is a bracketed paste operation in | |||
// progress. | |||
pasteActive bool | |||
// cursorX contains the current X value of the cursor where the left | |||
// edge is 0. cursorY contains the row number where the first row of | |||
// the current line is 0. | |||
cursorX, cursorY int | |||
// maxLine is the greatest value of cursorY so far. | |||
maxLine int | |||
termWidth, termHeight int | |||
// outBuf contains the terminal data to be sent. | |||
outBuf []byte | |||
// remainder contains the remainder of any partial key sequences after | |||
// a read. It aliases into inBuf. | |||
remainder []byte | |||
inBuf [256]byte | |||
// history contains previously entered commands so that they can be | |||
// accessed with the up and down keys. | |||
history stRingBuffer | |||
// historyIndex stores the currently accessed history entry, where zero | |||
// means the immediately previous entry. | |||
historyIndex int | |||
// When navigating up and down the history it's possible to return to | |||
// the incomplete, initial line. That value is stored in | |||
// historyPending. | |||
historyPending string | |||
} | |||
// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is | |||
// a local terminal, that terminal must first have been put into raw mode. | |||
// prompt is a string that is written at the start of each input line (i.e. | |||
// "> "). | |||
func NewTerminal(c io.ReadWriter, prompt string) *Terminal { | |||
return &Terminal{ | |||
Escape: &vt100EscapeCodes, | |||
c: c, | |||
prompt: []rune(prompt), | |||
termWidth: 80, | |||
termHeight: 24, | |||
echo: true, | |||
historyIndex: -1, | |||
} | |||
} | |||
const ( | |||
keyCtrlD = 4 | |||
keyCtrlU = 21 | |||
keyEnter = '\r' | |||
keyEscape = 27 | |||
keyBackspace = 127 | |||
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota | |||
keyUp | |||
keyDown | |||
keyLeft | |||
keyRight | |||
keyAltLeft | |||
keyAltRight | |||
keyHome | |||
keyEnd | |||
keyDeleteWord | |||
keyDeleteLine | |||
keyClearScreen | |||
keyPasteStart | |||
keyPasteEnd | |||
) | |||
var ( | |||
crlf = []byte{'\r', '\n'} | |||
pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} | |||
pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} | |||
) | |||
// bytesToKey tries to parse a key sequence from b. If successful, it returns | |||
// the key and the remainder of the input. Otherwise it returns utf8.RuneError. | |||
func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { | |||
if len(b) == 0 { | |||
return utf8.RuneError, nil | |||
} | |||
if !pasteActive { | |||
switch b[0] { | |||
case 1: // ^A | |||
return keyHome, b[1:] | |||
case 5: // ^E | |||
return keyEnd, b[1:] | |||
case 8: // ^H | |||
return keyBackspace, b[1:] | |||
case 11: // ^K | |||
return keyDeleteLine, b[1:] | |||
case 12: // ^L | |||
return keyClearScreen, b[1:] | |||
case 23: // ^W | |||
return keyDeleteWord, b[1:] | |||
} | |||
} | |||
if b[0] != keyEscape { | |||
if !utf8.FullRune(b) { | |||
return utf8.RuneError, b | |||
} | |||
r, l := utf8.DecodeRune(b) | |||
return r, b[l:] | |||
} | |||
if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { | |||
switch b[2] { | |||
case 'A': | |||
return keyUp, b[3:] | |||
case 'B': | |||
return keyDown, b[3:] | |||
case 'C': | |||
return keyRight, b[3:] | |||
case 'D': | |||
return keyLeft, b[3:] | |||
case 'H': | |||
return keyHome, b[3:] | |||
case 'F': | |||
return keyEnd, b[3:] | |||
} | |||
} | |||
if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { | |||
switch b[5] { | |||
case 'C': | |||
return keyAltRight, b[6:] | |||
case 'D': | |||
return keyAltLeft, b[6:] | |||
} | |||
} | |||
if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { | |||
return keyPasteStart, b[6:] | |||
} | |||
if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { | |||
return keyPasteEnd, b[6:] | |||
} | |||
// If we get here then we have a key that we don't recognise, or a | |||
// partial sequence. It's not clear how one should find the end of a | |||
// sequence without knowing them all, but it seems that [a-zA-Z~] only | |||
// appears at the end of a sequence. | |||
for i, c := range b[0:] { | |||
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { | |||
return keyUnknown, b[i+1:] | |||
} | |||
} | |||
return utf8.RuneError, b | |||
} | |||
// queue appends data to the end of t.outBuf | |||
func (t *Terminal) queue(data []rune) { | |||
t.outBuf = append(t.outBuf, []byte(string(data))...) | |||
} | |||
var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'} | |||
var space = []rune{' '} | |||
func isPrintable(key rune) bool { | |||
isInSurrogateArea := key >= 0xd800 && key <= 0xdbff | |||
return key >= 32 && !isInSurrogateArea | |||
} | |||
// moveCursorToPos appends data to t.outBuf which will move the cursor to the | |||
// given, logical position in the text. | |||
func (t *Terminal) moveCursorToPos(pos int) { | |||
if !t.echo { | |||
return | |||
} | |||
x := visualLength(t.prompt) + pos | |||
y := x / t.termWidth | |||
x = x % t.termWidth | |||
up := 0 | |||
if y < t.cursorY { | |||
up = t.cursorY - y | |||
} | |||
down := 0 | |||
if y > t.cursorY { | |||
down = y - t.cursorY | |||
} | |||
left := 0 | |||
if x < t.cursorX { | |||
left = t.cursorX - x | |||
} | |||
right := 0 | |||
if x > t.cursorX { | |||
right = x - t.cursorX | |||
} | |||
t.cursorX = x | |||
t.cursorY = y | |||
t.move(up, down, left, right) | |||
} | |||
func (t *Terminal) move(up, down, left, right int) { | |||
movement := make([]rune, 3*(up+down+left+right)) | |||
m := movement | |||
for i := 0; i < up; i++ { | |||
m[0] = keyEscape | |||
m[1] = '[' | |||
m[2] = 'A' | |||
m = m[3:] | |||
} | |||
for i := 0; i < down; i++ { | |||
m[0] = keyEscape | |||
m[1] = '[' | |||
m[2] = 'B' | |||
m = m[3:] | |||
} | |||
for i := 0; i < left; i++ { | |||
m[0] = keyEscape | |||
m[1] = '[' | |||
m[2] = 'D' | |||
m = m[3:] | |||
} | |||
for i := 0; i < right; i++ { | |||
m[0] = keyEscape | |||
m[1] = '[' | |||
m[2] = 'C' | |||
m = m[3:] | |||
} | |||
t.queue(movement) | |||
} | |||
func (t *Terminal) clearLineToRight() { | |||
op := []rune{keyEscape, '[', 'K'} | |||
t.queue(op) | |||
} | |||
const maxLineLength = 4096 | |||
func (t *Terminal) setLine(newLine []rune, newPos int) { | |||
if t.echo { | |||
t.moveCursorToPos(0) | |||
t.writeLine(newLine) | |||
for i := len(newLine); i < len(t.line); i++ { | |||
t.writeLine(space) | |||
} | |||
t.moveCursorToPos(newPos) | |||
} | |||
t.line = newLine | |||
t.pos = newPos | |||
} | |||
func (t *Terminal) advanceCursor(places int) { | |||
t.cursorX += places | |||
t.cursorY += t.cursorX / t.termWidth | |||
if t.cursorY > t.maxLine { | |||
t.maxLine = t.cursorY | |||
} | |||
t.cursorX = t.cursorX % t.termWidth | |||
if places > 0 && t.cursorX == 0 { | |||
// Normally terminals will advance the current position | |||
// when writing a character. But that doesn't happen | |||
// for the last character in a line. However, when | |||
// writing a character (except a new line) that causes | |||
// a line wrap, the position will be advanced two | |||
// places. | |||
// | |||
// So, if we are stopping at the end of a line, we | |||
// need to write a newline so that our cursor can be | |||
// advanced to the next line. | |||
t.outBuf = append(t.outBuf, '\r', '\n') | |||
} | |||
} | |||
func (t *Terminal) eraseNPreviousChars(n int) { | |||
if n == 0 { | |||
return | |||
} | |||
if t.pos < n { | |||
n = t.pos | |||
} | |||
t.pos -= n | |||
t.moveCursorToPos(t.pos) | |||
copy(t.line[t.pos:], t.line[n+t.pos:]) | |||
t.line = t.line[:len(t.line)-n] | |||
if t.echo { | |||
t.writeLine(t.line[t.pos:]) | |||
for i := 0; i < n; i++ { | |||
t.queue(space) | |||
} | |||
t.advanceCursor(n) | |||
t.moveCursorToPos(t.pos) | |||
} | |||
} | |||
// countToLeftWord returns then number of characters from the cursor to the | |||
// start of the previous word. | |||
func (t *Terminal) countToLeftWord() int { | |||
if t.pos == 0 { | |||
return 0 | |||
} | |||
pos := t.pos - 1 | |||
for pos > 0 { | |||
if t.line[pos] != ' ' { | |||
break | |||
} | |||
pos-- | |||
} | |||
for pos > 0 { | |||
if t.line[pos] == ' ' { | |||
pos++ | |||
break | |||
} | |||
pos-- | |||
} | |||
return t.pos - pos | |||
} | |||
// countToRightWord returns then number of characters from the cursor to the | |||
// start of the next word. | |||
func (t *Terminal) countToRightWord() int { | |||
pos := t.pos | |||
for pos < len(t.line) { | |||
if t.line[pos] == ' ' { | |||
break | |||
} | |||
pos++ | |||
} | |||
for pos < len(t.line) { | |||
if t.line[pos] != ' ' { | |||
break | |||
} | |||
pos++ | |||
} | |||
return pos - t.pos | |||
} | |||
// visualLength returns the number of visible glyphs in s. | |||
func visualLength(runes []rune) int { | |||
inEscapeSeq := false | |||
length := 0 | |||
for _, r := range runes { | |||
switch { | |||
case inEscapeSeq: | |||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { | |||
inEscapeSeq = false | |||
} | |||
case r == '\x1b': | |||
inEscapeSeq = true | |||
default: | |||
length++ | |||
} | |||
} | |||
return length | |||
} | |||
// handleKey processes the given key and, optionally, returns a line of text | |||
// that the user has entered. | |||
func (t *Terminal) handleKey(key rune) (line string, ok bool) { | |||
if t.pasteActive && key != keyEnter { | |||
t.addKeyToLine(key) | |||
return | |||
} | |||
switch key { | |||
case keyBackspace: | |||
if t.pos == 0 { | |||
return | |||
} | |||
t.eraseNPreviousChars(1) | |||
case keyAltLeft: | |||
// move left by a word. | |||
t.pos -= t.countToLeftWord() | |||
t.moveCursorToPos(t.pos) | |||
case keyAltRight: | |||
// move right by a word. | |||
t.pos += t.countToRightWord() | |||
t.moveCursorToPos(t.pos) | |||
case keyLeft: | |||
if t.pos == 0 { | |||
return | |||
} | |||
t.pos-- | |||
t.moveCursorToPos(t.pos) | |||
case keyRight: | |||
if t.pos == len(t.line) { | |||
return | |||
} | |||
t.pos++ | |||
t.moveCursorToPos(t.pos) | |||
case keyHome: | |||
if t.pos == 0 { | |||
return | |||
} | |||
t.pos = 0 | |||
t.moveCursorToPos(t.pos) | |||
case keyEnd: | |||
if t.pos == len(t.line) { | |||
return | |||
} | |||
t.pos = len(t.line) | |||
t.moveCursorToPos(t.pos) | |||
case keyUp: | |||
entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) | |||
if !ok { | |||
return "", false | |||
} | |||
if t.historyIndex == -1 { | |||
t.historyPending = string(t.line) | |||
} | |||
t.historyIndex++ | |||
runes := []rune(entry) | |||
t.setLine(runes, len(runes)) | |||
case keyDown: | |||
switch t.historyIndex { | |||
case -1: | |||
return | |||
case 0: | |||
runes := []rune(t.historyPending) | |||
t.setLine(runes, len(runes)) | |||
t.historyIndex-- | |||
default: | |||
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) | |||
if ok { | |||
t.historyIndex-- | |||
runes := []rune(entry) | |||
t.setLine(runes, len(runes)) | |||
} | |||
} | |||
case keyEnter: | |||
t.moveCursorToPos(len(t.line)) | |||
t.queue([]rune("\r\n")) | |||
line = string(t.line) | |||
ok = true | |||
t.line = t.line[:0] | |||
t.pos = 0 | |||
t.cursorX = 0 | |||
t.cursorY = 0 | |||
t.maxLine = 0 | |||
case keyDeleteWord: | |||
// Delete zero or more spaces and then one or more characters. | |||
t.eraseNPreviousChars(t.countToLeftWord()) | |||
case keyDeleteLine: | |||
// Delete everything from the current cursor position to the | |||
// end of line. | |||
for i := t.pos; i < len(t.line); i++ { | |||
t.queue(space) | |||
t.advanceCursor(1) | |||
} | |||
t.line = t.line[:t.pos] | |||
t.moveCursorToPos(t.pos) | |||
case keyCtrlD: | |||
// Erase the character under the current position. | |||
// The EOF case when the line is empty is handled in | |||
// readLine(). | |||
if t.pos < len(t.line) { | |||
t.pos++ | |||
t.eraseNPreviousChars(1) | |||
} | |||
case keyCtrlU: | |||
t.eraseNPreviousChars(t.pos) | |||
case keyClearScreen: | |||
// Erases the screen and moves the cursor to the home position. | |||
t.queue([]rune("\x1b[2J\x1b[H")) | |||
t.queue(t.prompt) | |||
t.cursorX, t.cursorY = 0, 0 | |||
t.advanceCursor(visualLength(t.prompt)) | |||
t.setLine(t.line, t.pos) | |||
default: | |||
if t.AutoCompleteCallback != nil { | |||
prefix := string(t.line[:t.pos]) | |||
suffix := string(t.line[t.pos:]) | |||
t.lock.Unlock() | |||
newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) | |||
t.lock.Lock() | |||
if completeOk { | |||
t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) | |||
return | |||
} | |||
} | |||
if !isPrintable(key) { | |||
return | |||
} | |||
if len(t.line) == maxLineLength { | |||
return | |||
} | |||
t.addKeyToLine(key) | |||
} | |||
return | |||
} | |||
// addKeyToLine inserts the given key at the current position in the current | |||
// line. | |||
func (t *Terminal) addKeyToLine(key rune) { | |||
if len(t.line) == cap(t.line) { | |||
newLine := make([]rune, len(t.line), 2*(1+len(t.line))) | |||
copy(newLine, t.line) | |||
t.line = newLine | |||
} | |||
t.line = t.line[:len(t.line)+1] | |||
copy(t.line[t.pos+1:], t.line[t.pos:]) | |||
t.line[t.pos] = key | |||
if t.echo { | |||
t.writeLine(t.line[t.pos:]) | |||
} | |||
t.pos++ | |||
t.moveCursorToPos(t.pos) | |||
} | |||
func (t *Terminal) writeLine(line []rune) { | |||
for len(line) != 0 { | |||
remainingOnLine := t.termWidth - t.cursorX | |||
todo := len(line) | |||
if todo > remainingOnLine { | |||
todo = remainingOnLine | |||
} | |||
t.queue(line[:todo]) | |||
t.advanceCursor(visualLength(line[:todo])) | |||
line = line[todo:] | |||
} | |||
} | |||
// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. | |||
func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { | |||
for len(buf) > 0 { | |||
i := bytes.IndexByte(buf, '\n') | |||
todo := len(buf) | |||
if i >= 0 { | |||
todo = i | |||
} | |||
var nn int | |||
nn, err = w.Write(buf[:todo]) | |||
n += nn | |||
if err != nil { | |||
return n, err | |||
} | |||
buf = buf[todo:] | |||
if i >= 0 { | |||
if _, err = w.Write(crlf); err != nil { | |||
return n, err | |||
} | |||
n++ | |||
buf = buf[1:] | |||
} | |||
} | |||
return n, nil | |||
} | |||
func (t *Terminal) Write(buf []byte) (n int, err error) { | |||
t.lock.Lock() | |||
defer t.lock.Unlock() | |||
if t.cursorX == 0 && t.cursorY == 0 { | |||
// This is the easy case: there's nothing on the screen that we | |||
// have to move out of the way. | |||
return writeWithCRLF(t.c, buf) | |||
} | |||
// We have a prompt and possibly user input on the screen. We | |||
// have to clear it first. | |||
t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) | |||
t.cursorX = 0 | |||
t.clearLineToRight() | |||
for t.cursorY > 0 { | |||
t.move(1 /* up */, 0, 0, 0) | |||
t.cursorY-- | |||
t.clearLineToRight() | |||
} | |||
if _, err = t.c.Write(t.outBuf); err != nil { | |||
return | |||
} | |||
t.outBuf = t.outBuf[:0] | |||
if n, err = writeWithCRLF(t.c, buf); err != nil { | |||
return | |||
} | |||
t.writeLine(t.prompt) | |||
if t.echo { | |||
t.writeLine(t.line) | |||
} | |||
t.moveCursorToPos(t.pos) | |||
if _, err = t.c.Write(t.outBuf); err != nil { | |||
return | |||
} | |||
t.outBuf = t.outBuf[:0] | |||
return | |||
} | |||
// ReadPassword temporarily changes the prompt and reads a password, without | |||
// echo, from the terminal. | |||
func (t *Terminal) ReadPassword(prompt string) (line string, err error) { | |||
t.lock.Lock() | |||
defer t.lock.Unlock() | |||
oldPrompt := t.prompt | |||
t.prompt = []rune(prompt) | |||
t.echo = false | |||
line, err = t.readLine() | |||
t.prompt = oldPrompt | |||
t.echo = true | |||
return | |||
} | |||
// ReadLine returns a line of input from the terminal. | |||
func (t *Terminal) ReadLine() (line string, err error) { | |||
t.lock.Lock() | |||
defer t.lock.Unlock() | |||
return t.readLine() | |||
} | |||
func (t *Terminal) readLine() (line string, err error) { | |||
// t.lock must be held at this point | |||
if t.cursorX == 0 && t.cursorY == 0 { | |||
t.writeLine(t.prompt) | |||
t.c.Write(t.outBuf) | |||
t.outBuf = t.outBuf[:0] | |||
} | |||
lineIsPasted := t.pasteActive | |||
for { | |||
rest := t.remainder | |||
lineOk := false | |||
for !lineOk { | |||
var key rune | |||
key, rest = bytesToKey(rest, t.pasteActive) | |||
if key == utf8.RuneError { | |||
break | |||
} | |||
if !t.pasteActive { | |||
if key == keyCtrlD { | |||
if len(t.line) == 0 { | |||
return "", io.EOF | |||
} | |||
} | |||
if key == keyPasteStart { | |||
t.pasteActive = true | |||
if len(t.line) == 0 { | |||
lineIsPasted = true | |||
} | |||
continue | |||
} | |||
} else if key == keyPasteEnd { | |||
t.pasteActive = false | |||
continue | |||
} | |||
if !t.pasteActive { | |||
lineIsPasted = false | |||
} | |||
line, lineOk = t.handleKey(key) | |||
} | |||
if len(rest) > 0 { | |||
n := copy(t.inBuf[:], rest) | |||
t.remainder = t.inBuf[:n] | |||
} else { | |||
t.remainder = nil | |||
} | |||
t.c.Write(t.outBuf) | |||
t.outBuf = t.outBuf[:0] | |||
if lineOk { | |||
if t.echo { | |||
t.historyIndex = -1 | |||
t.history.Add(line) | |||
} | |||
if lineIsPasted { | |||
err = ErrPasteIndicator | |||
} | |||
return | |||
} | |||
// t.remainder is a slice at the beginning of t.inBuf | |||
// containing a partial key sequence | |||
readBuf := t.inBuf[len(t.remainder):] | |||
var n int | |||
t.lock.Unlock() | |||
n, err = t.c.Read(readBuf) | |||
t.lock.Lock() | |||
if err != nil { | |||
return | |||
} | |||
t.remainder = t.inBuf[:n+len(t.remainder)] | |||
} | |||
} | |||
// SetPrompt sets the prompt to be used when reading subsequent lines. | |||
func (t *Terminal) SetPrompt(prompt string) { | |||
t.lock.Lock() | |||
defer t.lock.Unlock() | |||
t.prompt = []rune(prompt) | |||
} | |||
func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { | |||
// Move cursor to column zero at the start of the line. | |||
t.move(t.cursorY, 0, t.cursorX, 0) | |||
t.cursorX, t.cursorY = 0, 0 | |||
t.clearLineToRight() | |||
for t.cursorY < numPrevLines { | |||
// Move down a line | |||
t.move(0, 1, 0, 0) | |||
t.cursorY++ | |||
t.clearLineToRight() | |||
} | |||
// Move back to beginning. | |||
t.move(t.cursorY, 0, 0, 0) | |||
t.cursorX, t.cursorY = 0, 0 | |||
t.queue(t.prompt) | |||
t.advanceCursor(visualLength(t.prompt)) | |||
t.writeLine(t.line) | |||
t.moveCursorToPos(t.pos) | |||
} | |||
func (t *Terminal) SetSize(width, height int) error { | |||
t.lock.Lock() | |||
defer t.lock.Unlock() | |||
if width == 0 { | |||
width = 1 | |||
} | |||
oldWidth := t.termWidth | |||
t.termWidth, t.termHeight = width, height | |||
switch { | |||
case width == oldWidth: | |||
// If the width didn't change then nothing else needs to be | |||
// done. | |||
return nil | |||
case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: | |||
// If there is nothing on current line and no prompt printed, | |||
// just do nothing | |||
return nil | |||
case width < oldWidth: | |||
// Some terminals (e.g. xterm) will truncate lines that were | |||
// too long when shinking. Others, (e.g. gnome-terminal) will | |||
// attempt to wrap them. For the former, repainting t.maxLine | |||
// works great, but that behaviour goes badly wrong in the case | |||
// of the latter because they have doubled every full line. | |||
// We assume that we are working on a terminal that wraps lines | |||
// and adjust the cursor position based on every previous line | |||
// wrapping and turning into two. This causes the prompt on | |||
// xterms to move upwards, which isn't great, but it avoids a | |||
// huge mess with gnome-terminal. | |||
if t.cursorX >= t.termWidth { | |||
t.cursorX = t.termWidth - 1 | |||
} | |||
t.cursorY *= 2 | |||
t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) | |||
case width > oldWidth: | |||
// If the terminal expands then our position calculations will | |||
// be wrong in the future because we think the cursor is | |||
// |t.pos| chars into the string, but there will be a gap at | |||
// the end of any wrapped line. | |||
// | |||
// But the position will actually be correct until we move, so | |||
// we can move back to the beginning and repaint everything. | |||
t.clearAndRepaintLinePlusNPrevious(t.maxLine) | |||
} | |||
_, err := t.c.Write(t.outBuf) | |||
t.outBuf = t.outBuf[:0] | |||
return err | |||
} | |||
type pasteIndicatorError struct{} | |||
func (pasteIndicatorError) Error() string { | |||
return "terminal: ErrPasteIndicator not correctly handled" | |||
} | |||
// ErrPasteIndicator may be returned from ReadLine as the error, in addition | |||
// to valid line data. It indicates that bracketed paste mode is enabled and | |||
// that the returned line consists only of pasted data. Programs may wish to | |||
// interpret pasted data more literally than typed data. | |||
var ErrPasteIndicator = pasteIndicatorError{} | |||
// SetBracketedPasteMode requests that the terminal bracket paste operations | |||
// with markers. Not all terminals support this but, if it is supported, then | |||
// enabling this mode will stop any autocomplete callback from running due to | |||
// pastes. Additionally, any lines that are completely pasted will be returned | |||
// from ReadLine with the error set to ErrPasteIndicator. | |||
func (t *Terminal) SetBracketedPasteMode(on bool) { | |||
if on { | |||
io.WriteString(t.c, "\x1b[?2004h") | |||
} else { | |||
io.WriteString(t.c, "\x1b[?2004l") | |||
} | |||
} | |||
// stRingBuffer is a ring buffer of strings. | |||
type stRingBuffer struct { | |||
// entries contains max elements. | |||
entries []string | |||
max int | |||
// head contains the index of the element most recently added to the ring. | |||
head int | |||
// size contains the number of elements in the ring. | |||
size int | |||
} | |||
func (s *stRingBuffer) Add(a string) { | |||
if s.entries == nil { | |||
const defaultNumEntries = 100 | |||
s.entries = make([]string, defaultNumEntries) | |||
s.max = defaultNumEntries | |||
} | |||
s.head = (s.head + 1) % s.max | |||
s.entries[s.head] = a | |||
if s.size < s.max { | |||
s.size++ | |||
} | |||
} | |||
// NthPreviousEntry returns the value passed to the nth previous call to Add. | |||
// If n is zero then the immediately prior value is returned, if one, then the | |||
// next most recent, and so on. If such an element doesn't exist then ok is | |||
// false. | |||
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { | |||
if n >= s.size { | |||
return "", false | |||
} | |||
index := s.head - n | |||
if index < 0 { | |||
index += s.max | |||
} | |||
return s.entries[index], true | |||
} | |||
// readPasswordLine reads from reader until it finds \n or io.EOF. | |||
// The slice returned does not include the \n. | |||
// readPasswordLine also ignores any \r it finds. | |||
func readPasswordLine(reader io.Reader) ([]byte, error) { | |||
var buf [1]byte | |||
var ret []byte | |||
for { | |||
n, err := reader.Read(buf[:]) | |||
if n > 0 { | |||
switch buf[0] { | |||
case '\n': | |||
return ret, nil | |||
case '\r': | |||
// remove \r from passwords on Windows | |||
default: | |||
ret = append(ret, buf[0]) | |||
} | |||
continue | |||
} | |||
if err != nil { | |||
if err == io.EOF && len(ret) > 0 { | |||
return ret, nil | |||
} | |||
return ret, err | |||
} | |||
} | |||
} |
@@ -0,0 +1,114 @@ | |||
// Copyright 2011 The Go Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd | |||
// Package terminal provides support functions for dealing with terminals, as | |||
// commonly found on UNIX systems. | |||
// | |||
// Putting a terminal into raw mode is the most common requirement: | |||
// | |||
// oldState, err := terminal.MakeRaw(0) | |||
// if err != nil { | |||
// panic(err) | |||
// } | |||
// defer terminal.Restore(0, oldState) | |||
package terminal // import "golang.org/x/crypto/ssh/terminal" | |||
import ( | |||
"golang.org/x/sys/unix" | |||
) | |||
// State contains the state of a terminal. | |||
type State struct { | |||
termios unix.Termios | |||
} | |||
// IsTerminal returns whether the given file descriptor is a terminal. | |||
func IsTerminal(fd int) bool { | |||
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios) | |||
return err == nil | |||
} | |||
// MakeRaw put the terminal connected to the given file descriptor into raw | |||
// mode and returns the previous state of the terminal so that it can be | |||
// restored. | |||
func MakeRaw(fd int) (*State, error) { | |||
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) | |||
if err != nil { | |||
return nil, err | |||
} | |||
oldState := State{termios: *termios} | |||
// This attempts to replicate the behaviour documented for cfmakeraw in | |||
// the termios(3) manpage. | |||
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON | |||
termios.Oflag &^= unix.OPOST | |||
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN | |||
termios.Cflag &^= unix.CSIZE | unix.PARENB | |||
termios.Cflag |= unix.CS8 | |||
termios.Cc[unix.VMIN] = 1 | |||
termios.Cc[unix.VTIME] = 0 | |||
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { | |||
return nil, err | |||
} | |||
return &oldState, nil | |||
} | |||
// GetState returns the current state of a terminal which may be useful to | |||
// restore the terminal after a signal. | |||
func GetState(fd int) (*State, error) { | |||
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &State{termios: *termios}, nil | |||
} | |||
// Restore restores the terminal connected to the given file descriptor to a | |||
// previous state. | |||
func Restore(fd int, state *State) error { | |||
return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) | |||
} | |||
// GetSize returns the dimensions of the given terminal. | |||
func GetSize(fd int) (width, height int, err error) { | |||
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) | |||
if err != nil { | |||
return -1, -1, err | |||
} | |||
return int(ws.Col), int(ws.Row), nil | |||
} | |||
// passwordReader is an io.Reader that reads from a specific file descriptor. | |||
type passwordReader int | |||
func (r passwordReader) Read(buf []byte) (int, error) { | |||
return unix.Read(int(r), buf) | |||
} | |||
// ReadPassword reads a line of input from a terminal without local echo. This | |||
// is commonly used for inputting passwords and other sensitive data. The slice | |||
// returned does not include the \n. | |||
func ReadPassword(fd int) ([]byte, error) { | |||
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) | |||
if err != nil { | |||
return nil, err | |||
} | |||
newState := *termios | |||
newState.Lflag &^= unix.ECHO | |||
newState.Lflag |= unix.ICANON | unix.ISIG | |||
newState.Iflag |= unix.ICRNL | |||
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { | |||
return nil, err | |||
} | |||
defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) | |||
return readPasswordLine(passwordReader(fd)) | |||
} |
@@ -0,0 +1,12 @@ | |||
// Copyright 2018 The Go Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
// +build aix | |||
package terminal | |||
import "golang.org/x/sys/unix" | |||
const ioctlReadTermios = unix.TCGETS | |||
const ioctlWriteTermios = unix.TCSETS |