extwee 2.0.6 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/.eslintrc.json +25 -25
  2. package/.github/FUNDING.yml +3 -0
  3. package/.github/dependabot.yml +11 -0
  4. package/.github/workflows/nodejs.yml +25 -24
  5. package/.travis.yml +13 -13
  6. package/CODE_OF_CONDUCT.md +82 -82
  7. package/LICENSE +21 -21
  8. package/README.md +173 -36
  9. package/SECURITY.md +12 -12
  10. package/babel.config.json +18 -22
  11. package/build/extwee +0 -0
  12. package/build/extwee.exe +0 -0
  13. package/build/extwee.web.min.js +2 -0
  14. package/build/extwee.web.min.js.LICENSE.txt +1 -0
  15. package/docs/.nojekyll +0 -0
  16. package/docs/README.md +167 -0
  17. package/docs/_sidebar.md +19 -0
  18. package/docs/examples/dynamicPassages.md +28 -0
  19. package/docs/examples/jsonToTwee.md +23 -0
  20. package/docs/examples/twsToTwee.md +25 -0
  21. package/docs/formats/json.md +17 -0
  22. package/docs/formats/twee.md +13 -0
  23. package/docs/formats/twine1HTML.md +13 -0
  24. package/docs/formats/twine2ArchiveHTML.md +13 -0
  25. package/docs/formats/twine2HTML.md +13 -0
  26. package/docs/formats/tws.md +9 -0
  27. package/docs/index.html +26 -0
  28. package/docs/install/binaries.md +9 -0
  29. package/docs/install/npm.md +20 -0
  30. package/docs/install/npx.md +9 -0
  31. package/docs/objects/passage.md +47 -0
  32. package/docs/objects/story.md +70 -0
  33. package/docs/objects/storyformat.md +27 -0
  34. package/index.html +22 -0
  35. package/index.js +29 -31
  36. package/package.json +65 -58
  37. package/src/JSON/parse.js +128 -0
  38. package/src/Passage.js +298 -202
  39. package/src/Story.js +650 -523
  40. package/src/StoryFormat/parse.js +134 -0
  41. package/src/StoryFormat.js +259 -300
  42. package/src/TWS/parse.js +86 -0
  43. package/src/Twee/parse.js +157 -0
  44. package/src/Twine1HTML/compile.js +58 -0
  45. package/src/Twine1HTML/parse.js +134 -0
  46. package/src/Twine2ArchiveHTML/compile.js +36 -0
  47. package/src/Twine2ArchiveHTML/parse.js +49 -0
  48. package/src/Twine2HTML/compile.js +35 -0
  49. package/src/Twine2HTML/parse.js +348 -0
  50. package/src/extwee.js +206 -0
  51. package/test/CLI/CLI.test.js +49 -0
  52. package/test/CLI/files/example.json +1 -0
  53. package/test/CLI/files/example6.twee +22 -0
  54. package/test/{Roundtrip → CLI/files}/harlowe.js +2 -2
  55. package/test/CLI/{input.html → files/input.html} +47 -47
  56. package/test/CLI/files/output/test.twee +0 -0
  57. package/test/CLI/{tweeExample.twee → files/tweeExample.twee} +17 -17
  58. package/test/CLI/files/twine1/LICENSE.txt +32 -0
  59. package/test/CLI/files/twine1/code.js +5 -0
  60. package/test/CLI/files/twine1/engine.js +43 -0
  61. package/test/CLI/files/twine1/header.html +325 -0
  62. package/test/CLI/files/twine1Test.html +371 -0
  63. package/test/CLI/{twineExample.html → files/twineExample.html} +16 -15
  64. package/test/JSON/JSON.Parse.test.js +316 -0
  65. package/test/Passage.test.js +175 -104
  66. package/test/Roundtrip/{Example1.html → Files/Example1.html} +63 -63
  67. package/test/Roundtrip/Files/LICENSE +19 -0
  68. package/test/Roundtrip/Files/example1.twee +10 -0
  69. package/test/Roundtrip/{example2.twee → Files/example2.twee} +27 -18
  70. package/test/Roundtrip/{example4.twee → Files/example4.twee} +27 -27
  71. package/test/{StoryFormatParser → Roundtrip/Files}/harlowe.js +2 -2
  72. package/test/Roundtrip/{round.html → Files/round.html} +6 -4
  73. package/test/Roundtrip/Roundtrip.test.js +54 -0
  74. package/test/Story.test.js +638 -423
  75. package/test/StoryFormat/StoryFormat.Parse.test.js +91 -0
  76. package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/example.js +3 -3
  77. package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/example2.js +3 -3
  78. package/test/{CLI → StoryFormat/StoryFormatParser}/harlowe.js +2 -2
  79. package/test/StoryFormat.test.js +152 -152
  80. package/test/TWS/Parse.test.js +78 -0
  81. package/test/TWS/TWSParser/Example1.tws +150 -0
  82. package/test/TWS/TWSParser/Example5.tws +414 -0
  83. package/test/TWS/TWSParser/noscale.tws +0 -0
  84. package/test/TWS/TWSParser/nostory.tws +0 -0
  85. package/test/Twee/Twee.Parse.test.js +76 -0
  86. package/test/{TweeParser → Twee/TweeParser}/emptytags.twee +2 -2
  87. package/test/{TweeParser → Twee/TweeParser}/example.twee +32 -32
  88. package/test/{TweeParser → Twee/TweeParser}/missing.twee +19 -19
  89. package/test/{TweeParser → Twee/TweeParser}/multipleScriptPassages.twee +19 -19
  90. package/test/{TweeParser → Twee/TweeParser}/multipleStyleTag.twee +19 -19
  91. package/test/{TweeParser → Twee/TweeParser}/multipletags.twee +10 -10
  92. package/test/{TweeParser → Twee/TweeParser}/noTitle.twee +2 -2
  93. package/test/{TweeParser → Twee/TweeParser}/notes.twee +16 -16
  94. package/test/{TweeParser → Twee/TweeParser}/pasagemetadataerror.twee +2 -2
  95. package/test/{TweeParser → Twee/TweeParser}/scriptPassage.twee +16 -16
  96. package/test/{TweeParser → Twee/TweeParser}/singletag.twee +13 -13
  97. package/test/{TweeParser → Twee/TweeParser}/startMetadata.twee +14 -14
  98. package/test/{TweeParser → Twee/TweeParser}/storydataerror.twee +25 -25
  99. package/test/{TweeParser → Twee/TweeParser}/stylePassage.twee +16 -16
  100. package/test/{Story → Twee/TweeParser}/test.twee +25 -25
  101. package/test/Twine1HTML/Twine1HTML.Compile.test.js +180 -0
  102. package/test/Twine1HTML/Twine1HTML.Parse.test.js +183 -0
  103. package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/LICENSE +674 -0
  104. package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/engine.js +43 -0
  105. package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/jquery.js +4 -0
  106. package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/modernizr.js +4 -0
  107. package/test/Twine1HTML/Twine1HTMLCompiler/engineTest.html +1 -0
  108. package/test/Twine1HTML/Twine1HTMLCompiler/jonah-1.4.2/LICENSE +32 -0
  109. package/test/Twine1HTML/Twine1HTMLCompiler/jonah-1.4.2/code.js +4 -0
  110. package/test/Twine1HTML/Twine1HTMLCompiler/jonah-1.4.2/header.html +327 -0
  111. package/test/Twine1HTML/Twine1HTMLCompiler/test.html +0 -0
  112. package/test/Twine1HTML/Twine1HTMLCompiler/test1.html +6 -0
  113. package/test/Twine1HTML/Twine1HTMLCompiler/test2.html +6 -0
  114. package/test/Twine1HTML/Twine1HTMLCompiler/test3.html +43 -0
  115. package/test/Twine1HTML/Twine1HTMLCompiler/test4.html +372 -0
  116. package/test/Twine1HTML/Twine1HTMLCompiler/test5.html +372 -0
  117. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Compile.test.js +35 -0
  118. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.test.js +25 -0
  119. package/test/Twine2ArchiveHTML/Twine2ArchiveHTMLCompiler/test1.html +6 -0
  120. package/test/Twine2ArchiveHTML/Twine2ArchiveHTMLParser/test1.html +3 -0
  121. package/test/Twine2HTML/Twine2HTML.Compile.test.js +224 -0
  122. package/test/Twine2HTML/Twine2HTML.Parse.test.js +172 -0
  123. package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/creator.html +4 -4
  124. package/test/{CLI → Twine2HTML/Twine2HTMLCompiler}/example6.twee +15 -15
  125. package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/missingStoryTitle.twee +29 -29
  126. package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test2.html +10 -8
  127. package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test3.html +1 -1
  128. package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test4.html +4 -4
  129. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/Example1.html +52 -52
  130. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/Tags.html +15 -15
  131. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/lyingStartnode.html +15 -15
  132. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/lyingTagColors.html +48 -48
  133. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingCreator.html +11 -11
  134. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingCreatorVersion.html +11 -11
  135. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingFormat.html +11 -11
  136. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingFormatVersion.html +11 -11
  137. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingIFID.html +11 -11
  138. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingName.html +33 -33
  139. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPID.html +15 -15
  140. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPassageName.html +15 -15
  141. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPassageTags.html +15 -15
  142. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPosition.html +15 -15
  143. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingScript.html +14 -14
  144. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingSize.html +35 -35
  145. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingStartnode.html +11 -11
  146. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingStyle.html +14 -14
  147. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingZoom.html +11 -11
  148. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/twineExample.html +23 -23
  149. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/twineExample2.html +15 -15
  150. package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/twineExample3.html +15 -15
  151. package/web-index.js +29 -0
  152. package/webpack.config.js +12 -0
  153. package/bin/extwee.js +0 -47
  154. package/src/FileReader.js +0 -33
  155. package/src/HTMLParser.js +0 -345
  156. package/src/HTMLWriter.js +0 -231
  157. package/src/StoryFormatParser.js +0 -142
  158. package/src/TweeParser.js +0 -161
  159. package/src/TweeWriter.js +0 -121
  160. package/story-formats/chapbook-1.2.0/format.js +0 -1
  161. package/story-formats/chapbook-1.2.0/logo.svg +0 -1
  162. package/story-formats/harlowe-1.2.4/format.js +0 -1
  163. package/story-formats/harlowe-1.2.4/icon.svg +0 -78
  164. package/story-formats/harlowe-2.1.0/format.js +0 -2
  165. package/story-formats/harlowe-2.1.0/icon.svg +0 -78
  166. package/story-formats/harlowe-3.1.0/format.js +0 -3
  167. package/story-formats/harlowe-3.1.0/icon.svg +0 -78
  168. package/story-formats/paperthin-1.0.0/format.js +0 -1
  169. package/story-formats/paperthin-1.0.0/icon.svg +0 -5
  170. package/story-formats/snowman-1.4.0/format.js +0 -1
  171. package/story-formats/snowman-1.4.0/icon.svg +0 -436
  172. package/story-formats/snowman-2.0.2/format.js +0 -1
  173. package/story-formats/snowman-2.0.2/icon.svg +0 -436
  174. package/story-formats/sugarcube-1.0.35/LICENSE +0 -23
  175. package/story-formats/sugarcube-1.0.35/format.js +0 -1
  176. package/story-formats/sugarcube-1.0.35/icon.svg +0 -56
  177. package/story-formats/sugarcube-2.31.1/LICENSE +0 -22
  178. package/story-formats/sugarcube-2.31.1/format.js +0 -1
  179. package/story-formats/sugarcube-2.31.1/icon.svg +0 -56
  180. package/test/CLI/test2.html +0 -45
  181. package/test/CLI.test.js +0 -30
  182. package/test/FileReader/t1.txt +0 -1
  183. package/test/FileReader.test.js +0 -14
  184. package/test/HTMLParser.test.js +0 -177
  185. package/test/HTMLWriter/example6.twee +0 -16
  186. package/test/HTMLWriter.test.js +0 -287
  187. package/test/Roundtrip/example1.twee +0 -21
  188. package/test/Roundtrip.test.js +0 -48
  189. package/test/Story/startmeta.twee +0 -29
  190. package/test/StoryFormatParser.test.js +0 -91
  191. package/test/TweeParser/test.twee +0 -25
  192. package/test/TweeParser.test.js +0 -79
  193. package/test/TweeWriter/test1.twee +0 -18
  194. package/test/TweeWriter/test3.twee +0 -12
  195. package/test/TweeWriter/test4.twee +0 -14
  196. package/test/TweeWriter/test5.twee +0 -20
  197. package/test/TweeWriter/test6.twee +0 -15
  198. package/test/TweeWriter/test7.twee +0 -15
  199. package/test/TweeWriter.test.js +0 -107
  200. /package/test/CLI/{test.twee → files/test.twee} +0 -0
  201. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/format.js +0 -0
  202. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/format_doublename.js +0 -0
  203. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingAuthor.js +0 -0
  204. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingDescription.js +0 -0
  205. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingImage.js +0 -0
  206. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingLicense.js +0 -0
  207. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingName.js +0 -0
  208. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingProofing.js +0 -0
  209. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingSource.js +0 -0
  210. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingURL.js +0 -0
  211. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingVersion.js +0 -0
  212. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/versionWrong.js +0 -0
  213. /package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/TestTags.html +0 -0
  214. /package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test11.html +0 -0
  215. /package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test6.html +0 -0
  216. /package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/tagColors.html +0 -0
