2015-02-24 16:22:31 +01:00
|
|
|
package asciicast
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
2015-03-02 10:46:15 +01:00
|
|
|
"fmt"
|
2015-09-18 19:55:02 +02:00
|
|
|
"io"
|
2015-02-24 16:22:31 +01:00
|
|
|
"io/ioutil"
|
2015-09-18 19:55:02 +02:00
|
|
|
"net/http"
|
2015-02-24 16:22:31 +01:00
|
|
|
"os"
|
2015-09-18 19:55:02 +02:00
|
|
|
"strings"
|
2016-02-21 11:49:22 +01:00
|
|
|
|
|
|
|
|
"github.com/asciinema/asciinema/Godeps/_workspace/src/golang.org/x/net/html"
|
2015-02-24 16:22:31 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Env struct {
|
|
|
|
|
Term string `json:"TERM"`
|
|
|
|
|
Shell string `json:"SHELL"`
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-02 10:46:15 +01:00
|
|
|
type Duration float64
|
|
|
|
|
|
|
|
|
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
|
|
|
|
return []byte(fmt.Sprintf(`%.6f`, d)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2015-02-24 16:22:31 +01:00
|
|
|
type Asciicast struct {
|
2015-03-02 10:46:15 +01:00
|
|
|
Version int `json:"version"`
|
|
|
|
|
Width int `json:"width"`
|
|
|
|
|
Height int `json:"height"`
|
|
|
|
|
Duration Duration `json:"duration"`
|
|
|
|
|
Command string `json:"command"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Env *Env `json:"env"`
|
|
|
|
|
Stdout []Frame `json:"stdout"`
|
2015-02-24 16:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
2015-03-11 12:07:26 +01:00
|
|
|
func NewAsciicast(width, height int, duration float64, command, title string, frames []Frame, env map[string]string) *Asciicast {
|
2015-03-02 10:52:28 +01:00
|
|
|
return &Asciicast{
|
|
|
|
|
Version: 1,
|
|
|
|
|
Width: width,
|
|
|
|
|
Height: height,
|
|
|
|
|
Duration: Duration(duration),
|
|
|
|
|
Command: command,
|
|
|
|
|
Title: title,
|
2015-03-11 12:07:26 +01:00
|
|
|
Env: &Env{Term: env["TERM"], Shell: env["SHELL"]},
|
2015-03-02 10:52:28 +01:00
|
|
|
Stdout: frames,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-02-24 16:22:31 +01:00
|
|
|
func Save(asciicast *Asciicast, path string) error {
|
|
|
|
|
bytes, err := json.MarshalIndent(asciicast, "", " ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = ioutil.WriteFile(path, bytes, 0644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-18 19:55:02 +02:00
|
|
|
// asciinema play file.json
|
|
|
|
|
// asciinema play https://asciinema.org/a/123.json
|
2016-02-21 11:49:22 +01:00
|
|
|
// asciinema play https://asciinema.org/a/123
|
2015-09-19 10:52:58 +02:00
|
|
|
// asciinema play ipfs://ipfs/QmbdpNCwqeZgnmAWBCQcs8u6Ts6P2ku97tfKAycE1XY88p
|
2015-09-18 19:55:02 +02:00
|
|
|
// asciinema play -
|
|
|
|
|
|
2016-02-21 11:49:22 +01:00
|
|
|
func getAttr(t *html.Token, name string) string {
|
|
|
|
|
for _, a := range t.Attr {
|
|
|
|
|
if a.Key == name {
|
|
|
|
|
return a.Val
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractJSONURL(htmlDoc io.Reader) (string, error) {
|
|
|
|
|
z := html.NewTokenizer(htmlDoc)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
tt := z.Next()
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case tt == html.ErrorToken:
|
|
|
|
|
return "", fmt.Errorf("expected alternate <link> not found in fetched HTML document")
|
|
|
|
|
case tt == html.StartTagToken:
|
|
|
|
|
t := z.Token()
|
|
|
|
|
|
|
|
|
|
if t.Data == "link" && getAttr(&t, "rel") == "alternate" && getAttr(&t, "type") == "application/asciicast+json" {
|
|
|
|
|
return getAttr(&t, "href"), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-18 19:55:02 +02:00
|
|
|
func getSource(url string) (io.ReadCloser, error) {
|
2016-02-21 11:49:22 +01:00
|
|
|
var source io.ReadCloser
|
|
|
|
|
var isHTML bool
|
|
|
|
|
var err error
|
|
|
|
|
|
2016-02-21 20:23:00 +01:00
|
|
|
if strings.HasPrefix(url, "ipfs:/") {
|
|
|
|
|
url = fmt.Sprintf("https://ipfs.io/%v", url[6:])
|
|
|
|
|
} else if strings.HasPrefix(url, "fs:/") {
|
|
|
|
|
url = fmt.Sprintf("https://ipfs.io/%v", url[4:])
|
2015-09-18 19:55:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if url == "-" {
|
2016-02-21 11:49:22 +01:00
|
|
|
source = os.Stdin
|
|
|
|
|
} else if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
2015-09-18 19:55:02 +02:00
|
|
|
resp, err := http.Get(url)
|
2015-06-23 17:50:09 +02:00
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2015-09-18 19:55:02 +02:00
|
|
|
|
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
|
resp.Body.Close()
|
|
|
|
|
return nil, fmt.Errorf("got status %v when requesting %v", resp.StatusCode, url)
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-21 11:49:22 +01:00
|
|
|
source = resp.Body
|
|
|
|
|
|
|
|
|
|
if strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
|
|
|
|
|
isHTML = true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
source, err = os.Open(url)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.HasSuffix(url, ".html") {
|
|
|
|
|
isHTML = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isHTML {
|
|
|
|
|
defer source.Close()
|
|
|
|
|
url, err = extractJSONURL(source)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getSource(url)
|
2015-02-24 16:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
2016-02-21 11:49:22 +01:00
|
|
|
return source, nil
|
2015-09-18 19:55:02 +02:00
|
|
|
}
|
2015-02-24 16:22:31 +01:00
|
|
|
|
2015-09-18 19:55:02 +02:00
|
|
|
func Load(url string) (*Asciicast, error) {
|
|
|
|
|
source, err := getSource(url)
|
2015-02-24 16:22:31 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2015-09-18 19:55:02 +02:00
|
|
|
defer source.Close()
|
|
|
|
|
|
|
|
|
|
dec := json.NewDecoder(source)
|
|
|
|
|
asciicast := &Asciicast{}
|
|
|
|
|
|
|
|
|
|
if err = dec.Decode(asciicast); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2015-02-24 16:22:31 +01:00
|
|
|
|
|
|
|
|
return asciicast, nil
|
|
|
|
|
}
|