]> git.gir.st - VimFx.git/blob - gulpfile.coffee
Make `gulp readme` and `gulp changelg` AMO compatible
[VimFx.git] / gulpfile.coffee
1 ###
2 # Copyright Simon Lydell 2014.
3 #
4 # This file is part of VimFx.
5 #
6 # VimFx is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # VimFx is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
18 ###
19
20 fs = require('fs')
21 path = require('path')
22 gulp = require('gulp')
23 coffee = require('gulp-coffee')
24 coffeelint = require('gulp-coffeelint')
25 git = require('gulp-git')
26 header = require('gulp-header')
27 mustache = require('gulp-mustache')
28 sloc = require('gulp-sloc')
29 tap = require('gulp-tap')
30 zip = require('gulp-zip')
31 marked = require('marked')
32 merge = require('merge2')
33 precompute = require('require-precompute')
34 request = require('request')
35 rimraf = require('rimraf')
36 runSequence = require('run-sequence')
37 pkg = require('./package.json')
38
39 DEST = 'build'
40 XPI = 'VimFx.xpi'
41 LOCALE = 'extension/locale'
42 TEST = 'extension/test'
43
44 BASE_LOCALE = 'en-US'
45
46 test = '--test' in process.argv or '-t' in process.argv
47 ifTest = (value) -> if test then [value] else []
48
49 { join } = path
50 read = (filepath) -> fs.readFileSync(filepath).toString()
51 template = (data) -> mustache(data, {extension: ''})
52
53 gulp.task('default', ['push'])
54
55 gulp.task('clean', (callback) ->
56 rimraf(DEST, callback)
57 )
58
59 gulp.task('copy', ->
60 gulp.src(['extension/**/!(*.coffee|*.tmpl)', 'COPYING', 'LICENSE'])
61 .pipe(gulp.dest(DEST))
62 )
63
64 gulp.task('node_modules', ->
65 dependencies = (name for name of pkg.dependencies)
66 # Note! When installing or updating node modules, make sure that the following
67 # glob does not include too much or too little!
68 gulp.src("node_modules/+(#{ dependencies.join('|') })/\
69 {LICENSE*,{,**/!(test|examples)/}!(*min|*test*|*bench*).js}")
70 .pipe(gulp.dest("#{ DEST }/node_modules"))
71 )
72
73 gulp.task('coffee', ->
74 gulp.src([
75 'extension/bootstrap.coffee'
76 'extension/lib/**/*.coffee'
77 ifTest('extension/test/**/*.coffee')...
78 ], {base: 'extension'})
79 .pipe(coffee({bare: true}))
80 .pipe(gulp.dest(DEST))
81 )
82
83 gulp.task('chrome.manifest', ->
84 gulp.src('extension/chrome.manifest.tmpl')
85 .pipe(template({locales: fs.readdirSync(LOCALE).map((locale) -> {locale})}))
86 .pipe(gulp.dest(DEST))
87 )
88
89 gulp.task('install.rdf', ->
90 [ [ { name: creator } ], developers, contributors, translators ] =
91 read('PEOPLE.md').trim().replace(/^#.+\n|^\s*-\s*/mg, '').split('\n\n')
92 .map((block) -> block.split('\n').map((name) -> {name}))
93
94 getDescription = (locale) -> read(join(LOCALE, locale, 'description')).trim()
95
96 descriptions = fs.readdirSync(LOCALE)
97 .filter((locale) -> locale != BASE_LOCALE)
98 .map((locale) -> {locale, description: getDescription(locale)})
99
100 gulp.src('extension/install.rdf.tmpl')
101 .pipe(template({
102 version: pkg.version
103 minVersion: pkg.firefoxVersions.min
104 maxVersion: pkg.firefoxVersions.max
105 creator, developers, contributors, translators
106 defaultDescription: getDescription(BASE_LOCALE)
107 descriptions
108 }))
109 .pipe(gulp.dest(DEST))
110 )
111
112 gulp.task('require-data', ['node_modules'], ->
113 data = JSON.stringify(precompute('.'), null, 2)
114 gulp.src('extension/require-data.js.tmpl')
115 .pipe(template({data}))
116 .pipe(gulp.dest(DEST))
117 )
118
119 gulp.task('tests-list', ->
120 list = JSON.stringify(fs.readdirSync(TEST)
121 .map((name) -> name.match(/^(test-.+)\.coffee$/)?[1])
122 .filter(Boolean)
123 )
124 gulp.src("#{ TEST }/tests-list.js.tmpl", {base: 'extension'})
125 .pipe(template({list}))
126 .pipe(gulp.dest(DEST))
127 )
128
129 gulp.task('templates', [
130 'chrome.manifest'
131 'install.rdf'
132 'require-data'
133 ifTest('tests-list')...
134 ])
135
136 gulp.task('build', (callback) ->
137 runSequence(
138 'clean',
139 ['copy', 'node_modules', 'coffee', 'templates'],
140 callback
141 )
142 )
143
144 gulp.task('xpi', ['build'], ->
145 gulp.src("#{ DEST }/**/*")
146 .pipe(zip(XPI, {compress: false}))
147 .pipe(gulp.dest(DEST))
148 )
149
150 gulp.task('push', ['xpi'], ->
151 body = fs.readFileSync(join(DEST, XPI))
152 request.post({url: 'http://localhost:8888', body})
153 )
154
155 gulp.task('lint', ->
156 gulp.src(['extension/**/*.coffee', 'gulpfile.coffee'])
157 .pipe(coffeelint())
158 .pipe(coffeelint.reporter())
159 )
160
161 gulp.task('sloc', ->
162 gulp.src([
163 'extension/bootstrap.coffee'
164 'extension/lib/!(migrations|legacy).coffee']
165 )
166 .pipe(sloc())
167 )
168
169 gulp.task('release', (callback) ->
170 { version } = pkg
171 message = "VimFx v#{ version }"
172 today = new Date().toISOString()[...10]
173 merge([
174 gulp.src('package.json')
175 gulp.src('CHANGELOG.md')
176 .pipe(header("### #{ version } (#{ today })\n\n"))
177 .pipe(gulp.dest('.'))
178 ])
179 .pipe(git.commit(message))
180 .on('end', ->
181 git.tag("v#{ version }", message, callback)
182 )
183 return
184 )
185
186 gulp.task('changelog', ->
187 num = 1
188 for arg in process.argv when /^-[1-9]$/.test(arg)
189 num = Number(arg[1])
190 entries = read('CHANGELOG.md').split(/^### .+/m)[1..num].join('')
191 process.stdout.write(html(entries))
192 )
193
194 gulp.task('readme', ->
195 process.stdout.write(html(read('README.md')))
196 )
197
198 # Reduce markdown to the small subset of HTML that AMO allows. Note that AMO
199 # converts newlines to `<br>`.
200 html = (string) ->
201 return marked(string)
202 .replace(/// <h\d [^>]*> ([^<>]+) </h\d> ///g, '\n\n<b>$1</b>')
203 .replace(///\s* <p> ((?: [^<] | <(?!/p>) )+) </p>///g, (match, text) ->
204 return "\n#{ text.replace(/\s*\n\s*/g, ' ') }\n\n"
205 )
206 .replace(/<br>/g, '\n')
207 .replace(///<(/?)kbd>///g, '<$1code>')
208 .replace(/<img[^>]*>\s*/g, '')
209 .replace(/\n\s*\n/g, '\n\n')
210 .trim() + '\n'
211
212 gulp.task('faster', ->
213 gulp.src('gulpfile.coffee')
214 .pipe(coffee({bare: true}))
215 .pipe(gulp.dest('.'))
216 )
217
218 gulp.task('sync-locales', ->
219 baseLocale = BASE_LOCALE
220 for arg in process.argv when arg[...2] == '--'
221 baseLocale = arg[2..]
222 results = fs.readdirSync(join(LOCALE, baseLocale))
223 .filter((file) -> path.extname(file) == '.properties')
224 .map(syncLocale.bind(null, baseLocale))
225 if baseLocale == BASE_LOCALE
226 report = []
227 for {fileName, translatedCount, total} in results
228 report.push("#{ fileName }:")
229 for localeName, count of translatedCount
230 paddedName = "#{ localeName }: "[...6]
231 percentage = Math.round((count / total) * 100)
232 report.push(" #{ paddedName } #{ percentage }%")
233 process.stdout.write(report.join('\n') + '\n')
234 )
235
236 syncLocale = (baseLocaleName, fileName) ->
237 basePath = join(LOCALE, baseLocaleName, fileName)
238 base = parseLocaleFile(read(basePath))
239 oldBasePath = "#{basePath}.old"
240 if fs.existsSync(oldBasePath)
241 oldBase = parseLocaleFile(read(oldBasePath))
242 translatedCount = {}
243 for localeName in fs.readdirSync(LOCALE) when localeName != baseLocaleName
244 localePath = join(LOCALE, localeName, fileName)
245 locale = parseLocaleFile(read(localePath))
246 translatedCount[localeName] = 0
247 newLocale = base.template.map((line) ->
248 if Array.isArray(line)
249 [ key ] = line
250 oldValue = oldBase?.keys[key]
251 value =
252 if (oldValue? and oldValue != base.keys[key]) or
253 key not of locale.keys
254 base.keys[key]
255 else
256 locale.keys[key]
257 translatedCount[localeName]++ if value != base.keys[key] or value == ''
258 return "#{ key }=#{ value }"
259 else
260 return line
261 )
262 fs.writeFileSync(localePath, newLocale.join(base.newline))
263 return {fileName, translatedCount, total: Object.keys(base.keys).length}
264
265 parseLocaleFile = (fileContents) ->
266 keys = {}
267 lines = []
268 [ newline ] = fileContents.match(/\r?\n/)
269 for line in fileContents.split(newline)
270 line = line.trim()
271 [ match, key, value ] = line.match(///^ ([^=]+) = (.*) $///) ? []
272 if match
273 keys[key] = value
274 lines.push([key])
275 else
276 lines.push(line)
277 return {keys, template: lines, newline}
278
279 helpHTMLFile = 'help.html'
280 gulp.task(helpHTMLFile, ->
281 unless fs.existsSync(helpHTMLFile)
282 process.stdout.write("""
283 First enable the “Copy to clipboard” line in help.coffee, show the help
284 dialog and finally dump the clipboard into #{ helpHTMLFile }.
285 """)
286 return
287 gulp.src('help.html')
288 .pipe(tap((file) ->
289 file.contents = new Buffer(generateHelpHTML(file.contents.toString()))
290 ))
291 .pipe(gulp.dest('.'))
292 )
293
294 helpHTMLPrelude = '''
295 <!doctype html>
296 <meta charset=utf-8>
297 <title>VimFx help</title>
298 <style>
299 * { margin: 0; }
300 body > :first-child { min-height: 100vh; }
301 </style>
302 <link rel=stylesheet href=extension/skin/style.css>
303 '''
304
305 generateHelpHTML = (dumpedHTML) ->
306 return helpHTMLPrelude + dumpedHTML
307 .replace(/^<\w+ xmlns="[^"]+"/, '<div')
308 .replace(/\w+>$/, 'div>')
309 .replace(/<(\w+)([^>]*)\/>/g, '<$1$2></$1>')
Imprint / Impressum