extwee 2.0.6 → 2.2.1

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 (233) 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 +20 -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 +68 -58
  37. package/src/JSON/parse.js +128 -0
  38. package/src/Passage.js +279 -202
  39. package/src/Story.js +653 -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 +349 -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 +244 -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 +677 -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 +173 -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/test/Twine2HTML/Twine2HTMLParser/unescaping.html +33 -0
  152. package/tsconfig.json +21 -0
  153. package/types/index.d.ts +14 -0
  154. package/types/src/JSON/parse.d.ts +8 -0
  155. package/types/src/Passage.d.ts +72 -0
  156. package/types/src/Story.d.ts +161 -0
  157. package/types/src/StoryFormat/parse.d.ts +7 -0
  158. package/types/src/StoryFormat.d.ts +97 -0
  159. package/types/src/TWS/parse.d.ts +10 -0
  160. package/types/src/Twee/parse.d.ts +10 -0
  161. package/types/src/Twine1HTML/compile.d.ts +17 -0
  162. package/types/src/Twine1HTML/parse.d.ts +10 -0
  163. package/types/src/Twine2ArchiveHTML/compile.d.ts +6 -0
  164. package/types/src/Twine2ArchiveHTML/parse.d.ts +6 -0
  165. package/types/src/Twine2HTML/compile.d.ts +9 -0
  166. package/types/src/Twine2HTML/parse.d.ts +17 -0
  167. package/types/src/extwee.d.ts +2 -0
  168. package/web-index.js +29 -0
  169. package/webpack.config.js +12 -0
  170. package/bin/extwee.js +0 -47
  171. package/src/FileReader.js +0 -33
  172. package/src/HTMLParser.js +0 -345
  173. package/src/HTMLWriter.js +0 -231
  174. package/src/StoryFormatParser.js +0 -142
  175. package/src/TweeParser.js +0 -161
  176. package/src/TweeWriter.js +0 -121
  177. package/story-formats/chapbook-1.2.0/format.js +0 -1
  178. package/story-formats/chapbook-1.2.0/logo.svg +0 -1
  179. package/story-formats/harlowe-1.2.4/format.js +0 -1
  180. package/story-formats/harlowe-1.2.4/icon.svg +0 -78
  181. package/story-formats/harlowe-2.1.0/format.js +0 -2
  182. package/story-formats/harlowe-2.1.0/icon.svg +0 -78
  183. package/story-formats/harlowe-3.1.0/format.js +0 -3
  184. package/story-formats/harlowe-3.1.0/icon.svg +0 -78
  185. package/story-formats/paperthin-1.0.0/format.js +0 -1
  186. package/story-formats/paperthin-1.0.0/icon.svg +0 -5
  187. package/story-formats/snowman-1.4.0/format.js +0 -1
  188. package/story-formats/snowman-1.4.0/icon.svg +0 -436
  189. package/story-formats/snowman-2.0.2/format.js +0 -1
  190. package/story-formats/snowman-2.0.2/icon.svg +0 -436
  191. package/story-formats/sugarcube-1.0.35/LICENSE +0 -23
  192. package/story-formats/sugarcube-1.0.35/format.js +0 -1
  193. package/story-formats/sugarcube-1.0.35/icon.svg +0 -56
  194. package/story-formats/sugarcube-2.31.1/LICENSE +0 -22
  195. package/story-formats/sugarcube-2.31.1/format.js +0 -1
  196. package/story-formats/sugarcube-2.31.1/icon.svg +0 -56
  197. package/test/CLI/test2.html +0 -45
  198. package/test/CLI.test.js +0 -30
  199. package/test/FileReader/t1.txt +0 -1
  200. package/test/FileReader.test.js +0 -14
  201. package/test/HTMLParser.test.js +0 -177
  202. package/test/HTMLWriter/example6.twee +0 -16
  203. package/test/HTMLWriter.test.js +0 -287
  204. package/test/Roundtrip/example1.twee +0 -21
  205. package/test/Roundtrip.test.js +0 -48
  206. package/test/Story/startmeta.twee +0 -29
  207. package/test/StoryFormatParser.test.js +0 -91
  208. package/test/TweeParser/test.twee +0 -25
  209. package/test/TweeParser.test.js +0 -79
  210. package/test/TweeWriter/test1.twee +0 -18
  211. package/test/TweeWriter/test3.twee +0 -12
  212. package/test/TweeWriter/test4.twee +0 -14
  213. package/test/TweeWriter/test5.twee +0 -20
  214. package/test/TweeWriter/test6.twee +0 -15
  215. package/test/TweeWriter/test7.twee +0 -15
  216. package/test/TweeWriter.test.js +0 -107
  217. /package/test/CLI/{test.twee → files/test.twee} +0 -0
  218. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/format.js +0 -0
  219. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/format_doublename.js +0 -0
  220. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingAuthor.js +0 -0
  221. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingDescription.js +0 -0
  222. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingImage.js +0 -0
  223. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingLicense.js +0 -0
  224. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingName.js +0 -0
  225. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingProofing.js +0 -0
  226. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingSource.js +0 -0
  227. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingURL.js +0 -0
  228. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingVersion.js +0 -0
  229. /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/versionWrong.js +0 -0
  230. /package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/TestTags.html +0 -0
  231. /package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test11.html +0 -0
  232. /package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test6.html +0 -0
  233. /package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/tagColors.html +0 -0
