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