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 pkg = require('./package.json')
22 LOCALE = 'extension/locale'
23 TEST = 'extension/test'
26 UPDATE_ALL = /\s*UPDATE_ALL$/
28 ADDON_PATH = 'chrome://vimfx'
29 BUILD_TIME = Date.now()
31 argv = process.argv.slice(2)
34 read = (filepath) -> fs.readFileSync(filepath).toString()
35 template = (data) -> mustache(data, {extension: ''})
37 gulp.task('clean', (callback) ->
38 rimraf(DEST, callback)
42 gulp.src(['extension/**/!(*.coffee|*.tmpl)', 'LICENSE', 'LICENSE-MIT'])
43 .pipe(gulp.dest(DEST))
46 gulp.task('node_modules', ->
47 dependencies = (name for name of pkg.dependencies)
48 # Note: When installing or updating node modules, make sure that the following
49 # glob does not include too much or too little!
51 "node_modules/+(#{dependencies.join('|')})/\
52 {LICENSE*,{,**/!(test|examples)/}!(*min|*test*|*bench*).js}"
54 .pipe(gulp.dest("#{DEST}/node_modules"))
58 gulp.src('documentation/*.md')
60 renderer = new marked.Renderer()
61 renderer.link = (href, title, text) ->
62 href = href.replace(/\.md$/, ".html")
63 link = marked.Renderer.prototype.link.call(this, href, title, text)
64 marked.setOptions({ renderer, gfm: true })
69 <title>VimFx Documentation</title>
71 body { font-family: sans-serif; max-width: 35em; margin: auto; }
75 file.contents = new Buffer.from(HTMLPrelude + marked(file.contents.toString()))
78 file.path = file.path.replace(/\/README\./, '/index.')
79 file.path = file.path.replace(/\.md$/, '.html')
81 .pipe(gulp.dest("#{DEST}/documentation"))
84 gulp.task('coffee', ->
85 test = '--test' in argv or '-t' in argv
88 'extension/bootstrap.coffee'
89 'extension/lib/**/*.coffee'
90 ].concat(if test then 'extension/test/**/*.coffee' else []),
93 .pipe(preprocess({context: {
95 ADDON_PATH: JSON.stringify(ADDON_PATH)
96 REQUIRE_DATA: JSON.stringify(precompute('.'), null, 2)
99 JSON.stringify(fs.readdirSync(TEST)
100 .map((name) -> name.match(/^(test-.+)\.coffee$/)?[1])
106 .pipe(coffee({bare: true}))
107 .pipe(gulp.dest(DEST))
110 gulp.task('bootstrap-frame.js', ->
111 gulp.src('extension/bootstrap-frame.js.tmpl')
112 .pipe(mustache({ADDON_PATH}))
114 file.path = file.path.replace(/\.js\.tmpl$/, "-#{BUILD_TIME}.js")
116 .pipe(gulp.dest(DEST))
119 gulp.task('chrome.manifest', ->
120 gulp.src('extension/chrome.manifest.tmpl')
121 .pipe(template({locales: fs.readdirSync(LOCALE).map((locale) -> {locale})}))
122 .pipe(gulp.dest(DEST))
125 gulp.task('install.rdf', ->
126 [[{name: creator}], developers, contributors, translators] =
127 read('PEOPLE.md').trim().replace(/^#.+\n|^\s*-\s*/mg, '').split('\n\n')
128 .map((block) -> block.split('\n').map((name) -> {name}))
130 getDescription = (locale) -> read(join(LOCALE, locale, 'description')).trim()
132 descriptions = fs.readdirSync(LOCALE)
133 .filter((locale) -> locale != BASE_LOCALE)
134 .map((locale) -> {locale, description: getDescription(locale)})
136 gulp.src('extension/install.rdf.tmpl')
138 idSuffix: if '--unlisted' in argv or '-u' in argv then '-unlisted' else ''
140 minVersion: pkg.firefoxVersions.min
141 maxVersion: pkg.firefoxVersions.max
142 creator, developers, contributors, translators
143 defaultDescription: getDescription(BASE_LOCALE)
146 .pipe(gulp.dest(DEST))
149 gulp.task('templates', gulp.parallel(
155 gulp.task('build', gulp.series(
157 gulp.parallel('copy', 'node_modules', 'coffee', 'templates')
160 gulp.task('xpi-only', ->
161 gulp.src("#{DEST}/**/*")
162 .pipe(zip(XPI, {compress: false}))
163 .pipe(gulp.dest(DEST))
166 gulp.task('xpi', gulp.series('build', 'xpi-only'))
168 gulp.task('default', gulp.series('xpi', 'docs'))
170 # coffeelint-forbidden-keywords has `require('coffee-script/register');` in its
172 gulp.task('lint-workaround', ->
173 gulp.src('node_modules/coffeescript/')
174 .pipe(gulp.symlink('node_modules/coffee-script'))
177 gulp.task('lint-only', ->
178 gulp.src(['extension/**/*.coffee', 'gulpfile.coffee'])
180 .pipe(coffeelint.reporter())
181 .pipe(coffeelint.reporter('fail'))
184 gulp.task('lint', gulp.series('lint-workaround', 'lint-only'))
188 'extension/bootstrap.coffee'
189 'extension/lib/!(migrations|legacy).coffee'
194 gulp.task('release', (callback) ->
196 message = "VimFx v#{version}"
197 today = new Date().toISOString()[...10]
199 gulp.src('package.json')
200 gulp.src('CHANGELOG.md')
201 .pipe(header("### #{version} (#{today})\n\n"))
202 .pipe(gulp.dest('.'))
204 .pipe(git.commit(message))
206 git.tag("v#{version}", message, callback)
211 gulp.task('changelog', (callback) ->
213 for arg in argv when /^-[1-9]$/.test(arg)
215 entries = read('CHANGELOG.md').split(/^### .+/m)[1..num].join('')
216 process.stdout.write(html(entries))
220 gulp.task('readme', (callback) ->
221 process.stdout.write(html(read('README.md')))
225 # Reduce markdown to the small subset of HTML that AMO allows. Note that AMO
226 # converts newlines to `<br>`.
228 return marked(string)
229 .replace(/// <h\d [^>]*> ([^<>]+) </h\d> ///g, '\n\n<b>$1</b>')
230 .replace(///\s* <p> ((?: [^<] | <(?!/p>) )+) </p>///g, (match, text) ->
231 return "\n#{text.replace(/\s*\n\s*/g, ' ')}\n\n"
233 .replace(///<li> ((?: [^<] | <(?!/li>) )+) </li>///g, (match, text) ->
234 return "<li>#{text.replace(/\s*\n\s*/g, ' ')}</li>"
236 .replace(/<br>/g, '\n')
237 .replace(///<(/?)kbd>///g, '<$1code>')
238 .replace(/<img[^>]*>\s*/g, '')
239 .replace(/\n\s*\n/g, '\n\n')
242 gulp.task('faster', ->
243 gulp.src('gulpfile.coffee')
244 .pipe(coffee({bare: true}))
245 .pipe(gulp.dest('.'))
248 gulp.task('sync-locales', (callback) ->
249 baseLocale = BASE_LOCALE
251 for arg in argv when arg[...2] == '--'
253 if name[-1..] == '?' then compareLocale = name[...-1] else baseLocale = name
255 results = fs.readdirSync(join(LOCALE, baseLocale))
256 .filter((file) -> path.extname(file) == '.properties')
257 .map(syncLocale.bind(null, baseLocale))
259 if baseLocale == BASE_LOCALE
261 for {fileName, untranslated, total} in results
262 report.push("#{fileName}:")
263 for localeName, strings of untranslated
264 paddedName = "#{localeName}: "[...6]
265 percentage = Math.round((1 - strings.length / total) * 100)
266 if localeName == compareLocale or compareLocale == null
267 report.push(" #{paddedName} #{percentage}%")
268 if localeName == compareLocale
269 report.push(strings.map((string) -> " #{string}")...)
270 process.stdout.write(report.join('\n') + '\n')
275 syncLocale = (baseLocaleName, fileName) ->
276 basePath = join(LOCALE, baseLocaleName, fileName)
277 base = parseLocaleFile(read(basePath))
279 for localeName in fs.readdirSync(LOCALE)
280 localePath = join(LOCALE, localeName, fileName)
281 locale = parseLocaleFile(read(localePath))
282 untranslated[localeName] = []
283 newLocale = base.template.map((line, index) ->
284 if Array.isArray(line)
286 baseValue = base.keys[key]
288 if UPDATE_ALL.test(baseValue) or key not of locale.keys
289 baseValue.replace(UPDATE_ALL, '')
292 result = "#{key}=#{value}"
293 if value == base.keys[key] and value != ''
294 untranslated[localeName].push("#{index + 1}: #{result}")
299 fs.writeFileSync(localePath, newLocale.join(base.newline))
300 delete untranslated[baseLocaleName]
301 return {fileName, untranslated, total: Object.keys(base.keys).length}
303 parseLocaleFile = (fileContents) ->
306 [newline] = fileContents.match(/\r?\n/)
307 for line in fileContents.split(newline)
309 [match, key, value] = line.match(///^ ([^=]+) = (.*) $///) ? []
315 return {keys, template: lines, newline}
317 generateHTMLTask = (filename, message) ->
318 gulp.task(filename, (callback) ->
319 unless fs.existsSync(filename)
320 process.stdout.write(message(filename))
325 file.contents = new Buffer(generateTestHTML(file.contents.toString()))
327 .pipe(gulp.dest('.'))
330 generateHTMLTask('help.html', (filename) -> """
331 First enable the “Copy to clipboard” line in help.coffee, show the help
332 dialog and finally dump the clipboard into #{filename}.
335 generateHTMLTask('hints.html', (filename) -> """
336 First enable the “Copy to clipboard” line in modes.coffee, show the
337 hint markers, activate the “Increase count” command and finally dump the
338 clipboard into #{filename}.
341 testHTMLPrelude = '''
344 <title>VimFx test</title>
347 body > :first-child {min-height: 100vh; width: 100vw;}
349 <link rel=stylesheet href=extension/skin/style.css>
352 generateTestHTML = (dumpedHTML) ->
353 return testHTMLPrelude + dumpedHTML
354 .replace(/^<\w+ xmlns="[^"]+"/, '<div')
355 .replace(/\w+>$/, 'div>')
356 .replace(/<(\w+)([^>]*)\/>/g, '<$1$2></$1>')