@eit6609/storyteller 1.0.6 → 2.0.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.
package/.eslintrc CHANGED
@@ -79,7 +79,7 @@ rules:
79
79
  # Custom rules
80
80
  no-warning-comments: 1
81
81
  max-depth: [1, 3]
82
- max-statements: [1, 15]
82
+ max-statements: [1, 20]
83
83
  camelcase: [2, {properties: "never"}]
84
84
  brace-style: [2, "1tbs", {allowSingleLine: true}]
85
85
  block-scoped-var: 2
package/README.md CHANGED
@@ -116,12 +116,13 @@ The methods `reset()` and `move()` are modifiers, and will be used with the `got
116
116
 
117
117
  The pages of the ebook are generated by a set of XHTML templates, one for every location of the game.
118
118
 
119
- After many experiments with the most popular templating engines for Node.js, I have chosen
120
- [Pug](https://pugjs.org/api/getting-started.html) because it gives you enough freedom to call JavaScript code inside
121
- the template. This is vital, but many engines (the very popular [Handlebars](https://handlebarsjs.com) among others)
122
- make the call of methods on a class instance a nightmare.
119
+ After many experiments with the most popular templating engines for Node.js, I have chosen:
123
120
 
124
- Of course [EJS](https://ejs.co) gives you *complete* freedom, but I prefer higher level engines like Pug.
121
+ * [EJS](https://ejs.co)
122
+ * [Pug](https://pugjs.org/api/getting-started.html)
123
+
124
+ There are many engines (the very popular [Handlebars](https://handlebarsjs.com) among others) but most
125
+ of them make the call of methods on a class instance a nightmare.
125
126
 
126
127
  I have added **experimental** support for markdown templating by means of my
127
128
  [Markdown Templates](https://github.com/eit6609/markdown-templates) engine.
@@ -129,7 +130,7 @@ I have added **experimental** support for markdown templating by means of my
129
130
  Let's see an example of template:
130
131
 
131
132
  ```pug
132
- doctype strict
133
+ doctype 1.1
133
134
  html(xmlns='http://www.w3.org/1999/xhtml', xml:lang='en')
134
135
  head
135
136
  link(href="style-epub.css", rel="stylesheet", type="text/css")
@@ -144,7 +145,7 @@ html(xmlns='http://www.w3.org/1999/xhtml', xml:lang='en')
144
145
  li Peg 2: #{state.pegs[1]}
145
146
  li Peg 3: #{state.pegs[2]}
146
147
  if state.isFinished()
147
- h1 YOU’VE WON!
148
+ h1 YOU WIN!
148
149
  br
149
150
  p.first Want to #[a(href=goto((state) => state.reset())) play again]?
150
151
  else