@@ -0,0 +1,349 @@
1
+ import { parse as HtmlParser } from 'node-html-parser';
2
+ import { Story } from '../Story.js';
3
+ import Passage from '../Passage.js';
4
+ import { decode } from 'html-entities';
5
+
6
+ /**
7
+ * Parse Twine 2 HTML into Story object.
8
+ *
9
+ * See: Twine 2 HTML Output Specification
10
+ * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md)
11
+ * @param {string} content - Twine 2 HTML content to parse.
12
+ * @returns {Story} Story
13
+ */
14
+ function parse (content) {
15
+ // Create new story.
16
+ const story = new Story();
17
+
18
+ // Can only parse string values.
19
+ if (typeof content !== 'string') {
20
+ throw new TypeError('Content is not a string!');
21
+ }
22
+
23
+ // Set default start node.
24
+ let startNode = null;
25
+
26
+ // Send to node-html-parser
27
+ // Enable getting the content of 'script', 'style', and 'pre' elements
28
+ // Get back a DOM
29
+ const dom = new HtmlParser(
30
+ content,
31
+ {
32
+ lowerCaseTagName: false,
33
+ script: true,
34
+ style: true,
35
+ pre: true
36
+ });
37
+
38
+ // Pull out the `<tw-storydata>` element.
39
+ const storyDataElements = dom.getElementsByTagName('tw-storydata');
40
+
41
+ // Did we find any elements?
42
+ if (storyDataElements.length === 0) {
43
+ // If there is not a single `<tw-storydata>` element, this is not a Twine 2 story!
44
+ throw new Error('Not Twine 2 HTML content!');
45
+ }
46
+
47
+ // We only parse the first element found.
48
+ const storyData = storyDataElements[0];
49
+
50
+ /**
51
+ * name: (string) Required.
52
+ * The name of the story.
53
+ */
54
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'name')) {
55
+ // Set the story name
56
+ story.name = storyData.attributes.name;
57
+ } else {
58
+ // Name is a required field. Warn user.
59
+ console.warn('Twine 2 HTML must have a name!');
60
+ }
61
+
62
+ /**
63
+ * ifid: (string) Required.
64
+ * An IFID is a sequence of between 8 and 63 characters,
65
+ * each of which shall be a digit, a capital letter or a
66
+ * hyphen that uniquely identify a story (see Treaty of Babel).
67
+ */
68
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'ifid')) {
69
+ // Update story IFID.
70
+ story.IFID = storyData.attributes.ifid;
71
+ } else {
72
+ // Name is a required filed. Warn user.
73
+ console.warn('Twine 2 HTML must have an IFID!');
74
+ }
75
+
76
+ /**
77
+ * creator: (string) Optional.
78
+ * The name of program used to create the file.
79
+ */
80
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator')) {
81
+ // Update story creator
82
+ story.creator = storyData.attributes.creator;
83
+ }
84
+
85
+ /**
86
+ * creator-version: (string) Optional.
87
+ * The version of the program used to create the file.
88
+ */
89
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator-version')) {
90
+ // Update story creator version
91
+ story.creatorVersion = storyData.attributes['creator-version'];
92
+ }
93
+
94
+ /**
95
+ * format: (string) Optional.
96
+ * The story format used to create the story.
97
+ */
98
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format')) {
99
+ // Update story format
100
+ story.format = storyData.attributes.format;
101
+ }
102
+
103
+ /**
104
+ * format-version: (string) Optional.
105
+ * The version of the story format used to create the story.
106
+ */
107
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format-version')) {
108
+ // Update story format version
109
+ story.formatVersion = storyData.attributes['format-version'];
110
+ }
111
+
112
+ /**
113
+ * zoom: (string) Optional.
114
+ * The decimal level of zoom (i.e. 1.0 is 100% and 1.2 would be 120% zoom level).
115
+ */
116
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'zoom')) {
117
+ // Update story zoom
118
+ story.zoom = Number(Number.parseFloat(storyData.attributes.zoom).toFixed(2));
119
+ }
120
+
121
+ /**
122
+ * startnode: (string) Optional.
123
+ * The PID matching a `<tw-passagedata>` element whose content should be displayed first.
124
+ */
125
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'startnode')) {
126
+ // Take string value and convert to Int
127
+ startNode = Number.parseInt(storyData.attributes.startnode, 10);
128
+ } else {
129
+ // Throw error without start node.
130
+ throw new Error('Missing startnode in <tw-storydata>!');
131
+ }
132
+
133
+ // Pull out the `<tw-passagedata>` element.
134
+ const storyPassages = dom.querySelectorAll('tw-passagedata');
135
+
136
+ // Move through the passages
137
+ for (const passage in storyPassages) {
138
+ // Get the passage attributes
139
+ const attr = storyPassages[passage].attributes;
140
+ // Get the passage text
141
+ const text = storyPassages[passage].rawText;
142
+
143
+ /**
144
+ * position: (string) Optional.
145
+ * Comma-separated X and Y position of the upper-left
146
+ * of the passage when viewed within the Twine 2 editor.
147
+ */
148
+ // Set a default position.
149
+ let position = null;
150
+ // Does position exist?
151
+ if (Object.prototype.hasOwnProperty.call(attr, 'position')) {
152
+ // Update position.
153
+ position = attr.position;
154
+ }
155
+
156
+ /**
157
+ * size: (string) Optional.
158
+ * Comma-separated width and height of the
159
+ * passage when viewed within the Twine 2 editor.
160
+ */
161
+ // Set a default size.
162
+ let size = null;
163
+ // Does size exist?
164
+ if (Object.prototype.hasOwnProperty.call(attr, 'size')) {
165
+ // Update size.
166
+ size = attr.size;
167
+ }
168
+
169
+ /**
170
+ * name: (string) Required.
171
+ * The name of the passage.
172
+ *
173
+ * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
174
+ */
175
+ // Create a default value
176
+ let name = null;
177
+ // Does name exist?
178
+ if (Object.prototype.hasOwnProperty.call(attr, 'name')) {
179
+ // Escape the name
180
+ name = escapeMetacharacters(attr.name);
181
+ } else {
182
+ throw new Error('Cannot parse passage data without name!');
183
+ }
184
+
185
+ // Create empty tag array.
186
+ let tags = [];
187
+ // Does the tags attribute exist?
188
+ if (Object.prototype.hasOwnProperty.call(attr, 'tags')) {
189
+ // Escape any tags
190
+ // (Attributes can, themselves, be empty strings.)
191
+ if (attr.tags.length > 0 && attr.tags !== '""') {
192
+ // Escape the tags
193
+ tags = escapeMetacharacters(attr.tags);
194
+ // Split by spaces into an array
195
+ tags = tags.split(' ');
196
+ }
197
+
198
+ // Remove any empty strings.
199
+ tags = tags.filter(tag => tag !== '');
200
+ }
201
+
202
+ // Create metadata for passage.
203
+ const metadata = {};
204
+
205
+ // Does position exist?
206
+ if (position !== null) {
207
+ // Add the property to metadata
208
+ metadata.position = position;
209
+ }
210
+
211
+ // Does size exist?
212
+ if (size !== null) {
213
+ // Add the property to metadata
214
+ metadata.size = size;
215
+ }
216
+
217
+ /**
218
+ * pid: (string) Required.
219
+ * The Passage ID (PID).
220
+ * (Note: This is subject to change during editing with Twine 2.)
221
+ *
222
+ * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
223
+ */
224
+ // Create a default PID
225
+ let pid = -1;
226
+ // Does pid exist?
227
+ if (Object.prototype.hasOwnProperty.call(attr, 'pid')) {
228
+ // Parse string into int
229
+ // Update PID
230
+ pid = Number.parseInt(attr.pid, 10);
231
+ } else {
232
+ console.warn('Passages are required to have PID. Will not add!');
233
+ }
234
+
235
+ // Check the current PID against startNode number.
236
+ if (pid === startNode) {
237
+ // These match.
238
+ // Save the passage name.
239
+ story.start = name;
240
+ }
241
+
242
+ // If passage is missing name and PID (required attributes),
243
+ // they are not added.
244
+ if (name !== null && pid !== -1) {
245
+ // Add a new Passage into an array
246
+ story.addPassage(
247
+ new Passage(
248
+ decode(name),
249
+ decode(text),
250
+ tags.map(tag => decode(tag)),
251
+ metadata
252
+ )
253
+ );
254
+ }
255
+ }
256
+
257
+ // There was an invalid startNode.
258
+ if (story.start === '') {
259
+ throw new Error('startNode does not exist within passages!');
260
+ }
261
+
262
+ // Look for the style element
263
+ const styleElement = dom.querySelector('#twine-user-stylesheet');
264
+
265
+ // Does the style element exist?
266
+ if (styleElement !== null) {
267
+ // Check if there is any content.
268
+ if (styleElement.rawText.length > 0) {
269
+ // Update stylesheet passage
270
+ story.addPassage(new Passage(
271
+ 'UserStylesheet',
272
+ styleElement.rawText,
273
+ ['stylesheet'])
274
+ );
275
+ }
276
+ }
277
+
278
+ // Look for the script element
279
+ const scriptElement = dom.querySelector('#twine-user-script');
280
+
281
+ // Does the script element exist?
282
+ if (scriptElement !== null) {
283
+ // Check if there is any content.
284
+ if (scriptElement.rawText.length > 0) {
285
+ story.addPassage(new Passage(
286
+ 'UserScript',
287
+ scriptElement.rawText,
288
+ ['script'])
289
+ );
290
+ }
291
+ }
292
+
293
+ // Look for all <tw-tag> elements.
294
+ const twTags = dom.querySelectorAll('tw-tag');
295
+
296
+ // Parse through the entries.
297
+ twTags.forEach((tags) => {
298
+ // Parse each tag element
299
+ const attributes = tags.attributes;
300
+
301
+ // Create default value for name
302
+ let name = '';
303
+
304
+ // Create default value for color
305
+ let color = '';
306
+
307
+ // Check for name
308
+ if (Object.prototype.hasOwnProperty.call(attributes, 'name')) {
309
+ name = attributes.name;
310
+ }
311
+
312
+ // Check for color
313
+ if (Object.prototype.hasOwnProperty.call(attributes, 'color')) {
314
+ color = attributes.color;
315
+ }
316
+
317
+ // If both are not empty strings, use them.
318
+ if (name !== '' && color !== '') {
319
+ // Add name and color to the object
320
+ story.tagColors[name] = color;
321
+ }
322
+ });
323
+
324
+ // Return the parsed story.
325
+ return story;
326
+ }
327
+
328
+ /**
329
+ * Try to escape Twine 2 meta-characters.
330
+ * @param {string} result - Text to parse.
331
+ * @returns {string} Escaped characters.
332
+ */
333
+ function escapeMetacharacters (result) {
334
+ // Replace any single backslash, \, with two of them, \\.
335
+ result = result.replace(/\\/g, '\\');
336
+ // Double-escape escaped {
337
+ result = result.replace(/\\\{/g, '\\\\{');
338
+ // Double-escape escaped }
339
+ result = result.replace(/\\\}/g, '\\\\}');
340
+ // Double-escape escaped [
341
+ result = result.replace(/\\\[/g, '\\\\[');
342
+ // Double-escape escaped ]
343
+ result = result.replace(/\\\]/g, '\\\\]');
344
+
345
+ return result;
346
+ }
347
+
348
+ export { parse, escapeMetacharacters };
349
+ 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;}