-
Notifications
You must be signed in to change notification settings - Fork 191
/
Copy pathgenerate-docs-markdown.js
242 lines (203 loc) · 6.81 KB
/
generate-docs-markdown.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
'use strict';
const fs = require( 'fs' );
const path = require( 'path' );
const kramed = require( 'kramed' );
const combyne = require( 'combyne' );
combyne.settings.delimiters = {
// Avoid conflict with Jekyll template delimiters
START_RAW: '[{{{',
END_RAW: '}}}]',
START_PROP: '[{{',
END_PROP: '}}]',
START_EXPR: '[{%',
END_EXPR: '%}]',
};
// Application Version
const version = require( '../../package.json' ).version;
// Paths
const projectRoot = path.join( __dirname, '../..' );
const docsDir = path.join( projectRoot, 'documentation' );
const readmePath = path.join( projectRoot, 'README.md' );
const changelogPath = path.join( projectRoot, 'CHANGELOG.md' );
const contributingPath = path.join( projectRoot, 'CONTRIBUTING.md' );
const licensePath = path.join( projectRoot, 'LICENSE' );
const indexTemplatePath = path.join( projectRoot, 'documentation', 'index.html.combyne' );
const err404TemplatePath = path.join( projectRoot, 'documentation', '404.html.combyne' );
// This constant defines the minimum importance of header for which files will
// be created: e.g. if this is 2, files will only be created for headers # & ##
const README_SPLIT_LEVEL = 2;
// This is a list of slugs to skip when rendering the page index on the homepage
const SKIP_SECTION_LINKS = [
// "About" content is embedded into the index page
'about',
// API Documentation provided via YUIDoc & the link is injected into the index
'api-documentation',
];
// This is a list of slugs to skip when generating pages from README sections
const SKIP_SECTIONS = SKIP_SECTION_LINKS.concat( [
// CONTRIBUTING.md supersedes the README's contributing section
'contributing',
] );
const pad = ( num, digits ) => {
let str = '' + parseInt( num, 10 );
while ( str.length < digits ) {
str = '0' + str;
}
return str;
};
const titleToSlug = title => title
.toLowerCase()
.replace( /[^\w]/g, ' ' )
.trim()
.split( /\s+/ )
.join( '-' );
const fileHeader = ( title ) => {
const slug = titleToSlug( title );
return `---\nlayout: page\ntitle: ${ title }\npermalink: /${ slug }/\n---`;
};
const titleRE = /^\n+#+([^\n]+)\n+$/;
const hasSectionsRE = /\n#+([^\n]+)\n/;
const isTitle = token => token.match( titleRE );
const getTitle = token => token.replace( titleRE, '$1' ).trim();
const getLevel = ( mdHeading ) => {
const match = mdHeading.match( /#+/ );
return match ? match[ 0 ].length : -1;
};
const getContents = ( entry ) => {
// Strip any top-level headings: those are rendered as titles elsewhere
const fileContents = entry.tokens.join( '' ).replace( /^# [^\n]+/, '' );
const title = fileHeader( entry.title );
// Only include the ToC placeholder if a ToC is needed
return hasSectionsRE.test( fileContents ) ?
[ title, '* TOC\n{:toc}', fileContents ].join( '\n\n' ) :
[ title, fileContents ].join( '\n\n' );
};
// Promise-based File System helpers
const readFile = sourcePath => new Promise( ( resolve, reject ) => {
fs.readFile( sourcePath, ( err, contents ) => {
if ( err ) {
return reject( err );
}
// contents is a Buffer
resolve( contents.toString() );
} );
} );
const writeFile = ( outputPath, fileContents ) => new Promise( ( resolve, reject ) => {
fs.writeFile( outputPath, fileContents, ( err ) => {
if ( err ) {
return reject( err );
}
resolve();
} );
} );
const copyFile = ( sourcePath, title ) => readFile( sourcePath ).then( ( contents ) => {
const outputPath = path.join( docsDir, `${ titleToSlug( title ) }.md` );
return writeFile( outputPath, getContents( {
title: title,
tokens: [ contents ],
} ) );
} );
// Break the README into individual files
const readmeOutput = readFile( readmePath ).then( ( contents ) => {
const tokens = contents.split( /(\n+#+ .*\n+)/ );
const entries = [];
let entry = null;
for ( let i = 0; i < tokens.length; i++ ) {
const token = tokens[ i ];
if ( ! isTitle( token ) ) {
if ( entry && entry.tokens ) {
entry.tokens.push( token );
}
continue;
}
const level = getLevel( token );
if ( level > README_SPLIT_LEVEL ) {
entry.tokens.push( token );
if ( level === README_SPLIT_LEVEL + 1 ) {
entry.subheadings.push( {
slug: `${ entry.slug }#${ titleToSlug( token ) }`,
title: getTitle( token ),
level: level,
} );
}
continue;
}
entry = {
heading: token,
slug: titleToSlug( token ),
title: getTitle( token ),
level: level,
subheadings: [],
tokens: [],
};
entries.push( entry );
}
entries.forEach( entry => entry.contents = getContents( entry ) );
return entries.reduce( ( previous, entry, idx ) => {
return previous.then( () => {
const outputPath = path.join( docsDir, `${ pad( idx, 2 ) }-${ entry.slug }.md` );
return SKIP_SECTIONS.indexOf( entry.slug ) === -1 ?
writeFile( outputPath, entry.contents ) :
Promise.resolve();
} );
}, Promise.resolve() ).then( () => {
entries.push( {
title: 'API Documentation',
slug: `api-reference/wpapi/${version}/`,
} );
return entries;
} );
} );
// Create the contributor guide (runs after the README files are processed in
// order to overwrite the "contributing" README section, if present)
const contributingOutput = readmeOutput.then( () => copyFile( contributingPath, 'Contributing' ) );
// Create the changelog page
const changelogOutput = copyFile( changelogPath, 'Changelog' );
// Create the license page
const licenseOutput = copyFile( licensePath, 'License' );
// Build the template context to use with the
const templateContext = readmeOutput.then( ( entries ) => {
return entries.reduce( ( context, entry ) => {
const isAboutPage = entry.slug === 'about';
if ( isAboutPage ) {
context.aboutContents = kramed( entry.tokens.join( '' ) );
} else if ( SKIP_SECTION_LINKS.indexOf( entry.slug ) === -1 ) {
entry.hasSubheadings = entry.subheadings && entry.subheadings.length;
context.readmeSections.push( entry );
}
return context;
}, {
aboutContents: null,
readmeSections: [],
} );
} );
const fileAndContext = filePath => Promise.all( [
readFile( filePath ),
templateContext,
] ).then( result => ( {
template: result[ 0 ],
context: result[ 1 ],
} ) );
// Create the index HTML page
const indexOutput = fileAndContext( indexTemplatePath ).then( ( result ) => {
console.log( 'index' );
const outputPath = path.join( docsDir, 'index.html' );
const fileContents = combyne( result.template ).render( result.context );
return writeFile( outputPath, fileContents );
} );
// Create the Error 404 page
const err404Output = fileAndContext( err404TemplatePath ).then( ( result ) => {
console.log( '404' );
const outputPath = path.join( docsDir, '404.html' );
const fileContents = combyne( result.template ).render( result.context );
return writeFile( outputPath, fileContents );
} );
module.exports = Promise.all( [
readmeOutput,
contributingOutput,
changelogOutput,
licenseOutput,
indexOutput,
err404Output,
] )
.catch( err => console.log( err && err.stack ) );