tweego/formats.go
2020-02-24 14:28:13 -06:00

293 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright © 20142020 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/Masterminds/semver/v3"
)
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 have, err := semver.NewVersion(f.version); err == nil {
if found == nil || have.GreaterThan(found) {
found = have
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.NewVersion(version); err == nil {
wanted = v
} else {
log.Printf("warning: format %q: Auto-selecting greatest version; Could not parse version %q.", name, version)
}
for _, f := range m {
if !f.twine2 {
continue
}
if f.name == name {
if have, err := semver.NewVersion(f.version); err == nil {
if wanted == nil || have.Major() == wanted.Major() && have.Compare(wanted) > -1 {
if found == nil || have.GreaterThan(found) {
found = have
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
}