@@ -235,16 +236,26 @@ These are the supported options:
235
236
  as input for the [ePUB creator](https://github.com/eit6609/epub-creator), so you can put in this directory any extra
236
237
  file (images, stylesheets) that you need in the ePUB.
237
238
  * `metadata`, object, required, the options for the ePUB creator, with these properties:
238
- * `title`, string, optional, default `untitled`: the title of the ePUB
239
+ * `title`, string, optional, default `Untitled`: the title of the ePUB
239
240
  * `author`, string, optional, default no author: the author of the ePUB
240
241
  * `language`, string, optional, default `en`: the language of the ePUB
242
+ * `description`, string, optional, default no description: the description of the ePUB
243
+ * `isbn`, string, optional, default no ISBN: the ISBN of the ePUB
244
+ * `tags`, array of string, optional, default no subjects: the subjects of the ePUB
241
245
  * `cover`, string, optional, default no cover: a path relative to `outputDir` of an image that will become the cover
242
246
  of the ePUB
243
247
  * `filename`, string, required: the path of the generated ePUB
244
- * `markdown`, boolean, optional, default `false`: if `false` the template engine is Pug, otherwise it is Markdown
245
- Templates
248
+ * `templateEngine`, string, required, possible values: `ejs`, `pug`, `mt`: the template engine used to
249
+ generate the pages.
246
250
  * `debug`, boolean, optional, default `false`: if `true` the `debug()` function called in the templates will return the
247
251
  page key, otherwise the empty string.
252
+ * `contentBefore`, array, optional, default `[]`. Extra, static pages to insert into the generated ePUB before the
253
+ generated pages. The items of the array are objects with these properties:
254
+ * `fileName`, string, required. The path of the file relative to `outputDir`.
255
+ * `tocLabel`, string, optional. The label to use in the TOC. Leave it out if you don't want the page to be added to
256
+ the TOC.
257
+ * `contentAfter`, array, optional, default `[]`. Extra, static pages to insert into the generated ePUB after the
258
+ generated pages. The items of the array are the same as `contentBefore`.
248
259
 
249
260
  #### `generate(initialTemplateName: string, initialState: object): promise`
250
261
 
@@ -280,15 +291,15 @@ The parameters are:
280
291
 
281
292
  * `templateName`: the path of a template file, without the extension, relative to the `templatesDir`. It defaults to
282
293
  the current template's name.
283
- * `action`: a function that receives a state as its only parameter and modifies it. It should return a falsy
294
+ * `action`: a function that receives a clone of the state as its only parameter and modifies it. It should return a falsy
284
295
  value unless it wants to replace the received state with a new one: in this case it should return the new state. This is
285
296
  handy for complex games because it enables you to move through independent stages of the game. More about this later,
286
297
  in the *Tips & Tricks* section.
287
298
 
288
299
  With this function you actually trigger the generation of the pages, because, if the requested page does not exist,
289
- the generator creates an empty page and enqueues it for the build, that is the execution of the template with the state
290
- of the page. That execution could find and execute some `goto()` that could trigger the creation of new pages, and so
291
- on.
300
+ the generator creates an empty page and enqueues it for the build, that is the execution of the template with a clone
301
+ of the state of the page. That execution could find and execute some `goto()` that could trigger the creation of new
302
+ pages, and so on.
292
303
 
293
304
  ## Examples
294
305
 
@@ -306,14 +317,19 @@ But if you are lazy you can just download the generated ePUBs.
306
317
 
307
318
  A classic puzzle!
308
319
 
309
- There are two scripts:
320
+ There are three scripts:
310
321
 
311
- * `main-xhtml.js`, that uses the Pug (.pug) templates
322
+ * `main-pug.js`, that uses the Pug (.pug) templates
323
+ * `main-ejs.js`, that uses the EJS (.html) templates
312
324
  * `main-markdown.js`, that uses the experimental Markdown Templates (.md) templates
313
325
 
314
326
  The generated ePUBs should be the same.
315
327
 
316
- You can download the generated ePUB [here](examples/goat-cabbage-wolf/code/goat-cabbage-wolf-xhtml.epub).
328
+ You can download the generated ePUBs here:
329
+
330
+ * [pug](examples/goat-cabbage-wolf/code/goat-cabbage-wolf-pug.epub)
331
+ * [ejs](examples/goat-cabbage-wolf/code/goat-cabbage-wolf-ejs.epub)
332
+ * [markdown](examples/goat-cabbage-wolf/code/goat-cabbage-wolf-markdown.epub)
317
333
 
318
334
  ### Desert Traversal
319
335
 
@@ -358,7 +374,7 @@ class Safe {
358
374
  }
359
375
 
360
376
  choose (digit) {
361
- this.ok = String(digit) === this.combination.charAt(this.index);
377
+ this.ok = this.ok && String(digit) === this.combination.charAt(this.index);
362
378
  this.index++;
363
379
  }
364
380
 
@@ -375,7 +391,7 @@ class Safe {
375
391
  with this template:
376
392
 
377
393
  ```pug
378
- doctype strict
394
+ doctype 1.1
379
395
  html(xmlns='http://www.w3.org/1999/xhtml', xml:lang='en')
380
396
  head
381
397
  link(href="style-epub.css", rel="stylesheet", type="text/css")
@@ -384,9 +400,9 @@ html(xmlns='http://www.w3.org/1999/xhtml', xml:lang='en')
384
400
  h3 Open the Safe
385
401
  hr
386
402
  if state.isRight()
387
- h1 YOU’VE WON!
403
+ h1 YOU WIN!
388
404
  else if state.isWrong()
389
- h1 YOU’VE LOST!
405
+ h1 YOU LOSE!
390
406
  else
391
407
  p.first Choose digit ##{state.index + 1}:
392
408
  ul
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eit6609/storyteller",
3
- "version": "1.0.6",
3
+ "version": "2.0.0",
4
4
  "description": "An interactive ebooks generator",
5
5
  "keywords": [
6
6
  "epub",
@@ -38,11 +38,12 @@
38
38
  "jasmine-spec-reporter": "^3.2.0"
39
39
  },
40
40
  "dependencies": {
41
- "@eit6609/epub-creator": "^1.0.0",
41
+ "@eit6609/epub-creator": "^1.1.0",
42
42
  "@eit6609/markdown-templates": "^1.0.0",
43
43
  "@hapi/joi": "^16.1.7",
44
+ "ejs": "^3.1.8",
44
45
  "lodash": "^4.17.10",
45
- "marked": "^0.8.2",
46
- "pug": "^2.0.4"
46
+ "marked": "^4.0.10",
47
+ "pug": "^3.0.1"
47
48
  }
48
49
  }
package/src/generator.js CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  const
4
4
  { inspect } = require('util'),
5
- { filter, forEach, get, isUndefined, pick, sortBy } = require('lodash'),
5
+ { filter, forEach, get, isUndefined, omit, sortBy } = require('lodash'),
6
6
  Joi = require('@hapi/joi'),
7
7
  { EPUBCreator } = require('@eit6609/epub-creator'),
8
8
  Page = require('./page.js'),
9
9
  Template = require('./template.js');
10
10
 
11
+ const extraContentSchema = Joi.object({
12
+ fileName: Joi.string().required(),
13
+ tocLabel: Joi.string()
14
+ });
15
+
11
16
  const optionsSchema = Joi.object({
12
17
  templatesDir: Joi.string().required(),
13
18
  outputDir: Joi.string().required(),
@@ -15,16 +20,22 @@ const optionsSchema = Joi.object({
15
20
  title: Joi.string(),
16
21
  author: Joi.string(),
17
22
  language: Joi.string(),
23
+ isbn: Joi.string(),
24
+ description: Joi.string(),
25
+ tags: Joi.array().items(Joi.string()),
18
26
  cover: Joi.string(),
19
- filename: Joi.string().required()
27
+ filename: Joi.string().required(),
20
28
  }).required(),
21
- markdown: Joi.boolean(),
29
+ advancedMetadata: Joi.array(),
30
+ templateEngine: Joi.string().valid('pug', 'ejs', 'mt').required(),
22
31
  debug: Joi.boolean(),
32
+ contentBefore: Joi.array().items(extraContentSchema),
33
+ contentAfter: Joi.array().items(extraContentSchema),
23
34
  factory: Joi.object({
24
35
  createPage: Joi.function(),
25
36
  createTemplate: Joi.function(),
26
37
  createEPUBCreator: Joi.function()
27
- })
38
+ }),
28
39
  });
29
40
 
30
41
  class Generator {
@@ -34,8 +45,11 @@ class Generator {
34
45
  this.templatesDir = options.templatesDir;
35
46
  this.outputDir = options.outputDir;
36
47
  this.metadata = options.metadata;
37
- this.markdown = options.markdown === true;
48
+ this.advancedMetadata = options.advancedMetadata || [];
49
+ this.templateEngine = options.templateEngine;
38
50
  this.debug = options.debug === true;
51
+ this.contentBefore = options.contentBefore || [];
52
+ this.contentAfter = options.contentAfter || [];
39
53
  this.createPage = get(options, 'factory.createPage', (...params) => new Page(...params));
40
54
  this.createTemplate = get(options, 'factory.createTemplate', (...params) => new Template(...params));
41
55
  this.createEPUBCreator = get(options, 'factory.createEPUBCreator', (...params) => new EPUBCreator(...params));
@@ -86,24 +100,41 @@ class Generator {
86
100
 
87
101
  createEpub () {
88
102
  const spine = [];
103
+ const toc = [];
104
+ if (this.metadata.cover) {
105
+ toc.push([{ label: 'Cover', href: 'cover-page.html' }]);
106
+ }
107
+ for (const { tocLabel, fileName } of this.contentBefore) {
108
+ spine.push(fileName);
109
+ if (tocLabel) {
110
+ toc.push([{ label: tocLabel, href: fileName }]);
111
+ }
112
+ }
89
113
  for (let i = 0; i < this.numberOfPages; i++) {
90
114
  spine.push(`${Page.numberFormat.format(i)}.html`);
91
115
  }
92
- const toc = [[{ label: 'Start', href: `${Page.numberFormat.format(0)}.html` }]];
116
+ toc.push([{ label: 'Game Start', href: `${Page.numberFormat.format(0)}.html` }]);
117
+ for (const { tocLabel, fileName } of this.contentAfter) {
118
+ spine.push(fileName);
119
+ if (tocLabel) {
120
+ toc.push([{ label: tocLabel, href: fileName }]);
121
+ }
122
+ }
93
123
  const { cover } = this.metadata;
94
124
  const epubCreator = this.createEPUBCreator({
95
125
  contentDir: this.outputDir,
96
126
  spine,
97
127
  toc,
98
128
  cover,
99
- simpleMetadata: pick(this.metadata, ['author', 'title', 'language'])
129
+ simpleMetadata: omit(this.metadata, ['cover', 'filename']),
130
+ metadata: this.advancedMetadata,
100
131
  });
101
132
  return epubCreator.create(this.metadata.filename);
102
133
  }
103
134
 
104
135
  printReport () {
105
136
  function makeFilterByTemplateName (templateName) {
106
- return (pageKey) => pageKey.substring(0, pageKey.lastIndexOf('/')) === templateName;
137
+ return (pageKey) => pageKey.substring(0, pageKey.indexOf('{') - 1) === templateName;
107
138
  }
108
139
 
109
140
  const templateNames = sortBy(Array.from(this.templates.keys()));
package/src/page.js CHANGED
@@ -15,7 +15,7 @@ const {
15
15
  toPairsIn
16
16
  } = require('lodash'),
17
17
  fs = require('fs'),
18
- marked = require('marked');
18
+ { marked } = require('marked');
19
19
 
20
20
  function wrapBody (body) {
21
21
  return `<?xml version="1.0" encoding="utf-8"?>
@@ -98,8 +98,8 @@ class Page {
98
98
  followLink (templateName, action) {
99
99
  templateName = templateName || this.template.name;
100
100
  let { state } = this;
101
+ state = cloneDeep(state);
101
102
  if (action) {
102
- state = cloneDeep(state);
103
103
  const newState = action.call(null, state);
104
104
  // this way the action can create a new state and initialize a new stage of the game:
105
105
  if (newState) {
@@ -110,7 +110,7 @@ class Page {
110
110
  }
111
111
 
112
112
  build () {
113
- if (this.builder.markdown) {
113
+ if (this.builder.templateEngine === 'mt') {
114
114
  const text = this.template.build(this);
115
115
  const markedOptions = {
116
116
  smartypants: true,
package/src/template.js CHANGED
@@ -1,28 +1,73 @@
1
1
  'use strict';
2
2
 
3
3
  const
4
+ fs = require('fs'),
4
5
  pug = require('pug'),
5
- mt = require('@eit6609/markdown-templates');
6
+ mt = require('@eit6609/markdown-templates'),
7
+ ejs = require('ejs');
8
+
9
+ class Pug {
10
+ getFileExtension () {
11
+ return 'pug';
12
+ }
13
+
14
+ loadTemplate (fileName, { mockPug }) {
15
+ return (mockPug || pug).compileFile(fileName, { pretty: true });
16
+ }
17
+ }
18
+
19
+ class EJS {
20
+ getFileExtension () {
21
+ return 'ejs';
22
+ }
23
+
24
+ loadTemplate (fileName, { mockEJS }) {
25
+ if (mockEJS) {
26
+ return mockEJS.compile(mockEJS.getFileContent());
27
+ }
28
+ const src = fs.readFileSync(fileName, 'utf-8');
29
+ return ejs.compile(src);
30
+ }
31
+ }
32
+
33
+ class MarkdownTemplates {
34
+ getFileExtension () {
35
+ return 'md';
36
+ }
37
+
38
+ loadTemplate (fileName, { mockMT }) {
39
+ return (mockMT || mt).compileFile(fileName);
40
+ }
41
+ }
6
42
 
7
43
  class Template {
8
44
 
9
- constructor (name, builder, mockMT, mockPug) {
45
+ static createTemplateEngineAdapter (name) {
46
+ switch (name) {
47
+ case 'pug':
48
+ return new Pug();
49
+ case 'ejs':
50
+ return new EJS();
51
+ case 'mt':
52
+ return new MarkdownTemplates();
53
+ default:
54
+ throw new Error(`Unknown template engine: "${name}"`);
55
+ }
56
+ }
57
+
58
+ constructor (name, builder, { mockMT, mockPug, mockEJS } = {}) {
10
59
  this.name = name;
11
60
  this.templatesDir = builder.templatesDir;
12
- this.markdown = builder.markdown;
13
- this.load(mockMT, mockPug);
61
+ this.adapter = Template.createTemplateEngineAdapter(builder.templateEngine || 'ejs');
62
+ this.load({ mockMT, mockPug, mockEJS });
14
63
  }
15
64
 
16
- load (mockMT, mockPug) {
17
- if (this.markdown) {
18
- this.template = (mockMT || mt).compileFile(this.getFileName());
19
- } else {
20
- this.template = (mockPug || pug).compileFile(this.getFileName(), { pretty: true });
21
- }
65
+ load ({ mockMT, mockPug, mockEJS }) {
66
+ this.template = this.adapter.loadTemplate(this.getFileName(), { mockMT, mockPug, mockEJS });
22
67
  }
23
68
 
24
69
  getFileName () {
25
- return `${this.templatesDir}/${this.name}.${this.markdown ? 'md' : 'pug'}`;
70
+ return `${this.templatesDir}/${this.name}.${this.adapter.getFileExtension()}`;
26
71
  }
27
72
 
28
73
  build (page) {
@@ -31,5 +76,4 @@ class Template {
31
76
 
32
77
  }
33
78
 
34
-
35
79
  module.exports = Template;