diff --git a/config.go b/config.go
index 71a12db..5347ccb 100644
--- a/config.go
+++ b/config.go
@@ -22,6 +22,7 @@ type outputMode int
const (
outModeHTML outputMode = iota
+ outModeJSON
outModeTwee3
outModeTwee1
outModeTwine2Archive
@@ -132,6 +133,7 @@ func newConfig() *config {
options.Add("format", "-f=s|--format=s")
options.Add("head", "--head=s")
options.Add("help", "-h|--help")
+ options.Add("json", "-j|--json")
options.Add("listcharsets", "--list-charsets")
options.Add("listformats", "--list-formats")
options.Add("logfiles", "--log-files")
@@ -164,6 +166,8 @@ func newConfig() *config {
c.headFile = val.(string)
case "help":
usage()
+ case "json":
+ c.outMode = outModeJSON
case "listcharsets":
usageCharsets()
case "listformats":
diff --git a/passage.go b/passage.go
index 81ddc46..61ce681 100644
--- a/passage.go
+++ b/passage.go
@@ -8,7 +8,6 @@ package main
import (
// standard packages
- "fmt"
"regexp"
"strings"
@@ -35,7 +34,7 @@ var infoPassages = []string{
// Compilers: Twine/Twee 1.4+, Twee2, & Tweego.
"StorySettings",
- // Compilers: Tweego & (whatever Dan Cox's compiler is called).
+ // Compilers: Tweego, Extwee, & others.
"StoryData",
// Compilers: Twine/Twee 1.4+ & Twee2.
@@ -143,95 +142,6 @@ func (p *passage) isStoryPassage() bool {
return !p.hasInfoName() && !p.hasInfoTags()
}
-func (p *passage) toTwee(outMode outputMode) string {
- var output string
- if outMode == outModeTwee3 {
- output = ":: " + tweeEscapeString(p.name)
- if len(p.tags) > 0 {
- output += " [" + tweeEscapeString(strings.Join(p.tags, " ")) + "]"
- }
- if p.hasAnyMetadata() {
- output += " " + string(p.marshalMetadata())
- }
- } else {
- output = ":: " + p.name
- if len(p.tags) > 0 {
- output += " [" + strings.Join(p.tags, " ") + "]"
- }
- }
- output += "\n"
- if len(p.text) > 0 {
- output += p.text + "\n"
- }
- output += "\n\n"
- return output
-}
-
-func (p *passage) toPassagedata(pid uint) string {
- var (
- position string
- size string
- )
- if p.hasMetadataPosition() {
- position = p.metadata.position
- } else {
- // No position metadata, so generate something sensible on the fly.
- x := pid % 10
- y := pid / 10
- if x == 0 {
- x = 10
- } else {
- y++
- }
- position = fmt.Sprintf("%d,%d", x*125-25, y*125-25)
- }
- if p.hasMetadataSize() {
- size = p.metadata.size
- } else {
- // No size metadata, so default to the normal size.
- size = "100,100"
- }
-
- /*
- …
- */
- return fmt.Sprintf(`%s`,
- pid,
- attrEscapeString(p.name),
- attrEscapeString(strings.Join(p.tags, " ")),
- attrEscapeString(position),
- attrEscapeString(size),
- htmlEscapeString(p.text),
- )
-}
-
-func (p *passage) toTiddler(pid uint) string {
- var position string
- if p.hasMetadataPosition() {
- position = p.metadata.position
- } else {
- // No position metadata, so generate something sensible on the fly.
- x := pid % 10
- y := pid / 10
- if x == 0 {
- x = 10
- } else {
- y++
- }
- position = fmt.Sprintf("%d,%d", x*140-130, y*140-130)
- }
-
- /*
-
…
- */
- return fmt.Sprintf(`%s
`,
- attrEscapeString(p.name),
- attrEscapeString(strings.Join(p.tags, " ")),
- attrEscapeString(position),
- tiddlerEscapeString(p.text),
- )
-}
-
func (p *passage) countWords() uint64 {
text := p.text
diff --git a/passagedata.go b/passagedata.go
index 182117a..a222a3c 100644
--- a/passagedata.go
+++ b/passagedata.go
@@ -10,6 +10,12 @@ import (
"encoding/json"
)
+type passageJSON struct {
+ Name string `json:"name"`
+ Tags []string `json:"tags,omitempty"`
+ Text string `json:"text"`
+}
+
type passageMetadataJSON struct {
Position string `json:"position,omitempty"` // Twine 2 (`position`) & Twine 1 (`twine-position`).
Size string `json:"size,omitempty"` // Twine 2 (`size`).
diff --git a/passageout.go b/passageout.go
new file mode 100644
index 0000000..912401c
--- /dev/null
+++ b/passageout.go
@@ -0,0 +1,110 @@
+/*
+ Copyright © 2014–2021 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
+ "fmt"
+ "strings"
+)
+
+func (p *passage) toJSON() *passageJSON {
+ return &passageJSON{
+ Name: p.name,
+ Tags: p.tags,
+ Text: p.text,
+ }
+}
+
+func (p *passage) toTwee(outMode outputMode) string {
+ var output string
+ if outMode == outModeTwee3 {
+ output = ":: " + tweeEscapeString(p.name)
+ if len(p.tags) > 0 {
+ output += " [" + tweeEscapeString(strings.Join(p.tags, " ")) + "]"
+ }
+ if p.hasAnyMetadata() {
+ output += " " + string(p.marshalMetadata())
+ }
+ } else {
+ output = ":: " + p.name
+ if len(p.tags) > 0 {
+ output += " [" + strings.Join(p.tags, " ") + "]"
+ }
+ }
+ output += "\n"
+ if len(p.text) > 0 {
+ output += p.text + "\n"
+ }
+ output += "\n\n"
+ return output
+}
+
+func (p *passage) toPassagedata(pid uint) string {
+ var (
+ position string
+ size string
+ )
+ if p.hasMetadataPosition() {
+ position = p.metadata.position
+ } else {
+ // No position metadata, so generate something sensible on the fly.
+ x := pid % 10
+ y := pid / 10
+ if x == 0 {
+ x = 10
+ } else {
+ y++
+ }
+ position = fmt.Sprintf("%d,%d", x*125-25, y*125-25)
+ }
+ if p.hasMetadataSize() {
+ size = p.metadata.size
+ } else {
+ // No size metadata, so default to the normal size.
+ size = "100,100"
+ }
+
+ /*
+ …
+ */
+ return fmt.Sprintf(`%s`,
+ pid,
+ attrEscapeString(p.name),
+ attrEscapeString(strings.Join(p.tags, " ")),
+ attrEscapeString(position),
+ attrEscapeString(size),
+ htmlEscapeString(p.text),
+ )
+}
+
+func (p *passage) toTiddler(pid uint) string {
+ var position string
+ if p.hasMetadataPosition() {
+ position = p.metadata.position
+ } else {
+ // No position metadata, so generate something sensible on the fly.
+ x := pid % 10
+ y := pid / 10
+ if x == 0 {
+ x = 10
+ } else {
+ y++
+ }
+ position = fmt.Sprintf("%d,%d", x*140-130, y*140-130)
+ }
+
+ /*
+ …
+ */
+ return fmt.Sprintf(`%s
`,
+ attrEscapeString(p.name),
+ attrEscapeString(strings.Join(p.tags, " ")),
+ attrEscapeString(position),
+ tiddlerEscapeString(p.text),
+ )
+}
diff --git a/storydata.go b/storydata.go
index 45108c2..1dd974f 100644
--- a/storydata.go
+++ b/storydata.go
@@ -13,6 +13,18 @@ import (
"strings"
)
+type storyJSON struct {
+ Name string `json:"name"`
+ Ifid string `json:"ifid,omitempty"`
+ Start string `json:"start,omitempty"`
+ Options []string `json:"options,omitempty"`
+ Format string `json:"format,omitempty"`
+ FormatVersion string `json:"format-version,omitempty"`
+ Creator string `json:"creator,omitempty"`
+ CreatorVersion string `json:"creator-version,omitempty"`
+ Passages []*passageJSON `json:"passages"`
+}
+
type storyDataJSON struct {
Ifid string `json:"ifid,omitempty"`
Format string `json:"format,omitempty"`
diff --git a/storyout.go b/storyout.go
index 42a05d0..5018a5a 100644
--- a/storyout.go
+++ b/storyout.go
@@ -8,6 +8,7 @@ package main
import (
"bytes"
+ "encoding/json"
"fmt"
"log"
"os"
@@ -17,6 +18,40 @@ import (
"time"
)
+func (s *story) toJSON(startName string) []byte {
+ var passages = make([]*passageJSON, 0, 32)
+ for _, p := range s.passages {
+ if p.name == "StoryTitle" || p.name == "StoryData" {
+ continue
+ }
+
+ passages = append(passages, p.toJSON())
+ }
+
+ marshaled, err := json.MarshalIndent(
+ &storyJSON{
+ s.name,
+ s.ifid,
+ startName,
+ twine2OptionsMapToSlice(s.twine2.options),
+ s.twine2.format,
+ s.twine2.formatVersion,
+ strings.Title(tweegoName),
+ tweegoVersion.Version(),
+ passages,
+ },
+ "",
+ "\t",
+ )
+ if err != nil {
+ // NOTE: We should never be able to see an error here. If we do,
+ // then something truly exceptional—in a bad way—has happened, so
+ // we get our panic on.
+ panic(err)
+ }
+ return marshaled
+}
+
func (s *story) toTwee(outMode outputMode) []byte {
var data []byte
for _, p := range s.passages {
diff --git a/tweego.go b/tweego.go
index 4c4a6a2..cbf5d58 100644
--- a/tweego.go
+++ b/tweego.go
@@ -64,6 +64,11 @@ func buildOutput(c *config) *story {
// Write the output.
switch c.outMode {
+ case outModeJSON:
+ // Write out the project as JSON.
+ if _, err := fileWriteAll(c.outFile, alignRecordSeparators(s.toJSON(c.startName))); err != nil {
+ log.Fatalf(`error: %s`, err.Error())
+ }
case outModeTwee3, outModeTwee1:
// Write out the project as Twee source.
if _, err := fileWriteAll(c.outFile, alignRecordSeparators(s.toTwee(c.outMode))); err != nil {
diff --git a/usage.go b/usage.go
index 180f4df..79f8c18 100644
--- a/usage.go
+++ b/usage.go
@@ -42,6 +42,7 @@ Options:
-h, --help Print this help, then exit.
--head=FILE Name of the file whose contents will be appended
as-is to the element of the compiled HTML.
+ -j, --json Output JSON, instead of compiled HTML.
--list-charsets List the supported input character sets, then exit.
--list-formats List the available story formats, then exit.
--log-files Log the processed input files.
@@ -121,7 +122,7 @@ func usageVersion() {
fmt.Fprintf(os.Stderr, "\n%s, %s\n", tweegoName, tweegoVersion)
fmt.Fprint(os.Stderr, `
Tweego (a Twee compiler in Go) [http://www.motoslave.net/tweego/]
-Copyright (c) 2014-2020 Thomas Michael Edwards. All rights reserved.
+Copyright (c) 2014-2021 Thomas Michael Edwards. All rights reserved.
`)
os.Exit(1)