/* Copyright © 2014–2020 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 ( "bytes" "fmt" "log" "os" "path/filepath" "strconv" "strings" "time" ) func (s *story) toTwee(outMode outputMode) []byte { var data []byte for _, p := range s.passages { data = append(data, p.toTwee(outMode)...) } return data } func (s *story) toTwine2Archive(startName string) []byte { return append(s.getTwine2DataChunk(startName), '\n') } func (s *story) toTwine1Archive(startName string) []byte { var ( count uint data []byte template []byte ) data, count = s.getTwine1PassageChunk() // NOTE: In Twine 1.4, the passage data wrapper is part of the story formats // themselves, so we have to create/forge one here. We use the Twine 1.4 vanilla // `storeArea` ID, rather than SugarCube's preferred `store-area` ID, for maximum // compatibility and interoperability. template = append(template, fmt.Sprintf(`\n"...) return template } func (s *story) toTwine2HTML(startName string) []byte { var template = s.format.source() // Story instance replacements. if bytes.Contains(template, []byte("{{STORY_NAME}}")) { template = bytes.Replace(template, []byte("{{STORY_NAME}}"), []byte(htmlEscapeString(s.name)), -1) } if bytes.Contains(template, []byte("{{STORY_DATA}}")) { template = bytes.Replace(template, []byte("{{STORY_DATA}}"), s.getTwine2DataChunk(startName), 1) } return template } func (s *story) toTwine1HTML(startName string) []byte { var ( formatDir = filepath.Dir(s.format.filename) parentDir = filepath.Dir(formatDir) template = s.format.source() count uint data []byte component []byte err error ) // Get the story data. data, count = s.getTwine1PassageChunk() // Story format compiler byline replacement. if search := []byte(`Twine`), []byte(`>Tweego`), 1) } // Story format component replacements (SugarCube). if search := []byte(`"USER_LIB"`); bytes.Contains(template, search) { component, err = fileReadAllAsUTF8(filepath.Join(formatDir, "userlib.js")) if err == nil { template = bytes.Replace(template, search, component, 1) } else if !os.IsNotExist(err) { log.Fatalf("error: %s", err.Error()) } } // Story format component replacements (Twine 1.4+ vanilla story formats). if search := []byte(`"ENGINE"`); bytes.Contains(template, search) { component, err = fileReadAllAsUTF8(filepath.Join(parentDir, "engine.js")) if err != nil { log.Fatalf("error: %s", err.Error()) } template = bytes.Replace(template, search, component, 1) } for _, pattern := range []string{`"SUGARCANE"`, `"JONAH"`} { if search := []byte(pattern); bytes.Contains(template, search) { component, err = fileReadAllAsUTF8(filepath.Join(formatDir, "code.js")) if err != nil { log.Fatalf("error: %s", err.Error()) } template = bytes.Replace(template, search, component, 1) } } if s.twine1.settings["jquery"] == "on" { if search := []byte(`"JQUERY"`); bytes.Contains(template, search) { component, err = fileReadAllAsUTF8(filepath.Join(parentDir, "jquery.js")) if err != nil { log.Fatalf("error: %s", err.Error()) } template = bytes.Replace(template, search, component, 1) } } if s.twine1.settings["modernizr"] == "on" { if search := []byte(`"MODERNIZR"`); bytes.Contains(template, search) { component, err = fileReadAllAsUTF8(filepath.Join(parentDir, "modernizr.js")) if err != nil { log.Fatalf("error: %s", err.Error()) } template = bytes.Replace(template, search, component, 1) } } // Story instance replacements. if startName == defaultStartName { startName = "" } template = bytes.Replace(template, []byte(`"VERSION"`), []byte(fmt.Sprintf("Compiled with %s, %s", tweegoName, tweegoVersion.Version())), 1) template = bytes.Replace(template, []byte(`"TIME"`), []byte(fmt.Sprintf("Built on %s", time.Now().Format(time.RFC1123Z))), 1) template = bytes.Replace(template, []byte(`"START_AT"`), []byte(fmt.Sprintf(`%q`, startName)), 1) template = bytes.Replace(template, []byte(`"STORY_SIZE"`), []byte(fmt.Sprintf(`"%d"`, count)), 1) if bytes.Contains(template, []byte(`"STORY"`)) { // Twine/Twee ≥1.4 style story format. template = bytes.Replace(template, []byte(`"STORY"`), data, 1) } else { // Twine/Twee <1.4 style story format. var footer []byte footer, err = fileReadAllAsUTF8(filepath.Join(formatDir, "footer.html")) if err != nil { if os.IsNotExist(err) { footer = []byte("\n\n\n") } else { log.Fatalf("error: %s", err.Error()) } } template = append(template, data...) template = append(template, footer...) } // IFID replacement. if s.ifid != "" { if bytes.Contains(template, []byte(`
… */ data = append(data, ``...) // Prepare the script element. /* */ data = append(data, ``...) // Prepare tw-tag elements. /* */ if s.twine2.tagColors != nil { for tag, color := range s.twine2.tagColors { data = append(data, fmt.Sprintf(``, tag, color)...) } } // Prepare normal passage elements. pid = 1 for _, p := range s.passages { if p.name == "StoryTitle" || p.name == "StoryData" || p.tagsHasAny("script", "stylesheet", "Twine.private") { continue } /* LEGACY */ // TODO: Should we actually drop an empty StorySettings passage? if p.name == "StorySettings" && len(s.twine1.settings) == 0 { continue } /* END LEGACY */ data = append(data, p.toPassagedata(pid)...) if startName == p.name { startID = fmt.Sprint(pid) } pid++ } // Add the wrapper. /* */ if optCount := len(s.twine2.options); optCount > 0 { opts := make([]string, 0, optCount) for opt, val := range s.twine2.options { if val { opts = append(opts, opt) } } options = strings.Join(opts, " ") } data = append([]byte(fmt.Sprintf( ``+ ``...) return data } func (s *story) getTwine1PassageChunk() ([]byte, uint) { var ( data []byte count uint ) for _, p := range s.passages { if p.tagsHas("Twine.private") { continue } count++ data = append(data, p.toTiddler(count)...) } return data, count }