@@ -0,0 +1,348 @@
1
+ import { parse as HtmlParser } from 'node-html-parser';
2
+ import Story from '../Story.js';
3
+ import Passage from '../Passage.js';
4
+
5
+ /**
6
+ * Parse Twine 2 HTML into Story object.
7
+ *
8
+ * See: Twine 2 HTML Output Specification
9
+ * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md)
10
+ * @param {string} content - Twine 2 HTML content to parse.
11
+ * @returns {Story} Story
12
+ */
13
+ function parse (content) {
14
+ // Create new story.
15
+ const story = new Story();
16
+
17
+ // Can only parse string values.
18
+ if (typeof content !== 'string') {
19
+ throw new TypeError('Content is not a string!');
20
+ }
21
+
22
+ // Set default start node.
23
+ let startNode = null;
24
+
25
+ // Send to node-html-parser
26
+ // Enable getting the content of 'script', 'style', and 'pre' elements
27
+ // Get back a DOM
28
+ const dom = new HtmlParser(
29
+ content,
30
+ {
31
+ lowerCaseTagName: false,
32
+ script: true,
33
+ style: true,
34
+ pre: true
35
+ });
36
+
37
+ // Pull out the `<tw-storydata>` element.
38
+ const storyDataElements = dom.getElementsByTagName('tw-storydata');
39
+
40
+ // Did we find any elements?
41
+ if (storyDataElements.length === 0) {
42
+ // If there is not a single `<tw-storydata>` element, this is not a Twine 2 story!
43
+ throw new Error('Not Twine 2 HTML content!');
44
+ }
45
+
46
+ // We only parse the first element found.
47
+ const storyData = storyDataElements[0];
48
+
49
+ /**
50
+ * name: (string) Required.
51
+ * The name of the story.
52
+ */
53
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'name')) {
54
+ // Set the story name
55
+ story.name = storyData.attributes.name;
56
+ } else {
57
+ // Name is a required field. Warn user.
58
+ console.warn('Twine 2 HTML must have a name!');
59
+ }
60
+
61
+ /**
62
+ * ifid: (string) Required.
63
+ * An IFID is a sequence of between 8 and 63 characters,
64
+ * each of which shall be a digit, a capital letter or a
65
+ * hyphen that uniquely identify a story (see Treaty of Babel).
66
+ */
67
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'ifid')) {
68
+ // Update story IFID.
69
+ story.IFID = storyData.attributes.ifid;
70
+ } else {
71
+ // Name is a required filed. Warn user.
72
+ console.warn('Twine 2 HTML must have an IFID!');
73
+ }
74
+
75
+ /**
76
+ * creator: (string) Optional.
77
+ * The name of program used to create the file.
78
+ */
79
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator')) {
80
+ // Update story creator
81
+ story.creator = storyData.attributes.creator;
82
+ }
83
+
84
+ /**
85
+ * creator-version: (string) Optional.
86
+ * The version of the program used to create the file.
87
+ */
88
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator-version')) {
89
+ // Update story creator version
90
+ story.creatorVersion = storyData.attributes['creator-version'];
91
+ }
92
+
93
+ /**
94
+ * format: (string) Optional.
95
+ * The story format used to create the story.
96
+ */
97
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format')) {
98
+ // Update story format
99
+ story.format = storyData.attributes.format;
100
+ }
101
+
102
+ /**
103
+ * format-version: (string) Optional.
104
+ * The version of the story format used to create the story.
105
+ */
106
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format-version')) {
107
+ // Update story format version
108
+ story.formatVersion = storyData.attributes['format-version'];
109
+ }
110
+
111
+ /**
112
+ * zoom: (string) Optional.
113
+ * The decimal level of zoom (i.e. 1.0 is 100% and 1.2 would be 120% zoom level).
114
+ */
115
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'zoom')) {
116
+ // Update story zoom
117
+ story.zoom = Number(Number.parseFloat(storyData.attributes.zoom).toFixed(2));
118
+ }
119
+
120
+ /**
121
+ * startnode: (string) Optional.
122
+ * The PID matching a `<tw-passagedata>` element whose content should be displayed first.
123
+ */
124
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'startnode')) {
125
+ // Take string value and convert to Int
126
+ startNode = Number.parseInt(storyData.attributes.startnode, 10);
127
+ } else {
128
+ // Throw error without start node.
129
+ throw new Error('Missing startnode in <tw-storydata>!');
130
+ }
131
+
132
+ // Pull out the `<tw-passagedata>` element.
133
+ const storyPassages = dom.querySelectorAll('tw-passagedata');
134
+
135
+ // Move through the passages
136
+ for (const passage in storyPassages) {
137
+ // Get the passage attributes
138
+ const attr = storyPassages[passage].attributes;
139
+ // Get the passage text
140
+ const text = storyPassages[passage].rawText;
141
+
142
+ /**
143
+ * position: (string) Optional.
144
+ * Comma-separated X and Y position of the upper-left
145
+ * of the passage when viewed within the Twine 2 editor.
146
+ */
147
+ // Set a default position.
148
+ let position = null;
149
+ // Does position exist?
150
+ if (Object.prototype.hasOwnProperty.call(attr, 'position')) {
151
+ // Update position.
152
+ position = attr.position;
153
+ }
154
+
155
+ /**
156
+ * size: (string) Optional.
157
+ * Comma-separated width and height of the
158
+ * passage when viewed within the Twine 2 editor.
159
+ */
160
+ // Set a default size.
161
+ let size = null;
162
+ // Does size exist?
163
+ if (Object.prototype.hasOwnProperty.call(attr, 'size')) {
164
+ // Update size.
165
+ size = attr.size;
166
+ }
167
+
168
+ /**
169
+ * name: (string) Required.
170
+ * The name of the passage.
171
+ *
172
+ * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
173
+ */
174
+ // Create a default value
175
+ let name = null;
176
+ // Does name exist?
177
+ if (Object.prototype.hasOwnProperty.call(attr, 'name')) {
178
+ // Escape the name
179
+ name = escapeMetacharacters(attr.name);
180
+ } else {
181
+ throw new Error('Cannot parse passage data without name!');
182
+ }
183
+
184
+ // Create empty tag array.
185
+ let tags = [];
186
+ // Does the tags attribute exist?
187
+ if (Object.prototype.hasOwnProperty.call(attr, 'tags')) {
188
+ // Escape any tags
189
+ // (Attributes can, themselves, be empty strings.)
190
+ if (attr.tags.length > 0 && attr.tags !== '""') {
191
+ // Escape the tags
192
+ tags = escapeMetacharacters(attr.tags);
193
+ // Split by spaces into an array
194
+ tags = tags.split(' ');
195
+ }
196
+
197
+ // Remove any empty strings.
198
+ tags = tags.filter(tag => tag !== '');
199
+ }
200
+
201
+ // Create metadata for passage.
202
+ const metadata = {};
203
+
204
+ // Does position exist?
205
+ if (position !== null) {
206
+ // Add the property to metadata
207
+ metadata.position = position;
208
+ }
209
+
210
+ // Does size exist?
211
+ if (size !== null) {
212
+ // Add the property to metadata
213
+ metadata.size = size;
214
+ }
215
+
216
+ /**
217
+ * pid: (string) Required.
218
+ * The Passage ID (PID).
219
+ * (Note: This is subject to change during editing with Twine 2.)
220
+ *
221
+ * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
222
+ */
223
+ // Create a default PID
224
+ let pid = -1;
225
+ // Does pid exist?
226
+ if (Object.prototype.hasOwnProperty.call(attr, 'pid')) {
227
+ // Parse string into int
228
+ // Update PID
229
+ pid = Number.parseInt(attr.pid, 10);
230
+ } else {
231
+ console.warn('Passages are required to have PID. Will not add!');
232
+ }
233
+
234
+ // Check the current PID against startNode number.
235
+ if (pid === startNode) {
236
+ // These match.
237
+ // Save the passage name.
238
+ story.start = name;
239
+ }
240
+
241
+ // If passage is missing name and PID (required attributes),
242
+ // they are not added.
243
+ if (name !== null && pid !== -1) {
244
+ // Add a new Passage into an array
245
+ story.addPassage(
246
+ new Passage(
247
+ name,
248
+ text,
249
+ tags,
250
+ metadata
251
+ )
252
+ );
253
+ }
254
+ }
255
+
256
+ // There was an invalid startNode.
257
+ if (story.start === '') {
258
+ throw new Error('startNode does not exist within passages!');
259
+ }
260
+
261
+ // Look for the style element
262
+ const styleElement = dom.querySelector('#twine-user-stylesheet');
263
+
264
+ // Does the style element exist?
265
+ if (styleElement !== null) {
266
+ // Check if there is any content.
267
+ if (styleElement.rawText.length > 0) {
268
+ // Update stylesheet passage
269
+ story.addPassage(new Passage(
270
+ 'UserStylesheet',
271
+ styleElement.rawText,
272
+ ['stylesheet'])
273
+ );
274
+ }
275
+ }
276
+
277
+ // Look for the script element
278
+ const scriptElement = dom.querySelector('#twine-user-script');
279
+
280
+ // Does the script element exist?
281
+ if (scriptElement !== null) {
282
+ // Check if there is any content.
283
+ if (scriptElement.rawText.length > 0) {
284
+ story.addPassage(new Passage(
285
+ 'UserScript',
286
+ scriptElement.rawText,
287
+ ['script'])
288
+ );
289
+ }
290
+ }
291
+
292
+ // Look for all <tw-tag> elements.
293
+ const twTags = dom.querySelectorAll('tw-tag');
294
+
295
+ // Parse through the entries.
296
+ twTags.forEach((tags) => {
297
+ // Parse each tag element
298
+ const attributes = tags.attributes;
299
+
300
+ // Create default value for name
301
+ let name = '';
302
+
303
+ // Create default value for color
304
+ let color = '';
305
+
306
+ // Check for name
307
+ if (Object.prototype.hasOwnProperty.call(attributes, 'name')) {
308
+ name = attributes.name;
309
+ }
310
+
311
+ // Check for color
312
+ if (Object.prototype.hasOwnProperty.call(attributes, 'color')) {
313
+ color = attributes.color;
314
+ }
315
+
316
+ // If both are not empty strings, use them.
317
+ if (name !== '' && color !== '') {
318
+ // Add name and color to the object
319
+ story.tagColors[name] = color;
320
+ }
321
+ });
322
+
323
+ // Return the parsed story.
324
+ return story;
325
+ }
326
+
327
+ /**
328
+ * Try to escape Twine 2 meta-characters.
329
+ * @param {string} result - Text to parse.
330
+ * @returns {string} Escaped characters.
331
+ */
332
+ function escapeMetacharacters (result) {
333
+ // Replace any single backslash, \, with two of them, \\.
334
+ result = result.replace(/\\/g, '\\');
335
+ // Double-escape escaped {
336
+ result = result.replace(/\\\{/g, '\\\\{');
337
+ // Double-escape escaped }
338
+ result = result.replace(/\\\}/g, '\\\\}');
339
+ // Double-escape escaped [
340
+ result = result.replace(/\\\[/g, '\\\\[');
341
+ // Double-escape escaped ]
342
+ result = result.replace(/\\\]/g, '\\\\]');
343
+
344
+ return result;
345
+ }
346
+
347
+ export { parse, escapeMetacharacters };
348
+ export default parse;
package/src/extwee.js ADDED
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file CLI for Extwee
5
+ * @author Dan Cox
6
+ */
7
+
8
+ // Import functions we need.
9
+ import {
10
+ parseTwine2HTML,
11
+ parseTwee,
12
+ parseStoryFormat,
13
+ parseTwine1HTML,
14
+ compileTwine2HTML,
15
+ compileTwine1HTML
16
+ } from '../index.js';
17
+
18
+ // Import fs.
19
+ import { readFileSync, writeFileSync, statSync } from 'node:fs';
20
+
21
+ // Import Commander.
22
+ import { Command, InvalidArgumentError } from 'commander';
23
+
24
+ // Create a new Command.
25
+ const program = new Command();
26
+
27
+ /*
28
+ * Check if a passed option is a valid file.
29
+ * @function isFile
30
+ * @description Check if a file exists.
31
+ * @param {string} path - Path to file.
32
+ * @returns {boolean} True if file exists, false if not.
33
+ */
34
+ const isFile = (path) => {
35
+ // set default.
36
+ let result = false;
37
+
38
+ try {
39
+ // Attempt tp get stats.
40
+ const stats = statSync(path);
41
+
42
+ // Return if path is a file.
43
+ result = stats.isFile();
44
+ } catch (e) {
45
+ // There was an error, so return false.
46
+ result = false;
47
+ }
48
+
49
+ // Return either the default (false) or the result (true).
50
+ return result;
51
+ };
52
+
53
+ program
54
+ .name('extwee')
55
+ .description('CLI for Extwee')
56
+ .version('2.2.0', '-v, -V, --version', 'Output the current version')
57
+ .option('-c, --compile', 'Compile input into output')
58
+ .option('-d, --decompile', 'De-compile input into output')
59
+ .option('-t1, --twine1', 'Enable Twine 1 processing')
60
+ .option('-name <storyFormatName>', 'Name of the Twine 1 story format (needed for `code.js` inclusion)')
61
+ .option('-codejs <codeJSFile>', 'Twine 1 code.js file for use with Twine 1 HTML', (value) => {
62
+ // Does the input file exist?
63
+ if (isFile(value) === false) {
64
+ // We cannot do anything without valid input.
65
+ throw new InvalidArgumentError(`Twine 1 code.js ${value} does not exist.`);
66
+ }
67
+
68
+ return value;
69
+ })
70
+ .option('-engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML', (value) => {
71
+ // Does the input file exist?
72
+ if (isFile(value) === false) {
73
+ // We cannot do anything without valid input.
74
+ throw new InvalidArgumentError(`Twine 1 engine.js ${value} does not exist.`);
75
+ }
76
+
77
+ return value;
78
+ })
79
+ .option('-header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML', (value) => {
80
+ // Does the input file exist?
81
+ if (isFile(value) === false) {
82
+ // We cannot do anything without valid input.
83
+ throw new InvalidArgumentError(`Twine 1 header.html ${value} does not exist.`);
84
+ }
85
+
86
+ return value;
87
+ })
88
+ .option('-s <storyformat>, --storyformat <storyformat>', 'Path to story format file for Twine 2', (value) => {
89
+ // Does the input file exist?
90
+ if (isFile(value) === false) {
91
+ // We cannot do anything without valid input.
92
+ throw new InvalidArgumentError(`Story format ${value} does not exist.`);
93
+ }
94
+
95
+ return value;
96
+ })
97
+ .option('-i <inputFile>, --input <inputFile>', 'Path to input file', (value) => {
98
+ // Does the input file exist?
99
+ if (isFile(value) === false) {
100
+ // We cannot do anything without valid input.
101
+ throw new InvalidArgumentError(`Input file ${value} does not exist.`);
102
+ }
103
+
104
+ return value;
105
+ })
106
+ .option('-o <outputFile>, --output <outputFile>', 'Path to output file');
107
+
108
+ // Parse the passed arguments.
109
+ program.parse(process.argv);
110
+
111
+ // Create object of passed arguments parsed by Commander.
112
+ const options = program.opts();
113
+
114
+ /*
115
+ * Prepare some (soon to be) global variables.
116
+ */
117
+ // Check if Twine 1 is enabled.
118
+ const isTwine1Mode = (options.twine1 === true);
119
+
120
+ // Check if Twine 2 is enabled.
121
+ const isTwine2Mode = (isTwine1Mode === false);
122
+
123
+ // Check if de-compile mode is enabled.
124
+ const isDecompileMode = (options.decompile === true);
125
+
126
+ // Check if compile mode is enabled.
127
+ const isCompileMode = (options.compile === true);
128
+
129
+ // De-compile Twine 2 HTML into Twee 3 branch.
130
+ // If -d is passed, -i and -o are required.
131
+ if (isTwine2Mode === true && isDecompileMode === true) {
132
+ // Read the input HTML file.
133
+ const inputHTML = readFileSync(options.i, 'utf-8');
134
+
135
+ // Parse the input HTML file into Story object.
136
+ const storyObject = parseTwine2HTML(inputHTML);
137
+
138
+ // Write the output file from Story as Twee 3.
139
+ writeFileSync(options.o, storyObject.toTwee());
140
+
141
+ // Exit the process.
142
+ process.exit();
143
+ }
144
+
145
+ // Compile Twee 3 into Twine 2 HTML branch.
146
+ // If -c is passed, -i, -o, and -s are required.
147
+ if (isTwine2Mode === true && isCompileMode === true) {
148
+ // Read the input file.
149
+ const inputTwee = readFileSync(options.i, 'utf-8');
150
+
151
+ // Parse the input file.
152
+ const story = parseTwee(inputTwee);
153
+
154
+ // Read the story format file.
155
+ const inputStoryFormat = readFileSync(options.s, 'utf-8');
156
+
157
+ // Parse the story format file.
158
+ const parsedStoryFormat = parseStoryFormat(inputStoryFormat);
159
+
160
+ // Compile the story.
161
+ const Twine2HTML = compileTwine2HTML(story, parsedStoryFormat);
162
+
163
+ // Write the output file.
164
+ writeFileSync(options.o, Twine2HTML);
165
+
166
+ // Exit the process.
167
+ process.exit();
168
+ }
169
+
170
+ // Compile Twee 3 into Twine 1 HTML branch.
171
+ // Twine 1 compilation is complicated, so we have to check for all the required options.
172
+ // * options.engine (from Twine 1 itself)
173
+ // * options.header (from Twine 1 story format)
174
+ // * options.name (from Twine 1 story format)
175
+ // * options.codejs (from Twine 1 story format)
176
+ if (isTwine1Mode === true && isCompileMode === true) {
177
+ // Read the input file.
178
+ const inputTwee = readFileSync(options.i, 'utf-8');
179
+
180
+ // Parse the input file.
181
+ const story = parseTwee(inputTwee);
182
+
183
+ // Does the engine file exist?
184
+ const Twine1HTML = compileTwine1HTML(story, options.engine, options.header, options.name, options.codejs);
185
+
186
+ // Write the output file.
187
+ writeFileSync(options.o, Twine1HTML);
188
+
189
+ // Exit the process.
190
+ process.exit();
191
+ }
192
+
193
+ // De-compile Twine 1 HTML into Twee 3 branch.
194
+ if (isTwine1Mode === true && isDecompileMode === true) {
195
+ // Read the input HTML file.
196
+ const inputHTML = readFileSync(options.i, 'utf-8');
197
+
198
+ // Parse the input HTML file into Story object.
199
+ const storyObject = parseTwine1HTML(inputHTML);
200
+
201
+ // Write the output file from Story as Twee 3.
202
+ writeFileSync(options.o, storyObject.toTwee());
203
+
204
+ // Exit the process.
205
+ process.exit();
206
+ }
@@ -0,0 +1,49 @@
1
+ import shell from 'shelljs';
2
+
3
+ // We could get this from process,
4
+ // but since we are using shelljs,
5
+ // we ask for the pwd() instead of cwd().
6
+ const currentPath = shell.pwd().stdout;
7
+ const testFilePath = currentPath + '/test/CLI/files';
8
+
9
+ describe('CLI', () => {
10
+ // Remove the test files, if they exist.
11
+ beforeAll(() => {
12
+ // Test for files beginning with "test." in the output directory.
13
+ if (shell.ls('-A', `${testFilePath}/output/`).length > 0) {
14
+ // Remove the files.
15
+ shell.rm(`${testFilePath}/output/*`);
16
+ }
17
+ });
18
+
19
+ it('Twine 2 - de-compile: Twine 2 HTML into Twee 3', () => {
20
+ shell.exec(`node ${currentPath}/src/extwee.js -d -i ${testFilePath}/input.html -o ${testFilePath}/output/test.twee`);
21
+ expect(shell.test('-e', `${testFilePath}/output/test.twee`)).toBe(true);
22
+ });
23
+
24
+ it('Twine 2 - compile: Twee 3 + StoryFormat into Twine 2 HTML', () => {
25
+ shell.exec(`node ${currentPath}/src/extwee.js -c -i ${testFilePath}/example6.twee -s ${testFilePath}/harlowe.js -o ${testFilePath}/output/test2.html`);
26
+ expect(shell.test('-e', `${testFilePath}/output/test2.html`)).toBe(true);
27
+ });
28
+
29
+ it('Twine 1 - compile: Twee 3 + Twine 1 engine.js + Twine 1 code.js + Twine 1 header.html', () => {
30
+ shell.exec(`node ${currentPath}/src/extwee.js -t1 -c -i ${testFilePath}/example6.twee -o ${testFilePath}/output/test3.html -codejs ${testFilePath}/twine1/code.js -engine ${testFilePath}/twine1/engine.js -header ${testFilePath}/twine1/header.html -name Test`);
31
+ expect(shell.test('-e', `${testFilePath}/output/test3.html`)).toBe(true);
32
+ });
33
+
34
+ it('Twine 1 - de-compile: Twine 1 HTML into Twee 3', () => {
35
+ shell.exec(`node ${currentPath}/src/extwee.js -t1 -d -i ${testFilePath}/twine1Test.html -o ${testFilePath}/output/test.twee`);
36
+ expect(shell.test('-e', `${testFilePath}/output/test.twee`)).toBe(true);
37
+ });
38
+
39
+ // Remove the test files, if they exist.
40
+ afterAll(() => {
41
+ // Test for files in the output directory.
42
+ if (shell.ls('-A', `${testFilePath}/output/`).length > 0) {
43
+ // Remove the files.
44
+ shell.rm(`${testFilePath}/output/*`);
45
+ }
46
+ // Create one file to prevent git from ignoring the folder.
47
+ shell.touch(`${testFilePath}/output/test.twee`);
48
+ });
49
+ });
@@ -0,0 +1 @@
1
+ {"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"},"text":"Word"}]}
@@ -0,0 +1,22 @@
1
+ :: StoryData
2
+ {
3
+ "ifid": "D674C58C-DEFA-4F70-B7A2-27742230C0FC",
4
+ "format": "SugarCube",
5
+ "format-version": "2.28.2",
6
+ "start": "Start",
7
+ "tag-colors": {
8
+ "bar": "green",
9
+ "foo": "red",
10
+ "qaz": "blue"
11
+ },
12
+ "zoom": 0.25
13
+ }
14
+
15
+ :: StoryTitle
16
+ twineExample
17
+
18
+ :: Start [tag tags] {"position": "200,200", "size": "100,100"}
19
+ Content
20
+
21
+ :: Style1 [stylesheet]
22
+ body {background-color: green;}