diff --git a/Gruntfile.js b/Gruntfile.js index 44a20bc..78df843 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -33,7 +33,10 @@ module.exports = function(grunt) { grunt.registerTask('default', 'archive'); // Bare minimum for debugging - grunt.registerTask('dev', 'lumbar:build'); + grunt.registerTask('dev', [ + 'lumbar:build', + 'generateLanguages' + ]); @@ -41,6 +44,7 @@ module.exports = function(grunt) { ----------------------------------------------------------------------------------------------------*/ grunt.registerTask('modules', 'Build the FullCalendar modules', [ + 'clean:modules', 'lumbar:build', 'concat:moduleVariables', 'uglify:modules' @@ -75,7 +79,37 @@ module.exports = function(grunt) { ext: '.min.js' } - config.clean.modules = 'build/out/*'; + config.clean.modules = 'build/out/**'; + + + + /* Languages + ----------------------------------------------------------------------------------------------------*/ + + grunt.registerTask('languages', [ + 'clean:languages', + 'generateLanguages', + 'uglify:languages' + ]); + + config.generateLanguages = { + moment: 'lib/moment/lang/', + datepicker: 'lib/jquery-ui/ui/i18n/', + fullCalendar: 'lang/', + dest: 'build/out/lang/' + }; + + config.uglify.languages = { + expand: true, + cwd: 'build/out/lang/', + src: '*.js', + dest: 'build/out/lang-min/' + }; + + config.clean.languages = [ + 'build/out/lang/*', + 'build/out/lang-min/*' + ]; @@ -83,10 +117,12 @@ module.exports = function(grunt) { ----------------------------------------------------------------------------------------------------*/ grunt.registerTask('archive', 'Create a distributable ZIP archive', [ - 'clean:modules', 'clean:archive', 'modules', + 'languages', 'copy:archiveModules', + 'copy:archiveLanguages', + 'copy:archiveMoment', 'copy:archiveJQuery', 'concat:archiveJQueryUI', 'copy:archiveDemos', @@ -103,6 +139,18 @@ module.exports = function(grunt) { dest: 'build/archive/fullcalendar/' }; + config.copy.archiveLanguages = { + expand: true, + cwd: 'build/out/lang-min/', + src: '*.js', + dest: 'build/archive/fullcalendar/lang/' + }; + + config.copy.archiveMoment = { + src: 'lib/moment/min/moment.min.js', + dest: 'build/archive/lib/moment.min.js' + }; + config.copy.archiveJQuery = { src: 'lib/jquery/jquery.min.js', dest: 'build/archive/lib/jquery.min.js' @@ -129,7 +177,7 @@ module.exports = function(grunt) { return content; } }, - src: 'demos/*', + src: 'demos/**', dest: 'build/archive/' }; @@ -138,14 +186,15 @@ module.exports = function(grunt) { expand: true, cwd: 'lib/jquery-ui/themes/cupertino/', src: [ 'jquery-ui.min.css', 'images/*' ], - dest: 'build/archive/demos/cupertino/' + dest: 'build/archive/lib/cupertino/' }; // in demo HTML, rewrites paths to work in the archive function transformDemoPath(path) { + path = path.replace('../lib/moment/moment.js', '../lib/moment.min.js'); path = path.replace('../lib/jquery/jquery.js', '../lib/jquery.min.js'); path = path.replace('../lib/jquery-ui/ui/jquery-ui.js', '../lib/jquery-ui.custom.min.js'); - path = path.replace('../lib/jquery-ui/themes/', ''); + path = path.replace('../lib/jquery-ui/themes/cupertino/', '../lib/cupertino/'); path = path.replace('../build/out/', '../fullcalendar/'); path = path.replace('/fullcalendar.js', '/fullcalendar.min.js'); return path; @@ -153,8 +202,10 @@ module.exports = function(grunt) { // copy license and changelog config.copy.archiveMisc = { - src: [ 'license.txt', 'changelog.txt' ], - dest: 'build/archive/' + files: { + 'build/archive/license.txt': 'license.txt', + 'build/archive/changelog.txt': 'changelog.md' + } }; // create the ZIP @@ -177,7 +228,6 @@ module.exports = function(grunt) { ----------------------------------------------------------------------------------------------------*/ grunt.registerTask('bower', 'Build the FullCalendar Bower component', [ - 'clean:modules', 'clean:bower', 'modules', 'copy:bowerModules', @@ -220,7 +270,6 @@ module.exports = function(grunt) { ----------------------------------------------------------------------------------------------------*/ grunt.registerTask('cdnjs', 'Build files for CDNJS\'s hosted version of FullCalendar', [ - 'clean:modules', 'clean:cdnjs', 'modules', 'copy:cdnjsModules', @@ -253,4 +302,7 @@ module.exports = function(grunt) { // finally, give grunt the config object... grunt.initConfig(config); + + + grunt.loadTasks('tasks'); }; diff --git a/tasks/generateLanguages.js b/tasks/generateLanguages.js new file mode 100644 index 0000000..e278b8c --- /dev/null +++ b/tasks/generateLanguages.js @@ -0,0 +1,209 @@ + +module.exports = function(grunt) { + + var config = grunt.config('generateLanguages'); + + + grunt.registerTask('generateLanguages', function() { + + var combinedJS = ''; + var languageCnt = 0; + var skippedLangCodes = []; + + grunt.file.mkdir(config.dest, 0755); + + grunt.file.expand(config.moment + '/*.js').forEach(function(momentPath) { + + var langCode = momentPath.match(/([^\/]*)\.js$/)[1]; + var js = getLangJS(langCode, momentPath); + + if (js) { + + grunt.file.write( + config.dest + '/' + langCode + '.js', + wrapWithUMD(js) + ); + + combinedJS += wrapWithClosure(js) + '\n'; + + languageCnt++; + } + else { + skippedLangCodes.push(langCode); + } + + }); + + // code for resetting the language back to English + combinedJS += '\nmoment.lang("en");'; + combinedJS += '\n$.fullCalendar.lang("en");'; + combinedJS += '\nif ($.datepicker) $.datepicker.setDefaults($.datepicker.regional[""]);'; + + grunt.file.write( + config.dest + '/all.js', + wrapWithUMD(combinedJS) + ); + + grunt.log.writeln(skippedLangCodes.length + ' skipped languages: ' + skippedLangCodes.join(', ')); + grunt.log.writeln(languageCnt + ' generated languages.'); + + }); + + + function getLangJS(langCode, momentPath) { + + var shortLangCode; + var momentLangJS; + var datepickerLangJS; + var fullCalendarLangJS; + + // given "fr-ca", get just "fr" + if (langCode.indexOf('-') != -1) { + shortLangCode = langCode.replace(/-.*/, ''); + } + + momentLangJS = getMomentLangJS(momentPath); + + datepickerLangJS = getDatepickerLangJS(langCode); + if (!datepickerLangJS && shortLangCode) { + datepickerLangJS = getDatepickerLangJS(shortLangCode, langCode); + } + + fullCalendarLangJS = getFullCalendarLangJS(langCode); + if (!fullCalendarLangJS && shortLangCode) { + fullCalendarLangJS = getFullCalendarLangJS(shortLangCode, langCode); + } + + // If this is an "en" language, only the Moment config is needed. + // For all other languages, all 3 configs are needed. + if (momentLangJS && (shortLangCode == 'en' || (datepickerLangJS && fullCalendarLangJS))) { + + // if there is no definition, we still need to tell FC to set the default + if (!fullCalendarLangJS) { + fullCalendarLangJS = '$.fullCalendar.lang("' + langCode + '");'; + } + + datepickerLangJS = datepickerLangJS || ''; + + return momentLangJS + '\n' + + datepickerLangJS + '\n' + + fullCalendarLangJS; + } + } + + + function wrapWithUMD(body) { + return [ + '(function(factory) {', + ' if (typeof define === "function" && define.amd) {', + ' define([ "jquery", "moment" ], factory);', + ' }', + ' else {', + ' factory(jQuery, moment);', + ' }', + '})(function($, moment) {', + '', + body, + '', + '});' + ].join('\n'); + } + + + function wrapWithClosure(body) { + return [ + '(function() {', + '', + body, + '', + '})();' + ].join('\n'); + } + + + function getMomentLangJS(path) { // file assumed to exist + + var js = grunt.file.read(path); + + js = js.replace( // remove the UMD wrap + /\(\s*function[\S\s]*?function\s*\(\s*moment\s*\)\s*\{([\S\s]*)\}\)\);?/, + function(m0, body) { + body = body.replace(/^ /mg, ''); // remove 1 level of indentation + return body; + } + ); + + js = js.replace( // replace the `return` statement so execution continues + /^(\s*)return moment\.lang\(/m, + '$1moment.lang(' + ); + + return js; + } + + + function getDatepickerLangJS(langCode, targetLangCode) { + + // convert "en-ca" to "en-CA" + var datepickerLangCode = langCode.replace(/\-(\w+)/, function(m0, m1) { + return '-' + m1.toUpperCase(); + }); + + var path = config.datepicker + '/jquery.ui.datepicker-' + datepickerLangCode + '.js'; + var js; + + try { + js = grunt.file.read(path); + } + catch (ex) { + return false; + } + + js = js.replace( + /^jQuery\([\S\s]*?\{([\S\s]*)\}\);?/m, // inside the jQuery(function) wrap, + function(m0, body) { // use only the function body, modified. + + var match = body.match(/\$\.datepicker\.regional[\S\s]*?(\{[\S\s]*?\});?/); + var props = match[1]; + + // remove 1 level of tab indentation + props = props.replace(/^\t/mg, ''); + + return "$.fullCalendar.datepickerLang(" + + "'" + (targetLangCode || langCode) + "', " + // for FullCalendar + "'" + datepickerLangCode + "', " + // for datepicker + props + + ");"; + } + ); + + return js; + } + + + function getFullCalendarLangJS(langCode, targetLangCode) { + + var path = config.fullCalendar + '/' + langCode + '.js'; + var js; + + try { + js = grunt.file.read(path); + } + catch (ex) { + return false; + } + + // if we originally wanted "ar-ma", but only "ar" is available, we have to adjust + // the declaration + if (targetLangCode && targetLangCode != langCode) { + js = js.replace( + /\$\.fullCalendar\.lang\(['"]([^'"]*)['"]/, + '$.fullCalendar.lang("' + targetLangCode + '"' + ); + } + + return js; + } + + +};