Initial Bitbucket→GitHub migration commit, based on release v2.0.0.

This commit is contained in:
Thomas M. Edwards 2019-12-23 13:45:15 -06:00
commit 57e1aa52ff
36 changed files with 5026 additions and 0 deletions

294
formats.go Normal file
View file

@ -0,0 +1,294 @@
/*
Copyright © 20142019 Thomas Michael Edwards. All rights reserved.
Use of this source code is governed by a Simplified BSD License which
can be found in the LICENSE file.
*/
package main
import (
// standard packages
"bytes"
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
"strings"
// external packages
"github.com/blang/semver"
)
type twine2FormatJSON struct {
Name string `json:"name"`
Version string `json:"version"`
// Description string `json:"description"`
// Author string `json:"author"`
// Image string `json:"image"`
// URL string `json:"url"`
// License string `json:"license"`
Proofing bool `json:"proofing"`
Source string `json:"source"`
// Setup string `json:"-"`
}
type storyFormat struct {
id string
filename string
twine2 bool
name string
version string
proofing bool
}
func (f *storyFormat) isTwine1Style() bool {
return !f.twine2
}
func (f *storyFormat) isTwine2Style() bool {
return f.twine2
}
func (f *storyFormat) getStoryFormatData(source []byte) (*twine2FormatJSON, error) {
if !f.twine2 {
return nil, errors.New("Not a Twine 2 style story format.")
}
// get the JSON chunk from the source
first := bytes.Index(source, []byte("{"))
last := bytes.LastIndex(source, []byte("}"))
if first == -1 || last == -1 {
return nil, errors.New("Could not find Twine 2 style story format JSON chunk.")
}
source = source[first : last+1]
// parse the JSON
data := &twine2FormatJSON{}
if err := json.Unmarshal(source, data); err != nil {
/*
START Harlowe malformed JSON chunk workaround
TODO: Remove this workaround that attempts to handle Harlowe's
broken JSON chunk.
NOTE: This worksaround is only possible because, currently,
Harlowe's "setup" property is the last entry in the chunk.
*/
if strings.HasPrefix(strings.ToLower(f.id), "harlowe") {
if i := bytes.LastIndex(source, []byte(`,"setup": function`)); i != -1 {
// cut the "setup" property and its invalid value
j := len(source) - 1
source = append(source[:i], source[j:]...)
return f.getStoryFormatData(source)
}
}
/*
If we've reached this point, either the format is not Harlowe
or we cannot find the start of its "setup" property, so just
return the JSON decoding error as normal.
END Harlowe malformed JSON chunk workaround
*/
return nil, errors.New("Could not decode story format JSON chunk.")
}
return data, nil
}
func (f *storyFormat) unmarshalMetadata() error {
if !f.twine2 {
return nil
}
var (
data *twine2FormatJSON
source []byte
err error
)
// read in the story format
if source, err = fileReadAllAsUTF8(f.filename); err != nil {
return err
}
// load various bits of metadata from the JSON
if data, err = f.getStoryFormatData(source); err != nil {
return err
}
f.name = data.Name
f.version = data.Version
f.proofing = data.Proofing
return nil
}
func (f *storyFormat) source() []byte {
var (
source []byte
err error
)
// read in the story format
if source, err = fileReadAllAsUTF8(f.filename); err != nil {
log.Fatalf("error: format %s", err.Error())
}
// if in Twine 2 style, extract the actual source from the JSON
if f.twine2 {
var data *twine2FormatJSON
if data, err = f.getStoryFormatData(source); err != nil {
log.Fatalf("error: format %s: %s", f.id, err.Error())
}
source = []byte(data.Source)
}
return source
}
type storyFormatsMap map[string]*storyFormat
func newStoryFormatsMap(searchPaths []string) storyFormatsMap {
var (
baseFilenames = []string{"format.js", "header.html"}
formats = make(storyFormatsMap)
)
for _, searchDirname := range searchPaths {
if info, err := os.Stat(searchDirname); err != nil || !info.IsDir() {
continue
}
d, err := os.Open(searchDirname)
if err != nil {
continue
}
baseDirnames, err := d.Readdirnames(0)
if err != nil {
continue
}
for _, baseDirname := range baseDirnames {
formatDirname := filepath.Join(searchDirname, baseDirname)
if info, err := os.Stat(formatDirname); err != nil || !info.IsDir() {
continue
}
for _, baseFilename := range baseFilenames {
formatFilename := filepath.Join(formatDirname, baseFilename)
if info, err := os.Stat(formatFilename); err == nil && info.Mode().IsRegular() {
f := &storyFormat{
id: baseDirname,
filename: formatFilename,
twine2: baseFilename == "format.js",
}
if err := f.unmarshalMetadata(); err != nil {
log.Printf("warning: format %s: Skipping format; %s", f.id, err.Error())
continue
}
formats[baseDirname] = f
break
}
}
}
}
return formats
}
func (m storyFormatsMap) isEmpty() bool {
return len(m) == 0
}
func (m storyFormatsMap) getIDFromTwine2Name(name string) string {
var (
found *semver.Version
id string
)
for _, f := range m {
if !f.twine2 {
continue
}
if f.name == name {
if v, err := semver.ParseTolerant(f.version); err == nil {
if found == nil || v.GT(*found) {
found = &v
id = f.id
}
}
}
}
return id
}
func (m storyFormatsMap) getIDFromTwine2NameAndVersion(name, version string) string {
var (
wanted *semver.Version
found *semver.Version
id string
)
if v, err := semver.ParseTolerant(version); err == nil {
wanted = &v
} else {
log.Printf("warning: format %q: Auto-selecting greatest version; Could not parse version %q.", name, version)
wanted = &semver.Version{Major: 0, Minor: 0, Patch: 0}
}
for _, f := range m {
if !f.twine2 {
continue
}
if f.name == name {
if v, err := semver.ParseTolerant(f.version); err == nil {
if wanted.Major == 0 || v.Major == wanted.Major && v.GTE(*wanted) {
if found == nil || v.GT(*found) {
found = &v
id = f.id
}
}
}
}
}
return id
}
func (m storyFormatsMap) hasByID(id string) bool {
_, ok := m[id]
return ok
}
func (m storyFormatsMap) hasByTwine2Name(name string) bool {
_, ok := m[m.getIDFromTwine2Name(name)]
return ok
}
func (m storyFormatsMap) hasByTwine2NameAndVersion(name, version string) bool {
_, ok := m[m.getIDFromTwine2NameAndVersion(name, version)]
return ok
}
func (m storyFormatsMap) getByID(id string) *storyFormat {
return m[id]
}
func (m storyFormatsMap) getByTwine2Name(name string) *storyFormat {
return m[m.getIDFromTwine2Name(name)]
}
func (m storyFormatsMap) getByTwine2NameAndVersion(name, version string) *storyFormat {
return m[m.getIDFromTwine2NameAndVersion(name, version)]
}
func (m storyFormatsMap) ids() []string {
var ids []string
for id := range m {
ids = append(ids, id)
}
return ids
}