diff --git a/package-lock.json b/package-lock.json index 48c6c15e..25362c07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jest": "^27.4.0", - "eslint-plugin-jsdoc": "^46.8.1", + "eslint-plugin-jsdoc": "^46.8.2", "eslint-plugin-node": "^11.0.0", "eslint-plugin-promise": "^6.1.1", "jest": "^29.7.0", @@ -5152,9 +5152,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.8.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.1.tgz", - "integrity": "sha512-uTce7IBluPKXIQMWJkIwFsI1gv7sZRmLjctca2K5DIxPi8fSBj9f4iru42XmGwuiMyH2f3nfc60sFmnSGv4Z/A==", + "version": "46.8.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz", + "integrity": "sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==", "dev": true, "dependencies": { "@es-joy/jsdoccomment": "~0.40.1", @@ -14811,9 +14811,9 @@ } }, "eslint-plugin-jsdoc": { - "version": "46.8.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.1.tgz", - "integrity": "sha512-uTce7IBluPKXIQMWJkIwFsI1gv7sZRmLjctca2K5DIxPi8fSBj9f4iru42XmGwuiMyH2f3nfc60sFmnSGv4Z/A==", + "version": "46.8.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz", + "integrity": "sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==", "dev": true, "requires": { "@es-joy/jsdoccomment": "~0.40.1", diff --git a/package.json b/package.json index 08e2b4a3..433401bf 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jest": "^27.4.0", - "eslint-plugin-jsdoc": "^46.8.1", + "eslint-plugin-jsdoc": "^46.8.2", "eslint-plugin-node": "^11.0.0", "eslint-plugin-promise": "^6.1.1", "jest": "^29.7.0", diff --git a/src/Passage.js b/src/Passage.js index 8a609a09..504b8a48 100644 --- a/src/Passage.js +++ b/src/Passage.js @@ -27,24 +27,16 @@ export default class Passage { */ #_text = ''; - /** - * Internal PID of passage - * @private - */ - #_pid = -1; - /** * A passage is the smallest unit of a story. - * (The 'pid' property is only used in Twine 2 HTML.) * @function Passage * @class * @param {string} name - Name * @param {string} text - Content * @param {Array} tags - Tags * @param {object} metadata - Metadata - * @param {number} pid - Passage ID (PID) */ - constructor (name = '', text = '', tags = [], metadata = {}, pid = -1) { + constructor (name = '', text = '', tags = [], metadata = {}) { // Set name this.name = name; @@ -56,9 +48,6 @@ export default class Passage { // Sets text this.text = text; - - // Sets pid - this.pid = pid; } /** @@ -140,26 +129,6 @@ export default class Passage { } } - /** - * Passage ID (PID) - * @public - * @memberof Passage - * @returns {number} Passage ID (PID) - */ - get pid () { return this.#_pid; } - - /** - * @param {number} p - Replacement PID - */ - set pid (p) { - // Test if PID is a number - if (Number.isInteger(p)) { - this.#_pid = p; - } else { - throw new Error('PID should be a number!'); - } - } - /** * Return a Twee representation. * @public @@ -170,7 +139,7 @@ export default class Passage { toTwee () { // Start empty string. let content = ''; - + // Write the name. content += `:: ${this.name}`; @@ -200,7 +169,7 @@ export default class Passage { * @memberof Passage * @returns {string} JSON string. */ - toJSON() { + toJSON () { // Create an initial object for later serialization. const p = { name: this.name, @@ -215,32 +184,34 @@ export default class Passage { /** * Return Twine 2 HTML representation. + * (Default Passage ID is 1.) * @public * @function toTwine2HTML + * @param {number} pid - Passage ID (PID) to record in HTML. * @returns {string} Twine 2 HTML string. */ - toTwine2HTML() { - // Start the passage element + toTwine2HTML (pid = 1) { + // Start the passage element. let passageData = '\t${escape(this.text)}\n`; @@ -290,27 +260,27 @@ export default class Passage { } /** - * Return Twine 1 HTML representation. - * @public - * @function toTwine2HTML - * @returns {string} Twine 1 HTML string. - */ - toTwine1HTML() { + * Return Twine 1 HTML representation. + * @public + * @function toTwine2HTML + * @returns {string} Twine 1 HTML string. + */ + toTwine1HTML () { /** - *
[[One passage]]
*/ - // Start the passage element - let passageData = '\t passage.pid === pid); - // Return passages or null - return results !== undefined ? results : null; - } - /** * Size (number of passages). * @public @@ -505,123 +475,122 @@ export default class Story { fromJSON (jsonString) { // Create future object. let result = {}; - + // Try to parse the string. try { - result = JSON.parse(jsonString); - } catch (error) { - throw new Error('Invalid JSON!'); - } - - // Name - if (Object.prototype.hasOwnProperty.call(result, 'name')) { - // Convert to String (if not String). - this.name = String(result.name); - } - - // Start Passage - if (Object.prototype.hasOwnProperty.call(result, 'start')) { - // Convert to String (if not String). - this.start = String(result.start); - } - - // IFID - if (Object.prototype.hasOwnProperty.call(result, 'ifid')) { - // Convert to String (if not String). - // Enforce the uppercase rule of Twine 2 and Twee 3. - this.IFID = String(result.ifid).toUpperCase(); - } - - // Format - if (Object.prototype.hasOwnProperty.call(result, 'format')) { - // Convert to String (if not String). - this.format = String(result.format); - } - - // Format Version - if (Object.prototype.hasOwnProperty.call(result, 'formatVersion')) { - // Convert to String (if not String). - this.formatVersion = String(result.formatVersion); - } - - // Creator - if (Object.prototype.hasOwnProperty.call(result, 'creator')) { - // Convert to String (if not String). - this.creator = String(result.creator); - } - - // Creator Version - if (Object.prototype.hasOwnProperty.call(result, 'creatorVersion')) { - // Convert to String (if not String). - this.creatorVersion = String(result.creatorVersion); - } - - // Zoom - if (Object.prototype.hasOwnProperty.call(result, 'zoom')) { - // Set Zoom. - this.zoom = result.zoom; - } - - // Tag Colors - if (Object.prototype.hasOwnProperty.call(result, 'tagColors')) { - // Set tagColors. - this.tagColors = result.tagColors; - } - - // Metadata - if (Object.prototype.hasOwnProperty.call(result, 'metadata')) { - // Set metadata. - this.metadata = result.metadata; - } - - // Passages - if (Object.prototype.hasOwnProperty.call(result, 'passages')) { - // Reset internal passages. - this.#_passages = []; - // Is this an array? - if (Array.isArray(result.passages)) { - // For each passage, convert into Passage objects. - result.passages.forEach((p) => { - // Create default passage. - const newP = new Passage(); - - // Does this have a name? - if (Object.prototype.hasOwnProperty.call(p, 'name')) { - // Set name. - newP.name = p.name; - } - - // Does this have tags? - if (Object.prototype.hasOwnProperty.call(p, 'tags')) { - // Set tags. - newP.tags = p.tags; - } - - // Does this have metadata? - if (Object.prototype.hasOwnProperty.call(p, 'metadata')) { - // Set metadata. - newP.metadata = p.metadata; - } - - // Does this have text? - if (Object.prototype.hasOwnProperty.call(p, 'text')) { - // Set text. - newP.text = p.text; - } - - // Add the new passage. - this.addPassage(newP); - }); - } + result = JSON.parse(jsonString); + } catch (error) { + throw new Error('Invalid JSON!'); + } + + // Name + if (Object.prototype.hasOwnProperty.call(result, 'name')) { + // Convert to String (if not String). + this.name = String(result.name); + } + + // Start Passage + if (Object.prototype.hasOwnProperty.call(result, 'start')) { + // Convert to String (if not String). + this.start = String(result.start); + } + + // IFID + if (Object.prototype.hasOwnProperty.call(result, 'ifid')) { + // Convert to String (if not String). + // Enforce the uppercase rule of Twine 2 and Twee 3. + this.IFID = String(result.ifid).toUpperCase(); + } + + // Format + if (Object.prototype.hasOwnProperty.call(result, 'format')) { + // Convert to String (if not String). + this.format = String(result.format); + } + + // Format Version + if (Object.prototype.hasOwnProperty.call(result, 'formatVersion')) { + // Convert to String (if not String). + this.formatVersion = String(result.formatVersion); + } + + // Creator + if (Object.prototype.hasOwnProperty.call(result, 'creator')) { + // Convert to String (if not String). + this.creator = String(result.creator); + } + + // Creator Version + if (Object.prototype.hasOwnProperty.call(result, 'creatorVersion')) { + // Convert to String (if not String). + this.creatorVersion = String(result.creatorVersion); + } + + // Zoom + if (Object.prototype.hasOwnProperty.call(result, 'zoom')) { + // Set Zoom. + this.zoom = result.zoom; + } + + // Tag Colors + if (Object.prototype.hasOwnProperty.call(result, 'tagColors')) { + // Set tagColors. + this.tagColors = result.tagColors; + } + + // Metadata + if (Object.prototype.hasOwnProperty.call(result, 'metadata')) { + // Set metadata. + this.metadata = result.metadata; + } + + // Passages + if (Object.prototype.hasOwnProperty.call(result, 'passages')) { + // Reset internal passages. + this.#_passages = []; + // Is this an array? + if (Array.isArray(result.passages)) { + // For each passage, convert into Passage objects. + result.passages.forEach((p) => { + // Create default passage. + const newP = new Passage(); + + // Does this have a name? + if (Object.prototype.hasOwnProperty.call(p, 'name')) { + // Set name. + newP.name = p.name; + } + + // Does this have tags? + if (Object.prototype.hasOwnProperty.call(p, 'tags')) { + // Set tags. + newP.tags = p.tags; + } + + // Does this have metadata? + if (Object.prototype.hasOwnProperty.call(p, 'metadata')) { + // Set metadata. + newP.metadata = p.metadata; + } + + // Does this have text? + if (Object.prototype.hasOwnProperty.call(p, 'text')) { + // Set text. + newP.text = p.text; + } + + // Add the new passage. + this.addPassage(newP); + }); } } + } /** * Return Twee representation. - * + * * See: Twee 3 Specification * (https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md) - * * @function toTwee * @memberof Story * @returns {string} Twee String @@ -633,6 +602,9 @@ export default class Story { // Create default object. const metadata = {}; + /** + * ifid: (string) Required. Maps to . + */ // Is there an IFID? if (this.IFID === '') { // Generate a new IFID for this work. @@ -643,32 +615,33 @@ export default class Story { metadata.ifid = this.IFID; } - // Is there a format? - if (this.format !== '') { - // Using existing format - metadata.format = this.format; - } - - // Is there a formatVersion? - if (this.formatVersion !== '') { - // Using existing format version - metadata['format-version'] = this.formatVersion; - } - - // Is there a zoom? - if (this.zoom !== 0) { - // Using existing zoom. - metadata.zoom = this.zoom; - } - - // Is there a start? - if (this.start !== '') { - // Using existing start - metadata.start = this.start; - } - - // Get number of colors. + /** + * format: (string) Optional. Maps to . + */ + metadata.format = this.format; + + /** + * format-version: (string) Optional. Maps to . + */ + metadata['format-version'] = this.formatVersion; + + /** + * zoom: (decimal) Optional. Maps to . + */ + metadata.zoom = this.zoom; + + /** + * start: (string) Optional. + * Maps to of the node whose pid matches . + */ + metadata.start = this.start; + + /** + * tag-colors: (object of tag(string):color(string) pairs) Optional. + * Pairs map to nodes as :. + */ const numberOfColors = Object.keys(this.tagColors).length; + // Are there any colors? if (numberOfColors > 0) { // Add a tag-colors property @@ -681,70 +654,69 @@ export default class Story { // Add two newlines. outputContents += '\n\n'; - // Look for StoryTitle - const storyTitlePassage = this.getPassageByName('StoryTitle'); - - // Does it exist? - if (storyTitlePassage !== null) { - // Append StoryTitle content - outputContents += storyTitlePassage.toTwee(); + // Is there an explicit StoryTitle passage? + // (If it does exist, do nothing.) + if (this.getPassageByName('StoryTitle') === null) { + // We do not have an explicit StoryTitle passage. + // Generate one. + const p = new Passage('StoryTitle', this.name); + // Add to story. + this.addPassage(p); } // For each passage, append it to the output. this.forEachPassage((passage) => { - // For each passage, append it to the output. outputContents += passage.toTwee(); }); - // Return the Twee string. - return outputContents; + // Return the Twee string. + return outputContents; } /** - * Combine StoryFormat to create Twine 2 HTML. + * Return Twine 2 HTML. + * + * See: Twine 2 HTML Output + * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md) * @public * @function toTwine2HTML - * @param {StoryFormat} storyFormat - StoryFormat to combine * @returns {string} Twine 2 HTML string */ - toTwine2HTML (storyFormat) { - if (!(storyFormat instanceof StoryFormat)) { - throw new Error('storyFormat must be a StoryFormat object!'); - } + toTwine2HTML () { + // Prepare HTML content. + let storyData = ` { + // Have we found the starting passage? + if (p.name === this.start) { + // If so, set the PID based on index. + startPID = PIDcounter; } - } else { - // Throw error if no starting passage exists - throw new Error('No starting passage found!'); - } + // Increase and keep looking. + PIDcounter++; + }); + + // Set starting passage PID. + storyData += ` startnode="${startPID}"`; // Defaults to 'extwee' if missing. storyData += ` creator="${this.creator}"`; @@ -766,12 +738,12 @@ export default class Story { storyData += ` zoom="${this.zoom}"`; // Write existing or default value. - storyData += ` format="${storyFormat.name}"`; + storyData += ` format="${this.#_format}"`; // Write existing or default value. - storyData += ` format-version="${storyFormat.version}"`; + storyData += ` format-version="${this.#_formatVersion}"`; - // Add the default. + // Add the default attributes. storyData += ' options hidden>\n'; // Start the STYLE. @@ -801,27 +773,50 @@ export default class Story { storyData += passage.text; }); - // Close SCRIPT + // Close SCRIPT. storyData += '\n'; - // Build the passages. + // Reset the PID counter. + PIDcounter = 1; + + // Build the passages HTML. this.forEachPassage((passage) => { - // Append each passage element. - storyData += passage.toTwine2HTML(); + // Append each passage element using the PID counter. + storyData += passage.toTwine2HTML(PIDcounter); + // Increase counter inside loop. + PIDcounter++; }); + // Close the HTML element. storyData += ''; - // Replace the story name in the source file. - storyFormat.source = storyFormat.source.replaceAll(/{{STORY_NAME}}/gm, this.name); + // Return HTML contents. + return storyData; + } + + /** + * Return Twine 1 HTML. + * + * See: Twine 1 HTML Output + * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-htmloutput-doc.md) + * @public + * @function toTwine1HTML + * @returns {string} Twine 1 HTML string. + */ + toTwine1HTML () { + // Begin HTML output. + let outputContents = `
`; - // Replace the story data. - storyFormat.source = storyFormat.source.replaceAll(/{{STORY_DATA}}/gm, storyData); + // Process passages (if any). + this.forEachPassage((p) => { + // Output HTML output per passage. + outputContents += `\t${p.toTwine1HTML()}`; + }); - // Combine everything together. - outputContents += storyFormat.source; + // Close HTML element. + outputContents += '
'; - // Return HTML contents. + // Return Twine 1 HTML content. return outputContents; } } diff --git a/src/TWSParser.js b/src/TWSParser.js index be77cc67..526e6bfb 100644 --- a/src/TWSParser.js +++ b/src/TWSParser.js @@ -5,9 +5,9 @@ import { Parser } from 'pickleparser'; /** * @class TWSParser * @module TWSParser - * + * * Parse TWS (Python Pickle) into Story object. - * + * * See: Twine 1 TWS Documentation [Approval Pending] */ export default class TWSParser { diff --git a/src/TweeParser.js b/src/TweeParser.js index cb3e92fd..13ab412f 100644 --- a/src/TweeParser.js +++ b/src/TweeParser.js @@ -3,9 +3,9 @@ import Story from './Story.js'; /** * @class TweeParser * @module TweeParser - * + * * Parses Twee 3 text into a Story object. - * + * * See: Twee 3 Specification * (https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md) */ @@ -152,7 +152,7 @@ export default class TweeParser { throw new Error('Malformed passage header!'); } - // addPassage() method does all the work + // addPassage() method does all the work. story.addPassage(new Passage(name, text, tags, metadata, pid)); // Increase pid diff --git a/src/Twine1HTMLParser.js b/src/Twine1HTMLParser.js index 7e85fb70..b92066a7 100644 --- a/src/Twine1HTMLParser.js +++ b/src/Twine1HTMLParser.js @@ -9,9 +9,9 @@ import Story from './Story.js'; /** * @class Twine1HTMLParser * @module Twine1HTMLParser - * + * * Parses Twine 1 HTML into a Story object. - * + * * See: Twine 1 HTML Output Documentation * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-htmloutput-doc.md) */ diff --git a/src/Twine2HTMLParser.js b/src/Twine2HTMLParser.js index 2cdc8e2f..1ae13aa4 100644 --- a/src/Twine2HTMLParser.js +++ b/src/Twine2HTMLParser.js @@ -9,9 +9,9 @@ import Passage from './Passage.js'; /** * @class Twine2HTMLParser * @module Twine2HTMLParser - * + * * Parses Twine 2 HTML into a Story object. - * + * * See: Twine 2 HTML Output Specification * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md) */ diff --git a/test/Passage.test.js b/test/Passage.test.js index 91a82e32..b77f2b76 100644 --- a/test/Passage.test.js +++ b/test/Passage.test.js @@ -8,7 +8,6 @@ describe('Passage', () => { expect(p.tags).toHaveLength(0); expect(p.text).toBe(''); expect(typeof p.metadata).toBe('object'); - expect(p.pid).toBe(-1); }); }); @@ -72,21 +71,6 @@ describe('Passage', () => { }); }); - describe('pid', () => { - it('Set PID', () => { - const p = new Passage(); - p.pid = 12; - expect(p.pid).toBe(12); - }); - - it('Throw error if pid is not a Number', () => { - const p = new Passage(); - expect(() => { - p.pid = []; - }).toThrow(); - }); - }); - describe('toTwee()', () => { it('Create name string', () => { const p = new Passage('Name', 'Test'); @@ -113,77 +97,76 @@ describe('Passage', () => { }); }); - describe('toTwine2HTML()', function() { - + describe('toTwine2HTML()', function () { let p = null; let result = null; beforeEach(() => { - p = new Passage("Test", "Word", ['tag1'], {'some': 'thing'}, 10); + p = new Passage('Test', 'Word', ['tag1'], { some: 'thing' }); result = p.toTwine2HTML(); }); - it('Should contain PID', function() { - expect(result.includes('pid="10"')).toBe(true); + it('Should contain default PID', function () { + expect(result.includes('pid="1"')).toBe(true); }); - it('Should include name', function() { + it('Should include name', function () { expect(result.includes('name="Test"')).toBe(true); }); - it('Should include single tag', function() { + it('Should include single tag', function () { expect(result.includes('tags="tag1"')).toBe(true); }); - it('Should include multiple tags', function() { + it('Should include multiple tags', function () { p.tags = ['tag1', 'tag2']; result = p.toTwine2HTML(); expect(result.includes('tags="tag1 tag2"')).toBe(true); }); - it('Should include position, if it exists', function() { - p.metadata = {'position': "102,99"}; + it('Should include position, if it exists', function () { + p.metadata = { position: '102,99' }; result = p.toTwine2HTML(); expect(result.includes('position="102,99"')).toBe(true); }); - it('Should include size, if it exists', function() { - p.metadata = {'size': "100,100"}; + it('Should include size, if it exists', function () { + p.metadata = { size: '100,100' }; result = p.toTwine2HTML(); expect(result.includes('size="100,100"')).toBe(true); }); }); - describe('toTwine1HTML()', function() { + describe('toTwine1HTML()', function () { let p = null; let result = null; beforeEach(() => { - p = new Passage("Test", "Word", ['tag1'], {'position': '12, 12'}); + p = new Passage('Test', 'Word', ['tag1'], { position: '12, 12' }); result = p.toTwine1HTML(); }); - it('Should include tiddler', function() { + it('Should include tiddler', function () { expect(result.includes('tiddler="Test"')).toBe(true); }); - it('Should include single tag', function() { + it('Should include single tag', function () { expect(result.includes('tags="tag1"')).toBe(true); }); - it('Should include multiple tags', function() { + it('Should include multiple tags', function () { p.tags = ['tag1', 'tag2']; result = p.toTwine1HTML(); expect(result.includes('tags="tag1 tag2"')).toBe(true); }); - it('Should include position, if it exists', function() { - p.metadata = {'position': "102,99"}; + it('Should include position, if it exists', function () { + p.metadata = { position: '102,99' }; result = p.toTwine1HTML(); expect(result.includes('position="102,99"')).toBe(true); }); - it('Should use default position', function() { + it('Should use default position', function () { p.metadata = {}; result = p.toTwine1HTML(); expect(result.includes('position="10,10"')).toBe(true); diff --git a/test/Story.test.js b/test/Story.test.js index e3000629..d25f6a36 100644 --- a/test/Story.test.js +++ b/test/Story.test.js @@ -1,13 +1,14 @@ import Story from '../src/Story.js'; import Passage from '../src/Passage'; -import FileReader from '../src/FileReader.js'; +import TweeParser from '../src/TweeParser.js'; +import { readFileSync } from 'node:fs'; // Pull the name and version of this project from package.json. // These are used as the 'creator' and 'creator-version'. -const { name, version } = JSON.parse(FileReader.read('package.json')); +const { name, version } = JSON.parse(readFileSync('package.json')); describe('Story', () => { - describe('#constructor()', () => { + describe('constructor()', () => { let s = null; beforeEach(() => { @@ -107,8 +108,8 @@ describe('Story', () => { }); it('Set using String', () => { - s.formatVersion = 'New'; - expect(s.formatVersion).toBe('New'); + s.formatVersion = '1.1.1'; + expect(s.formatVersion).toBe('1.1.1'); }); it('Should throw error if not String', () => { @@ -242,6 +243,45 @@ describe('Story', () => { s.addPassage(p2); expect(s.size()).toBe(1); }); + + it('addPassage() - should override StoryData: ifid', function () { + // Generate object. + const o = { + ifid: 'D674C58C-DEFA-4F70-B7A2-27742230C0FC' + }; + + // Add the passage. + s.addPassage(new Passage('StoryData', JSON.stringify(o))); + + // Test for IFID. + expect(s.IFID).toBe('D674C58C-DEFA-4F70-B7A2-27742230C0FC'); + }); + + it('addPassage() - should override StoryData: format', function () { + // Generate object. + const o = { + format: 'SugarCube' + }; + + // Add the passage. + s.addPassage(new Passage('StoryData', JSON.stringify(o))); + + // Test for format. + expect(s.format).toBe('SugarCube'); + }); + + it('addPassage() - should override StoryData: formatVersion', function () { + // Generate object. + const o = { + 'format-version': '2.28.2' + }; + + // Add the passage. + s.addPassage(new Passage('StoryData', JSON.stringify(o))); + + // Test for format. + expect(s.formatVersion).toBe('2.28.2'); + }); }); describe('removePassageByName()', () => { @@ -305,25 +345,6 @@ describe('Story', () => { }); }); - describe('getPassageByPID()', () => { - let s = null; - - beforeEach(() => { - s = new Story(); - }); - - it('getPassageByPID() - should get passage by PID', () => { - const p = new Passage('Find', '', [], {}, 12); - s.addPassage(p); - const passage = s.getPassageByPID(12); - expect(passage.name).toBe('Find'); - }); - - it('getPassageByPID() - should return null if not found', () => { - expect(s.getPassageByPID(12)).toBe(null); - }); - }); - describe('forEachPassage()', () => { let s = null; @@ -396,4 +417,543 @@ describe('Story', () => { expect(result.passages.length).toBe(1); }); }); + + describe('fromJSON()', function () { + it('Should throw error if JSON is invalid', function () { + const s = new Story(); + expect(() => { s.fromJSON('{'); }).toThrow(); + }); + + it('Should roundtrip default Story values using toJSON() and fromJSON()', function () { + // Create Story. + const s = new Story(); + // Convert to JSON and back. + s.fromJSON(s.toJSON()); + // Check all properties. + expect(s.name).toBe(''); + expect(Object.keys(s.tagColors).length).toBe(0); + expect(s.IFID).toBe(''); + expect(s.start).toBe(''); + expect(s.formatVersion).toBe(''); + expect(s.format).toBe(''); + expect(s.creator).toBe('extwee'); + expect(s.creatorVersion).toBe('2.2.0'); + expect(s.zoom).toBe(0); + expect(Object.keys(s.metadata).length).toBe(0); + }); + + it('Should parse passage data', function () { + // Create passage. + const p = new Passage('Test', 'Default'); + // Create Story. + const s = new Story(); + // Add a passage. + s.addPassage(p); + // Convert to JSON. + const js = s.toJSON(s); + // Convert back to Story. + const result = new Story(); + // Convert back. + result.fromJSON(js); + // Should have a single passage. + expect(result.size()).toBe(1); + }); + + describe('Partial Story Processing', function () { + it('Should parse everything but name', function () { + const s = '{"tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe(''); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but tagColors', function () { + const s = '{"name":"Test","ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(0); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but ifid', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe(''); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but start', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Star","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe(''); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but formatVersion', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe(''); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but format', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe(''); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but creator', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but creator version', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but zoom', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(0); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but metadata', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(0); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + }); + + it('Should parse everything but passages', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(0); + }); + + it('Should ignore non-arrays for passages', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":{}}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(0); + }); + + it('Should parse everything but passage name', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"tags":["tag1"],"metadata":{},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + expect(r.getPassageByName('').name).toBe(''); + }); + + it('Should parse everything but passage tags', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","metadata":{"s":"e"},"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + expect(r.getPassageByName('Start').metadata.s).toBe('e'); + expect(r.getPassageByName('Start').tags.length).toBe(0); + }); + + it('Should parse everything but passage text', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{"s":"e"}}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + expect(r.getPassageByName('Start').metadata.s).toBe('e'); + expect(r.getPassageByName('Start').text).toBe(''); + }); + + it('Parse everything but passage metadata', function () { + const s = '{"name":"Test","tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"text":"Word"}]}'; + const r = new Story(); + r.fromJSON(s); + expect(r.name).toBe('Test'); + expect(Object.keys(r.tagColors).length).toBe(1); + expect(r.IFID).toBe('DD'); + expect(r.start).toBe('Start'); + expect(r.formatVersion).toBe('1.0'); + expect(Object.keys(r.metadata).length).toBe(1); + expect(r.format).toBe('Snowman'); + expect(r.creator).toBe('extwee'); + expect(r.creatorVersion).toBe('2.2.0'); + expect(r.zoom).toBe(1); + expect(r.size()).toBe(1); + expect(Object.prototype.hasOwnProperty.call(r.getPassageByName('Start').metadata, 's')).toBe(false); + expect(r.getPassageByName('Start').text).toBe('Word'); + }); + }); + }); + + describe('toTwee()', function () { + let s = null; + + beforeEach(() => { + s = new Story(); + }); + + it('Should detect StoryTitle text', function () { + // Add one passage. + s.addPassage(new Passage('Start', 'Content')); + + // Change Story name. + s.name = 'Title'; + + // Convert to Twee. + const t = s.toTwee(); + + // Parse into a new story. + const story = TweeParser.parse(t); + + // Test for name. + expect(story.name).toBe('Title'); + }); + + it('Should encode IFID', () => { + // Add passages. + s.addPassage(new Passage('Start')); + s.addPassage(new Passage('StoryTitle', 'Title')); + + // Set an ifid property. + s.IFID = 'DE7DF8AD-E4CD-499E-A4E7-C5B98B73449A'; + + // Convert to Twee. + const t = s.toTwee(); + + // Parse file. + const tp = TweeParser.parse(t); + + // Verify IFID. + expect(tp.IFID).toBe('DE7DF8AD-E4CD-499E-A4E7-C5B98B73449A'); + }); + + it('Should encode format, formatVersion, zoom, and start', () => { + // Add passages. + s.addPassage(new Passage('Start', 'Content')); + s.addPassage(new Passage('Untitled', 'Some stuff')); + + s.name = 'Title'; + s.format = 'Test'; + s.formatVersion = '1.2.3'; + s.zoom = 1; + s.start = 'Untitled'; + + // Convert to Twee. + const t = s.toTwee(); + + // Parse Twee. + const story2 = TweeParser.parse(t); + + // Test for format, formatVersion, zoom, and start. + expect(story2.formatVersion).toBe('1.2.3'); + expect(story2.format).toBe('Test'); + expect(story2.zoom).toBe(1); + expect(story2.start).toBe('Untitled'); + }); + + it('Should write tag colors', () => { + // Add some passages. + s.addPassage(new Passage('Start', 'Content')); + s.addPassage(new Passage('Untitled', 'Some stuff')); + + // Add tag colors. + s.tagColors = { + bar: 'green', + foo: 'red', + qaz: 'blue' + }; + + // Convert to Twee. + const t = s.toTwee(); + + // Convert back into Story. + const story2 = TweeParser.parse(t); + + // Test for tag colors + expect(story2.tagColors.bar).toBe('green'); + expect(story2.tagColors.foo).toBe('red'); + expect(story2.tagColors.qaz).toBe('blue'); + }); + + it('Should encode "script" tag', () => { + // Add passages. + s.addPassage(new Passage('Test', 'Test', ['script'])); + s.addPassage(new Passage('Start', 'Content')); + + // Convert into Twee. + const t = s.toTwee(); + + // Convert back into Story. + const story = TweeParser.parse(t); + + // Search for 'script'. + const p = story.getPassagesByTag('script'); + + // Test for passage text. + expect(p[0].text).toBe('Test'); + }); + + it('Should encode "stylesheet" tag', () => { + // Add passages. + s.addPassage(new Passage('Test', 'Test', ['stylesheet'])); + s.addPassage(new Passage('Start', 'Content')); + + // Convert into Twee. + const t = s.toTwee(); + + // Convert back into Story. + const story = TweeParser.parse(t); + + // Search for 'stylesheet'. + const p = story.getPassagesByTag('stylesheet'); + + // Test for passage text. + expect(p[0].text).toBe('Test'); + }); + }); + + describe('toTwine2HTML()', () => { + let s = null; + + beforeEach(() => { + s = new Story(); + }); + + it('Should throw error if no starting passage', function () { + // No start set. + expect(() => { s.toTwine2HTML(); }).toThrow(); + }); + + it('Should throw error if starting passage cannot be found', function () { + // Set start. + s.start = 'Unknown'; + // Has a start, but not part of collection. + expect(() => { s.toTwine2HTML(); }).toThrow(); + }); + + it('Should encode name', () => { + // Add passage. + s.addPassage(new Passage('Start', 'Word')); + // Set start. + s.start = 'Start'; + // Set name. + s.name = 'Test'; + // Create HTML. + const result = s.toTwine2HTML(); + // Expect the name to be encoded. + expect(result.includes(' { + // Add passage. + s.addPassage(new Passage('Start', 'Word')); + // Set start. + s.start = 'Start'; + // Set IFID. + s.IFID = 'B94AC8AD-03E3-4496-96C8-FE958645FE61'; + // Create HTML. + const result = s.toTwine2HTML(); + // Expect the IFID to be encoded. + expect(result.includes('ifid="B94AC8AD-03E3-4496-96C8-FE958645FE61"')).toBe(true); + }); + + it('Should encode stylesheet passages', () => { + // Add passage. + s.addPassage(new Passage('Start', 'Word')); + // Set start. + s.start = 'Start'; + // Add a stylesheet passage. + s.addPassage(new Passage('Test', 'Word', ['stylesheet'])); + // Create HTML. + const result = s.toTwine2HTML(); + // Expect the stylesheet passage text to be encoded. + expect(result.includes('