1+ #!/usr/bin/env node
2+
3+ // Generate EPUB and MOBI ebooks
4+
5+ const fs = require ( 'fs' ) ;
6+ const path = require ( 'path' ) ;
7+ const exec = require ( 'child_process' ) . exec ;
8+ const fastmatter = require ( 'fastmatter' ) ;
9+ const mkdirp = require ( 'mkdirp' ) ;
10+ const slug = require ( 'slug' ) ;
11+ const unified = require ( 'unified' ) ;
12+ const parse = require ( 'remark-parse' ) ;
13+ const stringify = require ( 'remark-stringify' ) ;
14+ const visit = require ( 'unist-util-visit' ) ;
15+ const download = require ( 'download' ) ;
16+ const ebrew = require ( 'ebrew' ) ;
17+
18+ const processor = unified ( )
19+ . use ( parse )
20+ . use ( replaceAsides )
21+ . use ( replaceCheckboxes )
22+ . use ( fixImages )
23+ . use ( downloadImages )
24+ . use ( stringify ) ;
25+
26+ buildBooks ( [
27+ '_articles/how-to-contribute.md' ,
28+ '_articles/starting-a-project.md' ,
29+ '_articles/finding-users.md' ,
30+ '_articles/building-community.md' ,
31+ '_articles/best-practices.md' ,
32+ '_articles/leadership-and-governance.md' ,
33+ '_articles/getting-paid.md' ,
34+ '_articles/code-of-conduct.md' ,
35+ '_articles/metrics.md' ,
36+ '_articles/legal.md' ,
37+ ] ) ;
38+
39+ function buildBooks ( files ) {
40+ const out = 'assets/ebooks/open-source-guide' ;
41+ const tempFile = '_ebook.md' ;
42+
43+ mkdirp . sync ( 'assets/ebooks' ) ;
44+ mkdirp . sync ( '.asset-downloads' ) ;
45+
46+ let source = files . map ( readFile ) . join ( '\n\n' ) ;
47+ const { text, promises } = downloadImages ( source ) ;
48+ fs . writeFileSync ( tempFile , text ) ;
49+
50+ promises
51+ . then ( ( ) => markdownToEpub ( tempFile , `${ out } .epub` ) )
52+ . then ( ( ) => epubToMobi ( `${ out } .epub` , `${ out } .mobi` ) )
53+ . then ( ( ) => fs . unlinkSync ( tempFile ) )
54+ . catch ( err => console . error ( err ) ) ;
55+ }
56+
57+ function readFile ( file ) {
58+ const md = fs . readFileSync ( file , 'utf8' ) ;
59+
60+ // Replace frontmatter with chapter title
61+ const { attributes, body } = fastmatter ( md ) ;
62+ let text = `# ${ attributes . title } \n\n${ body } ` ;
63+
64+ return processor . process ( text ) . contents ;
65+ }
66+
67+ function markdownToEpub ( mdFile , epubFile ) {
68+ return new Promise ( ( resolve , reject ) => {
69+ console . log ( `Building ${ epubFile } ...` ) ;
70+
71+ const manifestFile = '_ebook.json' ;
72+ fs . writeFileSync ( manifestFile , JSON . stringify ( {
73+ tocDepth : 1 ,
74+ title : 'Open Source Guides' ,
75+ subtitle : 'Open source software is made by people just like you. Learn how to launch and grow your project.' ,
76+ author : 'GitHub' ,
77+ publisher : 'GitHub' ,
78+ rights : 'CC-BY-4.0' ,
79+ date : ( new Date ( ) ) . toISOString ( ) . slice ( 0 , 10 ) ,
80+ contents : path . resolve ( mdFile ) ,
81+ } ) ) ;
82+
83+ ebrew . generate ( manifestFile , epubFile , err => {
84+ fs . unlinkSync ( manifestFile ) ;
85+ if ( err ) {
86+ reject ( err . stack || err ) ;
87+ } else {
88+ resolve ( ) ;
89+ }
90+ } ) ;
91+ } ) ;
92+ }
93+
94+ function epubToMobi ( epubFile , mobiFile ) {
95+ return new Promise ( ( resolve , reject ) => {
96+ console . log ( `Building ${ mobiFile } ...` ) ;
97+
98+ const kindlegen = require . resolve ( 'kindlegen/bin/kindlegen' ) ;
99+ exec ( `${ kindlegen } ${ epubFile } -c2 -verbose -o ${ path . basename ( mobiFile ) } ` , ( err , stdout ) => {
100+ if ( err && stdout . includes ( 'MOBI file could not be generated because of errors!' ) ) {
101+ reject ( stdout ) ;
102+ } else {
103+ resolve ( ) ;
104+ }
105+ } ) ;
106+ } ) ;
107+ }
108+
109+ function downloadImages ( text ) {
110+ function image ( processor ) {
111+ return ast => visit ( ast , 'image' , node => {
112+ if ( node . url . startsWith ( 'http' ) ) {
113+ const url = node . url ;
114+ const filename = slug ( path . basename ( node . url ) ) ;
115+ const filepath = `./.asset-downloads/${ filename } .jpg`
116+ node . url = filepath ;
117+ if ( fs . existsSync ( filepath ) ) {
118+ promises . push ( Promise . resolve ( ) ) ;
119+ } else {
120+ const promise = download ( url )
121+ . then ( data => {
122+ fs . writeFileSync ( filepath , data ) ;
123+ } ) ;
124+ promises . push ( promise ) ;
125+ }
126+ }
127+ } ) ;
128+ }
129+
130+ let promises = [ ] ;
131+ const processor = unified ( )
132+ . use ( parse )
133+ . use ( image )
134+ . use ( stringify ) ;
135+
136+ text = processor . process ( text ) . contents ;
137+
138+ return {
139+ promises : Promise . all ( promises ) ,
140+ text,
141+ } ;
142+ }
143+
144+ /*
145+ Replace asides with Markdown blockquotes:
146+ <aside markdown="1" class="pquote">
147+ <img src="https://avatars2.githubusercontent.com/u/1976330?v=3&s=460" class="pquote-avatar" alt="avatar" alt="@lord avatar">
148+ I fumbled it. I didn't put in the effort to come up with a complete solution. Instead of an half-assed solution, I wish I had said "I don't have time for this right now, but I'll add it to the long term nice-to-have list."
149+ <p markdown="1" class="pquote-credit">
150+ — @lord, ["Tips for new open source maintainers"](https://lord.io/blog/2014/oss-tips/)
151+ </p>
152+ </aside>
153+ */
154+ function replaceAsides ( processor ) {
155+ return ast => visit ( ast , 'html' , node => {
156+ if ( node . value . startsWith ( '<aside' ) ) {
157+ node . value = node . value
158+ . replace ( / < i m g s r c = " ( .* ?) " [ ^ > ] * > / , '\n' )
159+ . replace ( / \( \/ a s s e t s \/ / , '(./assets/' )
160+ . replace ( / < \/ ? ( a s i d e | p ) .* ?> / g, '' )
161+ . trim ( )
162+ . replace ( / ( ^ | \n ) * / g, '\n> ' ) ;
163+ }
164+ } ) ;
165+ }
166+
167+ /*
168+ Replace checkboxes with unordered lists:
169+ <div class="clearfix mb-2">
170+ <input type="checkbox" id="cbox1" class="d-block float-left mt-1 mr-2" value="checkbox">
171+ <label for="cbox1" class="overflow-hidden d-block text-normal">
172+ Does it have a license? Usually, this is a file called LICENSE in the root of the repository.
173+ </label>
174+ </div>
175+ */
176+ function replaceCheckboxes ( processor ) {
177+ return ast => visit ( ast , 'html' , node => {
178+ if ( node . value . startsWith ( '<div' ) ) {
179+ node . value = node . value
180+ . replace ( / < \/ ? .* ?> / g, '' )
181+ . trim ( )
182+ . replace ( / ^ / , '* ' ) ;
183+ }
184+ } ) ;
185+ }
186+
187+ /*
188+ 1. Remove image alts, otherwise they will be visible in a book
189+ 2. Fix asset paths
190+ */
191+ function fixImages ( processor ) {
192+ return ast => visit ( ast , 'image' , node => {
193+ node . alt = null ;
194+ node . url = node . url . replace ( / ^ \/ a s s e t s \/ / , './assets/' ) ;
195+ } ) ;
196+ }
0 commit comments