@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 +1 -1
- package/README.md +37 -21
- package/package.json +5 -4
- package/src/generator.js +39 -8
- package/src/page.js +3 -3
- package/src/template.js +56 -12
package/.eslintrc
CHANGED
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
|
-
|
|
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
|
|
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
|
|
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 `
|
|
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
|
-
* `
|
|
245
|
-
|
|
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
|
|
290
|
-
of the page. That execution could find and execute some `goto()` that could trigger the creation of new
|
|
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
|
|
320
|
+
There are three scripts:
|
|
310
321
|
|
|
311
|
-
* `main-
|
|
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
|
|
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
|
|
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
|
|
403
|
+
h1 YOU WIN!
|
|
388
404
|
else if state.isWrong()
|
|
389
|
-
h1 YOU
|
|
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": "
|
|
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.
|
|
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.
|
|
46
|
-
"pug": "^
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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;
|