From b5a6def1478d504332c5aef6cb1952d8c7932a16 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 1 Jul 2018 08:25:11 +0300 Subject: [PATCH 001/218] readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb03be5..c7f6533 100755 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ Hi everyone! This is a standalone server for the javascript tutorial https://javascript.info. +You can use it to run the tutorial locally and translate it into your language. + # Installation 1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org). These are required to update and run the project. - (Optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/). + (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/). For non-Windows OS use standard install tools (packages or whatever convenient). From 6779450c89302746787fdc235bafec0c6f368721 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 1 Jul 2018 08:26:13 +0300 Subject: [PATCH 002/218] readme --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c7f6533..81c31a3 100755 --- a/README.md +++ b/README.md @@ -16,18 +16,24 @@ You can use it to run the tutorial locally and translate it into your language. For non-Windows OS use standard install tools (packages or whatever convenient). -2. Create the root folder. +2. Install global Node modules: + + ``` + npm install -g bunyan gulp + ``` + +3. Create the root folder. Create a folder `/js` for the project. You can use any other directory as well, just adjust the paths below. -3. Clone the tutorial server into it: +4. Clone the tutorial server into it: ``` cd /js git clone https://github.com/iliakan/javascript-tutorial-server ``` -4. Clone the tutorial text into it. +5. Clone the tutorial text into it. The text repository has "-language" at the end, e.g: ``` @@ -35,13 +41,8 @@ You can use it to run the tutorial locally and translate it into your language. git clone https://github.com/iliakan/javascript-tutorial-ru ``` -4. Install global modules: - - ``` - npm install -g bunyan gulp - ``` -5. Run the site +6. Run the site Run the site with the language: ``` @@ -53,7 +54,7 @@ You can use it to run the tutorial locally and translate it into your language. Then access the site at `http://127.0.0.1:3000`. -6. Edit the tutorial +7. Edit the tutorial The files are editable in the tutorial text folder (step 4). As you edit text files, the webpage gets reloaded automatically, so it's handy From 46210ef24793a334485a4650933b17e1ce9424fb Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 1 Jul 2018 08:27:29 +0300 Subject: [PATCH 003/218] readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81c31a3..cccc2cd 100755 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ You can use it to run the tutorial locally and translate it into your language. 7. Edit the tutorial - The files are editable in the tutorial text folder (step 4). + The files are editable in the tutorial text folder (cloned at step 5). As you edit text files, the webpage gets reloaded automatically, so it's handy - to split the screen at two sides: put the browser onn the left 50% and edit in the right 50%%. + to split the screen at two sides: put the browser to the left half and edit in the right half. # TroubleShooting From db4b5d1f3894cbfdf95bbd4cb001966e2ad6707c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 1 Jul 2018 08:27:40 +0300 Subject: [PATCH 004/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cccc2cd..1cc8fc5 100755 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ You can use it to run the tutorial locally and translate it into your language. # TroubleShooting -If something doesn't work -- [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). +If something doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). -- Yours, From 00f606ed56b33e90854035c91e321e16fdde2e80 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 1 Jul 2018 08:30:03 +0300 Subject: [PATCH 005/218] readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cc8fc5..bde4d75 100755 --- a/README.md +++ b/README.md @@ -47,10 +47,12 @@ You can use it to run the tutorial locally and translate it into your language. Run the site with the language: ``` cd /js/javascript-tutorial-server - ./edit en + ./edit ru ``` - Wait for a couple of seconds as it reads the tutorial from disk and builds static assets. + Please note that the argument of `edit` is exactly the language you cloned at step 5. + + Wait a bit as it reads the tutorial from disk and builds static assets. Then access the site at `http://127.0.0.1:3000`. From 9c4194d4bb48227d1a1824674bfa0c5df493e0ac Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 1 Jul 2018 11:01:52 +0300 Subject: [PATCH 006/218] readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index bde4d75..38fa250 100755 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ You can use it to run the tutorial locally and translate it into your language. # Installation +If you have an old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`. + + 1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org). These are required to update and run the project. @@ -62,6 +65,8 @@ You can use it to run the tutorial locally and translate it into your language. As you edit text files, the webpage gets reloaded automatically, so it's handy to split the screen at two sides: put the browser to the left half and edit in the right half. + + # TroubleShooting If something doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). From 6ae73b6d94e7872b732463b418e4038a7eb3eb58 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 3 Jul 2018 16:52:01 +0300 Subject: [PATCH 007/218] build improvements --- INSTALL.md | 99 --------------- gulpfile.js | 2 - handlers/render.js | 2 - handlers/tutorial/controller/article.js | 4 +- handlers/tutorial/controller/task.js | 2 +- handlers/tutorial/templates/frontpage.pug | 2 +- {modules/config/locales => locales}/en.yml | 0 {modules/config/locales => locales}/ru.yml | 0 modules/client/localeExample.js | 8 ++ modules/config/index.js | 16 ++- modules/config/webpack.js | 36 +++--- modules/i18n/index.js | 55 ++++----- modules/i18n/requireTranslation.js | 19 --- modules/i18n/t.js | 33 +++++ modules/lib/webpack/cssWatchRebuildPlugin.js | 120 +++++++++++++++++++ modules/textUtil/pluralize.js | 32 ----- package.json | 6 +- 17 files changed, 224 insertions(+), 212 deletions(-) delete mode 100755 INSTALL.md rename {modules/config/locales => locales}/en.yml (100%) rename {modules/config/locales => locales}/ru.yml (100%) create mode 100644 modules/client/localeExample.js delete mode 100644 modules/i18n/requireTranslation.js create mode 100755 modules/i18n/t.js create mode 100644 modules/lib/webpack/cssWatchRebuildPlugin.js delete mode 100755 modules/textUtil/pluralize.js diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100755 index c4ae542..0000000 --- a/INSTALL.md +++ /dev/null @@ -1,99 +0,0 @@ - -# Up and running locally - -## 1. OS - -The site works under MacOS and Unix systems, but not on Windows, because many 3rd party modules are not compatible with Windows. - -## 2. Create the folder - -Create a folder `/js` for the project. You can use any other directory as well, just adjust the paths below. - -## 3. Install and run Node.JS and MongoDB - -Node.JS – the last version from [https://nodejs.org](https://nodejs.org). - -MongoDB - can be either 2.6+ or newer. - -Use packages for Linux, [MacPorts](http://www.macports.org/install.php) or [Homebrew](http://brew.sh) for Mac. - -If you have macports then: -``` -sudo port install mongodb -sudo port load mongodb -``` - -## 3. Clone the tutorial server - -Hopefully, you have Git installed and running. - -Clone the tutorial server: - -``` -cd /js -git clone https://github.com/iliakan/javascript-tutorial-server -``` - -## 4. Install global modules - -Install these: - -``` -npm install -g mocha bunyan gulp nodemon -``` - -To set up `NODE_PATH` environment variable, further on we use an alias for running `gulp`: - -``` -alias glp="npm --silent run gulp -- " -``` - -Or, without the alias, you can run gulp as: `NODE_PATH=./handlers:./modules gulp ...`. - -Or like this: `npm --silent run gulp -- ...`. - -## 5. System packages - -You'll also need to install GraphicsMagick (for image resizing). - -For MacPorts, the commands are: - -``` -sudo port install GraphicsMagick -``` - -## 6. npm install - -In the directory with javascript-tutorial-server, run: - -``` -npm install -``` - - -## 7. Run the site - -Run the site with the language in `NODE_LANG` variable: -``` -./dev en -``` - -Then access the site at `http://127.0.0.1`. - -If you have `127.0.0.1 javascript.local` in `/etc/hosts`, then the address will be `http://javascript.local`. - -Wait for a couple of seconds to build static assets. - -## 10. Run in "Edit" mode - -In "Edit" mode the engine watches the tutorial directory, instantly picks the changes and reloads the page. Good for editing. - -``` -./edit en -``` - - -# TroubleShooting - -If something doesn't work -- [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). - diff --git a/gulpfile.js b/gulpfile.js index 4beeb35..df70032 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -74,8 +74,6 @@ gulp.task("client:livereload", lazyRequireTask("./tasks/livereload", { watch: [ "public/pack/**/*.*", - // not using this file, using only styles.css (extracttextplugin) - "!public/pack/styles.js", // this file changes every time we update styles // don't watch it, so that the page won't reload fully on style change "!public/pack/head.js" diff --git a/handlers/render.js b/handlers/render.js index 8258ce0..67aadda 100755 --- a/handlers/render.js +++ b/handlers/render.js @@ -12,7 +12,6 @@ const t = require('i18n'); const pugResolve = require('pugResolve'); const url = require('url'); const validate = require('validate'); -const pluralize = require('textUtil/pluralize'); const BasicParser = require('markit').BasicParser; // public.versions.json is regenerated and THEN node is restarted on redeploy @@ -60,7 +59,6 @@ function addStandardHelpers(locals, ctx) { locals.env = process.env; - locals.pluralize = pluralize; locals.domain = config.domain; // replace lone surrogates in json, -> <\/script> diff --git a/handlers/tutorial/controller/article.js b/handlers/tutorial/controller/article.js index a98261e..42d9f1a 100755 --- a/handlers/tutorial/controller/article.js +++ b/handlers/tutorial/controller/article.js @@ -84,7 +84,7 @@ exports.get = async function(ctx, next) { } section2.links.push({ - title: t('config.comments'), + title: t('locales.comments'), url: '#comments' }); @@ -236,7 +236,7 @@ async function renderArticle(ctx) { parent = a.parent; } path.push({ - title: t('config.tutorial'), + title: t('locales.tutorial'), url: '/' }); path = path.reverse(); diff --git a/handlers/tutorial/controller/task.js b/handlers/tutorial/controller/task.js index 8aabe12..e264080 100755 --- a/handlers/tutorial/controller/task.js +++ b/handlers/tutorial/controller/task.js @@ -34,7 +34,7 @@ exports.get = async function(ctx, next) { parentSlug = parent.parent; } breadcrumbs.push({ - title: t('config.tutorial'), + title: t('locales.tutorial'), url: '/' }); diff --git a/handlers/tutorial/templates/frontpage.pug b/handlers/tutorial/templates/frontpage.pug index 21fccce..afdcb2f 100755 --- a/handlers/tutorial/templates/frontpage.pug +++ b/handlers/tutorial/templates/frontpage.pug @@ -13,7 +13,7 @@ block append variables block append head !=js("tutorial", {defer: true}) - meta(name="description" content=t("config.meta.description")) + meta(name="description" content=t('locales.meta.description')) block content +frontpage-content({ diff --git a/modules/config/locales/en.yml b/locales/en.yml similarity index 100% rename from modules/config/locales/en.yml rename to locales/en.yml diff --git a/modules/config/locales/ru.yml b/locales/ru.yml similarity index 100% rename from modules/config/locales/ru.yml rename to locales/ru.yml diff --git a/modules/client/localeExample.js b/modules/client/localeExample.js new file mode 100644 index 0000000..77c51be --- /dev/null +++ b/modules/client/localeExample.js @@ -0,0 +1,8 @@ +// client-side locale example +// can be used as require('client/test') from server-side +// and from client side too +const t = require('i18n/t'); + +t.i18n.add('test', require('../../locales/' + require('config').lang + '.yml')); + +console.log(t('test.ilya_kantor')); \ No newline at end of file diff --git a/modules/config/index.js b/modules/config/index.js index 980943b..8c2b87f 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -1,10 +1,8 @@ -// make sure Promise is wrapped early, -// to assign mongoose.Promise = global.Promise the wrapped variant any time later let path = require('path'); let fs = require('fs'); +let yaml = require('js-yaml'); let env = process.env; - // NODE_ENV = development || test || production env.NODE_ENV = env.NODE_ENV || 'development'; @@ -68,11 +66,17 @@ let config = module.exports = { handlers: require('./handlers') }; +require.extensions['.yml'] = function(module, filename) { + module.exports = yaml.safeLoad(fs.readFileSync(filename, 'utf-8')); +}; + + +// after module.exports for circle dep w/ config +const t = require('i18n'); + +t.requirePhrase(''); // root locales // webpack config uses general config // we have a loop dep here config.webpack = require('./webpack')(config); -const t = require('i18n'); -t.requirePhrase('config'); - diff --git a/modules/config/webpack.js b/modules/config/webpack.js index e05ce9d..a2fcec0 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -4,9 +4,11 @@ let fs = require('fs'); let nib = require('nib'); let rupture = require('rupture'); let path = require('path'); +let chokidar = require('chokidar'); let config = require('config'); let webpack = require('webpack'); let WriteVersionsPlugin = require('lib/webpack/writeVersionsPlugin'); +let CssWatchRebuildPlugin = require('lib/webpack/cssWatchRebuildPlugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); @@ -61,7 +63,8 @@ module.exports = function (config) { mode: devMode ? 'development' : 'production', // for tests uses prod too watchOptions: { - aggregateTimeout: 10 + aggregateTimeout: 10, + ignored: /node_modules/ }, watch: devMode, @@ -84,6 +87,10 @@ module.exports = function (config) { test: /\.json$/, use: 'json-loader' }, + { + test: /\.i18n/, + use: 'translation-loader' + }, { test: /\.yml$/, use: ['json-loader', 'yaml-loader'] @@ -172,7 +179,7 @@ module.exports = function (config) { // allow require('styles') which looks for styles/index.styl extensions: ['.js', '.styl'], alias: { - config: 'client/config', + config: 'client/config' }, modules: modulesDirectories }, @@ -202,6 +209,14 @@ module.exports = function (config) { // https://github.com/webpack/webpack/issues/198 new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + // ignore site locale files except the lang + new webpack.IgnorePlugin({ + // ignore yml files not like LANG.yml + test: (arg) => arg.endsWith('.yml') && arg !== './' + config.lang + '.yml' + }, // under dirs like: ../locales/.. + /\/locales(\/|$)/ + ), + new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')), new MiniCssExtractPlugin({ @@ -209,24 +224,13 @@ module.exports = function (config) { chunkFilename: extHash("[id]", 'css'), }), - function () { - - let styles = glob.sync('{styles,templates}/**/*.styl', {cwd: config.projectRoot}); - - // config.handlers.forEach(handler => { - // let handlerStyles = glob.sync(`handlers/${handler}/client/styles/**/*.styl`, {cwd: config.projectRoot}); - // styles.push(...handlerStyles); - // }); - - let content = styles.map(s => `@require '../${s}'`).join("\n"); - - fs.writeFileSync(`${config.tmpRoot}/styles.styl`, content); - }, + new CssWatchRebuildPlugin({ + styles: '{templates,styles}' + }), { apply: function (compiler) { compiler.plugin("done", function (stats) { - console.log("Webpack done"); stats = stats.toJson(); fs.writeFileSync(`${config.tmpRoot}/stats.json`, JSON.stringify(stats)); }); diff --git a/modules/i18n/index.js b/modules/i18n/index.js index fe24387..336c9ad 100755 --- a/modules/i18n/index.js +++ b/modules/i18n/index.js @@ -1,50 +1,43 @@ 'use strict'; -const BabelFish = require('babelfish'); - -const i18n = new BabelFish('en'); const LANG = require('config').lang; -const requireTranslation = require('./requireTranslation'); - -let err = console.error; - -if (typeof IS_CLIENT === 'undefined') { - const log = require('log')(); - err = (...args) => log.error(...args) -} - -function t() { - - if (!i18n.hasPhrase(LANG, arguments[0])) { - err("No such phrase", arguments[0]); - } - return i18n.t(LANG, ...arguments); -} +const log = require('log')(); +const path = require('path'); +const fs = require('fs'); +let config = require('config'); +let yaml = require('js-yaml'); +let t = require('./t'); let docs = {}; -t.i18n = i18n; +t.requirePhrase = function(moduleName, packageName = '') { -if (LANG !== 'en') { - i18n.setFallback(LANG, 'en'); -} + // if same doc was processed - don't redo it + if (docs[moduleName] && docs[moduleName].includes(packageName)) { + return; + } -// packageName can be empty -t.requirePhrase = function(module, packageName = '') { + if (!docs[moduleName]) docs[moduleName] = []; + docs[moduleName].push(packageName); - // if same doc was processed - don't redo it - if (docs[module] && docs[module].includes(packageName)) return; - if (!docs[module]) docs[module] = []; - docs[module].push(packageName); + let translationPath = moduleName ? + path.join(path.dirname(require.resolve(moduleName)), 'locales', packageName) : + path.join(config.projectRoot, 'locales', packageName); - let doc = requireTranslation(module, packageName); + if (fs.existsSync(path.join(translationPath, LANG + '.yml'))) { + translationPath = path.join(translationPath, LANG + '.yml'); + } else { + translationPath = path.join(translationPath, 'en.yml'); + } - i18n.addPhrase(LANG, module + (packageName ? ('.' + packageName) : ''), doc); + let doc = yaml.safeLoad(fs.readFileSync(translationPath, 'utf-8')); + let name = (moduleName || 'locales') + (packageName ? ('.' + packageName) : ''); + t.i18n.add(name, doc); }; diff --git a/modules/i18n/requireTranslation.js b/modules/i18n/requireTranslation.js deleted file mode 100644 index 8469cfb..0000000 --- a/modules/i18n/requireTranslation.js +++ /dev/null @@ -1,19 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -let yaml = require('js-yaml'); - -const LANG = require('config').lang; - -module.exports = function (modulePath, packagePath) { - - let translationPath = path.join(path.dirname(require.resolve(modulePath)), 'locales', packagePath); - - if (fs.existsSync(path.join(translationPath, LANG + '.yml'))) { - translationPath = path.join(translationPath, LANG + '.yml'); - } else { - translationPath = path.join(translationPath, 'en.yml'); - } - - return yaml.safeLoad(fs.readFileSync(translationPath, 'utf-8')); - -}; \ No newline at end of file diff --git a/modules/i18n/t.js b/modules/i18n/t.js new file mode 100755 index 0000000..8a38685 --- /dev/null +++ b/modules/i18n/t.js @@ -0,0 +1,33 @@ +'use strict'; + +const BabelFish = require('babelfish'); + +const i18n = new BabelFish('en'); + +const LANG = require('config').lang; + +let err = console.error; + +if (typeof IS_CLIENT === 'undefined') { + const log = require('log')(); + err = (...args) => log.error(...args); +} + +function t(phrase) { + + if (!i18n.hasPhrase(LANG, phrase)) { + err("No such phrase", phrase); + } + + return i18n.t(LANG, ...arguments); +} + +if (LANG !== 'en') { + i18n.setFallback(LANG, 'en'); +} + +i18n.add = (...args) => i18n.addPhrase(LANG, ...args); + +t.i18n = i18n; + +module.exports = t; diff --git a/modules/lib/webpack/cssWatchRebuildPlugin.js b/modules/lib/webpack/cssWatchRebuildPlugin.js new file mode 100644 index 0000000..499ed15 --- /dev/null +++ b/modules/lib/webpack/cssWatchRebuildPlugin.js @@ -0,0 +1,120 @@ +const fs = require('fs'); +const Minimatch = require("minimatch").Minimatch; +const config = require('config'); +const glob = require('glob'); +const chokidar = require('chokidar'); + +class CssWatchRebuildPlugin { + constructor(roots) { + this.roots = roots; + } + + apply(compiler) { + compiler.hooks.afterEnvironment.tap("CssWatchRebuildPlugin", () => { + + compiler.watchFileSystem = new CssWatchFS( + compiler.watchFileSystem, + this.roots + ); + }); + } +} + +module.exports = CssWatchRebuildPlugin; + +class CssWatchFS { + constructor(wfs, roots) { + this.wfs = wfs; + this.roots = roots; + + this.rebuildAll(); + + for(let name in this.roots) { + chokidar.watch(`${this.roots[name]}/**/*.styl`, {ignoreInitial: true}).on('add', file => { + console.log("CHOKIDAR ADD"); + this.rebuildRoot(name); + })/*.on('unlink', file => { + console.log("CHOKIDAR UNNLINK"); + this.rebuildRoot(name); + })*/ + } + + } + + rebuildAll() { + for (let name in this.roots) { + this.rebuildRoot(name); + } + } + + rebuildRoot(name) { + + let styles = glob.sync(`${this.roots[name]}/**/*.styl`, {cwd: config.projectRoot}); + + // config.handlers.forEach(handler => { + // let handlerStyles = glob.sync(`handlers/${handler}/client/styles/**/*.styl`, {cwd: config.projectRoot}); + // styles.push(...handlerStyles); + // }); + + let content = styles.map(s => `@require '../${s}'`).join("\n"); + + fs.writeFileSync(`${config.tmpRoot}/${name}.styl`, content); + + this.wfs.inputFileSystem.purge(`${config.tmpRoot}/${name}.styl`); + + // console.log("REBUILD", name); + } + + // rebuild batch for deleted .styl + watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) { + const watcher = this.wfs.watch(files, dirs, missing, startTime, options, + ( + err, + filesModified, + dirsModified, + missingModified, + fileTimestamps, + dirTimestamps + ) => { + //console.log(fileTimestamps); + if (err) return callback(err); + + // console.log("Modified", filesModified, fs.existsSync(filesModified[0])); + for(let fileModified of filesModified) { + // deleted style + if (!fs.existsSync(fileModified)) { + for(let name in this.roots) { + + var mm = new Minimatch(`${this.roots[name]}/**/*.styl`); + let fn = fileModified.slice(config.projectRoot.length + 1); + //console.log("CHECK", fn); + + if (mm.match(fn)) { + this.rebuildRoot(name); + fileTimestamps.set(`${config.tmpRoot}/${name}.styl`, Date.now()); + } + + } + } + } + + callback( + err, + filesModified, + dirsModified, + missingModified, + fileTimestamps, + dirTimestamps + ); + }, + callbackUndelayed + ); + + return { + close: () => watcher.close(), + pause: () => watcher.pause(), + getContextTimestamps: () => watcher.getContextTimestamps(), + getFileTimestamps: () => watcher.getFileTimestamps() + }; + } +} diff --git a/modules/textUtil/pluralize.js b/modules/textUtil/pluralize.js deleted file mode 100755 index 13136f5..0000000 --- a/modules/textUtil/pluralize.js +++ /dev/null @@ -1,32 +0,0 @@ -function getPluralType(n) { - if ((n % 10) == 1 && (n % 100) != 11) { - return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 4 && ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { - return 'few'; - } - if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { - return 'many'; - } - return 'other'; -} - -// pluralize(10, 'груша', 'груши', 'груш') -function pluralize(count, strOne, strFew, strMany) { - - let type = getPluralType(count); - - switch(type) { - case 'one': - return strOne; - case 'few': - return strFew; - case 'many': - return strMany; - default: - throw new Error("Unsupported count: " + count); - } - -} - -module.exports = pluralize; diff --git a/package.json b/package.json index 28a4d34..63d1476 100755 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "babelfish": "^1.1.2", "bemto.pug": "github:kizu/bemto", "bunyan": "*", + "chokidar": "^2.0.4", "clarify": "^2.1.0", "css-loader": "^0", "file-loader": "^1.1", @@ -35,6 +36,7 @@ "gulp-util": "*", "image-size": "*", "js-yaml": "*", + "json-loader": "^0.5.7", "koa": "^2", "koa-bodyparser": "^4", "koa-conditional-get": "^2", @@ -48,6 +50,7 @@ "markdown-it-deflist": "*", "mime": "*", "mini-css-extract-plugin": "^0", + "minimatch": "^3.0.4", "multiparty": "*", "mz": "*", "nib": "*", @@ -70,7 +73,8 @@ "trace": "^3.1.0", "uglify-js": "^3", "uglifyjs-webpack-plugin": "^1", - "webpack": "^4" + "webpack": "^4", + "yaml-loader": "^0.5.0" }, "engineStrict": true, "repository": { From ffcf920ee32afd86f275856540193f2deaba5b93 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 5 Jul 2018 08:28:16 +0300 Subject: [PATCH 008/218] minor --- README.md | 12 ++++-------- modules/markit/plugins/resolveMdnSrc.js | 3 ++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 38fa250..584e841 100755 --- a/README.md +++ b/README.md @@ -14,10 +14,9 @@ If you have an old copy of the English tutorial, please rename `1-js/05-data-typ 1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org). These are required to update and run the project. + For non-Windows OS use standard install tools (packages or whatever convenient). (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/). - - For non-Windows OS use standard install tools (packages or whatever convenient). 2. Install global Node modules: @@ -38,13 +37,12 @@ If you have an old copy of the English tutorial, please rename `1-js/05-data-typ 5. Clone the tutorial text into it. - The text repository has "-language" at the end, e.g: + The text repository has `-language` at the end, e.g `ru`: ``` cd /js git clone https://github.com/iliakan/javascript-tutorial-ru ``` - 6. Run the site Run the site with the language: @@ -61,11 +59,9 @@ If you have an old copy of the English tutorial, please rename `1-js/05-data-typ 7. Edit the tutorial - The files are editable in the tutorial text folder (cloned at step 5). - As you edit text files, the webpage gets reloaded automatically, so it's handy - to split the screen at two sides: put the browser to the left half and edit in the right half. + As you edit text files in the tutorial text repository (cloned at step 5), + the webpage gets reloaded automatically. - # TroubleShooting diff --git a/modules/markit/plugins/resolveMdnSrc.js b/modules/markit/plugins/resolveMdnSrc.js index 0f05de6..ffcee06 100755 --- a/modules/markit/plugins/resolveMdnSrc.js +++ b/modules/markit/plugins/resolveMdnSrc.js @@ -5,6 +5,7 @@ */ const tokenUtils = require('../utils/token'); +const lang = require('config').lang; module.exports = function(md) { @@ -24,7 +25,7 @@ module.exports = function(md) { if (href.startsWith('mdn:')) { let parts = href.slice(4).split('/'); - let locale = process.env.NODE_LANG == 'en' ? 'en-US' : process.env.NODE_LANG; + let locale = lang == 'en' ? 'en-US' : lang; let prefix = `https://developer.mozilla.org/${locale}/docs/Web`; From cdfcf62eaf065dc3c7b708bfd4ed78e6a7267403 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 20:48:41 +0300 Subject: [PATCH 009/218] minor --- handlers/tutorial/tasks/edit.js | 68 -------------------------- handlers/tutorial/templates/folder.pug | 41 ---------------- modules/config/index.js | 6 +-- 3 files changed, 3 insertions(+), 112 deletions(-) delete mode 100755 handlers/tutorial/tasks/edit.js delete mode 100755 handlers/tutorial/templates/folder.pug diff --git a/handlers/tutorial/tasks/edit.js b/handlers/tutorial/tasks/edit.js deleted file mode 100755 index 03ca2dc..0000000 --- a/handlers/tutorial/tasks/edit.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -let fs = require('fs'); -let path = require('path'); -let log = require('log')(); -let Article = require('../models/article'); -let Task = require('../models/task'); -let url = require('url'); -let execSync = require('child_process').execSync; - -module.exports = function(options) { - - return function() { - - return async function() { - - let args = require('yargs') - .usage("tutorial url is required.") - .example("gulp tutorial:edit --url http://javascript.local/memory-leaks-jquery --root /js/javascript-tutorial") - .demand(['url', 'root']) - .argv; - - let urlPath = url.parse(args.url).pathname.split('/').filter(Boolean); - - if (urlPath.length == 1) { - let article = await Article.findOne({slug: urlPath[0]}); - if (!article) { - console.log("Not found!"); - return; - } - - let weight = article.weight + ''; - if (weight.length < 2) weight = 0 + weight; - - let dirName = weight + '-' + article.slug; - let cmd = "find '" + args.root + "' -path '*/" + dirName + "/article.md'"; - console.log(cmd); - - let result = execSync(cmd, {encoding: 'utf8'}).trim(); - - if (!result) { - return; - } - - console.log(path.dirname(result)); - execSync('s ' + result); - } - - if (urlPath[0] == 'task') { - let task = await Task.findOne({slug: urlPath[1]}); - if (!task) { - return; - } - - let dirName = task.weight + '-' + task.slug; - let result = execSync("find /js/javascript-tutorial -path '*/" + dirName + "/task.md'", {encoding: 'utf8'}).trim(); - - if (!result) { - return; - } - - console.log(path.dirname(result)); - execSync('s ' + result + ' ' + result.replace('task.md', 'solution.md')); - } - - }(); - }; -}; diff --git a/handlers/tutorial/templates/folder.pug b/handlers/tutorial/templates/folder.pug deleted file mode 100755 index ea6e4f0..0000000 --- a/handlers/tutorial/templates/folder.pug +++ /dev/null @@ -1,41 +0,0 @@ -extends /layouts/main - -block append variables - - let layout_main_class = "main_width-limit" - -block sidebar - include sidebar - -block sidebar-buttons - +b("a").map(href="/tutorial/map", data-action="tutorial-map", data-tooltip="Карта учебника") - -block append head - !=js("tutorial", {defer: true}) - -block content - - != content - - +b.lessons-list - +e('ol').lessons - if levelMax == 2 && level == 0 - // top-level folders in 2-level courses - each article in children - +e('li')(class="lesson #{article.children ? '_level_1' : ''}") - +e('a').link(href=article.url) #{article.title} - if article.children - +e('ol').lessons - each subChild in article.children - +e('li')(data-section-number=subChild.weight).lesson._level_2 - +e('a').link(href=subChild.url) #{subChild.title} - else if levelMax == 2 && level == 1 - // 1-level folders in 2-level courses - each article in children - +e('li')(data-section-number=weight, class="lesson #{article.children ? '_level_1' : ''}") - +e('a').link(href=article.url) #{article.title} - else - // folders in plain courses - each article in children - +e('li').lesson - +e('a').link(href=article.url) #{article.title} - diff --git a/modules/config/index.js b/modules/config/index.js index 8c2b87f..b3a82c5 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -43,12 +43,12 @@ let config = module.exports = { plnkrAuthId: secret.plnkrAuthId, - assetVersioning: env.ASSET_VERSIONING == 'file' ? 'file' : - env.ASSET_VERSIONING == 'query' ? 'query' : null, + assetVersioning: env.ASSET_VERSIONING === 'file' ? 'file' : + env.ASSET_VERSIONING === 'query' ? 'query' : null, pug: { basedir: path.join(process.cwd(), 'templates'), - cache: env.NODE_ENV != 'development' + cache: env.NODE_ENV !== 'development' }, supportEmail: 'iliakan@javascript.info', From d4e59110efd0c122da465192162b7652afd37b1d Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 20:55:26 +0300 Subject: [PATCH 010/218] minor --- handlers/error/index.js | 1 - handlers/error/templates/400.pug | 1 + handlers/error/templates/401.pug | 1 + handlers/error/templates/403.pug | 1 + handlers/error/templates/404.pug | 1 + handlers/error/templates/500.pug | 9 +++++---- handlers/error/templates/503.pug | 9 +++++---- 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/handlers/error/index.js b/handlers/error/index.js index 87fc35f..1dfe2ae 100755 --- a/handlers/error/index.js +++ b/handlers/error/index.js @@ -9,7 +9,6 @@ let isDevelopment = process.env.NODE_ENV == 'development'; const t = require('i18n'); t.requirePhrase('error'); - // can be called not from this MW, but from anywhere // this.templateDir can be anything function renderError(ctx, err) { diff --git a/handlers/error/templates/400.pug b/handlers/error/templates/400.pug index 904ff22..d5e8294 100755 --- a/handlers/error/templates/400.pug +++ b/handlers/error/templates/400.pug @@ -2,6 +2,7 @@ extends /layouts/main block append variables - let headTitle = t('error.400.title'); + - let layout_header_class = "main__header_center" block content +b.error diff --git a/handlers/error/templates/401.pug b/handlers/error/templates/401.pug index 5ecdaba..5bf0fd8 100755 --- a/handlers/error/templates/401.pug +++ b/handlers/error/templates/401.pug @@ -2,6 +2,7 @@ extends /layouts/main block append variables - let headTitle = t('error.401.title') + - let layout_header_class = "main__header_center" block content +b.error diff --git a/handlers/error/templates/403.pug b/handlers/error/templates/403.pug index 6a84269..3f29a68 100755 --- a/handlers/error/templates/403.pug +++ b/handlers/error/templates/403.pug @@ -2,6 +2,7 @@ extends /layouts/main block append variables - let headTitle = t('error.403.title') + - let layout_header_class = "main__header_center" block content +b.error diff --git a/handlers/error/templates/404.pug b/handlers/error/templates/404.pug index 4f352ac..10a31ca 100755 --- a/handlers/error/templates/404.pug +++ b/handlers/error/templates/404.pug @@ -2,6 +2,7 @@ extends /layouts/main block append variables - let headTitle = t('error.404.title') + - let layout_header_class = "main__header_center" block content +b.error diff --git a/handlers/error/templates/500.pug b/handlers/error/templates/500.pug index 4a832db..51fd462 100755 --- a/handlers/error/templates/500.pug +++ b/handlers/error/templates/500.pug @@ -1,18 +1,19 @@ extends /layouts/main block append variables - - let headTitle = t('errors.500.title') + - let headTitle = t('error.500.title') + - let layout_header_class = "main__header_center" block content +b.error - +e('h1').type= t('errors.500.title') + +e('h1').type= t('error.500.title') +e.code 500 if error.info +e.text p= error.info - - let subject = encodeURIComponent(t('errors.500.subject', {href: url.href})) - +e.text!= t('errors.500.description', { email: supportEmail, subject }) + - let subject = encodeURIComponent(t('error.500.subject', {href: url.href})) + +e.text!= t('error.500.description', { email: supportEmail, subject }) +e.text +e('span').request RequestId: #{requestId} diff --git a/handlers/error/templates/503.pug b/handlers/error/templates/503.pug index dfd46e6..0a3926d 100755 --- a/handlers/error/templates/503.pug +++ b/handlers/error/templates/503.pug @@ -1,18 +1,19 @@ extends /layouts/main block append variables - - let headTitle = t('errors.503.title') + - let headTitle = t('error.503.title') + - let layout_header_class = "main__header_center" block content +b.error - +e('h1').type= t('errors.503.title') + +e('h1').type= t('error.503.title') +e.code 500 if error.info +e.text p= error.info - - let subject = encodeURIComponent(t('errors.500.subject', {href: url.href})) - +e.text!= t('errors.500.description', { email: supportEmail, subject }) + - let subject = encodeURIComponent(t('error.500.subject', {href: url.href})) + +e.text!= t('error.500.description', { email: supportEmail, subject }) +e.text +e('span').request RequestId: #{requestId} From 1fe97fc30dd51c9e48ab82690960b2c01c7953f8 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:03:57 +0300 Subject: [PATCH 011/218] readme --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 584e841..24025bc 100755 --- a/README.md +++ b/README.md @@ -63,6 +63,27 @@ If you have an old copy of the English tutorial, please rename `1-js/05-data-typ the webpage gets reloaded automatically. +# Dev mode + +If you'd like to edit the server code, *not* the tutorial text (you know Node.js), run this once: + +``` +// import and cache the "ru" version of the tutorial from /js/javascript-tutorial-ru +// can be any language +cd /js/javascript-tutorial-server +NODE_LANG=ru npm run gulp tutorial:import +``` + +And then: + +``` +cd /js/javascript-tutorial-server +./dev ru +``` + +Running `./dev` does not watch tutorial text for changes, but reloads the server after code changes. +Again, that's for developing the server code itself, not writing the tutorial. + # TroubleShooting If something doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). From 46933db451ca9d64db477bdf297793de6d05f9e5 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:05:12 +0300 Subject: [PATCH 012/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24025bc..c7b21c8 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ You can use it to run the tutorial locally and translate it into your language. # Installation -If you have an old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`. +(If you have an old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.) 1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org). From 2575c171594327231975cb859829477fa77681c7 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:05:28 +0300 Subject: [PATCH 013/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7b21c8..cc61df9 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ You can use it to run the tutorial locally and translate it into your language. # Installation -(If you have an old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.) +(If you have an old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`). 1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org). From 81e7ea8c91491cd19fedb4ce4b0bd80c30454ff8 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:06:26 +0300 Subject: [PATCH 014/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc61df9..a4dcd81 100755 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ You can use it to run the tutorial locally and translate it into your language. 1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org). These are required to update and run the project. - For non-Windows OS use standard install tools (packages or whatever convenient). + For Windows just download and install, otherwise use standard OS install tools (packages or whatever convenient). (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/). From e4eb80321bb4df8cc1cc8db304109d2534e2aefc Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:07:27 +0300 Subject: [PATCH 015/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4dcd81..dfde7fc 100755 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can use it to run the tutorial locally and translate it into your language. 5. Clone the tutorial text into it. - The text repository has `-language` at the end, e.g `ru`: + The text repository has `"-language"` postfix at the end, e.g for the Russian version `ru`: ``` cd /js git clone https://github.com/iliakan/javascript-tutorial-ru From 39f035c9d832e7f755c766e9fa14600f15ecb0b1 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:07:54 +0300 Subject: [PATCH 016/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfde7fc..cbf5420 100755 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can use it to run the tutorial locally and translate it into your language. 5. Clone the tutorial text into it. - The text repository has `"-language"` postfix at the end, e.g for the Russian version `ru`: + The text repository has `"-language"` postfix at the end, e.g for the French version `fr`, for Russian – `ru` etc: ``` cd /js git clone https://github.com/iliakan/javascript-tutorial-ru From 644b3918a165823e1284a6d2c2ace430dcfc2b06 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:08:38 +0300 Subject: [PATCH 017/218] readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbf5420..a21b9e1 100755 --- a/README.md +++ b/README.md @@ -37,7 +37,9 @@ You can use it to run the tutorial locally and translate it into your language. 5. Clone the tutorial text into it. - The text repository has `"-language"` postfix at the end, e.g for the French version `fr`, for Russian – `ru` etc: + The text repository has `"-language"` postfix at the end, e.g for the French version `fr`, for Russian – `ru` etc. + + For the Russian version: ``` cd /js git clone https://github.com/iliakan/javascript-tutorial-ru @@ -45,7 +47,7 @@ You can use it to run the tutorial locally and translate it into your language. 6. Run the site - Run the site with the language: + Run the site with the same language: ``` cd /js/javascript-tutorial-server ./edit ru From 4e32731a761b71c384efe6f3589971a714812004 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:08:55 +0300 Subject: [PATCH 018/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a21b9e1..83310b5 100755 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ You can use it to run the tutorial locally and translate it into your language. Please note that the argument of `edit` is exactly the language you cloned at step 5. - Wait a bit as it reads the tutorial from disk and builds static assets. + Wait a bit while it reads the tutorial from disk and builds static assets. Then access the site at `http://127.0.0.1:3000`. From 86c12411b951e5751acce65a57349dc3138e4e75 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:09:25 +0300 Subject: [PATCH 019/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83310b5..5d6e057 100755 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ You can use it to run the tutorial locally and translate it into your language. # Dev mode -If you'd like to edit the server code, *not* the tutorial text (you know Node.js), run this once: +If you'd like to edit the server code, *not* the tutorial text (assuming you're familiar with Node.js), run this once: ``` // import and cache the "ru" version of the tutorial from /js/javascript-tutorial-ru From 9ae25511a98963dcf2e73e366ad5f7008b46ab18 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:09:47 +0300 Subject: [PATCH 020/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d6e057..535fd33 100755 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ If you'd like to edit the server code, *not* the tutorial text (assuming you're ``` // import and cache the "ru" version of the tutorial from /js/javascript-tutorial-ru -// can be any language +// (can be any language) cd /js/javascript-tutorial-server NODE_LANG=ru npm run gulp tutorial:import ``` From 2f22b1a43871ea8af1d482233a1ff5cab32e97b0 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:10:42 +0300 Subject: [PATCH 021/218] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 535fd33..72f0632 100755 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ cd /js/javascript-tutorial-server ./dev ru ``` -Running `./dev` does not watch tutorial text for changes, but reloads the server after code changes. +Running `./dev` uses the cached version of the tutorial, it does not watch tutorial text for changes. +But it reloads the server after code changes. Again, that's for developing the server code itself, not writing the tutorial. # TroubleShooting From 9e11162edbc10024863ac68b290f4ac36882c4dc Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 17 Jul 2018 21:10:58 +0300 Subject: [PATCH 022/218] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72f0632..f09df70 100755 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ cd /js/javascript-tutorial-server ./dev ru ``` -Running `./dev` uses the cached version of the tutorial, it does not watch tutorial text for changes. +Running `./dev` uses the cached version of the tutorial, it does not watch tutorial text. But it reloads the server after code changes. Again, that's for developing the server code itself, not writing the tutorial. From 02899450c67800bf3ae24befa49f82ba92d80b79 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 20 Jul 2018 18:27:05 +0300 Subject: [PATCH 023/218] minor --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f09df70..2a40db8 100755 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ Again, that's for developing the server code itself, not writing the tutorial. If something doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). +Write OS and Node.js version. + -- Yours, Ilya Kantor \ No newline at end of file From 2a1391128d7205bfc751a1577cc0e9dbd8e85d8c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 20 Jul 2018 18:29:17 +0300 Subject: [PATCH 024/218] minor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a40db8..4940dcd 100755 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Again, that's for developing the server code itself, not writing the tutorial. If something doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). -Write OS and Node.js version. +Please mention OS and Node.js version. -- Yours, From c614ab4a981a8ba8958e8e962f61ac6dea54ed0d Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 22 Jul 2018 16:28:34 +0300 Subject: [PATCH 025/218] fixes --- handlers/tutorial/lib/tutorialImporter.js | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/handlers/tutorial/lib/tutorialImporter.js b/handlers/tutorial/lib/tutorialImporter.js index c068b8d..ccd80e0 100755 --- a/handlers/tutorial/lib/tutorialImporter.js +++ b/handlers/tutorial/lib/tutorialImporter.js @@ -1,6 +1,5 @@ 'use strict'; -const util = require('util'); const fs = require('fs'); const fse = require('fs-extra'); const path = require('path'); @@ -76,7 +75,7 @@ module.exports = class TutorialImporter { let parent = this.tree.bySlug(parentSlug); if (update) { - console.log("DESTROY", slug); + //console.log("DESTROY", slug); this.tree.destroyTree(slug); } await this['sync' + type](dir, parent); @@ -259,7 +258,7 @@ module.exports = class TutorialImporter { if (stat.isFile()) { if (ext === 'png' || ext === 'jpg' || ext === 'gif' || ext === 'svg') { - await importImage(sourcePath, destDir); + importImage(sourcePath, destDir); return; } copySync(sourcePath, destPath); @@ -542,7 +541,7 @@ function getFileExt(filePath) { return ext && ext[1]; } -function* importImage(srcPath, dstDir) { +function importImage(srcPath, dstDir) { log.info("importImage", srcPath, "to", dstDir); const filename = path.basename(srcPath); const dstPath = path.join(dstDir, filename); @@ -576,7 +575,7 @@ function readFs(dir) { hadErrors = true; } - let type = mime.lookup(file).split('/'); + let type = mime.getType(file).split('/'); if (type[0] != 'text' && type[1] != 'json' && type[1] != 'javascript' && type[1] != 'svg+xml') { log.error("Bad file extension: " + file); hadErrors = true; diff --git a/package.json b/package.json index 63d1476..0f813f5 100755 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "markdown-it": "*", "markdown-it-container": "*", "markdown-it-deflist": "*", - "mime": "*", + "mime": "^2.3", "mini-css-extract-plugin": "^0", "minimatch": "^3.0.4", "multiparty": "*", From 468b7c7c7d7dbff24fc1faa9a202049171b4cc93 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 23 Jul 2018 19:03:46 +0300 Subject: [PATCH 026/218] fix folders --- handlers/render.js | 2 ++ handlers/tutorial/templates/article.pug | 2 +- handlers/tutorial/templates/folder.pug | 41 +++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100755 handlers/tutorial/templates/folder.pug diff --git a/handlers/render.js b/handlers/render.js index 67aadda..d8510a9 100755 --- a/handlers/render.js +++ b/handlers/render.js @@ -176,6 +176,8 @@ exports.init = function(app) { resolve: pugResolve }]; + log.debug("render template", templatePath); + return pug.renderFile(pugResolve(templatePath, null, loc), loc); }; diff --git a/handlers/tutorial/templates/article.pug b/handlers/tutorial/templates/article.pug index ceeafe8..c98ddf1 100755 --- a/handlers/tutorial/templates/article.pug +++ b/handlers/tutorial/templates/article.pug @@ -7,7 +7,7 @@ block sidebar include sidebar block sidebar-buttons - +b("a").map(href="/tutorial/map", data-action="tutorial-map", data-tooltip="Карта учебника") + +b("a").map(href="/tutorial/map", data-action="tutorial-map", data-tooltip=t('locales.tutorial_map')) block append head meta(property="og:title", content=title || headTitle) diff --git a/handlers/tutorial/templates/folder.pug b/handlers/tutorial/templates/folder.pug new file mode 100755 index 0000000..d31f38b --- /dev/null +++ b/handlers/tutorial/templates/folder.pug @@ -0,0 +1,41 @@ +extends /layouts/main + +block append variables + - let layout_main_class = "main_width-limit" + +block sidebar + include sidebar + +block sidebar-buttons + +b("a").map(href="/tutorial/map", data-action="tutorial-map", data-tooltip=t('locales.tutorial_map')) + +block append head + !=js("tutorial", {defer: true}) + +block content + + != content + + +b.lessons-list + +e('ol').lessons + if levelMax == 2 && level == 0 + // top-level folders in 2-level courses + each article in children + +e('li')(class="lesson #{article.children ? '_level_1' : ''}") + +e('a').link(href=article.url) #{article.title} + if article.children + +e('ol').lessons + each subChild in article.children + +e('li')(data-section-number=subChild.weight).lesson._level_2 + +e('a').link(href=subChild.url) #{subChild.title} + else if levelMax == 2 && level == 1 + // 1-level folders in 2-level courses + each article in children + +e('li')(data-section-number=weight, class="lesson #{article.children ? '_level_1' : ''}") + +e('a').link(href=article.url) #{article.title} + else + // folders in plain courses + each article in children + +e('li').lesson + +e('a').link(href=article.url) #{article.title} + From cf24c7f6bb8bfe39e2f723d89636639bec8f6fca Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 28 Jul 2018 11:33:42 +0300 Subject: [PATCH 027/218] refactor config domains -> urlBase, urlBaseProduction --- assets/libs/domtree.css | 22 +++++++++++++++++++ dev | 1 - dev.cmd | 2 -- edit | 1 - edit.cmd | 2 -- handlers/render.js | 4 ++-- handlers/tutorial/lib/tutorialImporter.js | 16 +++++++------- handlers/tutorial/renderer/articleRenderer.js | 2 +- modules/config/index.js | 19 ++++++++-------- modules/markit/serverParser.js | 2 +- templates/blocks/head.pug | 2 +- 11 files changed, 45 insertions(+), 28 deletions(-) create mode 100755 assets/libs/domtree.css diff --git a/assets/libs/domtree.css b/assets/libs/domtree.css new file mode 100755 index 0000000..f16a1be --- /dev/null +++ b/assets/libs/domtree.css @@ -0,0 +1,22 @@ +/* +.domtree .node rect { + cursor: pointer; + fill: #fff; +} + +.domtree .node text { + fill: #333333; + font: 12px "Helvetica Nue", sans-serif; + pointer-events: none; +} + +.domtree path.link { + fill: none; + stroke: #BEC3C7; + stroke-width: 1px; +} +*/ +.domtree { + border: 1px solid #f5f2f0; + border-radius: 4px; +} diff --git a/dev b/dev index 837de23..8c8dd09 100755 --- a/dev +++ b/dev @@ -9,7 +9,6 @@ export NODE_ENV=development export ASSET_VERSIONING=query export WATCH=1 export SITE_HOST=http://javascript.local -export PORT=3000 export TUTORIAL_EDIT= npm --silent run gulp dev | bunyan -o short -l debug diff --git a/dev.cmd b/dev.cmd index 170060a..041bd64 100644 --- a/dev.cmd +++ b/dev.cmd @@ -4,8 +4,6 @@ set NODE_LANG=%1 @set NODE_ENV=development @set ASSET_VERSIONING=query @set WATCH=1 -@set SITE_HOST=http://javascript.local -@set PORT=3000 @set NODE_PATH=./handlers;./modules call gulp dev diff --git a/edit b/edit index 65b47e2..493103b 100755 --- a/edit +++ b/edit @@ -7,7 +7,6 @@ BRANCH="${1:-$NODE_LANG}" export NODE_LANG=$BRANCH export NODE_ENV=production export TUTORIAL_EDIT=1 -export PORT=3000 npm --silent run -- gulp edit diff --git a/edit.cmd b/edit.cmd index 9a09c21..a6d760c 100644 --- a/edit.cmd +++ b/edit.cmd @@ -5,8 +5,6 @@ @set TUTORIAL_EDIT=1 @set ASSET_VERSIONING=query @set WATCH=1 -@set SITE_HOST=http://javascript.local -@set PORT=3000 @set NODE_PATH=./handlers;./modules call gulp edit diff --git a/handlers/render.js b/handlers/render.js index d8510a9..278c65a 100755 --- a/handlers/render.js +++ b/handlers/render.js @@ -59,7 +59,7 @@ function addStandardHelpers(locals, ctx) { locals.env = process.env; - locals.domain = config.domain; + locals.urlBase = config.urlBase; // replace lone surrogates in json, -> <\/script> locals.escapeJSON = function(s) { @@ -162,7 +162,7 @@ exports.init = function(app) { loc.canonicalPath = loc.canonicalPath.replace(/\/+$/, ''); } - loc.canonicalUrl = config.server.siteHost + loc.canonicalPath; + loc.canonicalUrl = config.urlBase.main + loc.canonicalPath; if (ctx.templateDir) { loc.roots = [ctx.templateDir]; diff --git a/handlers/tutorial/lib/tutorialImporter.js b/handlers/tutorial/lib/tutorialImporter.js index ccd80e0..19155bf 100755 --- a/handlers/tutorial/lib/tutorialImporter.js +++ b/handlers/tutorial/lib/tutorialImporter.js @@ -112,7 +112,7 @@ module.exports = class TutorialImporter { //this.tree.destroyTree(data.slug); let options = { - staticHost: config.server.staticHost, + staticHost: config.urlBase.static, resourceWebRoot: Article.getResourceWebRootBySlug(data.slug) }; @@ -183,7 +183,7 @@ module.exports = class TutorialImporter { // this.tree.destroyTree(data.slug); const options = { - staticHost: config.server.staticHost, + staticHost: config.urlBase.static, resourceWebRoot: Article.getResourceWebRootBySlug(data.slug) }; @@ -296,7 +296,7 @@ module.exports = class TutorialImporter { //this.tree.destroyTree(data.slug); const options = { - staticHost: config.server.staticHost, + staticHost: config.urlBase.static, resourceWebRoot: Task.getResourceWebRootBySlug(data.slug) }; @@ -376,7 +376,7 @@ module.exports = class TutorialImporter { } else { view = new TutorialView({ webPath, - description: "Fork from https://" + config.domain.main + description: "Fork from " + config.urlBase.main }); log.debug("Created new plunk (db empty)", view); } @@ -434,7 +434,7 @@ module.exports = class TutorialImporter { if (!sourceView) { sourceView = new TutorialView({ webPath: sourceWebPath, - description: "Fork from https://" + config.domain.main + description: "Fork from " + config.urlBase.main }); TutorialViewStorage.instance().set(sourceWebPath, sourceView); } @@ -463,7 +463,7 @@ module.exports = class TutorialImporter { if (!solutionView) { solutionView = new TutorialView({ webPath: solutionWebPath, - description: "Fork from https://" + config.domain.main + description: "Fork from " + config.urlBase.main }); TutorialViewStorage.instance().set(solutionWebPath, solutionView); } @@ -491,7 +491,7 @@ module.exports = class TutorialImporter { function makeSource(sourceJs, testJs) { let source = "\n\n\n \n"; if (testJs) { - source += " \n"; + source += " \n"; source += " \n"; } source += "\n\n\n \n"; + solution += " \n"; solution += " \n"; } solution += "\n\n\n `; - }; - - - locals.css = function(name) { - let src = locals.pack(name, 'css'); - - return ``; - }; - - locals.env = process.env; - - locals.urlBase = config.urlBase; - - // replace lone surrogates in json, -> <\/script> - locals.escapeJSON = function(s) { - let json = JSON.stringify(s); - return json.replace(/\//g, '\\/') - .replace(/[\u003c\u003e]/g, - function(c) { - return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4).toUpperCase(); - } - ).replace(/[\u007f-\uffff]/g, - function(c) { - return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); - } - ); - }; - - locals.markit = function(text, options) { - return new BasicParser(options).render(text); - }; - - locals.markitInline = function(text, options) { - return new BasicParser(options).renderInline(text); - }; - - locals.t = t; - //locals.bem = require('bemPug')(); - - locals.pack = function(name, ext) { - let versions = JSON.parse( - fs.readFileSync(path.join(config.cacheRoot, 'webpack.versions.json'), {encoding: 'utf-8'}) - ); - let versionName = versions[name]; - // e.g style = [ style.js, style.js.map, style.css, style.css.map ] - - if (!Array.isArray(versionName)) return versionName; - - let extTestReg = new RegExp(`.${ext}\\b`); - - // select right .js\b extension from files - for (let i = 0; i < versionName.length; i++) { - let versionNameItem = versionName[i]; // e.g. style.css.map - if (/\.map/.test(versionNameItem)) continue; // we never need a map - if (extTestReg.test(versionNameItem)) return versionNameItem; - } - - throw new Error(`Not found pack name:${name} ext:${ext}`); - /* - if (process.env.NODE_ENV == 'development') { - // webpack-dev-server url - versionName = process.env.STATIC_HOST + ':' + config.webpack.devServer.port + versionName; - }*/ - - }; - - - locals._hasStandardHelpers = true; -} - - -// (!) this.render does not assign this.body to the result -// that's because render can be used for different purposes, e.g to send emails -exports.init = function(app) { - app.use(async function(ctx, next) { - - let renderFileCache = {}; - - ctx.locals = Object.assign({}, config.pug); - - /** - * Render template - * Find the file: - * if locals.useAbsoluteTemplatePath => use templatePath - * else if templatePath starts with / => lookup in locals.basedir - * otherwise => lookup in ctx.templateDir (MW should set it) - * @param templatePath file to find (see the logic above) - * @param locals - * @returns {String} - */ - ctx.render = function(templatePath, locals) { - - // add helpers at render time, not when middleware is used time - // probably we will have more stuff initialized here - addStandardHelpers(ctx.locals, ctx); - - // warning! - // Object.assign does NOT copy defineProperty - // so I use ctx.locals as a root and merge all props in it, instead of cloning ctx.locals - let loc = Object.create(ctx.locals); - - Object.assign(loc, locals); - - if (!loc.schema) { - loc.schema = {}; - } - - if (!loc.canonicalPath) { - // strip query - loc.canonicalPath = ctx.request.originalUrl.replace(/\?.*/, ''); - // /intro/ -> /intro - loc.canonicalPath = loc.canonicalPath.replace(/\/+$/, ''); - } - - loc.canonicalUrl = config.urlBase.main + loc.canonicalPath; - - if (ctx.templateDir) { - loc.roots = [ctx.templateDir]; - } - - if (loc.sitetoolbar === undefined) { - loc.sitetoolbar = true; - } - - loc.plugins = [{ - resolve: pugResolve - }]; - - log.debug("render template", templatePath); - - return pug.renderFile(pugResolve(templatePath, null, loc), loc); - }; - - - await next(); - }); - -}; diff --git a/handlers/requestId.js b/handlers/requestId.js deleted file mode 100755 index f548629..0000000 --- a/handlers/requestId.js +++ /dev/null @@ -1,11 +0,0 @@ -let uuid = require('node-uuid').v4; - -// RequestCaptureStream wants "req_id" to identify the request -// we take it from upper chain (varnish? nginx on top?) OR generate -exports.init = function(app) { - app.use(async function(ctx, next) { - /* jshint -W106 */ - ctx.requestId = ctx.get('X-Request-Id') || uuid(); - await next(); - }); -}; diff --git a/handlers/requestLog.js b/handlers/requestLog.js deleted file mode 100755 index 4253ded..0000000 --- a/handlers/requestLog.js +++ /dev/null @@ -1,13 +0,0 @@ - -exports.init = function(app) { - app.use(async function(ctx, next) { - - /* jshint -W106 */ - ctx.log = app.log.child({ - requestId: ctx.requestId - }); - - await next(); - }); - -}; diff --git a/handlers/tutorial/client/assets/libs/animate.js b/handlers/tutorial/client/assets/libs/animate.js deleted file mode 100755 index d2ecc45..0000000 --- a/handlers/tutorial/client/assets/libs/animate.js +++ /dev/null @@ -1,20 +0,0 @@ -function animate(options) { - - var start = performance.now(); - - requestAnimationFrame(function animate(time) { - // timeFraction от 0 до 1 - var timeFraction = (time - start) / options.duration; - if (timeFraction > 1) timeFraction = 1; - - // текущее состояние анимации - var progress = options.timing(timeFraction) - - options.draw(progress); - - if (timeFraction < 1) { - requestAnimationFrame(animate); - } - - }); -} \ No newline at end of file diff --git a/handlers/tutorial/client/assets/libs/class-extend.js b/handlers/tutorial/client/assets/libs/class-extend.js deleted file mode 100755 index dba2445..0000000 --- a/handlers/tutorial/client/assets/libs/class-extend.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Синтаксис: - * Class.extend(props) - * Class.extend(props, staticProps) - * Class.extend([mixins], props) - * Class.extend([mixins], props, staticProps) -*/ -!function() { - - window.Class = function() { /* вся магия - в Class.extend */ }; - - - Class.extend = function(props, staticProps) { - - var mixins = []; - - // если первый аргумент -- массив, то переназначить аргументы - if ({}.toString.apply(arguments[0]) == "[object Array]") { - mixins = arguments[0]; - props = arguments[1]; - staticProps = arguments[2]; - } - - // эта функция будет возвращена как результат работы extend - function Constructor() { - this.init && this.init.apply(this, arguments); - } - - // this -- это класс "перед точкой", для которого вызван extend (Animal.extend) - // наследуем от него: - Constructor.prototype = Class.inherit(this.prototype); - - // constructor был затёрт вызовом inherit - Constructor.prototype.constructor = Constructor; - - // добавим возможность наследовать дальше - Constructor.extend = Class.extend; - - // скопировать в Constructor статические свойства - copyWrappedProps(staticProps, Constructor, this); - - // скопировать в Constructor.prototype свойства из примесей и props - for (var i = 0; i < mixins.length; i++) { - copyWrappedProps(mixins[i], Constructor.prototype, this.prototype); - } - copyWrappedProps(props, Constructor.prototype, this.prototype); - - return Constructor; - }; - - - //---------- вспомогательные методы ---------- - - // fnTest -- регулярное выражение, - // которое проверяет функцию на то, есть ли в её коде вызов _super - // - // для его объявления мы проверяем, поддерживает ли функция преобразование - // в код вызовом toString: /xyz/.test(function() {xyz}) - // в редких мобильных браузерах -- не поддерживает, поэтому регэксп будет /./ - var fnTest = /xyz/.test(function() {xyz}) ? /\b_super\b/ : /./; - - - // копирует свойства из props в targetPropsObj - // третий аргумент -- это свойства родителя - // - // при копировании, если выясняется что свойство есть и в родителе тоже, - // и является функцией -- его вызов оборачивается в обёртку, - // которая ставит this._super на метод родителя, - // затем вызывает его, затем возвращает this._super - function copyWrappedProps(props, targetPropsObj, parentPropsObj) { - if (!props) return; - - for (var name in props) { - if (typeof props[name] == "function" - && typeof parentPropsObj[name] == "function" - && fnTest.test(props[name])) { - // скопировать, завернув в обёртку - targetPropsObj[name] = wrap(props[name], parentPropsObj[name]); - } else { - targetPropsObj[name] = props[name]; - } - } - - } - - // возвращает обёртку вокруг method, которая ставит this._super на родителя - // и возвращает его потом - function wrap(method, parentMethod) { - return function() { - var backup = this._super; - - this._super = parentMethod; - - try { - return method.apply(this, arguments); - } finally { - this._super = backup; - } - } - } - - // эмуляция Object.create для старых IE - Class.inherit = Object.create || function(proto) { - function F() {} - F.prototype = proto; - return new F; - }; -}(); diff --git a/handlers/tutorial/client/assets/libs/compareDocumentPosition.js b/handlers/tutorial/client/assets/libs/compareDocumentPosition.js deleted file mode 100755 index 0feca5b..0000000 --- a/handlers/tutorial/client/assets/libs/compareDocumentPosition.js +++ /dev/null @@ -1,28 +0,0 @@ - -// полифилл для compareDocumentPosition в ie8 - -!function(){ - var el = document.documentElement; - if( !el.compareDocumentPosition && el.sourceIndex !== undefined ){ - - /* ?? - Node = Element; - Node.DOCUMENT_POSITION_DISCONNECTED = 1; - Node.DOCUMENT_POSITION_PRECEDING = 2 - Node.DOCUMENT_POSITION_FOLLOWING = 4; - Node.DOCUMENT_POSITION_CONTAINS = 8; - Node.DOCUMENT_POSITION_CONTAINED_BY = 16; - Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 32; - */ - - Element.prototype.compareDocumentPosition = function(other){ - return (this != other && this.contains(other) && 16) + - (this != other && other.contains(this) && 8) + - (this.sourceIndex >= 0 && other.sourceIndex >= 0 ? - (this.sourceIndex < other.sourceIndex && 4) + - (this.sourceIndex > other.sourceIndex && 2) - : 1 - ) + 0; - } - } -}(); diff --git a/handlers/tutorial/client/assets/libs/d3.js b/handlers/tutorial/client/assets/libs/d3.js deleted file mode 100755 index d4cd8be..0000000 --- a/handlers/tutorial/client/assets/libs/d3.js +++ /dev/null @@ -1,5 +0,0 @@ -!function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(n){return aa+n in this}function o(n){return n=aa+n,n in this&&delete this[n]}function a(){var n=[];return this.forEach(function(t){n.push(t)}),n}function c(){var n=0;for(var t in this)t.charCodeAt(0)===ca&&++n;return n}function s(){for(var n in this)if(n.charCodeAt(0)===ca)return!1;return!0}function l(){}function f(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function h(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=sa.length;r>e;++e){var u=sa[e]+t;if(u in n)return u}}function g(){}function p(){}function v(n){function t(){for(var t,r=e,u=-1,i=r.length;++ue;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function D(n){return fa(n,ya),n}function P(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t0&&(n=n.substring(0,a));var s=Ma.get(n);return s&&(n=s,c=F),a?t?u:r:t?g:i}function H(n,t){return function(e){var r=Xo.event;Xo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Xo.event=r}}}function F(n,t){var e=H(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function O(){var n=".dragsuppress-"+ ++ba,t="click"+n,e=Xo.select(Go).on("touchmove"+n,d).on("dragstart"+n,d).on("selectstart"+n,d);if(_a){var r=Jo.style,u=r[_a];r[_a]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),_a&&(r[_a]=u),i&&(e.on(t,function(){d(),o()},!0),setTimeout(o,0))}}function Y(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>wa&&(Go.scrollX||Go.scrollY)){e=Xo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();wa=!(u.f||u.e),e.remove()}return wa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function I(n){return n>0?1:0>n?-1:0}function Z(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function V(n){return n>1?0:-1>n?Sa:Math.acos(n)}function X(n){return n>1?Ea:-1>n?-Ea:Math.asin(n)}function $(n){return((n=Math.exp(n))-1/n)/2}function B(n){return((n=Math.exp(n))+1/n)/2}function W(n){return((n=Math.exp(2*n))-1)/(n+1)}function J(n){return(n=Math.sin(n/2))*n}function G(){}function K(n,t,e){return new Q(n,t,e)}function Q(n,t,e){this.h=n,this.s=t,this.l=e}function nt(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,gt(u(n+120),u(n),u(n-120))}function tt(n,t,e){return new et(n,t,e)}function et(n,t,e){this.h=n,this.c=t,this.l=e}function rt(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),ut(e,Math.cos(n*=Na)*t,Math.sin(n)*t)}function ut(n,t,e){return new it(n,t,e)}function it(n,t,e){this.l=n,this.a=t,this.b=e}function ot(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=ct(u)*Fa,r=ct(r)*Oa,i=ct(i)*Ya,gt(lt(3.2404542*u-1.5371385*r-.4985314*i),lt(-.969266*u+1.8760108*r+.041556*i),lt(.0556434*u-.2040259*r+1.0572252*i))}function at(n,t,e){return n>0?tt(Math.atan2(e,t)*La,Math.sqrt(t*t+e*e),n):tt(0/0,0/0,n)}function ct(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function st(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function lt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function ft(n){return gt(n>>16,255&n>>8,255&n)}function ht(n){return ft(n)+""}function gt(n,t,e){return new pt(n,t,e)}function pt(n,t,e){this.r=n,this.g=t,this.b=e}function vt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function dt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Mt(u[0]),Mt(u[1]),Mt(u[2]))}return(i=Va.get(n))?t(i.r,i.g,i.b):(null!=n&&"#"===n.charAt(0)&&(4===n.length?(o=n.charAt(1),o+=o,a=n.charAt(2),a+=a,c=n.charAt(3),c+=c):7===n.length&&(o=n.substring(1,3),a=n.substring(3,5),c=n.substring(5,7)),o=parseInt(o,16),a=parseInt(a,16),c=parseInt(c,16)),t(o,a,c))}function mt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),K(r,u,c)}function yt(n,t,e){n=xt(n),t=xt(t),e=xt(e);var r=st((.4124564*n+.3575761*t+.1804375*e)/Fa),u=st((.2126729*n+.7151522*t+.072175*e)/Oa),i=st((.0193339*n+.119192*t+.9503041*e)/Ya);return ut(116*u-16,500*(r-u),200*(u-i))}function xt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Mt(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function _t(n){return"function"==typeof n?n:function(){return n}}function bt(n){return n}function wt(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),St(t,e,n,r)}}function St(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Xo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Go.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Xo.event;Xo.event=n;try{o.progress.call(i,c)}finally{Xo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Bo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Xo.rebind(i,o,"on"),null==r?i:i.get(kt(r))}function kt(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Et(){var n=At(),t=Ct()-n;t>24?(isFinite(t)&&(clearTimeout(Wa),Wa=setTimeout(Et,t)),Ba=0):(Ba=1,Ga(Et))}function At(){var n=Date.now();for(Ja=Xa;Ja;)n>=Ja.t&&(Ja.f=Ja.c(n-Ja.t)),Ja=Ja.n;return n}function Ct(){for(var n,t=Xa,e=1/0;t;)t.f?t=n?n.n=t.n:Xa=t.n:(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function zt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:bt;return function(n){var e=Qa.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=nc.get(g)||qt;var y=s&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var c=Xo.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x=n.lastIndexOf("."),M=0>x?n:n.substring(0,x),_=0>x?"":t+n.substring(x+1);!s&&f&&(M=i(M));var b=v.length+M.length+_.length+(y?0:u.length),w=l>b?new Array(b=l-b+1).join(r):"";return y&&(M=i(w+M)),u+=v,n=M+_,("<"===o?u+n+w:">"===o?w+u+n:"^"===o?w.substring(0,b>>=1)+u+n+w.substring(b):u+(y?n:w+n))+e}}}function qt(n){return n+""}function Tt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Rt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new ec(e-1)),1),e}function i(n,e){return t(n=new ec(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{ec=Tt;var r=new Tt;return r._=n,o(r,t,e)}finally{ec=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Dt(n);return c.floor=c,c.round=Dt(r),c.ceil=Dt(u),c.offset=Dt(i),c.range=a,n}function Dt(n){return function(t,e){try{ec=Tt;var r=new Tt;return r._=t,n(r,e)._}finally{ec=Date}}}function Pt(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++aa;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in uc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{ec=Tt;var t=new ec;return t._=n,r(t)}finally{ec=Date}}var r=t(n);return e.parse=function(n){try{ec=Tt;var t=r.parse(n);return t&&t._}finally{ec=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ee;var x=Xo.map(),M=jt(v),_=Ht(v),b=jt(d),w=Ht(d),S=jt(m),k=Ht(m),E=jt(y),A=Ht(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Ut(n.getDate(),t,2)},e:function(n,t){return Ut(n.getDate(),t,2)},H:function(n,t){return Ut(n.getHours(),t,2)},I:function(n,t){return Ut(n.getHours()%12||12,t,2)},j:function(n,t){return Ut(1+tc.dayOfYear(n),t,3)},L:function(n,t){return Ut(n.getMilliseconds(),t,3)},m:function(n,t){return Ut(n.getMonth()+1,t,2)},M:function(n,t){return Ut(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Ut(n.getSeconds(),t,2)},U:function(n,t){return Ut(tc.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Ut(tc.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Ut(n.getFullYear()%100,t,2)},Y:function(n,t){return Ut(n.getFullYear()%1e4,t,4)},Z:ne,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Bt,e:Bt,H:Jt,I:Jt,j:Wt,L:Qt,m:$t,M:Gt,p:l,S:Kt,U:Ot,w:Ft,W:Yt,x:c,X:s,y:Zt,Y:It,Z:Vt,"%":te};return t}function Ut(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function jt(n){return new RegExp("^(?:"+n.map(Xo.requote).join("|")+")","i")}function Ht(n){for(var t=new u,e=-1,r=n.length;++e68?1900:2e3)}function $t(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Bt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Wt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Jt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Gt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Kt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function Qt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ne(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(oa(t)/60),u=oa(t)%60;return e+Ut(r,"0",2)+Ut(u,"0",2)}function te(n,t,e){oc.lastIndex=0;var r=oc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function ee(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,c=Math.cos(t),s=Math.sin(t),l=i*s,f=u*c+l*Math.cos(a),h=l*o*Math.sin(a);hc.add(Math.atan2(h,f)),r=n,u=c,i=s}var t,e,r,u,i;gc.point=function(o,a){gc.point=n,r=(t=o)*Na,u=Math.cos(a=(e=a)*Na/2+Sa/4),i=Math.sin(a)},gc.lineEnd=function(){n(t,e)}}function se(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function le(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function fe(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function he(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function ge(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function pe(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function ve(n){return[Math.atan2(n[1],n[0]),X(n[2])]}function de(n,t){return oa(n[0]-t[0])a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new ke(e,n,null,!0),s=new ke(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new ke(r,n,null,!1),s=new ke(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),Se(i),Se(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function Se(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ae))}}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[],i.polygonStart()},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Xo.merge(g);var n=Le(m,p);g.length?we(g,Ne,n,e,i):n&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ce(),M=t(x);return y}}function Ae(n){return n.length>1}function Ce(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:g,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ne(n,t){return((n=n.x)[0]<0?n[1]-Ea-Aa:Ea-n[1])-((t=t.x)[0]<0?t[1]-Ea-Aa:Ea-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;hc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+Sa/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+Sa/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=_>=0?1:-1,w=b*_,S=w>Sa,k=p*x;if(hc.add(Math.atan2(k*b*Math.sin(w),v*M+k*Math.cos(w))),i+=S?_+b*ka:_,S^h>=e^m>=e){var E=fe(se(f),se(n));pe(E);var A=fe(u,E);pe(A);var C=(S^_>=0?-1:1)*X(A[2]);(r>C||r===C&&(E[0]||E[1]))&&(o+=S^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-Aa>i||Aa>i&&0>hc)^1&o}function ze(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?Sa:-Sa,c=oa(i-e);oa(c-Sa)0?Ea:-Ea),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=Sa&&(oa(e-u)Aa?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function Te(n,t,e,r){var u;if(null==n)u=e*Ea,r.point(-Sa,u),r.point(0,u),r.point(Sa,u),r.point(Sa,0),r.point(Sa,-u),r.point(0,-u),r.point(-Sa,-u),r.point(-Sa,0),r.point(-Sa,u);else if(oa(n[0]-t[0])>Aa){var i=n[0]i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?Sa:-Sa),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(de(e,g)||de(p,g))&&(p[0]+=Aa,p[1]+=Aa,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&de(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=se(n),u=se(t),o=[1,0,0],a=fe(r,u),c=le(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=fe(o,a),p=ge(o,f),v=ge(a,h);he(p,v);var d=g,m=le(p,d),y=le(d,d),x=m*m-y*(le(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=ge(d,(-m-M)/y);if(he(_,p),_=ve(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=oa(A-Sa)A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(oa(_[0]-w)Sa^(w<=_[0]&&_[0]<=S)){var L=ge(d,(-m+M)/y);return he(L,p),[_,ve(L)]}}}function u(t,e){var r=o?n:Sa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=oa(i)>Aa,c=cr(n,6*Na);return Ee(t,e,c,o?[0,-n]:[-Sa,n-Sa])}function De(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Pe(n,t,e,r){function u(r,u){return oa(r[0]-n)0?0:3:oa(r[0]-e)0?2:1:oa(r[1]-t)0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&Z(s,i,n)>0&&++t:i[1]<=r&&Z(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Ac,Math.min(Ac,n)),t=Math.max(-Ac,Math.min(Ac,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ce(),C=De(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Xo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&we(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function Ue(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function je(n){var t=0,e=Sa/3,r=nr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Sa/180,e=n[1]*Sa/180):[180*(t/Sa),180*(e/Sa)]},u}function He(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,X((i-(n*n+e*e)*u*u)/(2*u))]},e}function Fe(){function n(n,t){Nc+=u*n-r*t,r=n,u=t}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,t=r=i,e=u=o},Rc.lineEnd=function(){n(t,e)}}function Oe(n,t){Lc>n&&(Lc=n),n>qc&&(qc=n),zc>t&&(zc=t),t>Tc&&(Tc=t)}function Ye(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ie(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ie(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ie(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ze(n,t){dc+=n,mc+=t,++yc}function Ve(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);xc+=o*(t+n)/2,Mc+=o*(e+r)/2,_c+=o,Ze(t=n,e=r)}var t,e;Pc.point=function(r,u){Pc.point=n,Ze(t=r,e=u)}}function Xe(){Pc.point=Ze}function $e(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);xc+=o*(r+n)/2,Mc+=o*(u+t)/2,_c+=o,o=u*n-r*t,bc+=o*(r+n),wc+=o*(u+t),Sc+=3*o,Ze(r=n,u=t)}var t,e,r,u;Pc.point=function(i,o){Pc.point=n,Ze(t=r=i,e=u=o)},Pc.lineEnd=function(){n(t,e)}}function Be(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,ka)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:g};return a}function We(n){function t(n){return(a?r:e)(n)}function e(t){return Ke(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=se([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=oa(oa(w)-1)i||oa((y*L+x*z)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Na),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Je(n){var t=We(function(t,e){return n([t*La,e*La])});return function(n){return tr(t(n))}}function Ge(n){this.stream=n}function Ke(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function Qe(n){return nr(function(){return n})()}function nr(n){function t(n){return n=a(n[0]*Na,n[1]*Na),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*La,n[1]*La]}function r(){a=Ue(o=ur(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=We(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Ec,_=bt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=tr(M(o,f(_(n)))),l.valid=!0,l -},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Ec):Re((b=+n)*Na),u()):b},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Pe(n[0][0],n[0][1],n[1][0],n[1][1]):bt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Na,d=n[1]%360*Na,r()):[v*La,d*La]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Na,y=n[1]%360*Na,x=n.length>2?n[2]%360*Na:0,r()):[m*La,y*La,x*La]},Xo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function tr(n){return Ke(n,function(t,e){n.point(t*Na,e*Na)})}function er(n,t){return[n,t]}function rr(n,t){return[n>Sa?n-ka:-Sa>n?n+ka:n,t]}function ur(n,t,e){return n?t||e?Ue(or(n),ar(t,e)):or(n):t||e?ar(t,e):rr}function ir(n){return function(t,e){return t+=n,[t>Sa?t-ka:-Sa>t?t+ka:t,e]}}function or(n){var t=ir(n);return t.invert=ir(-n),t}function ar(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),X(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),X(l*r-a*u)]},e}function cr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=sr(e,u),i=sr(e,i),(o>0?i>u:u>i)&&(u+=o*ka)):(u=n+o*ka,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=ve([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function sr(n,t){var e=se(t);e[0]-=n,pe(e);var r=V(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Aa)%(2*Math.PI)}function lr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function fr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function hr(n){return n.source}function gr(n){return n.target}function pr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(J(r-t)+u*o*J(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*La,Math.atan2(o,Math.sqrt(r*r+u*u))*La]}:function(){return[n*La,t*La]};return p.distance=h,p}function vr(){function n(n,u){var i=Math.sin(u*=Na),o=Math.cos(u),a=oa((n*=Na)-t),c=Math.cos(a);Uc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;jc.point=function(u,i){t=u*Na,e=Math.sin(i*=Na),r=Math.cos(i),jc.point=n},jc.lineEnd=function(){jc.point=jc.lineEnd=g}}function dr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function mr(n,t){function e(n,t){var e=oa(oa(t)-Ea)u;u++){for(;r>1&&Z(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function kr(n,t){return n[0]-t[0]||n[1]-t[1]}function Er(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Ar(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Cr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Nr(){Jr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Jc.pop()||new Nr;return t.site=n,t}function zr(n){Or(n),$c.remove(n),Jc.push(n),Jr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];zr(n);for(var c=i;c.circle&&oa(e-c.circle.x)l;++l)s=a[l],c=a[l-1],$r(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Vr(c.site,s.site,null,u),Fr(c),Fr(s)}function Tr(n){for(var t,e,r,u,i=n.x,o=n.y,a=$c._;a;)if(r=Rr(a,o)-i,r>Aa)a=a.L;else{if(u=i-Dr(a,o),!(u>Aa)){r>-Aa?(t=a.P,e=a):u>-Aa?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if($c.insert(t,c),t||e){if(t===e)return Or(t),e=Lr(t.site),$c.insert(c,e),c.edge=e.edge=Vr(t.site,c.site),Fr(t),Fr(e),void 0;if(!e)return c.edge=Vr(t.site,c.site),void 0;Or(t),Or(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};$r(e.edge,s,p,M),c.edge=Vr(s,n,null,M),e.edge=Vr(n,p,null,M),Fr(t),Fr(e)}}function Rr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Dr(n,t){var e=n.N;if(e)return Rr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Pr(n){this.site=n,this.edges=[]}function Ur(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Xc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(oa(r-t)>Aa||oa(u-e)>Aa)&&(a.splice(o,0,new Br(Xr(i.site,l,oa(r-f)Aa?{x:f,y:oa(t-f)Aa?{x:oa(e-p)Aa?{x:h,y:oa(t-h)Aa?{x:oa(e-g)=-Ca)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Gc.pop()||new Hr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=Wc._;x;)if(m.yd||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.yr||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.yg){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.xr;++r)if(o=l[r],o.x==e[0]){if(o.i)if(null==s[o.i+1])for(s[o.i-1]+=o.x,s.splice(o.i,1),u=r+1;i>u;++u)l[u].i--;else for(s[o.i-1]+=o.x+s[o.i+1],s.splice(o.i,2),u=r+1;i>u;++u)l[u].i-=2;else if(null==s[o.i+1])s[o.i]=o.x;else for(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1),u=r+1;i>u;++u)l[u].i--;l.splice(r,1),i--,r--}else o.x=su(parseFloat(e[0]),parseFloat(o.x));for(;i>r;)o=l.pop(),null==s[o.i+1]?s[o.i]=o.x:(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1)),i--;return 1===s.length?null==s[0]?(o=l[0].x,function(n){return o(n)+""}):function(){return t}:function(n){for(r=0;i>r;++r)s[(o=l[r]).i]=o.x(n);return s.join("")}}function fu(n,t){for(var e,r=Xo.interpolators.length;--r>=0&&!(e=Xo.interpolators[r](n,t)););return e}function hu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(fu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function gu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function pu(n){return function(t){return 1-n(1-t)}}function vu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function du(n){return n*n}function mu(n){return n*n*n}function yu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function xu(n){return function(t){return Math.pow(t,n)}}function Mu(n){return 1-Math.cos(n*Ea)}function _u(n){return Math.pow(2,10*(n-1))}function bu(n){return 1-Math.sqrt(1-n*n)}function wu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ka*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ka/t)}}function Su(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function ku(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Eu(n,t){n=Xo.hcl(n),t=Xo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return rt(e+i*n,r+o*n,u+a*n)+""}}function Au(n,t){n=Xo.hsl(n),t=Xo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return nt(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Xo.lab(n),t=Xo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=zu(t,e),i=qu(Tu(e,t,-u))||0;t[0]*e[1]180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:su(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:su(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:su(g[0],p[0])},{i:e-2,x:su(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++ie;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ei(n){return n.reduce(ri,0)}function ri(n,t){return n+t[1]}function ui(n,t){return ii(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ii(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function oi(n){return[Xo.min(n),Xo.max(n)]}function ai(n,t){return n.parent==t.parent?1:2}function ci(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function si(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function li(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i0&&(n=r);return n}function fi(n,t){return n.x-t.x}function hi(n,t){return t.x-n.x}function gi(n,t){return n.depth-t.depth}function pi(n,t){function e(n,r){var u=n.children;if(u&&(o=u.length))for(var i,o,a=null,c=-1;++c=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function di(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function mi(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function yi(n,t){return n.value-t.value}function xi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Mi(n,t){n._pack_next=t,t._pack_prev=n}function _i(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function bi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(wi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],Ei(r,u,i),t(i),xi(r,i),r._pack_prev=i,xi(i,u),u=r._pack_next,o=3;s>o;o++){Ei(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(_i(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!_i(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.ro;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(Si)}}function wi(n){n._pack_next=n._pack_prev=n}function Si(n){delete n._pack_next,delete n._pack_prev}function ki(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++iu&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Ti(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ri(n){return n.rangeExtent?n.rangeExtent():Ti(n.range())}function Di(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Pi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Ui(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ls}function ji(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?ji:Di,c=r?Pu:Du;return o=u(n,t,c,e),a=u(t,n,c,fu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Nu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Ii(n,t)},i.tickFormat=function(t,e){return Zi(n,t,e)},i.nice=function(t){return Oi(n,t),u()},i.copy=function(){return Hi(n,t,e,r)},u()}function Fi(n,t){return Xo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Oi(n,t){return Pi(n,Ui(Yi(n,t)[2]))}function Yi(n,t){null==t&&(t=10);var e=Ti(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Ii(n,t){return Xo.range.apply(Xo,Yi(n,t))}function Zi(n,t,e){var r=Yi(n,t);return Xo.format(e?e.replace(Qa,function(n,t,e,u,i,o,a,c,s,l){return[t,e,u,i,o,a,c,s||"."+Xi(l,r),l].join("")}):",."+Vi(r[2])+"f")}function Vi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Xi(n,t){var e=Vi(t[2]);return n in fs?Math.abs(e-Vi(Math.max(Math.abs(t[0]),Math.abs(t[1]))))+ +("e"!==n):e-2*("%"===n)}function $i(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Pi(r.map(u),e?Math:gs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Ti(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++0;h--)o.push(i(s)*h);for(s=0;o[s]c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return hs;arguments.length<2?t=hs:"function"!=typeof t&&(t=Xo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return $i(n.copy(),t,e,r)},Fi(o,n)}function Bi(n,t,e){function r(t){return n(u(t))}var u=Wi(t),i=Wi(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Ii(e,n)},r.tickFormat=function(n,t){return Zi(e,n,t)},r.nice=function(n){return r.domain(Oi(e,n))},r.exponent=function(o){return arguments.length?(u=Wi(t=o),i=Wi(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Bi(n.copy(),t,e)},Fi(r,n)}function Wi(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Ji(n,t){function e(e){return o[((i.get(e)||"range"===t.t&&i.set(e,n.push(e)))-1)%o.length]}function r(t,e){return Xo.range(n.length).map(function(n){return t+e*n})}var i,o,a;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new u;for(var o,a=-1,c=r.length;++ae?[0/0,0/0]:[e>0?u[e-1]:n[0],et?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ki(n,t,e)},u()}function Qi(n,t){function e(e){return e>=e?t[Xo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Qi(n,t)},e}function no(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ii(n,t)},t.tickFormat=function(t,e){return Zi(n,t,e)},t.copy=function(){return no(n)},t}function to(n){return n.innerRadius}function eo(n){return n.outerRadius}function ro(n){return n.startAngle}function uo(n){return n.endAngle}function io(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=_t(e),p=_t(r);++f1&&u.push("H",r[0]),u.join("")}function so(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function Eo(n){return n.length<3?oo(n):n[0]+po(n,ko(n))}function Ao(n){for(var t,e,r,u=-1,i=n.length;++ue?s():(i.active=e,o.event&&o.event.start.call(n,l,t),o.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Xo.timer(function(){return p.c=c(r||1)?be:c,1},0,a),void 0)}function c(r){if(i.active!==e)return s();for(var u=r/g,a=f(u),c=v.length;c>0;)v[--c].call(n,a);return u>=1?(o.event&&o.event.end.call(n,l,t),s()):void 0}function s(){return--i.count?delete i[e]:delete n.__transition__,1}var l=n.__data__,f=o.ease,h=o.delay,g=o.duration,p=Ja,v=[];return p.t=h+a,r>=h?u(r-h):(p.c=u,void 0)},0,a)}}function Ho(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function Fo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Oo(n){return n.toISOString()}function Yo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Xo.bisect(js,u); -return i==js.length?[t.year,Yi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/js[i-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=Io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Ti(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Yo(n.copy(),t,e)},Fi(r,n)}function Io(n){return new Date(n)}function Zo(n){return JSON.parse(n.responseText)}function Vo(n){var t=Wo.createRange();return t.selectNode(Wo.body),t.createContextualFragment(n.responseText)}var Xo={version:"3.4.3"};Date.now||(Date.now=function(){return+new Date});var $o=[].slice,Bo=function(n){return $o.call(n)},Wo=document,Jo=Wo.documentElement,Go=window;try{Bo(Jo.childNodes)[0].nodeType}catch(Ko){Bo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{Wo.createElement("div").style.setProperty("opacity",0,"")}catch(Qo){var na=Go.Element.prototype,ta=na.setAttribute,ea=na.setAttributeNS,ra=Go.CSSStyleDeclaration.prototype,ua=ra.setProperty;na.setAttribute=function(n,t){ta.call(this,n,t+"")},na.setAttributeNS=function(n,t,e){ea.call(this,n,t,e+"")},ra.setProperty=function(n,t,e){ua.call(this,n,t+"",e)}}Xo.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},Xo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Xo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=e);)e=void 0;for(;++ur&&(e=r)}else{for(;++u=e);)e=void 0;for(;++ur&&(e=r)}return e},Xo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=e);)e=void 0;for(;++ue&&(e=r)}else{for(;++u=e);)e=void 0;for(;++ue&&(e=r)}return e},Xo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i=e);)e=u=void 0;for(;++ir&&(e=r),r>u&&(u=r))}else{for(;++i=e);)e=void 0;for(;++ir&&(e=r),r>u&&(u=r))}return[e,u]},Xo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i1&&(t=t.map(e)),t=t.filter(n),t.length?Xo.quantile(t.sort(Xo.ascending),.5):void 0},Xo.bisector=function(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n.call(t,t[i],i)r;){var i=r+u>>>1;er?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Xo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,e=Xo.min(arguments,t),r=new Array(e);++n=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var oa=Math.abs;Xo.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw new Error("infinite range");var u,i=[],o=e(oa(r)),a=-1;if(n*=o,t*=o,r*=o,0>r)for(;(u=n+r*++a)>t;)i.push(u/o);else for(;(u=n+r*++a)=o.length)return r?r.call(i,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=o[c++],d=new u;++g=o.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},o=[],a=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(Xo.map,e,0),0)},i.key=function(n){return o.push(n),i},i.sortKeys=function(n){return a[o.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},Xo.set=function(n){var t=new l;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},r(l,{has:i,add:function(n){return this[aa+n]=!0,n},remove:function(n){return n=aa+n,n in this&&delete this[n]},values:a,size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1))}}),Xo.behavior={},Xo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Xo.event=null,Xo.requote=function(n){return n.replace(la,"\\$&")};var la=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,fa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ha=function(n,t){return t.querySelector(n)},ga=function(n,t){return t.querySelectorAll(n)},pa=Jo[h(Jo,"matchesSelector")],va=function(n,t){return pa.call(n,t)};"function"==typeof Sizzle&&(ha=function(n,t){return Sizzle(n,t)[0]||null},ga=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},va=Sizzle.matchesSelector),Xo.selection=function(){return xa};var da=Xo.selection.prototype=[];da.select=function(n){var t,e,r,u,i=[];n=M(n);for(var o=-1,a=this.length;++o=0&&(e=n.substring(0,t),n=n.substring(t+1)),ma.hasOwnProperty(e)?{space:ma[e],local:n}:n}},da.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Xo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(b(t,n[t]));return this}return this.each(b(n,t))},da.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=k(n)).length,u=-1;if(t=e.classList){for(;++ur){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return Go.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(C(n,t,e))},da.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(N(t,n[t]));return this}return this.each(N(n,t))},da.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},da.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},da.append=function(n){return n=L(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},da.insert=function(n,t){return n=L(n),t=M(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},da.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},da.data=function(n,t){function e(n,e){var r,i,o,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new u,y=new u,x=[];for(r=-1;++rr;++r)p[r]=z(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,i,o=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++oi;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return x(u)},da.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},da.sort=function(n){n=T.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},da.size=function(){var n=0;return this.each(function(){++n}),n};var ya=[];Xo.selection.enter=D,Xo.selection.enter.prototype=ya,ya.append=da.append,ya.empty=da.empty,ya.node=da.node,ya.call=da.call,ya.size=da.size,ya.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(j(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(j(n,t,e))};var Ma=Xo.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ma.forEach(function(n){"on"+n in Wo&&Ma.remove(n)});var _a="onselectstart"in Wo?null:h(Jo.style,"userSelect"),ba=0;Xo.mouse=function(n){return Y(n,m())};var wa=/WebKit/.test(Go.navigator.userAgent)?-1:0;Xo.touches=function(n,t){return arguments.length<2&&(t=m().touches),t?Bo(t).map(function(t){var e=Y(n,t);return e.identifier=t.identifier,e}):[]},Xo.behavior.drag=function(){function n(){this.on("mousedown.drag",o).on("touchstart.drag",a)}function t(){return Xo.event.changedTouches[0].identifier}function e(n,t){return Xo.touches(n).filter(function(n){return n.identifier===t})[0]}function r(n,t,e,r){return function(){function o(){var n=t(l,g),e=n[0]-v[0],r=n[1]-v[1];d|=e|r,v=n,f({type:"drag",x:n[0]+c[0],y:n[1]+c[1],dx:e,dy:r})}function a(){m.on(e+"."+p,null).on(r+"."+p,null),y(d&&Xo.event.target===h),f({type:"dragend"})}var c,s=this,l=s.parentNode,f=u.of(s,arguments),h=Xo.event.target,g=n(),p=null==g?"drag":"drag-"+g,v=t(l,g),d=0,m=Xo.select(Go).on(e+"."+p,o).on(r+"."+p,a),y=O();i?(c=i.apply(s,arguments),c=[c.x-v[0],c.y-v[1]]):c=[0,0],f({type:"dragstart"})}}var u=y(n,"drag","dragstart","dragend"),i=null,o=r(g,Xo.mouse,"mousemove","mouseup"),a=r(t,e,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},Xo.rebind(n,u,"on")};var Sa=Math.PI,ka=2*Sa,Ea=Sa/2,Aa=1e-6,Ca=Aa*Aa,Na=Sa/180,La=180/Sa,za=Math.SQRT2,qa=2,Ta=4;Xo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=B(v),o=i/(qa*h)*(e*W(za*t+v)-$(v));return[r+o*s,u+o*l,i*e/B(za*t+v)]}return[r+n*s,u+n*l,i*Math.exp(za*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+Ta*f)/(2*i*qa*h),p=(c*c-i*i-Ta*f)/(2*c*qa*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/za;return e.duration=1e3*y,e},Xo.behavior.zoom=function(){function n(n){n.on(A,s).on(Pa+".zoom",f).on(C,h).on("dblclick.zoom",g).on(L,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(M.range().map(function(n){return(n-S.x)/S.k}).map(M.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Xo.mouse(r),g),a(i)}function e(){f.on(C,Go===r?h:null).on(N,null),p(l&&Xo.event.target===s),c(i)}var r=this,i=z.of(r,arguments),s=Xo.event.target,l=0,f=Xo.select(Go).on(C,n).on(N,e),g=t(Xo.mouse(r)),p=O();U.call(r),o(i)}function l(){function n(){var n=Xo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){for(var t=Xo.event.changedTouches,e=0,i=t.length;i>e;++e)v[t[e].identifier]=null;var o=n(),c=Date.now();if(1===o.length){if(500>c-x){var s=o[0],l=v[s.identifier];r(2*S.k),u(s,l),d(),a(p)}x=c}else if(o.length>1){var s=o[0],f=o[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function i(){for(var n,t,e,i,o=Xo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=m&&Math.sqrt(l/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}x=null,u(n,t),a(p)}function f(){if(Xo.event.touches.length){for(var t=Xo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}b.on(M,null).on(_,null),w.on(A,s).on(L,l),k(),c(p)}var h,g=this,p=z.of(g,arguments),v={},m=0,y=Xo.event.changedTouches[0].identifier,M="touchmove.zoom-"+y,_="touchend.zoom-"+y,b=Xo.select(Go).on(M,i).on(_,f),w=Xo.select(g).on(A,null).on(L,e),k=O();U.call(g),e(),o(p)}function f(){var n=z.of(this,arguments);m?clearTimeout(m):(U.call(this),o(n)),m=setTimeout(function(){m=null,c(n)},50),d();var e=v||Xo.mouse(this);p||(p=t(e)),r(Math.pow(2,.002*Ra())*S.k),u(e,p),a(n)}function h(){p=null}function g(){var n=z.of(this,arguments),e=Xo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Xo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var p,v,m,x,M,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=Da,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",L="touchstart.zoom",z=y(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=z.of(this,arguments),t=S;ks?Xo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Xo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?Da:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,M=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Xo.rebind(n,z,"on")};var Ra,Da=[0,1/0],Pa="onwheel"in Wo?(Ra=function(){return-Xo.event.deltaY*(Xo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in Wo?(Ra=function(){return Xo.event.wheelDelta},"mousewheel"):(Ra=function(){return-Xo.event.detail},"MozMousePixelScroll");G.prototype.toString=function(){return this.rgb()+""},Xo.hsl=function(n,t,e){return 1===arguments.length?n instanceof Q?K(n.h,n.s,n.l):dt(""+n,mt,K):K(+n,+t,+e)};var Ua=Q.prototype=new G;Ua.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,this.l/n)},Ua.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,n*this.l)},Ua.rgb=function(){return nt(this.h,this.s,this.l)},Xo.hcl=function(n,t,e){return 1===arguments.length?n instanceof et?tt(n.h,n.c,n.l):n instanceof it?at(n.l,n.a,n.b):at((n=yt((n=Xo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):tt(+n,+t,+e)};var ja=et.prototype=new G;ja.brighter=function(n){return tt(this.h,this.c,Math.min(100,this.l+Ha*(arguments.length?n:1)))},ja.darker=function(n){return tt(this.h,this.c,Math.max(0,this.l-Ha*(arguments.length?n:1)))},ja.rgb=function(){return rt(this.h,this.c,this.l).rgb()},Xo.lab=function(n,t,e){return 1===arguments.length?n instanceof it?ut(n.l,n.a,n.b):n instanceof et?rt(n.l,n.c,n.h):yt((n=Xo.rgb(n)).r,n.g,n.b):ut(+n,+t,+e)};var Ha=18,Fa=.95047,Oa=1,Ya=1.08883,Ia=it.prototype=new G;Ia.brighter=function(n){return ut(Math.min(100,this.l+Ha*(arguments.length?n:1)),this.a,this.b)},Ia.darker=function(n){return ut(Math.max(0,this.l-Ha*(arguments.length?n:1)),this.a,this.b)},Ia.rgb=function(){return ot(this.l,this.a,this.b)},Xo.rgb=function(n,t,e){return 1===arguments.length?n instanceof pt?gt(n.r,n.g,n.b):dt(""+n,gt,nt):gt(~~n,~~t,~~e)};var Za=pt.prototype=new G;Za.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),gt(Math.min(255,~~(t/n)),Math.min(255,~~(e/n)),Math.min(255,~~(r/n)))):gt(u,u,u)},Za.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),gt(~~(n*this.r),~~(n*this.g),~~(n*this.b))},Za.hsl=function(){return mt(this.r,this.g,this.b)},Za.toString=function(){return"#"+vt(this.r)+vt(this.g)+vt(this.b)};var Va=Xo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Va.forEach(function(n,t){Va.set(n,ft(t))}),Xo.functor=_t,Xo.xhr=wt(bt),Xo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=St(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new l,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Xo.csv=Xo.dsv(",","text/csv"),Xo.tsv=Xo.dsv(" ","text/tab-separated-values");var Xa,$a,Ba,Wa,Ja,Ga=Go[h(Go,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Xo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};$a?$a.n=i:Xa=i,$a=i,Ba||(Wa=clearTimeout(Wa),Ba=1,Ga(Et))},Xo.timer.flush=function(){At(),Ct()},Xo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ka=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Xo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Xo.round(n,Nt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((0>=e?e+1:e-1)/3)))),Ka[8+e/3]};var Qa=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,nc=Xo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Xo.round(n,Nt(n,t))).toFixed(Math.max(0,Math.min(20,Nt(n*(1+1e-15),t))))}}),tc=Xo.time={},ec=Date;Tt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){rc.setUTCDate.apply(this._,arguments)},setDay:function(){rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){rc.setTime.apply(this._,arguments)}};var rc=Date.prototype;tc.year=Rt(function(n){return n=tc.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),tc.years=tc.year.range,tc.years.utc=tc.year.utc.range,tc.day=Rt(function(n){var t=new ec(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),tc.days=tc.day.range,tc.days.utc=tc.day.utc.range,tc.dayOfYear=function(n){var t=tc.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=tc[n]=Rt(function(n){return(n=tc.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});tc[n+"s"]=e.range,tc[n+"s"].utc=e.utc.range,tc[n+"OfYear"]=function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)}}),tc.week=tc.sunday,tc.weeks=tc.sunday.range,tc.weeks.utc=tc.sunday.utc.range,tc.weekOfYear=tc.sundayOfYear;var uc={"-":"",_:" ",0:"0"},ic=/^\s*\d+/,oc=/^%/;Xo.locale=function(n){return{numberFormat:zt(n),timeFormat:Pt(n)}};var ac=Xo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Xo.format=ac.numberFormat,Xo.geo={},re.prototype={s:0,t:0,add:function(n){ue(n,this.t,cc),ue(cc.s,this.s,this),this.s?this.t+=cc.t:this.s=cc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var cc=new re;Xo.geo.stream=function(n,t){n&&sc.hasOwnProperty(n.type)?sc[n.type](n,t):ie(n,t)};var sc={Feature:function(n,t){ie(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++rn?4*Sa+n:n,gc.lineStart=gc.lineEnd=gc.point=g}};Xo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=se([t*Na,e*Na]);if(m){var u=fe(m,r),i=[u[1],-u[0],0],o=fe(i,u);pe(o),o=ve(o);var c=t-p,s=c>0?1:-1,v=o[0]*La*s,d=oa(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*La;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*La;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=oa(r)>180?r+(r>0?360:-360):r}else v=n,d=e;gc.point(n,e),t(n,e)}function i(){gc.lineStart()}function o(){u(v,d),gc.lineEnd(),oa(y)>Aa&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nhc?(l=-(h=180),f=-(g=90)):y>Aa?g=90:-Aa>y&&(f=-90),M[0]=l,M[1]=h -}};return function(n){g=h=-(l=f=1/0),x=[],Xo.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Xo.geo.centroid=function(n){pc=vc=dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,kc);var t=bc,e=wc,r=Sc,u=t*t+e*e+r*r;return Ca>u&&(t=xc,e=Mc,r=_c,Aa>vc&&(t=dc,e=mc,r=yc),u=t*t+e*e+r*r,Ca>u)?[0/0,0/0]:[Math.atan2(e,t)*La,X(r/Math.sqrt(u))*La]};var pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc,Sc,kc={sphere:g,point:me,lineStart:xe,lineEnd:Me,polygonStart:function(){kc.lineStart=_e},polygonEnd:function(){kc.lineStart=xe}},Ec=Ee(be,ze,Te,[-Sa,-Sa/2]),Ac=1e9;Xo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Pe(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Xo.geo.conicEqualArea=function(){return je(He)}).raw=He,Xo.geo.albers=function(){return Xo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Xo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Xo.geo.albers(),o=Xo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Xo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+Aa,f+.12*s+Aa],[l-.214*s-Aa,f+.234*s-Aa]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+Aa,f+.166*s+Aa],[l-.115*s-Aa,f+.234*s-Aa]]).stream(c).point,n},n.scale(1070)};var Cc,Nc,Lc,zc,qc,Tc,Rc={point:g,lineStart:g,lineEnd:g,polygonStart:function(){Nc=0,Rc.lineStart=Fe},polygonEnd:function(){Rc.lineStart=Rc.lineEnd=Rc.point=g,Cc+=oa(Nc/2)}},Dc={point:Oe,lineStart:g,lineEnd:g,polygonStart:g,polygonEnd:g},Pc={point:Ze,lineStart:Ve,lineEnd:Xe,polygonStart:function(){Pc.lineStart=$e},polygonEnd:function(){Pc.point=Ze,Pc.lineStart=Ve,Pc.lineEnd=Xe}};Xo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Xo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Cc=0,Xo.geo.stream(n,u(Rc)),Cc},n.centroid=function(n){return dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,u(Pc)),Sc?[bc/Sc,wc/Sc]:_c?[xc/_c,Mc/_c]:yc?[dc/yc,mc/yc]:[0/0,0/0]},n.bounds=function(n){return qc=Tc=-(Lc=zc=1/0),Xo.geo.stream(n,u(Dc)),[[Lc,zc],[qc,Tc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Je(n):bt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ye:new Be(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Xo.geo.albersUsa()).context(null)},Xo.geo.transform=function(n){return{stream:function(t){var e=new Ge(t);for(var r in n)e[r]=n[r];return e}}},Ge.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Xo.geo.projection=Qe,Xo.geo.projectionMutator=nr,(Xo.geo.equirectangular=function(){return Qe(er)}).raw=er.invert=er,Xo.geo.rotation=function(n){function t(t){return t=n(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t}return n=ur(n[0]%360*Na,n[1]*Na,n.length>2?n[2]*Na:0),t.invert=function(t){return t=n.invert(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t},t},rr.invert=er,Xo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ur(-n[0]*Na,-n[1]*Na,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=La,n[1]*=La}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=cr((t=+r)*Na,u*Na),n):t},n.precision=function(r){return arguments.length?(e=cr(t*Na,(u=+r)*Na),n):u},n.angle(90)},Xo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Na,u=n[1]*Na,i=t[1]*Na,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Xo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Xo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Xo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Xo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return oa(n%d)>Aa}).map(l)).concat(Xo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return oa(n%m)>Aa}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=lr(a,o,90),f=fr(r,e,y),h=lr(s,c,90),g=fr(i,u,y),n):y},n.majorExtent([[-180,-90+Aa],[180,90-Aa]]).minorExtent([[-180,-80-Aa],[180,80+Aa]])},Xo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=hr,u=gr;return n.distance=function(){return Xo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Xo.geo.interpolate=function(n,t){return pr(n[0]*Na,n[1]*Na,t[0]*Na,t[1]*Na)},Xo.geo.length=function(n){return Uc=0,Xo.geo.stream(n,jc),Uc};var Uc,jc={sphere:g,point:g,lineStart:vr,lineEnd:g,polygonStart:g,polygonEnd:g},Hc=dr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Xo.geo.azimuthalEqualArea=function(){return Qe(Hc)}).raw=Hc;var Fc=dr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},bt);(Xo.geo.azimuthalEquidistant=function(){return Qe(Fc)}).raw=Fc,(Xo.geo.conicConformal=function(){return je(mr)}).raw=mr,(Xo.geo.conicEquidistant=function(){return je(yr)}).raw=yr;var Oc=dr(function(n){return 1/n},Math.atan);(Xo.geo.gnomonic=function(){return Qe(Oc)}).raw=Oc,xr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ea]},(Xo.geo.mercator=function(){return Mr(xr)}).raw=xr;var Yc=dr(function(){return 1},Math.asin);(Xo.geo.orthographic=function(){return Qe(Yc)}).raw=Yc;var Ic=dr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Xo.geo.stereographic=function(){return Qe(Ic)}).raw=Ic,_r.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ea]},(Xo.geo.transverseMercator=function(){var n=Mr(_r),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[-n[1],n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},n.rotate([0,0])}).raw=_r,Xo.geom={},Xo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=_t(e),i=_t(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(kr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=Sr(a),l=Sr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Aa)*Aa,y:Math.round(o(n,t)/Aa)*Aa,i:t}})}var r=br,u=wr,i=r,o=u,a=Kc;return n?t(n):(t.links=function(n){return nu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return nu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(jr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=iu()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=_t(a),M=_t(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.xm&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=iu();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){ou(n,k,v,d,m,y)},g=-1,null==t){for(;++g=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ts.get(e)||ns,r=es.get(r)||bt,gu(r(e.apply(null,$o.call(arguments,1))))},Xo.interpolateHcl=Eu,Xo.interpolateHsl=Au,Xo.interpolateLab=Cu,Xo.interpolateRound=Nu,Xo.transform=function(n){var t=Wo.createElementNS(Xo.ns.prefix.svg,"g");return(Xo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:rs)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var rs={a:1,b:0,c:0,d:1,e:0,f:0};Xo.interpolateTransform=Ru,Xo.layout={},Xo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Xo.event.x,n.py=Xo.event.y,a.resume()}var e,r,u,i,o,a={},c=Xo.dispatch("start","tick","end"),s=[1,1],l=.9,f=us,h=is,g=-30,p=os,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Zu(t=Xo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Xo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++at;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Xo.behavior.drag().origin(bt).on("dragstart.force",Fu).on("drag.force",t).on("dragend.force",Ou)),arguments.length?(this.on("mouseover.force",Yu).on("mouseout.force",Iu).call(e),void 0):e},Xo.rebind(a,c,"on")};var us=20,is=1,os=1/0;Xo.layout.hierarchy=function(){function n(t,o,a){var c=u.call(e,t,o);if(t.depth=o,a.push(t),c&&(s=c.length)){for(var s,l,f=-1,h=t.children=new Array(s),g=0,p=o+1;++fg;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=bt,e=Qu,r=ni,u=Ku,i=Ju,o=Gu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:cs.get(t)||Qu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:ss.get(t)||ni,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var cs=Xo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ti),i=n.map(ei),o=Xo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Xo.range(n.length).reverse()},"default":Qu}),ss=Xo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ni});Xo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i0)for(i=-1;++i=l[0]&&a<=l[1]&&(o=c[Xo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=oi,u=ui;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=_t(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ii(n,t)}:_t(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Xo.layout.tree=function(){function n(n,i){function o(n,t){var r=n.children,u=n._tree;if(r&&(i=r.length)){for(var i,a,s,l=r[0],f=l,h=-1;++h0&&(di(mi(a,n,r),n,u),s+=u,l+=u),f+=a._tree.mod,s+=i._tree.mod,h+=c._tree.mod,l+=o._tree.mod;a&&!si(o)&&(o._tree.thread=a,o._tree.mod+=f-l),i&&!ci(c)&&(c._tree.thread=i,c._tree.mod+=s-h,r=n)}return r}var s=t.call(this,n,i),l=s[0];pi(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),o(l),a(l,-l._tree.prelim);var f=li(l,hi),h=li(l,fi),g=li(l,gi),p=f.x-e(f,h)/2,v=h.x+e(h,f)/2,d=g.depth||1;return pi(l,u?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(v-p)*r[0],n.y=n.depth/d*r[1],delete n._tree}),s}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,pi(a,function(n){n.r=+l(n.value)}),pi(a,bi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;pi(a,function(n){n.r+=f}),pi(a,bi),pi(a,function(n){n.r-=f})}return ki(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Xo.layout.hierarchy().sort(yi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Vu(n,e)},Xo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;pi(c,function(n){var t=n.children;t&&t.length?(n.x=Ci(t),n.y=Ai(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ni(c),f=Li(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return pi(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++ut?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++oe&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++ie.dx)&&(l=e.dx);++ie&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Xo.random.normal.apply(Xo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Xo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Xo.scale={};var ls={floor:bt,ceil:bt};Xo.scale.linear=function(){return Hi([0,1],[0,1],fu,!1)};var fs={s:1,g:1,p:1,r:1,e:1};Xo.scale.log=function(){return $i(Xo.scale.linear().domain([0,1]),10,!0,[1,10])};var hs=Xo.format(".0e"),gs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Xo.scale.pow=function(){return Bi(Xo.scale.linear(),1,[0,1])},Xo.scale.sqrt=function(){return Xo.scale.pow().exponent(.5)},Xo.scale.ordinal=function(){return Ji([],{t:"range",a:[[]]})},Xo.scale.category10=function(){return Xo.scale.ordinal().range(ps)},Xo.scale.category20=function(){return Xo.scale.ordinal().range(vs)},Xo.scale.category20b=function(){return Xo.scale.ordinal().range(ds)},Xo.scale.category20c=function(){return Xo.scale.ordinal().range(ms)};var ps=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(ht),vs=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(ht),ds=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(ht),ms=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(ht);Xo.scale.quantile=function(){return Gi([],[]) -},Xo.scale.quantize=function(){return Ki(0,1,[0,1])},Xo.scale.threshold=function(){return Qi([.5],[0,1])},Xo.scale.identity=function(){return no([0,1])},Xo.svg={},Xo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ys,a=u.apply(this,arguments)+ys,c=(o>a&&(c=o,o=a,a=c),a-o),s=Sa>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);return c>=xs?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=to,e=eo,r=ro,u=uo;return n.innerRadius=function(e){return arguments.length?(t=_t(e),n):t},n.outerRadius=function(t){return arguments.length?(e=_t(t),n):e},n.startAngle=function(t){return arguments.length?(r=_t(t),n):r},n.endAngle=function(t){return arguments.length?(u=_t(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ys;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ys=-Ea,xs=ka-Aa;Xo.svg.line=function(){return io(bt)};var Ms=Xo.map({linear:oo,"linear-closed":ao,step:co,"step-before":so,"step-after":lo,basis:mo,"basis-open":yo,"basis-closed":xo,bundle:Mo,cardinal:go,"cardinal-open":fo,"cardinal-closed":ho,monotone:Eo});Ms.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var _s=[0,2/3,1/3,0],bs=[0,1/3,2/3,0],ws=[0,1/6,2/3,1/6];Xo.svg.line.radial=function(){var n=io(Ao);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},so.reverse=lo,lo.reverse=so,Xo.svg.area=function(){return Co(bt)},Xo.svg.area.radial=function(){var n=Co(Ao);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Xo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ys,l=s.call(n,u,r)+ys;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Sa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=hr,o=gr,a=No,c=ro,s=uo;return n.radius=function(t){return arguments.length?(a=_t(t),n):a},n.source=function(t){return arguments.length?(i=_t(t),n):i},n.target=function(t){return arguments.length?(o=_t(t),n):o},n.startAngle=function(t){return arguments.length?(c=_t(t),n):c},n.endAngle=function(t){return arguments.length?(s=_t(t),n):s},n},Xo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=hr,e=gr,r=Lo;return n.source=function(e){return arguments.length?(t=_t(e),n):t},n.target=function(t){return arguments.length?(e=_t(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Xo.svg.diagonal.radial=function(){var n=Xo.svg.diagonal(),t=Lo,e=n.projection;return n.projection=function(n){return arguments.length?e(zo(t=n)):t},n},Xo.svg.symbol=function(){function n(n,r){return(Ss.get(t.call(this,n,r))||Ro)(e.call(this,n,r))}var t=To,e=qo;return n.type=function(e){return arguments.length?(t=_t(e),n):t},n.size=function(t){return arguments.length?(e=_t(t),n):e},n};var Ss=Xo.map({circle:Ro,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Cs)),e=t*Cs;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Xo.svg.symbolTypes=Ss.keys();var ks,Es,As=Math.sqrt(3),Cs=Math.tan(30*Na),Ns=[],Ls=0;Ns.call=da.call,Ns.empty=da.empty,Ns.node=da.node,Ns.size=da.size,Xo.transition=function(n){return arguments.length?ks?n.transition():n:xa.transition()},Xo.transition.prototype=Ns,Ns.select=function(n){var t,e,r,u=this.id,i=[];n=M(n);for(var o=-1,a=this.length;++oi;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Do(u,this.id)},Ns.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):R(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Ns.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Ru:fu,a=Xo.ns.qualify(n);return Po(this,"attr."+n,t,a.local?i:u)},Ns.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Xo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Ns.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Go.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=fu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Po(this,"style."+n,t,u)},Ns.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Go.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Ns.text=function(n){return Po(this,"text",n,Uo)},Ns.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Ns.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Xo.ease.apply(Xo,arguments)),R(this,function(e){e.__transition__[t].ease=n}))},Ns.delay=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Ns.duration=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Ns.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Es,u=ks;ks=e,R(this,function(t,r,u){Es=t.__transition__[e],n.call(t,t.__data__,r,u)}),Es=r,ks=u}else R(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Xo.dispatch("start","end"))).on(n,t)});return this},Ns.transition=function(){for(var n,t,e,r,u=this.id,i=++Ls,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,jo(e,s,i,r)),n.push(e)}return Do(o,i)},Xo.svg.axis=function(){function n(n){n.each(function(){var n,s=Xo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):bt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Aa),d=Xo.transition(p.exit()).style("opacity",Aa).remove(),m=Xo.transition(p).style("opacity",1),y=Ri(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Xo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Ho,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Ho,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=Fo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=Fo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Xo.scale.linear(),r=zs,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in qs?t+"":zs,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var zs="bottom",qs={top:1,right:1,bottom:1,left:1};Xo.svg.brush=function(){function n(i){i.each(function(){var i=Xo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,bt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Ts[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Xo.transition(i),h=Xo.transition(o);c&&(l=Ri(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ri(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Xo.event.keyCode&&(C||(x=null,L[0]-=l[1],L[1]-=f[1],C=2),d())}function p(){32==Xo.event.keyCode&&2==C&&(L[0]+=l[1],L[1]+=f[1],C=0,d())}function v(){var n=Xo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Xo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),L[0]=l[+(n[0]p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function y(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Xo.select("body").style("cursor",null),z.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Xo.select(Xo.event.target),w=a.of(_,arguments),S=Xo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=O(),L=Xo.mouse(_),z=Xo.select(Go).on("keydown.brush",u).on("keyup.brush",p);if(Xo.event.changedTouches?z.on("touchmove.brush",v).on("touchend.brush",y):z.on("mousemove.brush",v).on("mouseup.brush",y),S.interrupt().selectAll("*").interrupt(),C)L[0]=l[0]-L[0],L[1]=f[0]-L[1];else if(k){var q=+/w$/.test(k),T=+/^n/.test(k);M=[l[1-q]-L[0],f[1-T]-L[1]],L[0]=l[q],L[1]=f[T]}else Xo.event.altKey&&(x=L.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Xo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=y(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=Rs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,ks?Xo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=hu(l,t.x),r=hu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=Rs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=Rs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Xo.rebind(n,a,"on")};var Ts={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Rs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Ds=tc.format=ac.timeFormat,Ps=Ds.utc,Us=Ps("%Y-%m-%dT%H:%M:%S.%LZ");Ds.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Oo:Us,Oo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Oo.toString=Us.toString,tc.second=Rt(function(n){return new ec(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),tc.seconds=tc.second.range,tc.seconds.utc=tc.second.utc.range,tc.minute=Rt(function(n){return new ec(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),tc.minutes=tc.minute.range,tc.minutes.utc=tc.minute.utc.range,tc.hour=Rt(function(n){var t=n.getTimezoneOffset()/60;return new ec(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),tc.hours=tc.hour.range,tc.hours.utc=tc.hour.utc.range,tc.month=Rt(function(n){return n=tc.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),tc.months=tc.month.range,tc.months.utc=tc.month.utc.range;var js=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Hs=[[tc.second,1],[tc.second,5],[tc.second,15],[tc.second,30],[tc.minute,1],[tc.minute,5],[tc.minute,15],[tc.minute,30],[tc.hour,1],[tc.hour,3],[tc.hour,6],[tc.hour,12],[tc.day,1],[tc.day,2],[tc.week,1],[tc.month,1],[tc.month,3],[tc.year,1]],Fs=Ds.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",be]]),Os={range:function(n,t,e){return Xo.range(+n,+t,e).map(Io)},floor:bt,ceil:bt};Hs.year=tc.year,tc.scale=function(){return Yo(Xo.scale.linear(),Hs,Fs)};var Ys=Hs.map(function(n){return[n[0].utc,n[1]]}),Is=Ps.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",be]]);Ys.year=tc.year.utc,tc.scale.utc=function(){return Yo(Xo.scale.linear(),Ys,Is)},Xo.text=wt(function(n){return n.responseText}),Xo.json=function(n,t){return St(n,"application/json",Zo,t)},Xo.html=function(n,t){return St(n,"text/html",Vo,t)},Xo.xml=wt(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Xo):"object"==typeof module&&module.exports?module.exports=Xo:this.d3=Xo}(); \ No newline at end of file diff --git a/handlers/tutorial/client/assets/libs/documentScroll.js b/handlers/tutorial/client/assets/libs/documentScroll.js deleted file mode 100755 index c780c28..0000000 --- a/handlers/tutorial/client/assets/libs/documentScroll.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Объект с информацией о прокрутке в документе - * @return {object} - * top: сколько пикселей прокручено сверху, верхняя граница видимой части - * bottom: top + высота окна, то есть нижняя граница видимой части пикселей низ, - * height: полная высота страницы - */ -function getDocumentScroll() { - return { - top: getDocumentScrollTop(), - bottom: getDocumentScrollBottom(), - height: getDocumentScrollHeight() - }; -} - - -function getDocumentScrollTop() { - var html = document.documentElement; - var body = document.body; - - var scrollTop = html.scrollTop || body && body.scrollTop || 0; - scrollTop -= html.clientTop; // IE<8 - - return scrollTop; -} - -function getDocumentScrollHeight() { - var scrollHeight = document.documentElement.scrollHeight; - var clientHeight = document.documentElement.clientHeight; - - scrollHeight = Math.max(scrollHeight, clientHeight); - - return scrollHeight; -} - -function getDocumentScrollBottom() { - return getDocumentScrollTop() + document.documentElement.clientHeight; -} - diff --git a/handlers/tutorial/client/assets/libs/domtree.css b/handlers/tutorial/client/assets/libs/domtree.css deleted file mode 100755 index f16a1be..0000000 --- a/handlers/tutorial/client/assets/libs/domtree.css +++ /dev/null @@ -1,22 +0,0 @@ -/* -.domtree .node rect { - cursor: pointer; - fill: #fff; -} - -.domtree .node text { - fill: #333333; - font: 12px "Helvetica Nue", sans-serif; - pointer-events: none; -} - -.domtree path.link { - fill: none; - stroke: #BEC3C7; - stroke-width: 1px; -} -*/ -.domtree { - border: 1px solid #f5f2f0; - border-radius: 4px; -} diff --git a/handlers/tutorial/client/assets/libs/domtree.js b/handlers/tutorial/client/assets/libs/domtree.js deleted file mode 100755 index 721490a..0000000 --- a/handlers/tutorial/client/assets/libs/domtree.js +++ /dev/null @@ -1,241 +0,0 @@ -// ebook-converter removes CSS which styles SVG -// that's why I style here in JS - -function drawHtmlTree(json, nodeTarget, w, h) { - - if (typeof nodeTarget == 'string') { - nodeTarget = document.querySelectorAll(nodeTarget); - nodeTarget = nodeTarget[nodeTarget.length - 1]; - } - - w = w || 960; - h = h || 800; - - var i = 0, - barHeight = 30, - barWidth = 250, - barMargin = 2.5, - barRadius = 4, - duration = 400, - root; - - var tree, diagonal, vis; - - function update(source) { - // Compute the flattened node list. TODO use d3.layout.hierarchy. - var nodes = tree.nodes(root); - // Compute the "layout". - nodes.forEach(function(n, i) { - n.x = i * barHeight; - }); - - // Update the nodes… - var node = vis.selectAll("g.node") - .data(nodes, function(d) { - return d.id || (d.id = ++i); - }); - - var nodeEnter = node.enter().append("svg:g") - .attr("class", "node") - .attr("transform", function(d) { - return "translate(" + (source.y0) + "," + (source.x0) + ")"; - }) - .style("opacity", 1e-6); - - // Enter any new nodes at the parent's previous position. - nodeEnter.append("svg:rect") - .attr("y", function () { return -barHeight / 2 + barMargin; }) - .attr("x", -5) - .attr("rx",barRadius) - .attr("ry",barRadius) - .attr("height", barHeight-barMargin*2) - .attr("width", barWidth) - .style("fill", color) - .style("cursor", "pointer") - .on("click", click); - - - nodeEnter.append("svg:text") - .attr("dy", 4.5) - .attr("dx", 3.5) - .style('fill', 'black') - .style("pointer-events", "none"); - - - nodeEnter.append("svg:text") - .attr("dy", 4.5) - .attr("dx", function(d) { - return d.content ? 5.5 : 16.5; - }) - .style('font', '14px Consolas, monospace') - .style('fill', '#333') - .style("pointer-events", "none") - .text(function(d) { - var text = d.name; - if (d.content) { - if (/^\s*$/.test(d.content)) { - text += " " + d.content.replace(/\n/g, "↵").replace(/ /g, '␣'); - } else { - text += " " + d.content; - } - } - return text; - }); - - // Transition nodes to their new position. - nodeEnter.transition() - .duration(duration) - .attr("transform", function(d, i) { - return "translate(" + (d.y) + "," + (d.x) + ")"; - }) - .style("opacity", 1); - - node.transition() - .duration(duration) - .attr("transform", function(d) { - return "translate(" + d.y + "," + (d.x) + ")"; - }) - .style("opacity", 1) - .select("text") - .text(function(d) { - if (d.content) return ""; - if (d._children) { - return "▸ "; - } else { - return "▾ "; - } - }); - - // Transition exiting nodes to the parent's new position. - node.exit().transition() - .duration(duration) - .attr("transform", function(d) { - return "translate(" + source.y + "," + source.x + ")"; - }) - .style("opacity", 1e-6) - .remove(); - - // Update the links… - var link = vis.selectAll("path.link") - .data(tree.links(nodes), function(d) { - return d.target.id; - }); - - // Enter any new links at the parent's previous position. - link.enter().insert("svg:path", "g") - .attr("class", "link") - .style('fill', 'none') - .style('stroke', '#BEC3C7') - .style('stroke-width', '1px') - .attr("d", function(d) { - var o = { - x: source.x0, - y: source.y0 - }; - return diagonal({ - source: o, - target: o - }); - }) - .transition() - .duration(duration) - .attr("d", diagonal); - - // Transition links to their new position. - link.transition() - .duration(duration) - .attr("d", diagonal); - - // Transition exiting nodes to the parent's new position. - link.exit().transition() - .duration(duration) - .attr("d", function(d) { - var o = { - x: source.x, - y: source.y - }; - return diagonal({ - source: o, - target: o - }); - }) - .remove(); - - // Stash the old positions for transition. - nodes.forEach(function(d) { - d.x0 = d.x; - d.y0 = d.y; - }); - } - - // Toggle children on click. - - function click(d) { - if (d.children) { - d._children = d.children; - d.children = null; - } else { - d.children = d._children; - d._children = null; - } - update(d); - } - - function color(d) { - return d.nodeType == 1 ? "#CEE0F4" : - d.nodeType == 3 ? '#FFDE99' : '#CFCE95'; - } - - function drawTree(json) { - - tree = d3.layout.tree() - .size([h, 100]); - - diagonal = function(d){ - var deltaX = 7; - var deltaY = 0; - var points = [ - "M", [d.source.y+deltaX, d.source.x+deltaY].join(","), - "L", [d.source.y+deltaX, d.target.x+deltaY].join(","), - "L", [d.target.y+deltaX, d.target.x+deltaY].join(","), - ]; - return points.join(""); - }; - - - vis = d3.select(nodeTarget).append("svg:svg") - .attr("width", w) - .attr("height", h) - .append("svg:g") - .attr("transform", "translate(20,30)"); - - json.x0 = 0; - json.y0 = 0; - update(root = json); - } - - nodeTarget.innerHTML = ""; - - drawTree(json); - -} - - -function node2json(node) { - var obj = { - name: node.nodeName, - nodeType: node.nodeType - }; - - if (node.nodeType != 1) { - obj.content = node.data; - return obj; - } - - obj.children = []; - for(var i=0; i1, because when visiting http://javascript.local/native-prototypes#native-prototype-change, - // top may be 0.375 or kind of... - if (h2.getBoundingClientRect().top > 1) break; - } - i--; // we need the one before it (currently reading) - - if (i >= 0) { - let href = h2s[i].firstElementChild && h2s[i].firstElementChild.getAttribute('href'); - let li = document.querySelector('.sidebar__navigation-link a[href="' + href + '"]'); - if (href && li) { - li.classList.add('sidebar__navigation-link_active'); - } - } - - } - - document.addEventListener('DOMContentLoaded', function() { - highlight(); - - window.addEventListener('scroll', highlight); - }); - - -} - - -function initTaskButtons() { - // solution button - delegate(document, '.task__solution', 'click', function(event) { - event.target.closest('.task').classList.toggle('task__answer_open'); - }); - - // close solution button - delegate(document, '.task__answer-close', 'click', function(event) { - event.target.closest('.task').classList.toggle('task__answer_open'); - }); - - // every step button (if any steps) - delegate(document, '.task__step-show', 'click', function(event) { - event.target.closest('.task__step').classList.toggle('task__step_open'); - }); -} - -function initFolderList() { - delegate(document, '.lessons-list__lesson_level_1 > .lessons-list__link', 'click', function(event) { - let link = event.delegateTarget; - let openFolder = link.closest('.lessons-list').querySelector('.lessons-list__lesson_open'); - // close the previous open folder (thus making an accordion) - if (openFolder && openFolder !== link.parentNode) { - openFolder.classList.remove('lessons-list__lesson_open'); - } - link.parentNode.classList.toggle('lessons-list__lesson_open'); - event.preventDefault(); - }); -} - -window.runDemo = function(button) { - - let demoElem; - let parent = button; - - /* jshint -W084 */ - while (parent = parent.parentElement) { - demoElem = parent.querySelector('[data-demo]'); - if (demoElem) break; - } - - if (!demoElem) { - alert("Error, no demo element"); - } else { - /* jshint -W061 */ - eval(demoElem.textContent); - } - -}; - -init(); diff --git a/handlers/tutorial/controller/article.js b/handlers/tutorial/controller/article.js deleted file mode 100755 index 42d9f1a..0000000 --- a/handlers/tutorial/controller/article.js +++ /dev/null @@ -1,312 +0,0 @@ -'use strict'; - -const TutorialTree = require('../models/tutorialTree'); -const Article = require('../models/article'); -const Task = require('../models/task'); -const ArticleRenderer = require('../renderer/articleRenderer'); -const TaskRenderer = require('../renderer/taskRenderer'); -const _ = require('lodash'); -const makeAnchor = require('textUtil/makeAnchor'); -const t = require('i18n'); -const localStorage = require('localStorage').instance(); - -exports.get = async function(ctx, next) { - - let renderedArticle = await localStorage.getOrGenerate( - 'tutorial:article:' + ctx.params.slug, - () => renderArticle(ctx), - process.env.TUTORIAL_EDIT - ); - - if (!renderedArticle) { - await next(); - return; - } - - let locals = renderedArticle; - - locals.sitetoolbar = true; - - locals.githubLink = renderedArticle.githubLink; - locals.siteToolbarCurrentSection = "tutorial"; - - if (!renderedArticle.isFolder) { - locals.comments = true; - } - - let sections = []; - if (renderedArticle.isFolder) { - - sections.push({ - title: t('tutorial.article.sibling_chapters'), - links: renderedArticle.siblings - }); - - } else { - - sections.push({ - title: t('tutorial.article.chapter'), - links: [renderedArticle.breadcrumbs[renderedArticle.breadcrumbs.length-1]] - }); - - let headerLinks = renderedArticle.headers - .filter(function(header) { - // [level, titleHtml, anchor] - return header.level === 2; - }).map(function(header) { - return { - title: header.title, - url: '#' + header.anchor - }; - }); - - if (headerLinks.length) { - sections.push({ - title: t('tutorial.article.lesson_navigation'), - links: headerLinks - }); - } - - } - - if (!renderedArticle.isFolder) { - - let section2 = { - class: '_separator_before', - links: [] - }; - - if (renderedArticle.tasks.length) { - section2.links.push({ - title: t('tutorial.article.tasks') + ' (' + renderedArticle.tasks.length + ')', - url: '#tasks' - }); - } - - section2.links.push({ - title: t('locales.comments'), - url: '#comments' - }); - - sections.push(section2); - - } - - locals.sidebar = { - sections: sections - }; - - ctx.body = ctx.render(renderedArticle.isFolder ? "folder" : "article", locals); - -}; - -// body -// metadata -// modified -// title -// isFolder -// prev -// next -// path -// siblings -async function renderArticle(ctx) { - - let slug = ctx.params.slug; - - const tree = TutorialTree.instance(); - - const article = tree.bySlug(slug); - - // console.log("HERE", slug, article); - - if (!article || !(article instanceof Article)) { - return null; - } - - ctx.log.debug("article", article); - - let renderer = new ArticleRenderer(); - - let rendered = await renderer.render(article); - - // ctx.log.debug("rendered"); - - rendered.isFolder = article.isFolder; - rendered.modified = article.modified; - rendered.title = article.title; - rendered.isFolder = article.isFolder; - rendered.weight = article.weight; - rendered.githubLink = article.githubLink; - rendered.canonicalPath = article.getUrl(); - - await renderProgress(); - - await renderPrevNext(); - await renderBreadCrumb(); - await renderSiblings(); - await renderChildren(); - - if (!article.isFolder) { - await renderTasks(); - } - - - // strip / and /tutorial - rendered.level = rendered.breadcrumbs.length - 2; // starts at 0 - - if (article.isFolder) { - // levelMax is 2 for deep courses or 1 for plain courses - - rendered.levelMax = rendered.level + 1; - if (article.children.length && tree.bySlug(article.children[0]).isFolder) { - rendered.levelMax++; - } - } - - - async function renderPrevNext() { - - let prev = tree.getPrev(article.slug); - - if (prev) { - prev = tree.bySlug(prev); - rendered.prev = { - url: prev.getUrl(), - title: prev.title - }; - } - - let next = tree.getNext(article.slug); - if (next) { - next = tree.bySlug(next); - rendered.next = { - url: next.getUrl(), - title: next.title - }; - } - } - - - - async function renderProgress() { - let parent = article; - while (parent.parent) { - parent = tree.bySlug(parent.parent); - } - - let bookRoot = parent; - // now bookroot is 1st level tree item, book root, let's count items in it - - //console.log(bookRoot); - - let bookLeafCount = 0; - let bookChildNumber; - function countChildren(article) { - if (tree === article) { - bookChildNumber = bookLeafCount + 1; - } - - if (!tree.children) { - bookLeafCount++; - } else { - tree.children.forEach(countChildren); - } - } - - countChildren(bookRoot); - - if (!(bookChildNumber == 1 && rendered.isFolder)) { - // not on top level first chapters - rendered.bookLeafCount = bookLeafCount; - rendered.bookChildNumber = bookChildNumber; - } - - //console.log(bookLeafCount, bookChildNumber); - } - - async function renderBreadCrumb() { - let path = []; - let parent = article.parent; - while (parent) { - let a = tree.bySlug(parent); - path.push({ - title: a.title, - url: a.getUrl() - }); - parent = a.parent; - } - path.push({ - title: t('locales.tutorial'), - url: '/' - }); - path = path.reverse(); - - rendered.breadcrumbs = path; - } - - async function renderSiblings() { - let siblings = tree.getSiblings(article.slug); - rendered.siblings = siblings.map(slug => { - let sibling = tree.bySlug(slug); - return { - title: sibling.title, - url: sibling.getUrl() - }; - }); - } - - async function renderChildren() { - if (!article.isFolder) return; - let children = article.children || []; - rendered.children = children.map(slug => { - let child = tree.bySlug(slug); - let renderedChild = { - title: child.title, - url: child.getUrl(), - weight: child.weight - }; - - if (child.isFolder) { - renderedChild.children = (child.children || []).map((slug) => { - let subChild = tree.bySlug(slug); - return { - title: subChild.title, - url: subChild.getUrl(), - weight: subChild.weight - }; - }); - } - - return renderedChild; - }); - } - - async function renderTasks() { - let tasks = article.children.map(slug => { - return tree.bySlug(slug); - }); - - const taskRenderer = new TaskRenderer(); - - rendered.tasks = []; - - for (let task of tasks) { - let taskRendered = await taskRenderer.render(task); - rendered.tasks.push({ - url: task.getUrl(), - title: task.title, - anchor: makeAnchor(task.title), - importance: task.importance, - content: taskRendered.content, - solution: taskRendered.solution - }); - - } - - } - - - return rendered; - -} - diff --git a/handlers/tutorial/controller/task.js b/handlers/tutorial/controller/task.js deleted file mode 100755 index e264080..0000000 --- a/handlers/tutorial/controller/task.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; - -const Task = require('../models/task'); -const Article = require('../models/article'); -const TutorialTree = require('../models/tutorialTree'); -const TaskRenderer = require('../renderer/taskRenderer'); -const t = require('i18n'); - -exports.get = async function(ctx, next) { - - const task = TutorialTree.instance().bySlug(ctx.params.slug); - - if (!task || !(task instanceof Task)) { - await next(); - return; - } - - const renderer = new TaskRenderer(); - - const rendered = await renderer.render(task); - - ctx.locals.githubLink = task.githubLink; - - let breadcrumbs = []; - - let parentSlug = task.parent; - while (true) { - let parent = TutorialTree.instance().bySlug(parentSlug); - if (!parent) break; - breadcrumbs.push({ - url: parent.getUrl(), - title: parent.title - }); - parentSlug = parent.parent; - } - breadcrumbs.push({ - title: t('locales.tutorial'), - url: '/' - }); - - ctx.locals.breadcrumbs = breadcrumbs.reverse(); - - ctx.locals.siteToolbarCurrentSection = "tutorial"; - - // No support for task.libs & head just yet (not needed?) - ctx.locals.title = task.title; - - ctx.locals.task = { - title: task.title, - importance: task.importance, - content: rendered.content, - solution: rendered.solution - }; - - ctx.locals.articleUrl = TutorialTree.instance().bySlug(task.parent).getUrl(); - - ctx.body = ctx.render("task"); -}; - diff --git a/handlers/tutorial/controller/zipview.js b/handlers/tutorial/controller/zipview.js deleted file mode 100755 index 2d88cdc..0000000 --- a/handlers/tutorial/controller/zipview.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const TutorialViewStorage = require('../models/tutorialViewStorage'); - -exports.get = async function(ctx) { - let view; - - let storage = TutorialViewStorage.instance(); - for(let webpath in storage.getAll()) { - view = storage.get(webpath); - if (view.plunkId == ctx.query.plunkId) { - ctx.set('Content-Type', 'application/zip'); - ctx.body = view.getZip(); - } - } - -}; \ No newline at end of file diff --git a/handlers/tutorial/figuresImporter.js b/handlers/tutorial/figuresImporter.js deleted file mode 100755 index 454278c..0000000 --- a/handlers/tutorial/figuresImporter.js +++ /dev/null @@ -1,126 +0,0 @@ -const util = require('util'); -const fs = require('fs'); -const fse = require('fs-extra'); -const path = require('path'); -const config = require('config'); -const glob = require("glob"); - -const log = require('log')(); - -const execSync = require('child_process').execSync; - -// TODO: use htmlhint/jslint for html/js examples - -module.exports = class FiguresImporter { - constructor(options) { - this.sketchtool = options.sketchtool || '/Applications/Sketch.app/Contents/Resources/sketchtool/bin/sketchtool'; - - this.root = fs.realpathSync(options.root); - this.figuresFilePath = options.figuresFilePath; - } - - async syncFigures() { - - if (!fs.existsSync(this.sketchtool)) { - log.info("No sketchtool"); - return; - } - - let outputDir = path.join(config.tmpRoot, 'sketchtool'); - - fse.removeSync(outputDir); - fse.mkdirsSync(outputDir); - - let artboardsByPages = JSON.parse(execSync(this.sketchtool + ' list artboards "' + this.figuresFilePath + '"', { - encoding: 'utf-8' - })); - - let artboards = artboardsByPages - .pages - .reduce(function (prev, current) { - return prev.concat(current.artboards); - }, []); - - let svgIds = []; - let pngIds = []; - let artboardsExported = []; - - for (let i = 0; i < artboards.length; i++) { - let artboard = artboards[i]; - - // only allow artboards with extensions are exported - // others are temporary / helpers - let ext = path.extname(artboard.name).slice(1); - if (ext == 'png') { - pngIds.push(artboard.id); - artboardsExported.push(artboard); - } - if (ext == 'svg') { - svgIds.push(artboard.id); - artboardsExported.push(artboard); - } - } - - // NB: Artboards are NOT trimmed (sketchtool doesn't do that yet) - execSync(this.sketchtool + ' export artboards "' + this.figuresFilePath + '" ' + - '--overwriting=YES --trimmed=YES --formats=png --scales=1,2 --output="' + outputDir + '" --items=' + pngIds.join(','), { - stdio: 'inherit', - encoding: 'utf-8' - }); - - // NB: Artboards are NOT trimmed (sketchtool doesn't do that yet) - execSync(this.sketchtool + ' export artboards "' + this.figuresFilePath + '" ' + - '--overwriting=YES --trimmed=YES --formats=svg --output="' + outputDir + '" --items=' + svgIds.join(','), { - stdio: 'inherit', - encoding: 'utf-8' - }); - - // files are exported as array-pop.svg.svg, metric-css.png@2x.png - // => remove first extension - let images = glob.sync(path.join(outputDir, '*.*')); - images.forEach(function (image) { - fs.renameSync(image, image.replace(/.(svg|png)/, '')); - }); - - let allFigureFilePaths = glob.sync(path.join(this.root, '**/*.{png,svg}')); - - function findArtboardPaths(artboard) { - - let paths = []; - for (let j = 0; j < allFigureFilePaths.length; j++) { - if (path.basename(allFigureFilePaths[j]) == artboard.name) { - paths.push(path.dirname(allFigureFilePaths[j])); - } - } - - return paths; - - } - - // copy should trigger folder resync on watch - // and that's right (img size changed, must be rerendered) - - for (let i = 0; i < artboardsExported.length; i++) { - let artboard = artboardsExported[i]; - let artboardPaths = findArtboardPaths(artboard); - if (!artboardPaths.length) { - log.error("Artboard path not found " + artboard.name); - continue; - } - - for (let j = 0; j < artboardPaths.length; j++) { - let artboardPath = artboardPaths[j]; - - log.info("syncFigure move " + artboard.name + " -> " + artboardPath); - fse.copySync(path.join(outputDir, artboard.name), path.join(artboardPath, artboard.name)); - if (path.extname(artboard.name) == '.png') { - let x2Name = artboard.name.replace('.png', '@2x.png'); - fse.copySync(path.join(outputDir, x2Name), path.join(artboardPath, x2Name)); - } - } - - } - - - }; -}; diff --git a/handlers/tutorial/index.js b/handlers/tutorial/index.js deleted file mode 100755 index 397e4a5..0000000 --- a/handlers/tutorial/index.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const mountHandlerMiddleware = require('lib/mountHandlerMiddleware'); - -const t = require('i18n'); - -t.requirePhrase('tutorial', 'article'); -t.requirePhrase('tutorial', 'task'); - - -exports.TutorialViewStorage = require('./models/tutorialViewStorage'); -exports.Article = require('./models/article'); -exports.Task = require('./models/task'); -exports.TutorialTree = require('./models/tutorialTree'); -exports.TutorialView = require('./models/tutorialView'); - -exports.TaskRenderer = require('./renderer/taskRenderer'); -exports.ArticleRenderer = require('./renderer/articleRenderer'); - -exports.runImport = require('./lib/runImport'); - - -exports.init = function(app) { - app.use(mountHandlerMiddleware('/', __dirname)); -}; - -exports.boot = require('./lib/boot'); \ No newline at end of file diff --git a/handlers/tutorial/lib/boot.js b/handlers/tutorial/lib/boot.js deleted file mode 100644 index ce184db..0000000 --- a/handlers/tutorial/lib/boot.js +++ /dev/null @@ -1,23 +0,0 @@ -const path = require('path'); -const fs = require('mz/fs'); -const config = require('config'); -const TutorialViewStorage = require('../models/tutorialViewStorage'); -const TutorialTree = require('../models/tutorialTree'); - -module.exports = async function() { - if (process.env.TUTORIAL_EDIT) { - // imported and watched by another task - return; - } - if (!await fs.exists(path.join(config.cacheRoot, 'tutorialTree.json'))) { - throw new Error("Tutorial not imported? No cache/tutorialTree.json"); - } - - let tree = await fs.readFile(path.join(config.cacheRoot, 'tutorialTree.json')); - tree = JSON.parse(tree); - TutorialTree.instance().load(tree); - - let views = await fs.readFile(path.join(config.cacheRoot, 'tutorialViewStorage.json')); - views = JSON.parse(views); - TutorialViewStorage.instance().load(tree); -}; \ No newline at end of file diff --git a/handlers/tutorial/lib/resolveTutorialLinks.js b/handlers/tutorial/lib/resolveTutorialLinks.js deleted file mode 100755 index 6a33cea..0000000 --- a/handlers/tutorial/lib/resolveTutorialLinks.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -/** - * Parser plugin - * For links info:articleSlug & info:task/taskSlug - * Load titles from db - */ -const assert = require('assert'); - -assert(typeof IS_CLIENT === 'undefined'); - -const TutorialTree = require('../models/tutorialTree'); - -const Token = require('markit').Token; -const t = require('i18n'); -const url = require('url'); -const tokenUtils = require('markit').tokenUtils; - -t.requirePhrase('tutorial', 'article'); -t.requirePhrase('tutorial', 'task'); - - -module.exports = async function (tokens) { - - let isEmptyLink, isAutoLink; - for (let idx = 0; idx < tokens.length; idx++) { - let token = tokens[idx]; - - if (token.type != 'inline' || !token.children) continue; - for (let i = 0; i < token.children.length; i++) { - let inlineToken = token.children[i]; - - if (inlineToken.type == 'link_open') { - let href = tokenUtils.attrGet(inlineToken, 'href'); - if (!href.startsWith('info:')) continue; - - let urlParsed = url.parse(href.slice(5)); - let pathname = urlParsed.pathname; - - isEmptyLink = token.children[i + 1].type == 'link_close'; - isAutoLink = token.children[i + 1].type == 'text' && - token.children[i + 1].content == href && - token.children[i + 2].type == 'link_close'; - - if (pathname.startsWith('task/')) { - let task = TutorialTree.instance().bySlug(pathname.slice('task/'.length)); - if (task) replaceLink(token.children, i, task.title, task.getUrl(), urlParsed); - else replaceLinkWithError(token.children, i, t('tutorial.task.task_not_found', {path: pathname})); - } else { - let article = TutorialTree.instance().bySlug(pathname); - if (article) replaceLink(token.children, i, article.title, article.getUrl(), urlParsed); - else replaceLinkWithError(token.children, i, t('tutorial.article.article_not_found', {path: pathname})); - } - - } - } - - } - - function replaceLinkWithError(children, linkOpenIdx, content) { - let token = new Token('markdown_error_inline', '', 0); - token.content = content; - token.level = children[linkOpenIdx].level; - - let linkCloseIdx = linkOpenIdx + 1; - while (children[linkCloseIdx].type != 'link_close') linkCloseIdx++; - children.splice(linkOpenIdx, linkCloseIdx - linkOpenIdx + 1, token); - } - - - function replaceLink(children, linkOpenIdx, title, url, prevUrlParsed) { - if (prevUrlParsed.query) url += '?' + prevUrlParsed.query; - if (prevUrlParsed.hash) url += prevUrlParsed.hash; - - tokenUtils.attrReplace(children[linkOpenIdx], 'href', url); - - // for empty & autolinks also replace children - if (isEmptyLink) { - let token = new Token('text', '', 0); - token.content = title; - token.level = children[linkOpenIdx].level; - children.splice(linkOpenIdx + 1, 0, token); - } else if (isAutoLink) { - children[linkOpenIdx + 1].content = title; - } - } - -}; - - diff --git a/handlers/tutorial/lib/runImport.js b/handlers/tutorial/lib/runImport.js deleted file mode 100644 index 6502143..0000000 --- a/handlers/tutorial/lib/runImport.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -let TutorialImporter = require('../lib/tutorialImporter'); -let TutorialTree = require('../models/tutorialTree'); -let TutorialViewStorage = require('../models/tutorialViewStorage'); -let config = require('config'); -let fs = require('mz/fs'); -let path = require('path'); -let log = require('log')(); - -module.exports = async function() { - - let tree = TutorialTree.instance(); - let viewStorage = TutorialViewStorage.instance(); - - let importer = new TutorialImporter({ - root: config.tutorialRoot - }); - - tree.clear(); - viewStorage.clear(); - - let subRoots = fs.readdirSync(config.tutorialRoot); - - for (let subRoot of subRoots) { - if (!parseInt(subRoot)) continue; - await importer.sync(path.join(config.tutorialRoot, subRoot)); - } - - await fs.writeFile(path.join(config.cacheRoot, 'tutorialTree.json'), JSON.stringify(tree.serialize())); - await fs.writeFile(path.join(config.cacheRoot, 'tutorialViewStorage.json'), JSON.stringify(viewStorage.serialize())); - // await importer.generateCaches(); - - log.info("Tutorial import complete"); -}; - - diff --git a/handlers/tutorial/lib/tutorialImporter.js b/handlers/tutorial/lib/tutorialImporter.js deleted file mode 100755 index 1dcfd35..0000000 --- a/handlers/tutorial/lib/tutorialImporter.js +++ /dev/null @@ -1,626 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const fse = require('fs-extra'); -const path = require('path'); -const config = require('config'); - -const Article = require('../models/article'); -const Task = require('../models/task'); -const ArticleRenderer = require('../renderer/articleRenderer'); -const TaskRenderer = require('../renderer/taskRenderer'); -const log = require('log')(); - -const TutorialTree = require('../models/tutorialTree'); -const TutorialView = require('../models/tutorialView'); -const TutorialViewStorage = require('../models/tutorialViewStorage'); -const TutorialParser = require('./tutorialParser'); -const stripTitle = require('markit').stripTitle; -const stripYamlMetadata = require('markit').stripYamlMetadata; -const mime = require('mime'); -const stripIndents = require('textUtil/stripIndents'); - -const t = require('i18n'); - -t.requirePhrase('tutorial', 'importer'); - -module.exports = class TutorialImporter { - constructor(options) { - this.root = fs.realpathSync(options.root); - this.onchange = options.onchange || function () {}; - this.tree = TutorialTree.instance(); - } - - /* - update=false => removes old entry and re-imports - update=true => doesnn't remove anything, for adding only (checks for dupe slugs) - */ - async sync(directory, update = false) { - - log.info("sync", directory); - let dir = fs.realpathSync(directory); - let type; - while (true) { - if (dir.endsWith('.view') && !dir.endsWith('/_js.view')) { - type = 'View'; - break; - } - if (fs.existsSync(path.join(dir, 'index.md'))) { - type = 'Folder'; - break; - } - if (fs.existsSync(path.join(dir, 'article.md'))) { - type = 'Article'; - break; - } - if (fs.existsSync(path.join(dir, 'task.md'))) { - type = 'Task'; - break; - } - - dir = path.dirname(dir); - - if (directory === this.root || directory === '/') { - throw new Error("Unknown directory type: " + directory); - } - } - - let slug = path.basename(dir).slice(path.basename(dir).indexOf('-') + 1); - - let parentDir = path.dirname(dir); - - let parentSlug = path.basename(parentDir); - parentSlug = parentSlug.slice(parentSlug.indexOf('-') + 1); - - let parent = this.tree.bySlug(parentSlug); - - if (update) { - //console.log("DESTROY", slug); - this.tree.destroyTree(slug); - } - await this['sync' + type](dir, parent); - - } - - /** - * Call this after all import is complete to generate caches/searches for ElasticSearch to consume - */ - async generateCaches() { - await ArticleRenderer.regenerateCaches(); - await TaskRenderer.regenerateCaches(); - } - - async syncFolder(sourceFolderPath, parent) { - log.info("syncFolder", sourceFolderPath); - - const contentPath = path.join(sourceFolderPath, 'index.md'); - let content = fs.readFileSync(contentPath, 'utf-8').trim(); - - const folderFileName = path.basename(sourceFolderPath); - - const data = { - isFolder: true - }; - - if (parent) { - data.parent = parent.slug; - } - - data.weight = parseInt(folderFileName); - data.slug = folderFileName.slice(folderFileName.indexOf('-') + 1); - - //this.tree.destroyTree(data.slug); - - let options = { - staticHost: config.urlBase.static, - resourceWebRoot: Article.getResourceWebRootBySlug(data.slug) - }; - - let tmp = stripYamlMetadata(content); - - content = tmp.text; - let meta = tmp.meta; - - if (meta.libs) data.libs = meta.libs; - - tmp = stripTitle(content); - - data.title = tmp.title; - content = tmp.text; - - data.content = content; - - const parser = new TutorialParser(options); - - await parser.parse(content); - - data.githubLink = config.tutorialGithubBaseUrl + sourceFolderPath.slice(this.root.length); - - log.debug(data); - const folder = new Article(data); - - this.tree.add(folder); - - const subPaths = fs.readdirSync(sourceFolderPath); - - for (let subPath of subPaths) { - if (subPath === 'index.md') continue; - - subPath = path.join(sourceFolderPath, subPath); - - if (fs.existsSync(path.join(subPath, 'index.md'))) { - await this.syncFolder(subPath, folder); - } else if (fs.existsSync(path.join(subPath, 'article.md'))) { - await this.syncArticle(subPath, folder); - } else { - await this.syncResource(subPath, folder.getResourceFsRoot()); - } - } - - this.onchange(folder.getUrl()); - - }; - - async syncArticle(articlePath, parent) { - log.info("syncArticle", articlePath); - - const contentPath = path.join(articlePath, 'article.md'); - let content = fs.readFileSync(contentPath, 'utf-8').trim(); - - const articlePathName = path.basename(articlePath); - - const data = { - isFolder: false - }; - - if (parent) { - data.parent = parent.slug; - } - - data.weight = parseInt(articlePathName); - data.slug = articlePathName.slice(articlePathName.indexOf('-') + 1); - - // this.tree.destroyTree(data.slug); - - const options = { - staticHost: config.urlBase.static, - resourceWebRoot: Article.getResourceWebRootBySlug(data.slug) - }; - - let tmp = stripYamlMetadata(content); - - content = tmp.text; - let meta = tmp.meta; - - if (meta.libs) data.libs = meta.libs; - - tmp = stripTitle(content); - - data.title = tmp.title; - content = tmp.text; - - data.content = content; - - const parser = new TutorialParser(options); - - // just make sure it parses - await parser.parse(content); - - data.githubLink = config.tutorialGithubBaseUrl + articlePath.slice(this.root.length) + '/article.md'; - - try { - data.headJs = fs.readFileSync(path.join(articlePath, 'head.js'), {encoding: 'utf8'}); - } catch (e) { - } - try { - data.headCss = fs.readFileSync(path.join(articlePath, 'head.css'), {encoding: 'utf8'}); - } catch (e) { - } - try { - data.headHtml = fs.readFileSync(path.join(articlePath, 'head.html'), {encoding: 'utf8'}); - } catch (e) { - } - - const article = new Article(data); - this.tree.add(article); - - const subPaths = fs.readdirSync(articlePath); - - for (let subPath of subPaths) { - if (subPath === 'article.md') continue; - - subPath = path.join(articlePath, subPath); - - if (fs.existsSync(path.join(subPath, 'task.md'))) { - await this.syncTask(subPath, article); - } else if (subPath.endsWith('.view')) { - await this.syncView(subPath, article); - } else { - // resources - await this.syncResource(subPath, article.getResourceFsRoot()); - } - - } - - this.onchange(article.getUrl()); - - }; - - - async syncResource(sourcePath, destDir) { - fse.ensureDirSync(destDir); - - log.debug("syncResource", sourcePath, destDir); - - const stat = fs.statSync(sourcePath); - const ext = getFileExt(sourcePath); - const destPath = path.join(destDir, path.basename(sourcePath)); - - if (stat.isFile()) { - if (ext === 'png' || ext === 'jpg' || ext === 'gif' || ext === 'svg') { - importImage(sourcePath, destDir); - return; - } - copySync(sourcePath, destPath); - } else if (stat.isDirectory()) { - fse.ensureDirSync(destPath); - const subPathNames = fs.readdirSync(sourcePath); - for (let subPath of subPathNames) { - subPath = path.join(sourcePath, subPath); - await this.syncResource(subPath, destPath); - } - - } else { - throw new Error("Unsupported file type at " + sourcePath); - } - - }; - - async syncTask(taskPath, parent) { - log.debug("syncTask", taskPath); - - const contentPath = path.join(taskPath, 'task.md'); - let content = fs.readFileSync(contentPath, 'utf-8').trim(); - - const taskPathName = path.basename(taskPath); - - const data = { - parent: parent.slug - }; - - data.weight = parseInt(taskPathName); - data.slug = taskPathName.slice(taskPathName.indexOf('-') + 1); - - data.githubLink = config.tutorialGithubBaseUrl + taskPath.slice(this.root.length); - - //this.tree.destroyTree(data.slug); - - const options = { - staticHost: config.urlBase.static, - resourceWebRoot: Task.getResourceWebRootBySlug(data.slug) - }; - - - let tmp = stripYamlMetadata(content); - - content = tmp.text; - let meta = tmp.meta; - - if (meta.libs) data.libs = meta.libs; - if (meta.importance) data.importance = meta.importance; - - tmp = stripTitle(content); - - data.title = tmp.title; - content = tmp.text; - - data.content = content; - - const parser = new TutorialParser(options); - - await parser.parse(content); - - // Solution, no title, no meta - const solutionPath = path.join(taskPath, 'solution.md'); - const solution = fs.readFileSync(solutionPath, 'utf-8').trim(); - data.solution = solution; - - log.debug("parsing solution"); - - await parser.parse(solution); - - const task = new Task(data); - this.tree.add(task); - - log.debug("task saved"); - - const subPaths = fs.readdirSync(taskPath); - - for (let subPath of subPaths) { - // names starting with _ don't sync - if (subPath === 'task.md' || subPath === 'solution.md' || subPath[0] === '_') continue; - - subPath = path.join(taskPath, subPath); - - if (subPath.endsWith('.view')) { - await this.syncView(subPath, task); - } else { - await this.syncResource(subPath, task.getResourceFsRoot()); - } - } - - if (fs.existsSync(path.join(taskPath, '_js.view'))) { - await this.syncTaskJs(path.join(taskPath, '_js.view'), task); - } - - this.onchange(task.getUrl()); - - }; - - - async syncView(dir, parent) { - - log.info("syncView: dir", dir); - let pathName = path.basename(dir).replace('.view', ''); - if (pathName === '_js') { - throw new Error("Must not syncView " + pathName); - } - - let webPath = parent.getResourceWebRoot() + '/' + pathName; - - log.debug("syncView webpath", webPath); - let view = TutorialViewStorage.instance().get(webPath); - - if (view) { - log.debug("Plunk from db", view); - } else { - view = new TutorialView({ - webPath, - description: "Fork from " + config.urlBase.main - }); - log.debug("Created new plunk (db empty)", view); - } - - let filesForPlunk = readFs(dir); - log.debug("Files for plunk", filesForPlunk); - - if (!filesForPlunk) return; // had errors - - log.debug("Merging plunk"); - - await view.mergeAndSyncPlunk(filesForPlunk, this.plunkerToken); - - log.debug("Plunk merged"); - - let dst = path.join(parent.getResourceFsRoot(), pathName); - - fse.ensureDirSync(dst); - fs.readdirSync(dir).forEach(function(dirFile) { - copySync(path.join(dir, dirFile), path.join(dst, dirFile)); - }); - - TutorialViewStorage.instance().set(webPath, view); - }; - - - async syncTaskJs(jsPath, task) { - - log.debug("syncTaskJs", jsPath); - - let sourceJs; - - try { - sourceJs = fs.readFileSync(path.join(jsPath, 'source.js'), 'utf8'); - } catch (e) { - sourceJs = "// " + t('tutorial.importer.your_code'); - } - - let testJs; - try { - testJs = fs.readFileSync(path.join(jsPath, 'test.js'), 'utf8'); - } catch (e) { - testJs = ""; - } - - let solutionJs = fs.readFileSync(path.join(jsPath, 'solution.js'), 'utf8'); - - // Source - let sourceWebPath = task.getResourceWebRoot() + '/source'; - - let source = makeSource(sourceJs, testJs); - - let sourceView = TutorialViewStorage.instance().get(sourceWebPath); - - if (!sourceView) { - sourceView = new TutorialView({ - webPath: sourceWebPath, - description: "Fork from " + config.urlBase.main - }); - TutorialViewStorage.instance().set(sourceWebPath, sourceView); - } - - let sourceFilesForView = { - 'index.html': { - content: source, - filename: 'index.html' - }, - 'test.js': !testJs ? null : { - content: testJs.trim(), - filename: 'test.js' - } - }; - - log.debug("save plunk for ", sourceWebPath); - await sourceView.mergeAndSyncPlunk(sourceFilesForView, this.plunkerToken); - - // Solution - let solutionWebPath = task.getResourceWebRoot() + '/solution'; - - let solution = makeSolution(solutionJs, testJs); - - let solutionView = TutorialViewStorage.instance().get(solutionWebPath); - - if (!solutionView) { - solutionView = new TutorialView({ - webPath: solutionWebPath, - description: "Fork from " + config.urlBase.main - }); - TutorialViewStorage.instance().set(solutionWebPath, solutionView); - } - - let solutionFilesForView = { - 'index.html': { - content: solution, - filename: 'index.html' - }, - 'test.js': !testJs ? null : { - content: testJs.trim(), - filename: 'test.js' - } - }; - - log.debug("save plunk for ", solutionWebPath); - await solutionView.mergeAndSyncPlunk(solutionFilesForView, this.plunkerToken); - - }; - -}; - - - -function makeSource(sourceJs, testJs) { - let source = "\n\n\n \n"; - if (testJs) { - source += " \n"; - source += " \n"; - } - source += "\n\n\n \n"; - source += "\n\n"; - return source; -} - - -function makeSolution(solutionJs, testJs) { - let solution = "\n\n\n \n"; - if (testJs) { - solution += " \n"; - solution += " \n"; - } - solution += "\n\n\n \n"; - solution += "\n\n"; - - return solution; -} - -function checkSameMtime(filePath1, filePath2) { - if (!fs.existsSync(filePath2)) return false; - - const stat1 = fs.statSync(filePath1); - if (!stat1.isFile()) { - throw new Error("not a file: " + filePath1); - } - - const stat2 = fs.statSync(filePath2); - if (!stat2.isFile()) { - throw new Error("not a file: " + filePath2); - } - - return stat1.mtime === stat2.mtime; -} - - -function getFileExt(filePath) { - let ext = filePath.match(/\.([^.]+)$/); - return ext && ext[1]; -} - -function importImage(srcPath, dstDir) { - log.info("importImage", srcPath, "to", dstDir); - const filename = path.basename(srcPath); - const dstPath = path.join(dstDir, filename); - - copySync(srcPath, dstPath); -} - -function copySync(srcPath, dstPath) { - if (checkSameMtime(srcPath, dstPath)) { - log.debug("copySync: same mtime %s = %s", srcPath, dstPath); - return; - } - - log.debug("copySync %s -> %s", srcPath, dstPath); - - fse.copySync(srcPath, dstPath); -} - - -function readFs(dir) { - - let files = fs.readdirSync(dir); - - let hadErrors = false; - files = files.filter(function(file) { - if (file[0] == ".") return false; - - let filePath = path.join(dir, file); - if (fs.statSync(filePath).isDirectory()) { - log.error("Directory not allowed: " + file); - hadErrors = true; - } - - let type = mime.getType(file).split('/'); - if (type[0] != 'text' && type[1] != 'json' && type[1] != 'javascript' && type[1] != 'svg+xml') { - log.error("Bad file extension: " + file); - hadErrors = true; - } - - return true; - }); - - if (hadErrors) { - return null; - } - - files = files.sort(function(fileA, fileB) { - let extA = fileA.slice(fileA.lastIndexOf('.') + 1); - let extB = fileB.slice(fileB.lastIndexOf('.') + 1); - - if (extA == extB) { - return fileA > fileB ? 1 : -1; - } - - // html always first - if (extA == 'html') return 1; - if (extB == 'html') return -1; - - // then goes CSS - if (extA == 'css') return 1; - if (extB == 'css') return -1; - - // then JS - if (extA == 'js') return 1; - if (extB == 'js') return -1; - - // then other extensions - return fileA > fileB ? 1 : -1; - }); - - let filesForPlunk = {}; - for (let i = 0; i < files.length; i++) { - let file = files[i]; - filesForPlunk[file] = { - filename: file, - content: stripIndents(fs.readFileSync(path.join(dir, file), 'utf-8')) - }; - } - - return filesForPlunk; -} - \ No newline at end of file diff --git a/handlers/tutorial/lib/tutorialParser.js b/handlers/tutorial/lib/tutorialParser.js deleted file mode 100755 index 3ef35aa..0000000 --- a/handlers/tutorial/lib/tutorialParser.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -/** - * Tutorial ArticleRenderer uses markit - * Markit should not use tutorial not to introduce circular dep - * So we put tutorial-specific staff here - * If in the future it becomes non-tutorial-specific, then refactor - */ -const resolveTutorialLinks = require('./resolveTutorialLinks'); -const ServerParser = require('markit').ServerParser; - -module.exports = class TutorialParser extends ServerParser { - - async parse(text) { - const tokens = await super.parse(text); - await resolveTutorialLinks(tokens, this.md.options); - return tokens; - } - -}; diff --git a/handlers/tutorial/locales/article/en.yml b/handlers/tutorial/locales/article/en.yml deleted file mode 100755 index 442568a..0000000 --- a/handlers/tutorial/locales/article/en.yml +++ /dev/null @@ -1,11 +0,0 @@ -lesson: - next: Next lesson - prev: Previous lesson - -sibling_chapters: Sibling chapters -lesson_navigation: Lesson navigation -tasks: Tasks -chapter: Chapter -lesson_of: "Lesson #{bookChildNumber} of #{bookLeafCount}" - -article_not_found: "Article \"#{path}\" not found" diff --git a/handlers/tutorial/locales/article/ru.yml b/handlers/tutorial/locales/article/ru.yml deleted file mode 100755 index ad063f4..0000000 --- a/handlers/tutorial/locales/article/ru.yml +++ /dev/null @@ -1,11 +0,0 @@ -lesson: - next: Следующий урок - prev: Предыдущий урок - -sibling_chapters: Смежные разделы -lesson_navigation: Навигация по уроку -tasks: Задачи -chapter: Раздел -lesson_of: "Урок #{bookChildNumber} из #{bookLeafCount}" - -article_not_found: "Не найдена статья \"#{path}\"" diff --git a/handlers/tutorial/locales/importer/en.yml b/handlers/tutorial/locales/importer/en.yml deleted file mode 100755 index c437086..0000000 --- a/handlers/tutorial/locales/importer/en.yml +++ /dev/null @@ -1 +0,0 @@ -your_code: ...Your code... diff --git a/handlers/tutorial/locales/importer/ru.yml b/handlers/tutorial/locales/importer/ru.yml deleted file mode 100755 index 7c504bd..0000000 --- a/handlers/tutorial/locales/importer/ru.yml +++ /dev/null @@ -1 +0,0 @@ -your_code: ...Ваш код... diff --git a/handlers/tutorial/locales/task/en.yml b/handlers/tutorial/locales/task/en.yml deleted file mode 100755 index 549f03c..0000000 --- a/handlers/tutorial/locales/task/en.yml +++ /dev/null @@ -1,17 +0,0 @@ -open_task: - sandbox: - tests: Open a sandbox with tests. - no_tests: Open a sandbox for the task. - -open_solution: - sandbox: - tests: Open the solution with tests in a sandbox. - no_tests: Open the solution in a sandbox. - -Importance: Importance -importance: importance -solution: solution -back: back to the lesson -importance_help: How important is the task, from 1 to 5 - -task_not_found: "Task \"#{path}\" not found" diff --git a/handlers/tutorial/locales/task/ru.yml b/handlers/tutorial/locales/task/ru.yml deleted file mode 100755 index 1d81e54..0000000 --- a/handlers/tutorial/locales/task/ru.yml +++ /dev/null @@ -1,17 +0,0 @@ - -open_task: - sandbox: - tests: Открыть песочницу с тестами для задачи. - no_tests: Открыть песочницу для задачи. - -open_solution: - sandbox: - tests: Открыть решение с тестами в песочнице. - no_tests: Открыть решение в песочнице. - -Importance: Важность -importance: важность -importance_help: Насколько эта задача важна для освоения материала, от 1 до 5 -solution: решение -back: вернуться к уроку -task_not_found: "Не найдена задача \"#{path}\"" diff --git a/handlers/tutorial/models/article.js b/handlers/tutorial/models/article.js deleted file mode 100755 index 5b71a07..0000000 --- a/handlers/tutorial/models/article.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const config = require('config'); -const path = require('path'); -const TutorialEntry = require('./tutorialEntry'); -const assert = require('assert'); - -module.exports = class Article extends TutorialEntry { - constructor(data) { - super(); - - 'title,slug,githubLink,isFolder,weight'.split(',').forEach(field => { - if (!(field in data)) { - throw new Error("No field in article " + field); - } - this[field] = data[field]; - }); - - if ('content' in data) { // can be empty string - this.content = data.content; - } - - this.slug = this.slug.toLowerCase().trim(); - this.title = this.title.trim(); - if (!this.isFolder && !this.content) { - throw new Error('Article content is required'); - } - - this.libs = data.libs || []; - this.children = data.children || []; - this.isFolder = data.isFolder || false; - - 'headJs,headCss,headHtml,parent'.split(',').forEach(field => { - if (field in data) { - this[field] = data[field]; - } - }); - } - - static deserialize(data) { - return new Article(data); - } - -}; diff --git a/handlers/tutorial/models/task.js b/handlers/tutorial/models/task.js deleted file mode 100755 index 649ca85..0000000 --- a/handlers/tutorial/models/task.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const config = require('config'); -const path = require('path'); -const TutorialEntry = require('./tutorialEntry'); - -module.exports = class Task extends TutorialEntry { - constructor(data) { - super(); - - 'title,slug,githubLink,weight'.split(',').forEach(field => { - if (!(field in data)) { - throw new Error("No field in task: " + field); - } - this[field] = data[field]; - }); - - this.slug = this.slug.toLowerCase().trim(); - this.title = this.title.trim(); - - this.libs = data.libs || []; - - 'importance,headJs,headCss,headHtml,content,solution,parent'.split(',').forEach(field => { - if (field in data) { - this[field] = data[field]; - } - }); - - } - - static getUrlBySlug(slug) { - return '/task/' + slug; - }; - - static deserialize(data) { - return new Task(data); - } - -}; \ No newline at end of file diff --git a/handlers/tutorial/models/tutorialEntry.js b/handlers/tutorial/models/tutorialEntry.js deleted file mode 100755 index 71061d2..0000000 --- a/handlers/tutorial/models/tutorialEntry.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -const config = require('config'); -const path = require('path'); - -module.exports = class TutorialEntry { - constructor() { - } - - static getUrlBySlug(slug) { - return '/' + slug; - }; - - getUrl() { - return this.constructor.getUrlBySlug(this.slug); - } - - static getResourceFsRootBySlug(slug) { - return path.join(config.publicRoot, this.name.toLowerCase(), slug); - }; - - getResourceFsRoot() { - return this.constructor.getResourceFsRootBySlug(this.slug); - }; - - static getResourceWebRootBySlug(slug) { - return '/' + this.name.toLowerCase() + '/' + slug; - }; - - getResourceWebRoot() { - return this.constructor.getResourceWebRootBySlug(this.slug); - }; - - -}; diff --git a/handlers/tutorial/models/tutorialTree.js b/handlers/tutorial/models/tutorialTree.js deleted file mode 100755 index 538cb5f..0000000 --- a/handlers/tutorial/models/tutorialTree.js +++ /dev/null @@ -1,166 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const Article = require('./article'); -const Task = require('./task'); -const log = require('log')(); - -module.exports = class TutorialTree { - - constructor() { - this.tree = []; - this.bySlugMap = Object.create(null); - } - - bySlug(slug) { - return this.bySlugMap[slug]; - } - - getSiblings(slug) { - let entry = this.bySlug(slug); - return entry.parent ? this.bySlug(entry.parent).children : this.tree; - } - - getPrev(slug) { - let entry = this.bySlug(slug); - let siblings = entry.parent ? this.bySlug(entry.parent).children : this.tree; - let idx = siblings.indexOf(slug); - assert(idx >= 0); - - if (idx === 0) { - return entry.parent; - } else { - if (!entry.isFolder) { - return siblings[idx - 1]; - } else { - // get last child of prev sibling - let prevSibling = this.bySlug(siblings[idx - 1]); - while (prevSibling.isFolder) { - if (!prevSibling.children.length) break; - prevSibling = this.bySlug(prevSibling.children[prevSibling.children.length - 1]); - } - return prevSibling.slug; - } - - } - } - - getNext(slug, canGoDown = true) { - let entry = this.bySlug(slug); - if (entry.isFolder && entry.children[0] && canGoDown) { - return entry.children[0]; - } - let siblings = entry.parent ? this.bySlug(entry.parent).children : this.tree; - let idx = siblings.indexOf(slug); - assert(idx >= 0); - - if (idx < siblings.length - 1) { - return siblings[idx + 1]; - } else { - return entry.parent ? this.getNext(entry.parent, false) : null; - } - } - - addToSlugMap(entry) { - this.bySlugMap[entry.slug] = entry; - } - - deleteFromSlugMap(slug) { - delete this.bySlugMap[slug]; - } - - destroyTree(slug) { - let entry = this.bySlug(slug); - - if (!entry) return; - - // console.log("DESTROY", slug); - - if (entry.children) { - for (let childSlug of entry.children) { - this.destroyTree(childSlug); - } - } - - this.deleteFromSlugMap(slug); - - let siblings = entry.parent ? this.bySlug(entry.parent).children : this.tree; - let idx = siblings.indexOf(slug); - - // if (idx == -1) console.log(entry, this.bySlug(entry.parent)); - assert(idx >= 0); - - siblings.splice(idx, 1); - } - - add(entry) { - - // console.log("ADD", entry.slug, "CHECK", this.bySlug(entry.slug)); - - if (this.bySlug(entry.slug)) { - log.error("Dupe entry", entry); - throw new Error("Already exists an entry with slug:" + entry.slug); - } - - this.addToSlugMap(entry); - let siblings = entry.parent ? this.bySlug(entry.parent).children : this.tree; - let i = 0; - while (i < siblings.length) { - if (this.bySlug(siblings[i]).weight >= entry.weight) { - break; - } - i++; - } - if (i === siblings.length) { - // console.log("INSERT", entry.slug, "IN", siblings, "PUSH"); - siblings.push(entry.slug); - } else { - // console.log("INSERT", entry.slug, "IN", siblings, "SPLICE", i); - siblings.splice(i, 0, entry.slug); - - } - - } - - - clear() { - for (let key in this.bySlugMap) { - delete this.bySlugMap[key]; - } - this.tree.length = 0; - } - - static instance() { - if (!this._instance) { - this._instance = new TutorialTree(); - } - return this._instance; - } - - serialize() { - let bySlugMap = Object.create(null); - for (let slug in this.bySlugMap) { - let value = this.bySlugMap[slug]; - bySlugMap[slug] = {type: value.constructor.name, value}; - } - return { - tree: this.tree, - bySlugMap - }; - } - - load({tree, bySlugMap}) { - this.tree.length = 0; - this.tree.push(...tree); - for (let slug in this.bySlugMap) { - delete this.bySlugMap[slug]; - } - let constructors = {Article, Task}; - for (let slug in bySlugMap) { - let {type, value} = bySlugMap[slug]; - this.bySlugMap[slug] = constructors[type].deserialize(value); - } - - } - -}; diff --git a/handlers/tutorial/models/tutorialView.js b/handlers/tutorial/models/tutorialView.js deleted file mode 100644 index 2c992de..0000000 --- a/handlers/tutorial/models/tutorialView.js +++ /dev/null @@ -1,184 +0,0 @@ -let assert = require('assert'); -let _ = require('lodash'); -let log = require('log')(); -let Zip = require('node-zip'); - - -let request = require('request-promise').defaults({ - simple: false, - resolveWithFullResponse: true -}); - - -module.exports = class TutorialView { - constructor(data) { - 'description,webPath,plunkId,files'.split(',').forEach(field => { - if (field in data) { - this[field] = data[field]; - } - }); - if (!this.files) { - this.files = []; - } - } - - getUrl() { - if (this.plunkId) { - return 'http://plnkr.co/edit/' + this.plunkId + '?p=preview'; - } else { - return null; - } - } - - getZip() { - let archive = new Zip(); - - for (let file of this.files) { - archive.file(file.filename, file.content); - } - - let buffer = archive.generate({type: 'nodebuffer'}); - - return buffer; - }; - - async mergeAndSyncPlunk(files, plunkerToken) { - - let changes = {}; - - log.debug("mergeAndSyncRemote " + this.plunkId); - log.debug("OLD files", this.files); - log.debug("NEW files", files); - - /* delete this.files which are absent in files */ - for (let i = 0; i < this.files.length; i++) { - let file = this.files[i]; - if (!files[file.filename]) { - this.files.splice(i--, 1); - changes[file.filename] = null; // for submitting to plnkr - } - } - - for (let name in files) { - let existingFile = null; - for (let i = 0; i < this.files.length; i++) { - let item = this.files[i]; - if (item.filename == name) { - existingFile = item; - break; - } - } - if (existingFile) { - if (existingFile.content == files[name].content) continue; - existingFile.content = files[name].content; - } else { - this.files.push(files[name]); - } - delete files[name].filename; - changes[name] = files[name]; - } - - log.debug("UPDATED files", this.files); - - if (_.isEmpty(changes)) { - log.debug("no changes, skip updating"); - return; - } else { - log.debug("plunk " + this.plunkId + " changes", changes); - } - - - if (this.plunkId) { - log.debug("update remotely", this.webPath, this.plunkId); - await this.updatePlunk(this.plunkId, changes, plunkerToken); - } else { - log.debug("create plunk remotely", this.webPath); - this.plunkId = await this.createPlunk(this.description, this.files, plunkerToken); - } - } - - async createPlunk(description, files, plunkerToken) { - - if (!process.env.PLUNK_ENABLED) { - return Math.random().toString(36).slice(2); - } - - let filesObj = {}; - files.forEach(function (file) { - filesObj[file.filename] = { - filename: file.filename, - content: file.content - }; // no _id - }); - - let form = { - description: description, - tags: [], - files: filesObj, - private: true - }; - - - /* let j = request.jar(); - let cookie = request.cookie('plnk_session'); - cookie.value = plunkerToken; - j.setCookie(cookie, "http://api.plnkr.co"); - */ - let data = { - method: 'POST', - headers: {'Content-Type': 'application/json;charset=utf-8'}, - json: true, - url: "http://api.plnkr.co/plunks/?sessid=" + plunkerToken, - body: form - }; - - - log.debug("plunk createRemote", data); - - let result = await this.requestPlunk(data); - - assert.equal(result.statusCode, 201); - - return result.body.id; - - }; - - async requestPlunk(data) { - let result = await request(data); - - if (result.statusCode == 404) { - throw new Error("result " + data.url + " status code 404, probably (plnkrAuthId is too old OR this plunk doesn't belong to plunk@javascript.ru (javascript-plunk) user)"); - } - if (result.statusCode == 400) { - throw new Error("invalid json, probably you don't need to stringify body (request will do it)"); - } - - return result; - }; - - async updatePlunk(plunkId, changes, plunkerToken) { - - if (!process.env.PLUNK_ENABLED) { - return; - } - - let form = { - files: changes - }; - - let options = { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - json: true, - url: "http://api.plnkr.co/plunks/" + plunkId + "?sessid=" + plunkerToken, - body: form - }; - - log.debug(options); - - let result = await this.requestPlunk(options); - - assert.equal(result.statusCode, 200); - }; - -}; \ No newline at end of file diff --git a/handlers/tutorial/models/tutorialViewStorage.js b/handlers/tutorial/models/tutorialViewStorage.js deleted file mode 100644 index b572d8b..0000000 --- a/handlers/tutorial/models/tutorialViewStorage.js +++ /dev/null @@ -1,53 +0,0 @@ -const TutorialView = require('./tutorialView'); - -module.exports = class TutorialViewStorage { - constructor() { - this.storage = Object.create(null); - } - - set(key, value) { - this.storage[key] = value; - } - - get(key) { - return this.storage[key]; - } - - getAll() { - return this.storage; - } - - has(key) { - return (key in this.storage); - } - - clear() { - for (let key in this.storage) { - delete this.storage[key]; - } - } - - static instance() { - if (!this._instance) { - this._instance = new TutorialViewStorage(); - } - return this._instance; - } - - serialize() { - return { - storage: this.storage - }; - } - - load({storage}) { - for(let key in this.storage) { - delete this.storage[key]; - } - - for(let key in storage) { - this.storage[key] = new TutorialView(storage[key]); - } - } - -}; \ No newline at end of file diff --git a/handlers/tutorial/renderer/articleRenderer.js b/handlers/tutorial/renderer/articleRenderer.js deleted file mode 100755 index e4d4493..0000000 --- a/handlers/tutorial/renderer/articleRenderer.js +++ /dev/null @@ -1,182 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const config = require('config'); -const log = require('log')(); -const Article = require('../models/article'); -const TutorialParser = require('../lib/tutorialParser'); - -// Порядок библиотек на странице -// - встроенный CSS -// - библиотеки CSS -// - [head] (css, important short js w/o libs, js waits libs on DocumentContentLoaded) -// ... -// - встроенный JS -// - библиотеки JS - -/** - * Can render many articles, joining metadata - */ -module.exports = class ArticleRenderer { - constructor() { - this.libs = []; - this.headCss = []; - this.headJs = []; - this.headHtml = []; - - // shared across all renderings - // shared headingsMap to prevent same ids - this.env = {}; - - } - -// gets content from metadata.libs & metadata.head - getHead() { - return [].concat( - this._libsToJsCss( - this._unmapLibsNames(this.libs) - ).css, - this._libsToJsCss( - this._unmapLibsNames(this.libs) - ).js, - this.headCss.length && ``, - this.headJs.length && ``, - this.headHtml.join('\n')) - .filter(Boolean).join('\n'); - } - -// Все библиотеки должны быть уникальны -// Если один ресурс требует JQUERY и другой тоже, то нужно загрузить только один раз JQUERY -// Именно форматтер окончательно форматирует библиотеки, т.к. он знает про эти мапппинги -// -// Кроме того, парсер может распарсить много документов для сбора метаданных - _unmapLibsNames(libs) { - let libsUnmapped = []; - - // заменить все-все короткие имена - // предполагается, что короткое имя при раскрытии не содержит другого короткого имени (легко заимплементить) - - libs.forEach(function (lib) { - switch (lib) { - case 'lodash': - libsUnmapped.push("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.0/lodash.min.js"); - break; - - case 'd3': - libsUnmapped.push("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"); - break; - - case 'domtree': - libsUnmapped.push("domtree.css", "domtree.js"); - break; - - default: - libsUnmapped.push(lib); - } - }); - - return libsUnmapped; - } - - - _libsToJsCss(libs) { - let js = []; - let css = []; - - _.uniq(libs).forEach(function (lib) { - if (!~lib.indexOf('://')) { - lib = config.urlBase.static + '/libs/' + lib; - } - - if (lib.slice(-3) == '.js') { - js.push(''); - } else if (lib.slice(-4) == '.css') { - css.push(""); - } else { - js.push(""); - } - }); - - return { - js: js, - css: css - }; - } - - /** - * Render, gather metadata to the renderer object - * @param article - * @param options - * options.headerLevelShift shifts all headers (to render in ebook as a subchapter0 - * @returns {{content: *, headers: *, head: *}} - */ - async render(article, options) { - - options = Object.assign({ - resourceWebRoot: article.getResourceWebRoot(), - env: this.env - }, options || {}); - - if (options.linkHeaderTag === undefined) options.linkHeaderTag = true; - - let parser = new TutorialParser(options); - - const tokens = await parser.parse(article.content); - - let headers = []; - - for (let idx = 0; idx < tokens.length; idx++) { - let token = tokens[idx]; - if (token.type === 'heading_open') { - let i = idx + 1; - while (tokens[i].type !== 'heading_close') i++; - - let headingTokens = tokens.slice(idx + 1, i); - - headers.push({ - level: +token.tag.slice(1), - anchor: token.anchor, - title: parser.render(headingTokens) - }); - - idx = i; - } - - } - - let content = parser.render(tokens); - - for (let i = 0; i < article.libs.length; i++) { - this.libs.push(article.libs[i]); - } - - if (article.headCss) { - this.headCss.push(article.headCss); - } - if (article.headJs) { - this.headJs.push(article.headJs); - } - if (article.headHtml) { - this.headHtml.push(article.headHtml); - } - - - return { - content: content, - headers: headers, - head: this.getHead() - }; - } -}; - -/* -ArticleRenderer.regenerateCaches = function*() { - let articles = await Article.find({}); - - for (let i = 0; i < articles.length; i++) { - let article = articles[i]; - log.debug("regenerate article", article._id); - await (new ArticleRenderer()).renderWithCache(article, {refreshCache: true}); - } -};*/ - diff --git a/handlers/tutorial/renderer/taskRenderer.js b/handlers/tutorial/renderer/taskRenderer.js deleted file mode 100755 index 0a5a301..0000000 --- a/handlers/tutorial/renderer/taskRenderer.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -const config = require('config'); -const Task = require('../models/task'); -const log = require('log')(); -const assert = require('assert'); - -const TutorialParser = require('../lib/tutorialParser'); -const TutorialViewStorage = require('../models/tutorialViewStorage'); - -const t = require('i18n'); - -t.requirePhrase('tutorial', 'task'); - - -/** - * Can render many articles, keeping metadata - */ -module.exports = class TaskRenderer { - - async renderContent(task, options) { - let parser = new TutorialParser(Object.assign({ - resourceWebRoot: task.getResourceWebRoot() - }, options)); - - - const tokens = await parser.parse(task.content); - - let content = parser.render(tokens); - - content = await this.addContentPlunkLink(task, content); - return content; - } - - - async addContentPlunkLink(task, content) { - - let sourcePlunk = TutorialViewStorage.instance().get(task.getResourceWebRoot() + '/source'); - - if (sourcePlunk) { - - let files = sourcePlunk.files; - let hasTest = false; - for (let file of files) { - if (file.filename === 'test.js') hasTest = true; - } - - let title = hasTest ? t('tutorial.task.open_task.sandbox.tests') : t('tutorial.task.open_task.sandbox.no_tests'); - - content += `

${title}

`; - } - - return content; - } - - async render(task, options) { - assert(task.constructor.name === 'Task'); - this.content = await this.renderContent(task, options); - this.solution = await this.renderSolution(task, options); - - return { - content: this.content, - solution: this.solution - }; - } - - async renderSolution(task, options) { - - let parser = new TutorialParser(Object.assign({ - resourceWebRoot: task.getResourceWebRoot() - }, options)); - - const tokens = await parser.parse(task.solution); - - const solutionParts = []; - - - // if no #header at start - // no parts, single solution - if (tokens.length == 0 || tokens[0].type != 'heading_open') { - let solution = parser.render(tokens); - solution = await this.addSolutionPlunkLink(task, solution); - return solution; - } - - // otherwise, split into parts - let currentPart; - for (let idx = 0; idx < tokens.length; idx++) { - let token = tokens[idx]; - if (token.type == 'heading_open') { - - let i = idx + 1; - while (tokens[i].type != 'heading_close') i++; - - let headingTokens = tokens.slice(idx + 1, i); - - currentPart = { - title: stripTags(parser.render(headingTokens)), - content: [] - }; - solutionParts.push(currentPart); - idx = i; - continue; - } - - currentPart.content.push(token); - } - - for (let i = 0; i < solutionParts.length; i++) { - let part = solutionParts[i]; - part.content = parser.render(part.content); - } - - let solutionPartLast = solutionParts[solutionParts.length - 1]; - solutionParts[solutionParts.length - 1].content = await this.addSolutionPlunkLink(task, solutionPartLast.content); - - return solutionParts; - } - - async addSolutionPlunkLink(task, solution) { - - let solutionPlunk = TutorialViewStorage.instance().get(task.getResourceWebRoot() + '/solution'); - - if (solutionPlunk) { - let files = solutionPlunk.files; - let hasTest = false; - for (let i = 0; i < files.length; i++) { - if (files[i].filename == 'test.js') hasTest = true; - } - - let title = hasTest ? t('tutorial.task.open_solution.sandbox.tests') : t('tutorial.task.open_solution.sandbox.no_tests'); - - solution += `

${title}

`; - } - - return solution; - } -}; - -/* -TaskRenderer.regenerateCaches = function*() { - let tasks = await Task.find({}); - - for (let i = 0; i < tasks.length; i++) { - let task = tasks[i]; - log.debug("regenerate task", task._id); - await (new TaskRenderer()).renderWithCache(task, {refreshCache: true}); - } -};*/ - - -function stripTags(text) { - return text.replace(/<\/?[a-z].*?>/gim, ''); -} - diff --git a/handlers/tutorial/router.js b/handlers/tutorial/router.js deleted file mode 100755 index ae5212a..0000000 --- a/handlers/tutorial/router.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -let Router = require('koa-router'); - -let task = require('./controller/task'); -let article = require('./controller/article'); -let frontpage = require('./controller/frontpage'); -let zipview = require('./controller/zipview'); - -let router = module.exports = new Router(); - -router.get('/task/:slug', task.get); -router.get('/tutorial/zipview/:name', zipview.get); -router.get('/', frontpage.get); -router.get('/tutorial', function*() { - this.status = 301; - this.redirect('/'); -}); - -router.get('/:slug', article.get); diff --git a/handlers/tutorial/tasks/beautify.js b/handlers/tutorial/tasks/beautify.js deleted file mode 100755 index 72bb044..0000000 --- a/handlers/tutorial/tasks/beautify.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -let fs = require('fs'); -let path = require('path'); -let log = require('log')(); -let glob = require('glob'); -let beautify = require('js-beautify'); -let readlineSync = require('readline-sync'); - -module.exports = function(options) { - - return function() { - - let args = require('yargs') - .usage("Path to tutorial root is required.") - .demand(['root']) - .argv; - - let root = fs.realpathSync(args.root); - - let options = { - indent_size: 2, - selector_separator_newline: true, - newline_between_rules: true, - preserve_newlines: true - //space_in_paren: true - }; - return async function() { - - let jsFiles = glob.sync( path.join( root, '**', '*.js' ) ); - - for (let i = 0; i < jsFiles.length; i++) { - let jsFile = jsFiles[i]; - let content = fs.readFileSync(jsFile, 'utf8'); - - fs.writeFileSync(jsFile, beautify.js(content, options), 'utf8'); - } - - let cssFiles = glob.sync(path.join(root, '**', '*.css')); - - for (let i = 0; i < cssFiles.length; i++) { - let cssFile = cssFiles[i]; - let content = fs.readFileSync(cssFile, 'utf8'); - fs.writeFileSync(cssFile, beautify.css(content, options), 'utf8'); - } - - - let htmlFiles = glob.sync(path.join(root, '**', '*.html')); - - for (let i = 0; i < htmlFiles.length; i++) { - let htmlFile = htmlFiles[i]; - let content = fs.readFileSync(htmlFile, 'utf8'); - fs.writeFileSync(htmlFile, beautify.html(content, options), 'utf8'); - } - - let mdFiles = glob.sync(path.join(root, '**', '*.md')); - - for (let i = 0; i < mdFiles.length; i++) { - let mdFile = mdFiles[i]; - let content = fs.readFileSync(mdFile, 'utf8'); - console.log(mdFile); - fs.writeFileSync(mdFile, beautifyMd(content, mdFile, options), 'utf8'); - } - - - }(); - }; -}; - -function beautifyMd(content, mdFile, options) { - - let contentNew; - - contentNew = content.replace(/```(js|html|css)\n([\s\S]*?)\n```/gim, function(match, lang, code) { - let codeOpts = code.match(/^\/\/\+.*\n/) || code.match(/^' : '/**!**/'); - beautified = beautified.replace(/^[ \t]*\*\/!\*/gim, lang == 'html' ? '' : '/**-!**/'); - - //console.log(beautified); - beautified = beautify[lang](beautified, options); - - //console.log(beautified); - beautified = beautified.replace(/^[ \t]*/gim, '*!*'); - beautified = beautified.replace(/^[ \t]*/gim, '*/!*'); - beautified = beautified.replace(/^[ \t]*\/\*\*!\*\*\//gim, '*!*'); - beautified = beautified.replace(/^[ \t]*\/\*\*-!\*\*\//gim, '*/!*'); - - beautified = beautified.replace(/alert\((\S.*?)\);/gim, 'alert( $1 );'); - - if (beautified === codeNoOpts) { - return match; // nothing changed (already beautified?), skip - } - - // clear console - process.stdout.write('\u001B[2J\u001B[0;0f'); - console.log("\n" + mdFile); - console.log("=======================================================\n"); - console.log(codeNoOpts); - console.log("-------------------------------------------------------"); - console.log(beautified); - - let keep = readlineSync.question('Beautify [y]?'); - - let result; - if (keep == 'y' || keep === '') { - result = codeOpts + beautified; - } else { - let codeOptsNoBeautify = codeOpts.slice(0, 2) == '//' ? codeOpts.replace("\n", " no-beautify\n") : - codeOpts.slice(0, 2) == '/*' ? codeOpts.replace("*/", " no-beautify */") : - codeOpts.slice(0, 2) == '", " no-beautify -->") : - lang == 'html' ? '\n' : - lang == 'css' ? '/*+ no-beautify */\n' : - '//+ no-beautify\n'; - - result = codeOptsNoBeautify + codeNoOpts; - } - - result = "```" + lang + "\n" + result + "\n```"; - return result; - }); - - - - return contentNew; - -} - diff --git a/handlers/tutorial/tasks/cacheRegenerate.js b/handlers/tutorial/tasks/cacheRegenerate.js deleted file mode 100755 index 795114b..0000000 --- a/handlers/tutorial/tasks/cacheRegenerate.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const ArticleRenderer = require('../renderer/articleRenderer'); -const TaskRenderer = require('../renderer/taskRenderer'); - -module.exports = function() { - - return function() { - return async function() { - await ArticleRenderer.regenerateCaches(); - await TaskRenderer.regenerateCaches(); - }(); - - }; -}; - diff --git a/handlers/tutorial/tasks/figuresImport.js b/handlers/tutorial/tasks/figuresImport.js deleted file mode 100755 index 819a66c..0000000 --- a/handlers/tutorial/tasks/figuresImport.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -/** - * Import figures.sketch into tutorial - * @type {FiguresImporter|exports} - */ - -let FiguresImporter = require('../figuresImporter'); - -let fs = require('fs'); -let path = require('path'); -let log = require('log')(); - -module.exports = function(options) { - - return function() { - - let args = require('yargs') - .usage("Path to tutorial root is required.") - .demand(['root']) - .argv; - - let root = fs.realpathSync(args.root); - - let importer = new FiguresImporter({ - root: root, - figuresFilePath: path.join(root, 'figures.sketch') - }); - - return async function() { - - await importer.syncFigures(); - - log.info("Figures imported"); - }(); - }; -}; - - diff --git a/handlers/tutorial/tasks/import.js b/handlers/tutorial/tasks/import.js deleted file mode 100755 index 2583f31..0000000 --- a/handlers/tutorial/tasks/import.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -let runImport = require('tutorial').runImport; -let log = require('log')(); - -module.exports = function(options) { - - return function() { - return runImport(); - }; -}; - - diff --git a/handlers/tutorial/tasks/importWatch.js b/handlers/tutorial/tasks/importWatch.js deleted file mode 100755 index 671e2ff..0000000 --- a/handlers/tutorial/tasks/importWatch.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -let TutorialImporter = require('../lib/tutorialImporter'); -let TutorialTree = require('../models/tutorialTree'); -let TutorialViewStorage = require('../models/tutorialViewStorage'); -let FiguresImporter = require('../figuresImporter'); -let fs = require('fs'); -let path = require('path'); -let livereload = require('gulp-livereload'); -let log = require('log')(); -let chokidar = require('chokidar'); -let os = require('os'); -let config = require('config'); - -module.exports = function(options) { - - return async function() { - - let tree = TutorialTree.instance(); - let viewStorage = TutorialViewStorage.instance(); - - let importer = new TutorialImporter({ - root: config.tutorialRoot - }); - - tree.clear(); - viewStorage.clear(); - - let subRoots = fs.readdirSync(config.tutorialRoot); - - for (let subRoot of subRoots) { - if (!parseInt(subRoot)) continue; - await importer.sync(path.join(config.tutorialRoot, subRoot)); - } - - log.info("Import complete"); - - watchTutorial(); - watchFigures(); - - livereload.listen(); - - await new Promise(resolve => {}); - }; - -}; - - -function watchTutorial() { - - - let importer = new TutorialImporter({ - root: config.tutorialRoot, - onchange: function(path) { - log.info("livereload.change", path); - livereload.changed(path); - } - }); - - - let subRoots = fs.readdirSync(config.tutorialRoot); - subRoots = subRoots.filter(function(subRoot) { - return parseInt(subRoot); - }).map(function(dir) { - return path.join(config.tutorialRoot, dir); - }); - - // under linux set WATCH_USE_POLLING - // to handle the case when linux VM uses shared folder from Windows - let tutorialWatcher = chokidar.watch(subRoots, {ignoreInitial: true, usePolling: process.env.WATCH_USE_POLLING}); - - tutorialWatcher.on('add', onTutorialModify.bind(null, false)); - tutorialWatcher.on('change', onTutorialModify.bind(null, false)); - tutorialWatcher.on('unlink', onTutorialModify.bind(null, false)); - tutorialWatcher.on('unlinkDir', onTutorialModify.bind(null, true)); - tutorialWatcher.on('addDir', onTutorialModify.bind(null, true)); - - function onTutorialModify(isDir, filePath) { - if (~filePath.indexOf('___jb_')) return; // ignore JetBrains Webstorm tmp files - - log.debug("ImportWatch Modify " + filePath); - - let folder; - if (isDir) { - folder = filePath; - } else { - folder = path.dirname(filePath); - } - - importer.sync(folder, true).catch(function(err) { - log.error(err); - }); - } - -} - -function watchFigures() { - - let figuresFilePath = path.join(config.tutorialRoot, 'figures.sketch'); - let importer = new FiguresImporter({ - root: config.tutorialRoot, - figuresFilePath: figuresFilePath - }); - - let figuresWatcher = chokidar.watch(figuresFilePath, {ignoreInitial: true}); - figuresWatcher.on('change', onFiguresModify); - - function onFiguresModify() { - - importer.syncFigures().catch(function(err) { - console.error(err); - }); - } - -} diff --git a/handlers/tutorial/templates/_task_content.pug b/handlers/tutorial/templates/_task_content.pug deleted file mode 100755 index 1c67918..0000000 --- a/handlers/tutorial/templates/_task_content.pug +++ /dev/null @@ -1,17 +0,0 @@ -+e.content - +e.answer - if (task.solution instanceof Array) - each step, i in task.solution - +e.step._open - +e('button').step-show(type="button", onclick="showStep(this)")= step.title - +e.answer-content - +e('h4').step-title= step.title - != step.content - - else - +e.answer-content - != task.solution - - +b('button').close-button.__answer-close(type="button", title="закрыть") - - != task.content diff --git a/handlers/tutorial/templates/article.pug b/handlers/tutorial/templates/article.pug deleted file mode 100755 index c98ddf1..0000000 --- a/handlers/tutorial/templates/article.pug +++ /dev/null @@ -1,49 +0,0 @@ -extends /layouts/main - -block append variables - - let layout_main_class = "main_width-limit" - -block sidebar - include sidebar - -block sidebar-buttons - +b("a").map(href="/tutorial/map", data-action="tutorial-map", data-tooltip=t('locales.tutorial_map')) - -block append head - meta(property="og:title", content=title || headTitle) - meta(property="og:type", content="article") - !=js("tutorial", {defer: true}) - -block content - article(itemscope itemtype="http://schema.org/TechArticle") - meta(itemprop="name", content=title) - div(itemprop="author", itemscope, itemtype="http://schema.org/Person") - meta(itemprop="email", content="iliakan@gmail.com") - meta(itemprop="name" content="Ilya Kantor") - div(itemprop="articleBody") - != content - - - if tasks.length - - +b.tasks - +e('h2').title#tasks - +e('a').title-anchor.main__anchor.main__anchor_noicon(href="#tasks")= t('tutorial.article.tasks') - - each task, i in tasks - +b.task.__task - +e.header - +e.title-wrap - +e('h3').title - a.main__anchor(href=("#"+task.anchor) name=task.anchor)= task.title - +e('a').open-link(href=task.url, target="_blank") - - +e.header-note - if task.importance - +e('span').importance(title=t("tutorial.task.importance_help")) #{t('tutorial.task.importance')}: #{task.importance} - +e('button').solution(type="button")= t('tutorial.task.solution') - - include _task_content - - - diff --git a/handlers/tutorial/templates/folder.pug b/handlers/tutorial/templates/folder.pug deleted file mode 100755 index d31f38b..0000000 --- a/handlers/tutorial/templates/folder.pug +++ /dev/null @@ -1,41 +0,0 @@ -extends /layouts/main - -block append variables - - let layout_main_class = "main_width-limit" - -block sidebar - include sidebar - -block sidebar-buttons - +b("a").map(href="/tutorial/map", data-action="tutorial-map", data-tooltip=t('locales.tutorial_map')) - -block append head - !=js("tutorial", {defer: true}) - -block content - - != content - - +b.lessons-list - +e('ol').lessons - if levelMax == 2 && level == 0 - // top-level folders in 2-level courses - each article in children - +e('li')(class="lesson #{article.children ? '_level_1' : ''}") - +e('a').link(href=article.url) #{article.title} - if article.children - +e('ol').lessons - each subChild in article.children - +e('li')(data-section-number=subChild.weight).lesson._level_2 - +e('a').link(href=subChild.url) #{subChild.title} - else if levelMax == 2 && level == 1 - // 1-level folders in 2-level courses - each article in children - +e('li')(data-section-number=weight, class="lesson #{article.children ? '_level_1' : ''}") - +e('a').link(href=article.url) #{article.title} - else - // folders in plain courses - each article in children - +e('li').lesson - +e('a').link(href=article.url) #{article.title} - diff --git a/handlers/tutorial/templates/sidebar.pug b/handlers/tutorial/templates/sidebar.pug deleted file mode 100755 index 39256bc..0000000 --- a/handlers/tutorial/templates/sidebar.pug +++ /dev/null @@ -1,2 +0,0 @@ -include /blocks/sidebar - diff --git a/handlers/tutorial/templates/task.pug b/handlers/tutorial/templates/task.pug deleted file mode 100755 index 5280b86..0000000 --- a/handlers/tutorial/templates/task.pug +++ /dev/null @@ -1,34 +0,0 @@ - -extends /layouts/body - -block append variables - - let layout_main_class = "main_width-limit" - -block append head - !=js("tutorial", {defer: true}) - -block main - - //- for schema.org microdata - +b.breadcrumbs(style="display:none") - include /blocks/breadcrumbs - - +b.task-single(itemscope itemtype="http://schema.org/TechArticle") - meta(itemprop="name", content=title) - div(itemprop="author", itemscope, itemtype="http://schema.org/Person") - meta(itemprop="email", content="iliakan@gmail.com") - meta(itemprop="name" content="Ilya Kantor") - - +e('a').back(href=articleUrl) - span= t('tutorial.task.back') - - +b.task.__task(itemprop="articleBody") - +e.header - +e.title-wrap - +e('h2').title= task.title - +e.header-note - if task.importance - +e('span').importance(title=t("tutorial.task.importance_help")) #{t("tutorial.task.importance")}: #{task.importance} - +e('button').solution(type="button")= t("tutorial.task.solution") - include _task_content - diff --git a/handlers/tutorial/test/renderer/articleRenderer.js b/handlers/tutorial/test/renderer/articleRenderer.js deleted file mode 100755 index 98b9d58..0000000 --- a/handlers/tutorial/test/renderer/articleRenderer.js +++ /dev/null @@ -1,24 +0,0 @@ - -const ArticleRenderer = require('../../renderer/articleRenderer'); -const Article = require('../../models/article'); - -describe("ArticleRenderer", function() { - - it("appends -2, -3... to header with same title", async function() { - - const article = new Article({ - title: "Title", - slug: "test", - githubLink: 'http://not.exists.com', - content: "## Title\n\n## Title\n\n" - }); - const renderer = new ArticleRenderer(); - - const result = await renderer.render(article); - result.content.replace(/\n/g, '').should.be.eql( - '

Title

Title

' - ); - - }); - -}); diff --git a/handlers/tutorial/test/renderer/taskRenderer.js b/handlers/tutorial/test/renderer/taskRenderer.js deleted file mode 100755 index c2167f6..0000000 --- a/handlers/tutorial/test/renderer/taskRenderer.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const TaskRenderer = require('../../renderer/taskRenderer'); -const Task = require('../../models/task'); - -describe("TaskRenderer", function() { - - it("renderContent", async function () { - - const task = new Task({ - "content": "Content", - "slug": "unique-slug-no-plunk-link-add", - "title": "Title", - "importance": 4, - "solution": "..." - }); - - const renderer = new TaskRenderer(); - - const result = await renderer.renderContent(task, {}); - - result.replace(/\n/g, '').should.be.eql('

Content

'); - }); - - - it("renderSolution", async function () { - - const task = new Task({ - "content": "# Title\n\nContent", - "slug": "unique-slug-no-plunk-link-add", - "title": "Title", - "importance": 4, - "solution": "# Part 1\n\nContent 1\n\n# Part 2\n\nContent 2" - }); - const renderer = new TaskRenderer(); - - const result = await renderer.renderSolution(task, {}); - - result.should.be.eql([{title: 'Part 1', content: '

Content 1

\n'}, - {title: 'Part 2', content: '

Content 2

\n'}]); - - }); -}); diff --git a/handlers/verboseLogger.js b/handlers/verboseLogger.js deleted file mode 100755 index 5c1bf3a..0000000 --- a/handlers/verboseLogger.js +++ /dev/null @@ -1,8 +0,0 @@ -const VerboseLogger = require('lib/verboseLogger'); - -exports.init = function(app) { - - app.verboseLogger = new VerboseLogger(); - app.use(app.verboseLogger.middleware()); - -}; diff --git a/locales/en.yml b/locales/en.yml index 6347a3a..d4a6437 100755 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,14 +1,15 @@ -comments: Comments +site: + comments: Comments -tutorial: Tutorial -tutorial_map: Tutorial map -additional_articles: Additional articles + tutorial: Tutorial + tutorial_map: Tutorial map + additional_articles: Additional articles -edit_on_github: Edit on Github + edit_on_github: Edit on Github -meta: - description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.' -title: The Modern JavaScript Tutorial -subtitle: "How it's done now. From the basics to advanced topics with simple, but detailed explanations." -toc: 'Table of contents' -description: 'Main course contains 2 parts which cover JavaScript as a programming language and working with a browser. There are also additional series of thematic articles.' \ No newline at end of file + meta: + description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.' + title: The Modern JavaScript Tutorial + subtitle: "How it's done now. From the basics to advanced topics with simple, but detailed explanations." + toc: 'Table of contents' + description: 'Main course contains 2 parts which cover JavaScript as a programming language and working with a browser. There are also additional series of thematic articles.' \ No newline at end of file diff --git a/locales/ru.yml b/locales/ru.yml index c6668cf..92ab994 100755 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -1,14 +1,15 @@ -comments: Комментарии +site: + comments: Комментарии -tutorial: Учебник -tutorial_map: Карта учебника -additional_articles: Дополнительно + tutorial: Учебник + tutorial_map: Карта учебника + additional_articles: Дополнительно -edit_on_github: Редактировать на Github + edit_on_github: Редактировать на Github -meta: - description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.' -title: The Modern JavaScript Tutorial -subtitle: "How it's done now. From the basics to advanced topics with simple, but detailed explanations." -toc: 'Table of contents' -description: 'Main course contains 2 parts which cover JavaScript as a programming language and working with a browser. There are also additional series of thematic articles.' + meta: + description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.' + title: The Modern JavaScript Tutorial + subtitle: "How it's done now. From the basics to advanced topics with simple, but detailed explanations." + toc: 'Table of contents' + description: 'Main course contains 2 parts which cover JavaScript as a programming language and working with a browser. There are also additional series of thematic articles.' diff --git a/mocha.sh b/mocha.sh index 0532fd8..966aa80 100755 --- a/mocha.sh +++ b/mocha.sh @@ -8,4 +8,4 @@ # tried also gulp-mocha and node `which gulp` test, # but it hangs after tests, not sure why, mocha.sh works fine so leave it as is - NODE_PATH=./handlers:./modules NODE_ENV=test mocha --reporter spec --colors --timeout 100000 --require should --require co --require co-mocha --recursive --ui bdd -d $* + NODE_PATH=./modules NODE_ENV=test mocha --reporter spec --colors --timeout 100000 --require should --require co --require co-mocha --recursive --ui bdd -d $* diff --git a/modules/app.js b/modules/app.js deleted file mode 100755 index c6ebc53..0000000 --- a/modules/app.js +++ /dev/null @@ -1,48 +0,0 @@ -//require("time-require"); - -const fs = require('fs'); -const config = require('config'); - -const Application = require('application'); -const app = new Application(); - - -if (process.env.NODE_ENV != 'development') { - - // only log.error in prod, otherwise just die - process.on('uncaughtException', function(err) { - // let bunyan handle the error - app.log.error({ - message: err.message, - name: err.name, - errors: err.errors, - stack: err.stack - }); - process.exit(255); - }); - -} - - -// The app is always behind Nginx which serves static -// (Maybe behind Cloudflare as well) -// trust all headers from the proxy -// X-Forwarded-Host -// X-Forwarded-Proto -// X-Forwarded-For -> ip -app.proxy = true; - -// ========= Helper handlers =========== - -for(const handlerName in config.handlers) { - app.requireHandler(handlerName); -} - -// must be last -app.requireHandler('404'); - - -// uncomment for time-require to work -//process.emit('exit'); - -module.exports = app; diff --git a/modules/application.js b/modules/application.js deleted file mode 100755 index 54473fa..0000000 --- a/modules/application.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -/** - * Custom application, inherits from Koa Application - * Gets requireModules which adds a module to handlers. - * - * Handlers are called on: - * - init (sync) - initial requires - * - boot (async) - ensure ready to get a request - * - close (async) - close connections - * - * @type {Application} - */ - - -const KoaApplication = require('koa'); - -const log = require('log')(); -const Cookies = require('cookies'); - - -module.exports = class Application extends KoaApplication { - constructor() { - super(); - this.handlers = {}; - this.log = log; - } - -// wait for full app load and all associated warm-ups to finish -// mongoose buffers queries, -// so for TEST/DEV there's no reason to wait -// for PROD, there is a reason: to check if DB is ok before taking a request - async waitBoot() { - - for (let path in this.handlers) { - let handler = this.handlers[path]; - if (!handler.boot) continue; - await handler.boot(); - } - - } - -// adding middlewares only possible *before* app.run -// (before server.listen) -// assigns server instance (meaning only 1 app can be run) -// -// app.listen can also be called from tests directly (and synchronously), without waitBoot (many times w/ random port) -// it's ok for tests, db requests are buffered, no need to waitBoot - - async waitBootAndListen(host, port) { - await this.waitBoot(); - - await new Promise((resolve) => { - this.server = this.listen(port, host, resolve); - }); - - this.log.info('Server is listening %s:%d', host, port); - } - - async close() { - this.log.info("Closing app server..."); - await new Promise(resolve => { - this.server.close(resolve); - }); - - this.log.info("App connections are closed"); - - for (let path in this.handlers) { - let handler = this.handlers[path]; - if (!handler.close) continue; - await handler.close(); - } - - this.log.info("App stopped"); - } - - createContext(req, res) { - let context = super.createContext(req, res); - context.cookies = new Cookies(req, res, { - // no secure!!! we allow https cookies to go over http for auth - // otherwise auth with soc networks returns 401 sometimes (https redirect sets secure auth cookie -> http, no cookies) - keys: this.keys - }); - return context; - } - - requireHandler(path) { - - // if debug is on => will log the middleware travel chain - if (process.env.NODE_ENV == 'development' || process.env.LOG_LEVEL) { - let log = this.log; - this.use(async function(ctx, next) { - log.trace("-> setup " + path); - let d = new Date(); - await next(); - log.trace("<- setup " + path, new Date() - d); - }); - } - - let handler = require(path); - - // init is always fast & sync, for tests to run fast - // boot may be slower and async - if (handler.init) { - handler.init(this); - } - - this.handlers[path] = handler; - - } - - -}; - diff --git a/modules/client/clientRender.js b/modules/client/clientRender.js index 21df15e..06e53ee 100755 --- a/modules/client/clientRender.js +++ b/modules/client/clientRender.js @@ -1,8 +1,7 @@ -const bem = require('bemPug')(); -const thumb = require('client/image').thumb; +// const bem = require('bemPug')(); const LANG = require('config').lang; -const t = require('i18n'); +const t = require('jsengine/i18n/t'); module.exports = function(template, locals) { locals = locals ? Object.create(locals) : {}; @@ -12,9 +11,8 @@ module.exports = function(template, locals) { }; function addStandardHelpers(locals) { - locals.bem = bem; + // locals.bem = bem; locals.t = t; - locals.thumb = thumb; locals.lang = LANG; } diff --git a/modules/client/getCsrfCookie.js b/modules/client/getCsrfCookie.js deleted file mode 100755 index 2421c35..0000000 --- a/modules/client/getCsrfCookie.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function() { - let csrfCookie = document.cookie.match(/XSRF-TOKEN=([\w-]+)/); - return csrfCookie ? csrfCookie[1] : null; -}; - diff --git a/modules/client/head/index.js b/modules/client/head/index.js index 016ac8a..c6484ca 100755 --- a/modules/client/head/index.js +++ b/modules/client/head/index.js @@ -23,30 +23,3 @@ require('./navigationArrows'); require('./hover'); require('./trackLinks'); -// must use CommonsChunkPlugin -// to ensure that other modules use exactly this (initialized) client/notify -require('client/notification').init(); - -exports.showTopNotification = function() { - let notification = document.querySelector('.notification_top'); - - let id = notification.id; - - notification.querySelector('button').onclick = function() { - localStorage.topNotificationHidden = id; - notification.style.display = 'none'; - }; - - if (!id) throw new Error('Top notification must have an id'); - - // topNotificationHidden has the id of the hidden notification (current or previous one) - let hiddenId = localStorage.topNotificationHidden; - if (hiddenId == id) return; - - // not same id or no id saved - delete localStorage.topNotificationHidden; - - notification.style.display = ''; -}; - - diff --git a/modules/client/head/resizeOnload/index.js b/modules/client/head/resizeOnload/index.js index f6dff44..4ce16ef 100755 --- a/modules/client/head/resizeOnload/index.js +++ b/modules/client/head/resizeOnload/index.js @@ -1,5 +1,6 @@ let iframeResize = require('./iframeResize'); -let throttle = require('lib/throttle'); + +let throttle = require('lodash/throttle'); // track resized iframes in window.onresize let onResizeQueue = []; diff --git a/modules/client/localeExample.js b/modules/client/localeExample.js index 77c51be..8296e27 100644 --- a/modules/client/localeExample.js +++ b/modules/client/localeExample.js @@ -1,7 +1,7 @@ // client-side locale example // can be used as require('client/test') from server-side // and from client side too -const t = require('i18n/t'); +const t = require('jsengine/i18n/t'); t.i18n.add('test', require('../../locales/' + require('config').lang + '.yml')); diff --git a/modules/client/notification/index.js b/modules/client/notification/index.js deleted file mode 100755 index 7d115b9..0000000 --- a/modules/client/notification/index.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * For new notification types extend Notification - */ - -let delegate = require('client/delegate'); - -/** - * Calculates translateY positions when notifications are added/removed - */ -class NotificationManager { - - constructor(options = {}) { - this.notifications = []; - this.verticalSpace = options.verticalSpace || 8; - } - - register(notification) { - this.notifications.unshift(notification); - setTimeout(() => this.recalculate(), 20); - } - - unregister(notification) { - let idx = this.notifications.indexOf(notification); - this.notifications.splice(idx, 1); - this.recalculate(); - } - - recalculate() { - let top = this.verticalSpace; - this.notifications.forEach(notification => { - notification.top = top; - top += notification.height + this.verticalSpace; - }); - } - -} - -let manager; - -exports.init = function(options) { - manager = new NotificationManager(options); -}; - - -class Notification { - - constructor(html, type, timeout) { - let elemHtml = `
-
${html}
-
`; - - document.body.insertAdjacentHTML("beforeEnd", elemHtml); - - this.elem = document.body.lastElementChild; - - switch(timeout) { - case undefined: - this.timeout = this.TIMEOUT_DEFAULT; - break; - case 'slow': - this.timeout = this.TIMEOUT_SLOW; - break; - case 'fast': - this.timeout = this.TIMEOUT_FAST; - break; - default: - this.timeout = timeout; - } - - - manager.register(this); - this.setupCloseHandler(); - this.setupCloseTimeout(); - } - - get TIMEOUT_DEFAULT() { - return 2500; - } - - get TIMEOUT_SLOW() { - return 5000; - } - - get TIMEOUT_FAST() { - return 1500; - } - - - close() { - if (!this.elem.parentNode) return; // already closed (by user click?) - this.elem.remove(); - manager.unregister(this); - } - - setupCloseHandler() { - this.delegate('.notification__close', 'click', () => this.close()); - } - - setupCloseTimeout() { - if (this.timeout) { - setTimeout(() => this.close(), this.timeout); - } - } - - get height() { - return this.elem.offsetHeight; - } - - set top(value) { - this.elem.style.transform = 'translateY(' + value + 'px)'; - } - -} - -delegate.delegateMixin(Notification.prototype); - - -class Info extends Notification { - - constructor(html, timeout) { - super(html, 'info', timeout); - } - -} - -exports.Info = Info; - -class Warning extends Notification { - - constructor(html, timeout) { - super(html, 'warning', timeout); - } - -} - -exports.Warning = Warning; - -class Success extends Notification { - - constructor(html, timeout) { - super(html, 'success', timeout); - } - -} - -exports.Success = Success; - -export class Error extends Notification { - - constructor(html, timeout) { - super(html, 'error', timeout); - } - - - get TIMEOUT_DEFAULT() { - return 5000; - } - - -} - -exports.Error = Error; - -/* -export class Test extends Notification { - - constructor(html) { - super(html, 'error'); - } - - - get TIMEOUT_DEFAULT() { - return null; - } - - -} - -exports.Test = Test; -*/ \ No newline at end of file diff --git a/modules/client/notification/notification-popup.pug b/modules/client/notification/notification-popup.pug deleted file mode 100755 index 6edfe43..0000000 --- a/modules/client/notification/notification-popup.pug +++ /dev/null @@ -1,5 +0,0 @@ -include ~bemto.pug/lib/index - -+b(class="notification notification_popup notification_" + type) - +e.content= html - +e('button').close(title="Закрыть") diff --git a/modules/client/polyfill/index.js b/modules/client/polyfill/index.js index 95b2f70..326b54e 100755 --- a/modules/client/polyfill/index.js +++ b/modules/client/polyfill/index.js @@ -1,2 +1 @@ require('./dom'); -require('./string'); diff --git a/modules/client/polyfill/string.js b/modules/client/polyfill/string.js deleted file mode 100755 index 6f641fa..0000000 --- a/modules/client/polyfill/string.js +++ /dev/null @@ -1,22 +0,0 @@ -if (!String.prototype.startsWith) { - String.prototype.startsWith = function(string) { - let index = arguments.length < 2 ? 0 : arguments[1]; - - return this.slice(index).indexOf(string) === 0; - }; -} - -if (!String.prototype.endsWith) { - String.prototype.endsWith = function(string) { - let index = arguments.length < 2 ? this.length : arguments[1]; - let foundIndex = this.lastIndexOf(string); - return foundIndex !== -1 && foundIndex === index - string.length; - }; -} - -if (!String.prototype.includes) { - String.prototype.includes = function(string, index) { - if (typeof string === 'object' && string instanceof RegExp) throw new TypeError("First argument to String.prototype.includes must not be a regular expression"); - return this.indexOf(string, index) !== -1; - }; -} diff --git a/modules/client/prism/addLineNumbers.js b/modules/client/prism/addLineNumbers.js deleted file mode 100755 index ad54baf..0000000 --- a/modules/client/prism/addLineNumbers.js +++ /dev/null @@ -1,22 +0,0 @@ - -function addLineNumbers(pre) { - - let linesNum = (1 + pre.innerHTML.split('\n').length); - let lineNumbersWrapper; - - let lines = new Array(linesNum); - lines = lines.join(''); - - lineNumbersWrapper = document.createElement('span'); - lineNumbersWrapper.className = 'line-numbers-rows'; - lineNumbersWrapper.innerHTML = lines; - - if (pre.hasAttribute('data-start')) { - pre.style.counterReset = 'linenumber ' + Number(pre.dataset.start) - 1; - } - - pre.appendChild(lineNumbersWrapper); -} - - -module.exports = addLineNumbers; diff --git a/modules/client/prism/codeBox.js b/modules/client/prism/codeBox.js deleted file mode 100755 index 3ec4820..0000000 --- a/modules/client/prism/codeBox.js +++ /dev/null @@ -1,374 +0,0 @@ -let resizeOnload = require('client/head/resizeOnload'); -let isScrolledIntoView = require('client/isScrolledIntoView'); -let addLineNumbers = require('./addLineNumbers'); - -function CodeBox(elem) { - - let preElem = elem.querySelector('pre'); - let codeElem = preElem.querySelector('code'); - let code = codeElem.textContent; - - Prism.highlightElement(codeElem); - addLineNumbers(preElem); - - addBlockHighlight(preElem, elem.getAttribute('data-highlight-block')); - addInlineHighlight(preElem, elem.getAttribute('data-highlight-inline')); - - let isJS = preElem.classList.contains('language-javascript'); - let isHTML = preElem.classList.contains('language-markup'); - let isTrusted = +elem.getAttribute('data-trusted'); - let isNoStrict = +elem.getAttribute('data-no-strict'); - - if (!isNoStrict && isJS) code="'use strict';\n" + code; - - let jsFrame; - let globalFrame; - let htmlResult; - let isFirstRun = true; - - if (!isJS && !isHTML) return; - - let runElem = elem.querySelector('[data-action="run"]'); - if (runElem) { - runElem.onclick = function() { - this.blur(); - run(); - return false; - }; - } - - let editElem = elem.querySelector('[data-action="edit"]'); - if (editElem) { - editElem.onclick = function() { - this.blur(); - edit(); - return false; - }; - } - - // some code can't be shown by epub engine - if (elem.hasAttribute('data-autorun')) { - if(window.ebookType == 'epub' && elem.getAttribute('data-autorun') == 'no-epub') { - elem.querySelector('iframe').remove(); - } else { - // timeout should be small, around 10ms, or remove it to make crawler process the autorun - setTimeout(run, 100); - } - } - - function postJSFrame() { - let win = jsFrame.contentWindow; - if (typeof win.postMessage != 'function') { - alert("Извините, запуск кода требует более современный браузер"); - return; - } - win.postMessage(code, 'https://ru.lookatcode.com/showjs'); - } - - function runHTML() { - - let frame; - - if (htmlResult && elem.hasAttribute('data-refresh')) { - htmlResult.remove(); - htmlResult = null; - } - - if (!htmlResult) { - // take from HTML if exists there (in markup when autorun is specified) - htmlResult = elem.querySelector('.code-result'); - } - - if (!htmlResult) { - // otherwise create (or recreate if refresh) - htmlResult = document.createElement('div'); - htmlResult.className = "code-result code-example__result"; - - frame = document.createElement('iframe'); - frame.name = 'frame-' + Math.random(); - frame.className = 'code-result__iframe'; - - if (elem.getAttribute('data-demo-height') === "0") { - // this html has nothing to show - frame.style.display = 'none'; - } else if (elem.hasAttribute('data-demo-height')) { - let height = +elem.getAttribute('data-demo-height'); - frame.style.height = height + 'px'; - } - htmlResult.appendChild(frame); - - elem.appendChild(htmlResult); - } else { - frame = htmlResult.querySelector('iframe'); - } - - if (isTrusted) { - let doc = frame.contentDocument || frame.contentWindow.document; - - doc.open(); - doc.write(normalizeHtml(code)); - doc.close(); - - - if(window.ebookType == 'epub') { - setTimeout(function() { - // remove script from iframes - // firefox saves the file with full iframe content (including script-generated) and the scripts - // scripts must not execute and autogenerate content again - [].forEach.call(doc.querySelectorAll('script'), function(script) { - script.remove(); - }); - - // do it after timeout to allow external scripts (if any) to execute - }, 2000); - } - - if (!elem.hasAttribute('data-demo-height')) { - resizeOnload.iframe(frame); - } - - if (!(isFirstRun && elem.hasAttribute('data-autorun'))) { - if (!isScrolledIntoView(htmlResult)) { - htmlResult.scrollIntoView(false); - } - } - - } else { - let form = document.createElement('form'); - form.style.display = 'none'; - form.method = 'POST'; - form.enctype = "multipart/form-data"; - form.action = "https://ru.lookatcode.com/showhtml"; - form.target = frame.name; - - let textarea = document.createElement('textarea'); - textarea.name = 'code'; - textarea.value = normalizeHtml(code); - form.appendChild(textarea); - - frame.parentNode.insertBefore(form, frame.nextSibling); - form.submit(); - form.remove(); - - if (!(isFirstRun && elem.hasAttribute('data-autorun'))) { - frame.onload = function() { - - if (!elem.hasAttribute('data-demo-height')) { - resizeOnload.iframe(frame); - } - - if (!isScrolledIntoView(htmlResult)) { - htmlResult.scrollIntoView(false); - } - }; - } - } - - } - - // Evaluates a script in a global context - function globalEval( code ) { - let script = document.createElement( "script" ); - script.text = code; - document.head.appendChild( script ).parentNode.removeChild( script ); - } - - function runJS() { - - if (elem.hasAttribute('data-global')) { - if (!globalFrame) { - globalFrame = document.createElement('iframe'); - globalFrame.className = 'js-frame'; - globalFrame.style.width = 0; - globalFrame.style.height = 0; - globalFrame.style.border = 'none'; - globalFrame.name = 'js-global-frame'; - document.body.appendChild(globalFrame); - } - - let form = document.createElement('form'); - form.style.display = 'none'; - form.method = 'POST'; - form.enctype = "multipart/form-data"; - form.action = "https://ru.lookatcode.com/showhtml"; - form.target = 'js-global-frame'; - - let textarea = document.createElement('textarea'); - textarea.name = 'code'; - textarea.value = normalizeHtml(''); - form.appendChild(textarea); - - globalFrame.parentNode.insertBefore(form, globalFrame.nextSibling); - form.submit(); - form.remove(); - } else if (isTrusted) { - - if (elem.hasAttribute('data-autorun')) { - // make sure functions from "autorun" go to global scope - globalEval(code); - return; - } - - try { - /* jshint -W061 */ - window["eval"].call(window, code); - } catch (e) { - alert("Error: " + e.message); - } - - } else { - - if (elem.hasAttribute('data-refresh') && jsFrame) { - jsFrame.remove(); - jsFrame = null; - } - - if (!jsFrame) { - // create iframe for js - jsFrame = document.createElement('iframe'); - jsFrame.className = 'js-frame'; - jsFrame.src = 'https://ru.lookatcode.com/showjs'; - jsFrame.style.width = 0; - jsFrame.style.height = 0; - jsFrame.style.border = 'none'; - jsFrame.onload = function() { - postJSFrame(); - }; - document.body.appendChild(jsFrame); - } else { - postJSFrame(); - } - } - - } - - function edit() { - - let html; - if (isHTML) { - html = normalizeHtml(code); - } else { - let codeIndented = code.replace(/^/gim, ' '); - html = '\n\n\n\n \n\n\n'; - } - - let form = document.createElement('form'); - form.action = "http://plnkr.co/edit/?p=preview"; - form.method = "POST"; - form.target = "_blank"; - - document.body.appendChild(form); - - let textarea = document.createElement('textarea'); - textarea.name = "files[index.html]"; - textarea.value = html; - form.appendChild(textarea); - - let input = document.createElement('input'); - input.name = "description"; - input.value = "Fork from " + window.location; - form.appendChild(input); - - form.submit(); - form.remove(); - } - - - function normalizeHtml(code) { - let codeLc = code.toLowerCase(); - let hasBodyStart = codeLc.match(''); - let hasBodyEnd = codeLc.match(''); - let hasHtmlStart = codeLc.match(''); - let hasHtmlEnd = codeLc.match(''); - - let hasDocType = codeLc.match(/^\s*\n' + result; - } - - if (!hasHtmlEnd) { - result = result + '\n'; - } - - if (!hasBodyStart) { - result = result.replace('', '\n\n \n\n'); - } - - if (!hasBodyEnd) { - result = result.replace('', '\n\n'); - } - - result = '\n' + result; - - return result; - } - - - function run() { - if (isJS) { - runJS(); - } else { - runHTML(); - } - isFirstRun = false; - } - - -} - - -function addBlockHighlight(pre, lines) { - - if (!lines) { - return; - } - - let ranges = lines.replace(/\s+/g, '').split(','); - - /*jshint -W084 */ - for (let i = 0, range; range = ranges[i++];) { - range = range.split('-'); - - let start = +range[0], - end = +range[1] || start; - - - let mask = '' + - new Array(start + 1).join('\n') + - '' + new Array(end - start + 2).join('\n') + ''; - - pre.insertAdjacentHTML("afterBegin", mask); - } - -} - - -function addInlineHighlight(pre, ranges) { - - // select code with the language text, not block-highlighter - let codeElem = pre.querySelector('code[class*="language-"]'); - - ranges = ranges ? ranges.split(",") : []; - - for (let i = 0; i < ranges.length; i++) { - let piece = ranges[i].split(':'); - let lineNum = +piece[0], strRange = piece[1].split('-'); - let start = +strRange[0], end = +strRange[1]; - let mask = '' + - new Array(lineNum + 1).join('\n') + - new Array(start + 1).join(' ') + - '' + new Array(end - start + 1).join(' ') + ''; - - codeElem.insertAdjacentHTML("afterBegin", mask); - } -} - - -module.exports = CodeBox; diff --git a/modules/client/prism/codeTabsBox.js b/modules/client/prism/codeTabsBox.js deleted file mode 100755 index a7c8295..0000000 --- a/modules/client/prism/codeTabsBox.js +++ /dev/null @@ -1,97 +0,0 @@ -let delegate = require('client/delegate'); -let addLineNumbers = require('./addLineNumbers'); - -function CodeTabsBox(elem) { - if (window.ebookType) { - return; - } - - this.elem = elem; - this.translateX = 0; - - this.switchesElem = elem.querySelector('[data-code-tabs-switches]'); - this.switchesElemItems = this.switchesElem.firstElementChild; - this.arrowLeft = elem.querySelector('[data-code-tabs-left]'); - this.arrowRight = elem.querySelector('[data-code-tabs-right]'); - - - this.arrowLeft.onclick = function(e) { - e.preventDefault(); - - this.translateX = Math.max(0, this.translateX - this.switchesElem.offsetWidth); - this.renderTranslate(); - }.bind(this); - - - this.arrowRight.onclick = function(e) { - e.preventDefault(); - - this.translateX = Math.min(this.translateX +this.switchesElem.offsetWidth, this.switchesElemItems.offsetWidth - this.switchesElem.offsetWidth); - this.renderTranslate(); - }.bind(this); - - this.delegate('.code-tabs__switch', 'click', this.onSwitchClick); -} - -CodeTabsBox.prototype.onSwitchClick = function(e) { - e.preventDefault(); - - let siblings = e.delegateTarget.parentNode.children; - let tabs = this.elem.querySelector('[data-code-tabs-content]').children; - - - let selectedIndex; - for(let i=0; i start/stop() -// 2) new Spinner() -> somewhere.append(spinner.elem) -> start/stop -function Spinner(options) { - options = options || {}; - this.elem = options.elem; - - this.size = options.size || 'medium'; - // any class to add to spinner (make spinner special here) - this.class = options.class ? (' ' + options.class) : ''; - - // any class to add to element (to hide it's content for instance) - this.elemClass = options.elemClass; - - if (this.size != 'medium' && this.size != 'small' && this.size != 'large') { - throw new Error("Unsupported size: " + this.size); - } - - if (!this.elem) { - this.elem = document.createElement('div'); - } -} - -Spinner.prototype.start = function() { - if (this.elemClass) { - this.elem.classList.toggle(this.elemClass); - } - - this.elem.insertAdjacentHTML('beforeend', ''); -}; - -Spinner.prototype.stop = function() { - let spinnerElem = this.elem.querySelector('.spinner'); - if (!spinnerElem) return; // already stopped or never started - - spinnerElem.remove(); - - if (this.elemClass) { - this.elem.classList.toggle(this.elemClass); - } -}; - -module.exports = Spinner; diff --git a/modules/client/trackjs.js b/modules/client/trackjs.js deleted file mode 100755 index c88fa7f..0000000 --- a/modules/client/trackjs.js +++ /dev/null @@ -1,43 +0,0 @@ - -window._trackJs = { token: '8d286dd1cbf744b987a7226ee9a09324' }; -// COPYRIGHT (c) 2015 TrackJS LLC ALL RIGHTS RESERVED -(function(h,p,k){"use awesome";if(h.trackJs)h.console&&h.console.warn&&h.console.warn("TrackJS global conflict");else{let l=function(a,b,c,d,e){this.util=a;this.onError=b;this.onFault=c;this.options=e;e.enabled&&this.initialize(d)};l.prototype={initialize:function(a){a.addEventListener&&(this.wrapAndCatch(a.Element.prototype,"addEventListener",1),this.wrapAndCatch(a.XMLHttpRequest.prototype,"addEventListener",1),this.wrapRemoveEventListener(a.Element.prototype),this.wrapRemoveEventListener(a.XMLHttpRequest.prototype)); - this.wrapAndCatch(a,"setTimeout",0);this.wrapAndCatch(a,"setInterval",0)},wrapAndCatch:function(a,b,c){let d=this,e=a[b];d.util.hasFunction(e,"apply")&&(a[b]=function(){try{let f=Array.prototype.slice.call(arguments),g=f[c],u,h;if(d.options.bindStack)try{throw Error();}catch(k){h=k.stack,u=d.util.isoNow()}if("addEventListener"===b&&(this._trackJsEvt||(this._trackJsEvt=new m),this._trackJsEvt.getWrapped(f[0],g,f[2])))return;g&&d.util.hasFunction(g,"apply")&&(f[c]=function(){try{return g.apply(this, - arguments)}catch(a){throw d.onError("catch",a,{bindTime:u,bindStack:h}),d.util.wrapError(a);}},"addEventListener"===b&&this._trackJsEvt.add(f[0],g,f[2],f[c]));return e.apply(this,f)}catch(l){a[b]=e,d.onFault(l)}})},wrapRemoveEventListener:function(a){if(a&&a.removeEventListener&&this.util.hasFunction(a.removeEventListener,"call")){let b=a.removeEventListener;a.removeEventListener=function(a,d,e){if(this._trackJsEvt){let f=this._trackJsEvt.getWrapped(a,d,e);f&&this._trackJsEvt.remove(a,d,e);return b.call(this, - a,f,e)}return b.call(this,a,d,e)}}}};let m=function(){this.events=[]};m.prototype={add:function(a,b,c,d){-1>=this.indexOf(a,b,c)&&this.events.push([a,b,!!c,d])},remove:function(a,b,c){a=this.indexOf(a,b,!!c);0<=a&&this.events.splice(a,1)},getWrapped:function(a,b,c){a=this.indexOf(a,b,!!c);return 0<=a?this.events[a][3]:k},indexOf:function(a,b,c){for(let d=0;dthis.maxLength&&(this.appender=this.appender.slice(Math.max(this.appender.length-this.maxLength,0)))},add:function(a,b){let c=this.util.uuid();this.appender.push({key:c,category:a,value:b});this.truncate();return c},get:function(a,b){let c,d;for(d=0;db.indexOf("localhost:0")&&(this._trackJs={method:a,url:b});return c.apply(this,arguments)};a.prototype.send=function(){try{if(!this._trackJs)return d.apply(this,arguments);this._trackJs.logId=b.log.add("n",{startedOn:b.util.isoNow(),method:this._trackJs.method,url:this._trackJs.url});b.listenForNetworkComplete(this)}catch(a){b.onFault(a)}return d.apply(this,arguments)};return a},listenForNetworkComplete:function(a){let b=this; - b.window.ProgressEvent&&a.addEventListener&&a.addEventListener("readystatechange",function(){4===a.readyState&&b.finalizeNetworkEvent(a)},!0);a.addEventListener?a.addEventListener("load",function(){b.finalizeNetworkEvent(a);b.checkNetworkFault(a)},!0):setTimeout(function(){try{let c=a.onload;a.onload=function(){b.finalizeNetworkEvent(a);b.checkNetworkFault(a);"function"===typeof c&&b.util.hasFunction(c,"apply")&&c.apply(a,arguments)};let d=a.onerror;a.onerror=function(){b.finalizeNetworkEvent(a); - b.checkNetworkFault(a);"function"===typeof oldOnError&&d.apply(a,arguments)}}catch(e){b.onFault(e)}},0)},finalizeNetworkEvent:function(a){if(a._trackJs){let b=this.log.get("n",a._trackJs.logId);b&&(b.completedOn=this.util.isoNow(),b.statusCode=1223==a.status?204:a.status,b.statusText=1223==a.status?"No Content":a.statusText)}},checkNetworkFault:function(a){if(this.options.error&&400<=a.status&&1223!=a.status){let b=a._trackJs||{};this.onError("ajax",a.status+" "+a.statusText+": "+b.method+" "+b.url)}}, - report:function(){return this.log.all("n")}};let n=function(a){this.util=a;this.disabled=!1;this.throttleStats={attemptCount:0,throttledCount:0,lastAttempt:(new Date).getTime()};h.JSON&&h.JSON.stringify||(this.disabled=!0)};n.prototype={errorEndpoint:function(a,b){b=(b||"https://capture.trackjs.com/capture")+("?token="+a);return this.util.isBrowserIE()?"//"+b.split("://")[1]:b},usageEndpoint:function(a){return this.appendObjectAsQuery(a,"https://usage.trackjs.com/usage.gif")},trackerFaultEndpoint:function(a){return this.appendObjectAsQuery(a, - "https://usage.trackjs.com/fault.gif")},appendObjectAsQuery:function(a,b){b+="?";for(let c in a)a.hasOwnProperty(c)&&(b+=encodeURIComponent(c)+"="+encodeURIComponent(a[c])+"&");return b},getCORSRequest:function(a,b){let c=new h.XMLHttpRequest;"withCredentials"in c?(c.open(a,b),c.setRequestHeader("Content-Type","text/plain")):"undefined"!==typeof h.XDomainRequest?(c=new h.XDomainRequest,c.open(a,b)):c=null;return c},sendTrackerFault:function(a){this.throttle(a)||((new Image).src=this.trackerFaultEndpoint(a))}, - sendUsage:function(a){(new Image).src=this.usageEndpoint(a)},sendError:function(a,b){let c=this;if(!this.disabled&&!this.throttle(a))try{let d=this.getCORSRequest("POST",this.errorEndpoint(b));d.onreadystatechange=function(){4===d.readyState&&200!==d.status&&(c.disabled=!0)};d._trackJs=k;d.send(h.JSON.stringify(a))}catch(e){throw this.disabled=!0,e;}},throttle:function(a){let b=(new Date).getTime();this.throttleStats.attemptCount++;if(this.throttleStats.lastAttempt+1E3>=b){if(this.throttleStats.lastAttempt= - b,10 unless options.noGlobalEvents is set -// -// # Events -// triggers fail/success on load end: -// --> by default status=200 is ok, the others are failures -// --> options.normalStatuses = [201,409] allow given statuses -// --> fail event has .reason field -// --> success event has .result field -// -// # JSON -// --> send(object) calls JSON.stringify -// --> adds Accept: json (we want json) by default, unless options.raw -// if options.json or server returned json content type -// --> autoparse json -// --> fail if error -// -// # CSRF -// --> requests sends header X-XSRF-TOKEN from cookie - -function xhr(options) { - - let request = new XMLHttpRequest(); - - let method = options.method || 'GET'; - - let body = options.body; - let url = options.url; - - request.open(method, url, options.sync ? false : true); - - request.method = method; - - // token/header names same as angular $http for easier interop - let csrfCookie = getCsrfCookie(); - if (csrfCookie && !options.skipCsrf) { - request.setRequestHeader("X-XSRF-TOKEN", csrfCookie); - } - - if ({}.toString.call(body) == '[object Object]') { - // must be OPENed to setRequestHeader - request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - body = JSON.stringify(body); - } - - if (!options.noDocumentEvents) { - request.addEventListener('loadstart', event => { - request.timeStart = Date.now(); - let e = wrapEvent('xhrstart', event); - document.dispatchEvent(e); - }); - request.addEventListener('loadend', event => { - let e = wrapEvent('xhrend', event); - document.dispatchEvent(e); - }); - request.addEventListener('success', event => { - let e = wrapEvent('xhrsuccess', event); - e.result = event.result; - document.dispatchEvent(e); - }); - request.addEventListener('fail', event => { - let e = wrapEvent('xhrfail', event); - e.reason = event.reason; - document.dispatchEvent(e); - }); - } - - if (!options.raw) { // means we want json - request.setRequestHeader("Accept", "application/json"); - } - - request.setRequestHeader('X-Requested-With', "XMLHttpRequest"); - - let normalStatuses = options.normalStatuses || [200]; - - function wrapEvent(name, e) { - let event = new CustomEvent(name); - event.originalEvent = e; - return event; - } - - function fail(reason, originalEvent) { - let e = wrapEvent("fail", originalEvent); - e.reason = reason; - request.dispatchEvent(e); - } - - function success(result, originalEvent) { - let e = wrapEvent("success", originalEvent); - e.result = result; - request.dispatchEvent(e); - } - - request.addEventListener("error", e => { - fail("Ошибка связи с сервером.", e); - }); - - request.addEventListener("timeout", e => { - fail("Превышено максимально допустимое время ожидания ответа от сервера.", e); - }); - - request.addEventListener("abort", e => { - fail("Запрос был прерван.", e); - }); - - request.addEventListener("load", e => { - if (!request.status) { // does that ever happen? - fail("Не получен ответ от сервера.", e); - return; - } - - if (normalStatuses.indexOf(request.status) == -1) { - fail("Ошибка на стороне сервера (код " + request.status + "), попытайтесь позднее.", e); - return; - } - - let result = request.responseText; - let contentType = request.getResponseHeader("Content-Type"); - if (contentType.match(/^application\/json/) || options.json) { // autoparse json if WANT or RECEIVED json - try { - result = JSON.parse(result); - } catch (e) { - fail("Некорректный формат ответа от сервера.", e); - return; - } - } - - success(result, e); - }); - - // defer to let other handlers be assigned - setTimeout(function() { - request.send(body); - }, 0); - - return request; - -} - - -document.addEventListener('xhrfail', function(event) { - new notification.Error(event.reason); -}); - - -module.exports = xhr; diff --git a/modules/config/handlers.js b/modules/config/handlers.js index 010ed19..cc00041 100755 --- a/modules/config/handlers.js +++ b/modules/config/handlers.js @@ -5,42 +5,43 @@ const fs = require('fs'); let handlerNames = [ 'static', - 'requestId', - 'requestLog', - 'nocache', + 'jsengine/koa/requestId', + 'jsengine/koa/requestLog', + 'jsengine/koa/nocache', // this middleware adds this.render method // it is *before error*, because errors need this.render 'render', // errors wrap everything - 'error', + 'jsengine/koa/error', // this logger only logs HTTP status and URL // before everything to make sure it log all - 'accessLogger', + 'jsengine/koa/accessLogger', // pure node.js examples from tutorial // before session // before form parsing, csrf checking or whatever, bare node - 'nodeExample', + 'jsengine/koa/nodeExample', // before anything that may deal with body // it parses JSON & URLENCODED FORMS, // it does not parse form/multipart - 'bodyParser', + 'jsengine/koa/bodyParser', // parse FORM/MULTIPART // (many tweaks possible, lets the middleware decide how to parse it) - 'multipartParser', + 'jsengine/koa/multipartParser', // right after parsing body, make sure we logged for development - 'verboseLogger', + 'jsengine/koa/verboseLogger', - 'conditional', + 'jsengine/koa/conditional', - process.env.NODE_ENV=='development' && 'dev', - 'tutorial' + process.env.NODE_ENV === 'development' && 'dev', + 'jsengine/koa/tutorial', + 'frontpage' ].filter(Boolean); let handlers = {}; diff --git a/modules/config/index.js b/modules/config/index.js index 1ab7508..f9c9b9f 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -6,9 +6,6 @@ let env = process.env; // NODE_ENV = development || test || production env.NODE_ENV = env.NODE_ENV || 'development'; -//if (!env.SITE_HOST) throw new Error("env.SITE_HOST is not set"); -//if (!env.STATIC_HOST) throw new Error("env.STATIC_HOST is not set"); - let secret = require('./secret'); let lang = env.NODE_LANG || 'en'; @@ -24,13 +21,13 @@ if (env.DEV_TRACE) { let config = module.exports = { urlBase: { // node may be behind nginx, use this in documents - main: env.URL_BASE_MAIN || env.URL_BASE || 'http://localhost:3000', - static: env.URL_BASE_STATIC || env.URL_BASE || 'http://localhost:3000', + main: new URL(env.URL_BASE_MAIN || env.URL_BASE || 'http://localhost:3000'), + static: new URL(env.URL_BASE_STATIC || env.URL_BASE || 'http://localhost:3000'), }, urlBaseProduction: { // when even in dev mode we must reference prod, use this (maybe remove it?) - main: env.URL_BASE_PRODUCTION_MAIN || env.URL_BASE || 'http://localhost:3000', - static: env.URL_BASE_PRODUCTION_STATIC || env.URL_BASE || 'http://localhost:3000' + main: new URL(env.URL_BASE_PRODUCTION_MAIN || env.URL_BASE || 'http://localhost:3000'), + static: new URL(env.URL_BASE_PRODUCTION_STATIC || env.URL_BASE || 'http://localhost:3000') }, server: { @@ -74,9 +71,8 @@ require.extensions['.yml'] = function(module, filename) { // after module.exports for circle dep w/ config -const t = require('i18n'); - -t.requirePhrase(''); // root locales +const t = require('jsengine/i18n'); +t.requireHandlerLocales(); // webpack config uses general config // we have a loop dep here diff --git a/modules/config/webpack.js b/modules/config/webpack.js index 60042fa..99bc336 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -1,5 +1,3 @@ -'use strict'; - let fs = require('fs'); let nib = require('nib'); let rupture = require('rupture'); @@ -7,8 +5,8 @@ let path = require('path'); let chokidar = require('chokidar'); let config = require('config'); let webpack = require('webpack'); -let WriteVersionsPlugin = require('lib/webpack/writeVersionsPlugin'); -let CssWatchRebuildPlugin = require('lib/webpack/cssWatchRebuildPlugin'); +let WriteVersionsPlugin = require('jsengine/webpack/writeVersionsPlugin'); +let CssWatchRebuildPlugin = require('jsengine/webpack/cssWatchRebuildPlugin'); const CopyWebpackPlugin = require('copy-webpack-plugin') const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); @@ -20,7 +18,7 @@ const glob = require('glob'); // no webpack dependencies inside // no es6 (for 6to5 processing) inside // NB: includes angular-* -let noProcessModulesRegExp = /node_modules\/(angular|prismjs)/; +let noProcessModulesRegExp = new RegExp("node_modules" + (path.sep === '/' ? path.sep : '\\\\') + "(angular|prismjs|sanitize-html|i18n-iso-countries)"); let devMode = process.env.NODE_ENV == 'development'; @@ -35,28 +33,62 @@ module.exports = function (config) { `${name}.${ext}`; } - let modulesDirectories = ['node_modules']; + let modulesDirectories = [path.join(process.cwd(), 'node_modules')]; if (process.env.NODE_PATH) { modulesDirectories = modulesDirectories.concat(process.env.NODE_PATH.split(/[:;]/).map(p => path.resolve(p))); } + //console.log("MODULE DIRS", modulesDirectories); + + /** + * handler/client/assets/* goes to public/assets/ + */ let assetPaths = []; - for(let handlerName in config.handlers) { + for (let handlerName in config.handlers) { let handlerPath = config.handlers[handlerName].path; let from = `${handlerPath}/client/assets`; - console.log(from); if (fse.existsSync(from)) { assetPaths.push(from); } } - console.log(assetPaths); + //console.log("ASSET PATHS", assetPaths); + + /** + * handler/client becomes handler.js + * handler/templates makes handler.css (via CssWatchRebuiltPlugin) + */ + let entries = { + head: 'client/head', + footer: 'client/footer', + tutorial: 'jsengine/koa/tutorial/client', + styles: config.tmpRoot + '/styles.styl', + frontpage: config.tmpRoot + '/frontpage.styl' + }; + + /* + for(let handlerName in config.handlers) { + let handlerNameShort = path.basename(handlerName); + let handlerPath = config.handlers[handlerName].path; + let e = []; + if (fse.existsSync(`${handlerPath}/client`)) { + e.push(`${handlerPath}/client`); + } + if (fse.existsSync(`${handlerPath}/templates`)) { + e.push(config.tmpRoot + `/${handlerNameShort}.styl`); + } + + if (e.length) { + entries[handlerNameShort] = e; + } + }*/ + //console.log("WEBPACK ENTRIES", entries); let webpackConfig = { output: { // fs path - path: path.join(config.publicRoot, 'pack'), + path: path.join(config.publicRoot, 'pack'), // path as js sees it // if I use another domain here, need enable Allow-Access-.. header there // and add to scripts, to let error handler track errors @@ -65,11 +97,11 @@ module.exports = function (config) { // в prod-режиме не можем ?, т.к. CDN его обрезают, поэтому [hash] в имени // (какой-то [hash] здесь необходим, иначе к chunk'ам типа 3.js, которые генерируются require.ensure, // будет обращение без хэша при загрузке внутри сборки. при изменении - барузерный кеш их не подхватит) - filename: extHash("[name]", 'js'), + filename: extHash("[name]", 'js'), chunkFilename: extHash("[name]-[id]", 'js'), - library: '[name]', - pathinfo: devMode + library: '[name]', + pathinfo: devMode }, cache: devMode, @@ -79,54 +111,54 @@ module.exports = function (config) { watchOptions: { aggregateTimeout: 10, - ignored: /node_modules/ + ignored: /node_modules/ }, watch: devMode, devtool: devMode ? "cheap-inline-module-source-map" : // try "eval" ? - process.env.NODE_ENV == 'production' ? 'source-map' : false, + process.env.NODE_ENV == 'production' ? 'source-map' : false, - profile: false, + profile: Boolean(process.env.WEBPACK_STATS), - entry: { - styles: config.tmpRoot + '/styles.styl', - head: 'client/head', - tutorial: 'tutorial/client', - footer: 'client/footer', - }, + entry: entries, + + /* + entry: { + styles: config.tmpRoot + '/styles.styl', + head: 'client/head', + tutorial: 'jsengine/koa/tutorial/client', + footer: 'client/footer', + },*/ module: { - rules: [ + rules: [ { test: /\.json$/, - use: 'json-loader' - }, - { - test: /\.i18n/, - use: 'translation-loader' + use: 'json-loader' }, { test: /\.yml$/, - use: ['json-loader', 'yaml-loader'] + use: ['json-loader', 'yaml-loader'] }, { test: /\.pug$/, - use: 'pug-loader?root=' + config.projectRoot + '/templates' + use: 'pug-loader?root=' + config.projectRoot + '/templates' }, { - test: /\.js$/, + test: /\.js$/, // babel shouldn't process modules which contain ws/browser.js, // which must not be run in strict mode (global becomes undefined) // babel would make all modules strict! - exclude: /node_modules\/(angular|prismjs|moment|blueimp-canvas-to-blob|codemirror|markdown-it)/, - use: [ + exclude: noProcessModulesRegExp, + use: [ // babel will work first { - loader: 'babel-loader', + loader: 'babel-loader', options: { presets: [ - ['env', { + // use require.resolve here to build files from symlinks + [require.resolve('babel-preset-env'), { //useBuiltIns: true, targets: { browsers: "> 3%" @@ -140,29 +172,29 @@ module.exports = function (config) { { test: /\.styl$/, // MiniCssExtractPlugin breaks HMR for CSS - use: [ + use: [ MiniCssExtractPlugin.loader, { - loader: 'css-loader', + loader: 'css-loader', options: { importLoaders: 1 } }, { - loader: 'postcss-loader', + loader: 'postcss-loader', options: { plugins: [ require('autoprefixer') ] } }, - 'hover-loader', + 'jsengine/webpack/hover-loader', { - loader: 'stylus-loader', + loader: 'stylus-loader', options: { - linenos: true, + linenos: true, 'resolve url': true, - use: [ + use: [ rupture(), nib(), function (style) { @@ -175,7 +207,7 @@ module.exports = function (config) { }, { test: /\.(png|jpg|gif|woff|eot|otf|ttf|svg)$/, - use: extHash('file-loader?name=[path][name]', '[ext]') + use: extHash('file-loader?name=[path][name]', '[ext]') } ], noParse: function (path) { @@ -193,15 +225,16 @@ module.exports = function (config) { resolve: { // allow require('styles') which looks for styles/index.styl extensions: ['.js', '.styl'], - alias: { + alias: { + 'entities/maps/entities.json': 'jsengine/markit/emptyEntities', config: 'client/config' }, - modules: modulesDirectories + modules: modulesDirectories }, resolveLoader: { - modules: modulesDirectories, + modules: modulesDirectories, extensions: ['.js'] }, @@ -211,7 +244,7 @@ module.exports = function (config) { plugins: [ new webpack.DefinePlugin({ - LANG: JSON.stringify(config.lang), + LANG: JSON.stringify(config.lang), IS_CLIENT: true }), @@ -220,76 +253,88 @@ module.exports = function (config) { _: 'lodash' }), - // prevent autorequire all moment locales - // https://github.com/webpack/webpack/issues/198 - new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + // ignore all locales (will require manually from moment-with-locale + new webpack.IgnorePlugin({ + checkResource: (arg) => { + // locale requires that file back from it, need to keep it + if (arg === '../moment') return false; + tmp = arg; + return true; + }, + // under dirs like: ../locales/.. + checkContext: arg => { + let ignore = arg.endsWith(path.join('node_modules', 'moment', 'locale')); + if (ignore) { + //console.log("ignore moment locale", tmp, arg); + return true; + } + } + }), // ignore site locale files except the lang new webpack.IgnorePlugin({ - // ignore yml files not like LANG.yml - test: (arg) => arg.endsWith('.yml') && arg !== './' + config.lang + '.yml' - }, // under dirs like: ../locales/.. - /\/locales(\/|$)/ - ), + checkResource: (arg) => arg.endsWith('.yml') && arg !== './' + config.lang + '.yml', + // under dirs like: ../locales/.. + checkContext: arg => /\/locales(\/|$)/.test(arg) + }), new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')), new MiniCssExtractPlugin({ - filename: extHash("[name]", 'css'), + filename: extHash("[name]", 'css'), chunkFilename: extHash("[id]", 'css'), }), - new CssWatchRebuildPlugin({ - styles: '{templates,styles}' - }), + new CssWatchRebuildPlugin(), new CopyWebpackPlugin( assetPaths.map(path => { return { from: path, - to: config.publicRoot + to: config.publicRoot } }), - { debug: 'debug'} - // { debug: 'warning'} + {debug: 'warning'} ), { apply: function (compiler) { - compiler.plugin("done", function (stats) { - stats = stats.toJson(); - fs.writeFileSync(`${config.tmpRoot}/stats.json`, JSON.stringify(stats)); - }); + if (process.env.WEBPACK_STATS) { + compiler.plugin("done", function (stats) { + stats = stats.toJson(); + fs.writeFileSync(`${config.tmpRoot}/stats.json`, JSON.stringify(stats)); + }); + } } } ], recordsPath: path.join(config.tmpRoot, 'webpack.json'), - devServer: { - port: 3001, // dev server itself does not use it, but outer tasks do + devServer: { + port: 3001, // dev server itself does not use it, but outer tasks do historyApiFallback: true, - hot: true, - watchDelay: 10, + hot: true, + watchDelay: 10, //noInfo: true, - publicPath: process.env.STATIC_HOST + ':3001/pack/', - contentBase: config.publicRoot + publicPath: process.env.STATIC_HOST + ':3001/pack/', + contentBase: config.publicRoot }, optimization: { minimizer: [ new UglifyJsPlugin({ - cache: true, - parallel: 2, + cache: true, + parallel: 2, uglifyOptions: { - ecma: 8, + ecma: 8, warnings: false, compress: { - drop_console: true, + drop_console: true, drop_debugger: true }, - output: { - beautify: true, + output: { + beautify: true, indent_level: 0 // for error reporting, to see which line actually has the problem // source maps actually didn't work in Qbaka that's why I put it here } diff --git a/handlers/dev/index.js b/modules/dev/index.js similarity index 54% rename from handlers/dev/index.js rename to modules/dev/index.js index b9b535c..5317fe0 100755 --- a/handlers/dev/index.js +++ b/modules/dev/index.js @@ -1,4 +1,4 @@ -let mountHandlerMiddleware = require('lib/mountHandlerMiddleware'); +let mountHandlerMiddleware = require('jsengine/koa/mountHandlerMiddleware'); exports.init = function(app) { app.use( mountHandlerMiddleware('/dev', __dirname) ); diff --git a/handlers/dev/router.js b/modules/dev/router.js similarity index 100% rename from handlers/dev/router.js rename to modules/dev/router.js diff --git a/handlers/dev/templates/index.pug b/modules/dev/templates/index.pug similarity index 100% rename from handlers/dev/templates/index.pug rename to modules/dev/templates/index.pug diff --git a/handlers/tutorial/controller/frontpage.js b/modules/frontpage/controller/frontpage.js similarity index 60% rename from handlers/tutorial/controller/frontpage.js rename to modules/frontpage/controller/frontpage.js index 437270a..58e2c29 100755 --- a/handlers/tutorial/controller/frontpage.js +++ b/modules/frontpage/controller/frontpage.js @@ -1,23 +1,21 @@ -'use strict'; -const Article = require('../models/article'); -const TutorialTree = require('../models/tutorialTree'); -const Task = require('../models/task'); +const Article = require('jsengine/koa/tutorial').Article; +const TutorialTree = require('jsengine/koa/tutorial').TutorialTree; +const Task = require('jsengine/koa/tutorial').Task; const _ = require('lodash'); -const ArticleRenderer = require('../renderer/articleRenderer'); -const localStorage = require('localStorage').instance(); -const t = require('i18n'); - -t.requirePhrase('tutorial', 'frontpage'); +const ArticleRenderer = require('jsengine/koa/tutorial').ArticleRenderer; +const localStorage = require('jsengine/local-storage').instance(); +const t = require('jsengine/i18n'); +t.requirePhrase('frontpage'); exports.get = async function (ctx, next) { ctx.locals.sitetoolbar = true; ctx.locals.siteToolbarCurrentSection = "tutorial"; - ctx.locals.title = t('tutorial.frontpage.modern_javascript_tutorial'); + ctx.locals.title = t('frontpage.modern_javascript_tutorial'); - let topArticlesRendered = await localStorage.getOrGenerate('tutorial:frontpage', renderTop, process.env.TUTORIAL_EDIT); + let topArticlesRendered = await localStorage.getOrGenerate('frontpage', renderTop, process.env.TUTORIAL_EDIT); if (!Object.keys(topArticlesRendered).length) { ctx.throw(404, "Database is empty?"); // empty db diff --git a/handlers/nodeExample/index.js b/modules/frontpage/index.js similarity index 50% rename from handlers/nodeExample/index.js rename to modules/frontpage/index.js index 0973bb8..b3639bf 100755 --- a/handlers/nodeExample/index.js +++ b/modules/frontpage/index.js @@ -1,6 +1,4 @@ -'use strict'; - -const mountHandlerMiddleware = require('lib/mountHandlerMiddleware'); +const mountHandlerMiddleware = require('jsengine/koa/mountHandlerMiddleware'); exports.init = function(app) { app.use(mountHandlerMiddleware('/', __dirname)); diff --git a/handlers/tutorial/locales/frontpage/en.yml b/modules/frontpage/locales/en.yml old mode 100755 new mode 100644 similarity index 100% rename from handlers/tutorial/locales/frontpage/en.yml rename to modules/frontpage/locales/en.yml diff --git a/handlers/tutorial/locales/frontpage/ru.yml b/modules/frontpage/locales/ru.yml old mode 100755 new mode 100644 similarity index 100% rename from handlers/tutorial/locales/frontpage/ru.yml rename to modules/frontpage/locales/ru.yml diff --git a/modules/frontpage/router.js b/modules/frontpage/router.js new file mode 100755 index 0000000..2dbd98e --- /dev/null +++ b/modules/frontpage/router.js @@ -0,0 +1,8 @@ + +let Router = require('koa-router'); + +let frontpage = require('./controller/frontpage'); + +let router = module.exports = new Router(); + +router.get('/', frontpage.get); diff --git a/templates/blocks/frontpage-content/index.pug b/modules/frontpage/templates/blocks/_frontpage-content/index.pug similarity index 77% rename from templates/blocks/frontpage-content/index.pug rename to modules/frontpage/templates/blocks/_frontpage-content/index.pug index cee48f3..1ede8cf 100755 --- a/templates/blocks/frontpage-content/index.pug +++ b/modules/frontpage/templates/blocks/_frontpage-content/index.pug @@ -14,13 +14,13 @@ mixin frontpage-content(data) +e.title +e('a').link(href=subTopic.getUrl())= subTopic.title if (topic.children.length > 6) - +e('li').more= t('tutorial.frontpage.more') + +e('li').more= t('frontpage.more') +b.frontpage-content +e.container - let folder = tutorialTree.bySlug('js') +e.inner - +e.part= t('tutorial.frontpage.part', {num: 1}) + +e.part= t('frontpage.part', {num: 1}) +e.title= folder.title +e.description!= topArticlesRendered.js.content +b.list @@ -29,7 +29,7 @@ mixin frontpage-content(data) +e.container - let folder = tutorialTree.bySlug('ui') +e.inner - +e.part= t('tutorial.frontpage.part', {num: 2}) + +e.part= t('frontpage.part', {num: 2}) +e.title= folder.title +e.description!= topArticlesRendered.ui.content +b.list @@ -38,9 +38,9 @@ mixin frontpage-content(data) +e.container +e.inner - +e.part= t('tutorial.frontpage.part', {num: 3}) - +e.title= t('tutorial.frontpage.part3.title') - +e.description!= t('tutorial.frontpage.part3.content') + +e.part= t('frontpage.part', {num: 3}) + +e.title= t('frontpage.part3.title') + +e.description!= t('frontpage.part3.content') +b.list +partList(tutorialTree.tree.slice(2)) diff --git a/templates/blocks/frontpage-content/index.styl b/modules/frontpage/templates/blocks/_frontpage-content/index.styl similarity index 100% rename from templates/blocks/frontpage-content/index.styl rename to modules/frontpage/templates/blocks/_frontpage-content/index.styl diff --git a/handlers/tutorial/templates/frontpage.pug b/modules/frontpage/templates/frontpage.pug similarity index 61% rename from handlers/tutorial/templates/frontpage.pug rename to modules/frontpage/templates/frontpage.pug index afdcb2f..674cbf8 100755 --- a/handlers/tutorial/templates/frontpage.pug +++ b/modules/frontpage/templates/frontpage.pug @@ -1,19 +1,19 @@ extends /layouts/main -include ../../../templates/blocks/frontpage-content +include blocks/_frontpage-content block append variables - - var headTitle = 'The Modern Javascript Tutorial'; + - var headTitle = t('frontpage.modern_javascript_tutorial'); - var title = false - var header = false - - var comments = true - var layout_main_class = 'main_width-limit-wide' - var content_class = 'frontpage' block append head !=js("tutorial", {defer: true}) - meta(name="description" content=t('locales.meta.description')) + !=css("frontpage", {defer: true}) + meta(name="description" content=t('site.meta.description')) block content +frontpage-content({ diff --git a/modules/hover-loader.js b/modules/hover-loader.js deleted file mode 100755 index 5f3f460..0000000 --- a/modules/hover-loader.js +++ /dev/null @@ -1,7 +0,0 @@ - -module.exports = function(source) { - this.cacheable && this.cacheable(); - - return source.replace(/^(.*?:hover)/gim, '.working-hover $1'); - -}; diff --git a/modules/i18n/index.js b/modules/i18n/index.js deleted file mode 100755 index 336c9ad..0000000 --- a/modules/i18n/index.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - - -const LANG = require('config').lang; - -const log = require('log')(); -const path = require('path'); -const fs = require('fs'); -let config = require('config'); -let yaml = require('js-yaml'); - -let t = require('./t'); - -let docs = {}; - -t.requirePhrase = function(moduleName, packageName = '') { - - // if same doc was processed - don't redo it - if (docs[moduleName] && docs[moduleName].includes(packageName)) { - return; - } - - if (!docs[moduleName]) docs[moduleName] = []; - docs[moduleName].push(packageName); - - - let translationPath = moduleName ? - path.join(path.dirname(require.resolve(moduleName)), 'locales', packageName) : - path.join(config.projectRoot, 'locales', packageName); - - if (fs.existsSync(path.join(translationPath, LANG + '.yml'))) { - translationPath = path.join(translationPath, LANG + '.yml'); - } else { - translationPath = path.join(translationPath, 'en.yml'); - } - - let doc = yaml.safeLoad(fs.readFileSync(translationPath, 'utf-8')); - let name = (moduleName || 'locales') + (packageName ? ('.' + packageName) : ''); - - t.i18n.add(name, doc); -}; - - -module.exports = t; diff --git a/modules/i18n/t.js b/modules/i18n/t.js deleted file mode 100755 index 8a38685..0000000 --- a/modules/i18n/t.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -const BabelFish = require('babelfish'); - -const i18n = new BabelFish('en'); - -const LANG = require('config').lang; - -let err = console.error; - -if (typeof IS_CLIENT === 'undefined') { - const log = require('log')(); - err = (...args) => log.error(...args); -} - -function t(phrase) { - - if (!i18n.hasPhrase(LANG, phrase)) { - err("No such phrase", phrase); - } - - return i18n.t(LANG, ...arguments); -} - -if (LANG !== 'en') { - i18n.setFallback(LANG, 'en'); -} - -i18n.add = (...args) => i18n.addPhrase(LANG, ...args); - -t.i18n = i18n; - -module.exports = t; diff --git a/modules/jsengine b/modules/jsengine new file mode 160000 index 0000000..34aeab9 --- /dev/null +++ b/modules/jsengine @@ -0,0 +1 @@ +Subproject commit 34aeab95f13fc5345adc7a392841a19dd277c2bd diff --git a/modules/lib/capitalizeKeys.js b/modules/lib/capitalizeKeys.js deleted file mode 100755 index e4ae5f0..0000000 --- a/modules/lib/capitalizeKeys.js +++ /dev/null @@ -1,23 +0,0 @@ - - -function capitalizeKeys(obj) { - if (Array.isArray(obj)) { - return obj.map(capitalizeKeys); - } - - let output = {}; - - for (let key in obj) { - let keyCapitalized = key.replace(/_(\w)/g, function(match, letter) { - return letter.toUpperCase(); - }); - if (Object.prototype.toString.apply(obj[key]) === '[object Object]') { - output[keyCapitalized] = capitalizeKeys(obj[key]); - } else { - output[keyCapitalized] = obj[key]; - } - } - return output; -} - -module.exports = capitalizeKeys; diff --git a/modules/lib/debug.js b/modules/lib/debug.js deleted file mode 100755 index a2c74b4..0000000 --- a/modules/lib/debug.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - crap code to log & isolate steps for stackless errors when node-inspector dies - p() will print next number - */ -if (process.env.NODE_ENV == 'development') { - - global.p = function() { - let stack = new Error().stack.split("\n")[2].trim(); - console.log("----> " + global.p.counter++ + " at " + stack); - }; - global.p.counter = 1; - - - - -} else { - global.p = function() { - - }; -} - -/** - * When a code dies with a strange event error w/o trace - * Here I try to see what actually died - */ -if (process.env.DEBUG_ERROR) { - let proto = require('events').EventEmitter.prototype; - let emit = proto.emit; - proto.emit = function(type, err) { - if (type == 'error') { - console.log(this.test, this.constructor.name, err.message, err.stack); - process.exit(1); - } - else emit.apply(this, arguments); - }; - -} - -if (process.env.DEBUG_CONSOLE) { - // find where console.log is called from - console.log = function() { - require('fs').writeSync(1, new Error().stack.toString()) - }; -} \ No newline at end of file diff --git a/modules/lib/lazyRouterMiddleware.js b/modules/lib/lazyRouterMiddleware.js deleted file mode 100755 index 7e03e48..0000000 --- a/modules/lib/lazyRouterMiddleware.js +++ /dev/null @@ -1,16 +0,0 @@ -// router middleware which does require() on first activation -// instead of: -// require('./router').middleware() -// do: -// require('lib/lazyRouter')('./router') -// purpose: don't require everything on startup -module.exports = function(routerModulePath) { - let middleware = module.parent.require(routerModulePath).middleware(); - - return async function(ctx, next) { - await middleware(ctx, next); - }; - -}; - -delete require.cache[__filename]; diff --git a/modules/lib/logMemoryUsage.js b/modules/lib/logMemoryUsage.js deleted file mode 100755 index 94f45e6..0000000 --- a/modules/lib/logMemoryUsage.js +++ /dev/null @@ -1,13 +0,0 @@ -let csvPath = require('path').resolve(__dirname, 'memory.csv'); -let out = require('fs').createWriteStream(csvPath); - -setInterval(function() { - let time = Date.now(); - let memo = process.memoryUsage(); - out.write( - time + ',' + - memo.rss + ', ' + - memo.heapTotal + ', ' + - memo.heapUsed + '\n' - ); -}, 200); diff --git a/modules/lib/mountHandlerMiddleware.js b/modules/lib/mountHandlerMiddleware.js deleted file mode 100644 index 4ec0cd4..0000000 --- a/modules/lib/mountHandlerMiddleware.js +++ /dev/null @@ -1,54 +0,0 @@ -let path = require('path'); -let mount = require('koa-mount'); - - -// wrap('modulePath') -// is same as -// require('modulePath').middleware, -// but also calls apply/undo upon entering/leaving the middleware -// --> here it does: this.templateDir = handlerModule dirname -module.exports = function (prefix, moduleDir) { - - // actually includes router when the middleware is accessed (mount prefix matches) - let lazyRouterMiddleware = require('lib/lazyRouterMiddleware')(path.join(moduleDir, 'router')); - - let templateDir = path.join(moduleDir, 'templates'); - - // /users/me -> /me - return mount(prefix, async function wrapMiddleware(ctx, next) { - - // before entering middeware - let apply = () => { - // console.log("APPLY", templateDir); - ctx.templateDir = templateDir; - }; - - // before leaving middleware - let undo = () => { - // console.log("UNDO", templateDir); - delete ctx.templateDir; - }; - - apply(); - - try { - - await lazyRouterMiddleware(ctx, async function () { - // when middleware does await next, undo changes - undo(); - try { - await next(); - } finally { - // ...then apply back, when control goes back after await next - apply(); - } - }); - - } finally { - undo(); - } - - }); - -}; - diff --git a/modules/lib/serverPug/filterMarkit.js b/modules/lib/serverPug/filterMarkit.js deleted file mode 100755 index e2cce59..0000000 --- a/modules/lib/serverPug/filterMarkit.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -let filters = require('pug').filters; - -let BasicParser = require('markit').BasicParser; - -filters.markit = function(html) { - let parser = new BasicParser({ - html: true - }); - - return parser.render(html); -}; - diff --git a/modules/lib/serverPug/filterUglify.js b/modules/lib/serverPug/filterUglify.js deleted file mode 100755 index ca8955c..0000000 --- a/modules/lib/serverPug/filterUglify.js +++ /dev/null @@ -1,9 +0,0 @@ -let filters = require('pug').filters; - -let UglifyJS = require("uglify-js"); - -filters.uglify = function(str) { - let result = UglifyJS.minify(str); - return result.code; -}; - diff --git a/modules/lib/serverPug/index.js b/modules/lib/serverPug/index.js deleted file mode 100755 index 3f03af6..0000000 --- a/modules/lib/serverPug/index.js +++ /dev/null @@ -1,40 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const config = require('config'); -const pug = require('pug'); -const pugResolve = require('pugResolve'); - -/** - * extension for require('file.pug'), - * works in libs that are shared between client & server - */ -require.extensions['.pug'] = function(module, filename) { - - let compiled = pug.compile( - fs.readFileSync(filename, 'utf-8'), - Object.assign({}, config.pug, { - pretty: false, - compileDebug: false, - filename: filename, - plugins: [{ - resolve: pugResolve - }] - }) - ); - - - module.exports = function(locals) { - locals = locals || {}; - - return compiled(locals); - }; - -// console.log("---------------> HERE", fs.readFileSync(filename, 'utf-8'), module.exports); - -}; - -require('./filterMarkit'); - -require('./filterUglify'); - -module.exports = pug; diff --git a/modules/lib/stylusAsset.js b/modules/lib/stylusAsset.js deleted file mode 100755 index 32f1a2f..0000000 --- a/modules/lib/stylusAsset.js +++ /dev/null @@ -1,41 +0,0 @@ -let fs = require('fs'), - path = require('path'), - crypto = require('crypto'), - nodes = require('stylus').nodes, - utils = require('stylus').utils; - -module.exports = function(options) { - - let getVersion = options.getVersion || function(file) { - let buf = fs.readFileSync(file); - return crypto.createHash('md5').update(buf).digest('hex').substring(0, 8); - }; - - return function(style) { - let paths = style.options.paths || []; - - style.define('asset', function(url) { - let literal = new nodes.Literal('url("' + url.val + '")'); - - let evaluator = this; - let file = utils.lookup(url.val, paths); - - if (!file) { - throw new Error('File ' + literal + ' not be found'); - } - - let version = getVersion(file); - - let ext = path.extname(url.val); - let filepath = url.val.slice(0, url.val.length - ext.length); - - let newUrl = options.assetVersioning == 'query' ? (url.val + '?' + version) : - options.assetVersioning == 'file' ? (filepath + '.v' + version + ext) : - url.val; - - literal = new nodes.Literal('url("../i/' + newUrl + '")'); - - return literal; - }); - }; -}; diff --git a/modules/lib/throttle.js b/modules/lib/throttle.js deleted file mode 100755 index 4497627..0000000 --- a/modules/lib/throttle.js +++ /dev/null @@ -1,32 +0,0 @@ - -function throttle(func, ms) { - - let isThrottled = false, - savedArgs, - savedThis; - - function wrapper() { - - if (isThrottled) { - savedArgs = arguments; - savedThis = this; - return; - } - - func.apply(this, arguments); - - isThrottled = true; - - setTimeout(function() { - isThrottled = false; - if (savedArgs) { - wrapper.apply(savedThis, savedArgs); - savedArgs = savedThis = null; - } - }, ms); - } - - return wrapper; -} - -module.exports = throttle; diff --git a/modules/lib/treeUtil.js b/modules/lib/treeUtil.js deleted file mode 100755 index b08cffa..0000000 --- a/modules/lib/treeUtil.js +++ /dev/null @@ -1,27 +0,0 @@ -function walkArray(node, visitor) { - - for (let i = 0; i < node.children.length; i++) { - let treeNode = node.children[i]; - visitor(treeNode); - if (treeNode.children) { - walkArray(treeNode, visitor); - } - } - -} - -function flattenArray(root) { - - const flatten = []; - - walkArray(root, function(node) { - flatten.push(node); - }); - - return flatten; - -} - -exports.walkArray = walkArray; - -exports.flattenArray = flattenArray; diff --git a/modules/lib/verboseLogger.js b/modules/lib/verboseLogger.js deleted file mode 100755 index 54a7809..0000000 --- a/modules/lib/verboseLogger.js +++ /dev/null @@ -1,36 +0,0 @@ -const PathListCheck = require('pathListCheck'); - -module.exports = class VerboseLogger { - constructor() { - this.logPaths = new PathListCheck(); - } - - middleware() { - let self = this; - - return async function (ctx, next) { - - if (self.logPaths.check(ctx.path)) { - ctx.log.info({requestVerbose: ctx.request}); - } - - await next(); - }; - - } -}; - -/* -VerboseLogger.prototype.log = function(context) { - - for (let name in context.req.headers) { - console.log(name + ": " + context.req.headers[name]); - } - - if (context.request.body) { - console.log(context.request.body); - } - -}; -*/ - diff --git a/modules/lib/webpack/cssWatchRebuildPlugin.js b/modules/lib/webpack/cssWatchRebuildPlugin.js deleted file mode 100644 index 0f0fb52..0000000 --- a/modules/lib/webpack/cssWatchRebuildPlugin.js +++ /dev/null @@ -1,121 +0,0 @@ -const fs = require('fs'); -const Minimatch = require("minimatch").Minimatch; -const config = require('config'); -const glob = require('glob'); -const chokidar = require('chokidar'); - -class CssWatchRebuildPlugin { - constructor(roots) { - this.roots = roots; - } - - apply(compiler) { - compiler.hooks.afterEnvironment.tap("CssWatchRebuildPlugin", () => { - - compiler.watchFileSystem = new CssWatchFS( - compiler.watchFileSystem, - this.roots - ); - }); - } -} - -module.exports = CssWatchRebuildPlugin; - -class CssWatchFS { - constructor(wfs, roots) { - this.wfs = wfs; - this.roots = roots; - - this.rebuildAll(); - - for(let name in this.roots) { - chokidar.watch(`${this.roots[name]}/**/*.styl`, {ignoreInitial: true}).on('add', file => { - console.log("CHOKIDAR ADD"); - this.rebuildRoot(name); - })/*.on('unlink', file => { - console.log("CHOKIDAR UNNLINK"); - this.rebuildRoot(name); - })*/ - } - - } - - rebuildAll() { - for (let name in this.roots) { - this.rebuildRoot(name); - } - } - - rebuildRoot(name) { - - let styles = glob.sync(`${this.roots[name]}/**/*.styl`, {cwd: config.projectRoot}); - - for (const {path: handlerPath} of Object.values(config.handlers)) { - let handlerStyles = glob.sync(`${handlerPath}/client/styles/**/*.styl`, {cwd: config.projectRoot}); - styles.push(...handlerStyles); - } - - console.log("LOG STYLES", styles); - let content = styles.map(s => `@require '../${s}'`).join("\n"); - - fs.writeFileSync(`${config.tmpRoot}/${name}.styl`, content); - - this.wfs.inputFileSystem.purge(`${config.tmpRoot}/${name}.styl`); - - // console.log("REBUILD", name); - } - - // rebuild batch for deleted .styl - watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) { - const watcher = this.wfs.watch(files, dirs, missing, startTime, options, - ( - err, - filesModified, - dirsModified, - missingModified, - fileTimestamps, - dirTimestamps - ) => { - //console.log(fileTimestamps); - if (err) return callback(err); - - // console.log("Modified", filesModified, fs.existsSync(filesModified[0])); - for(let fileModified of filesModified) { - // deleted style - if (!fs.existsSync(fileModified)) { - for(let name in this.roots) { - - var mm = new Minimatch(`${this.roots[name]}/**/*.styl`); - let fn = fileModified.slice(config.projectRoot.length + 1); - //console.log("CHECK", fn); - - if (mm.match(fn)) { - this.rebuildRoot(name); - fileTimestamps.set(`${config.tmpRoot}/${name}.styl`, Date.now()); - } - - } - } - } - - callback( - err, - filesModified, - dirsModified, - missingModified, - fileTimestamps, - dirTimestamps - ); - }, - callbackUndelayed - ); - - return { - close: () => watcher.close(), - pause: () => watcher.pause(), - getContextTimestamps: () => watcher.getContextTimestamps(), - getFileTimestamps: () => watcher.getFileTimestamps() - }; - } -} diff --git a/modules/lib/webpack/writeVersionsPlugin.js b/modules/lib/webpack/writeVersionsPlugin.js deleted file mode 100755 index 4bebadc..0000000 --- a/modules/lib/webpack/writeVersionsPlugin.js +++ /dev/null @@ -1,29 +0,0 @@ -let fs = require('fs'); - -function WriteVersionsPlugin(file) { - this.file = file; -} - -WriteVersionsPlugin.prototype.writeStats = function(compiler, stats) { - stats = stats.toJson(); - let assetsByChunkName = stats.assetsByChunkName; - - for (let name in assetsByChunkName) { - if (assetsByChunkName[name] instanceof Array) { - assetsByChunkName[name] = assetsByChunkName[name].map(function(path) { - return compiler.options.output.publicPath + path; - }); - } else { - assetsByChunkName[name] = compiler.options.output.publicPath + assetsByChunkName[name]; - } - } - - //console.log(assetsByChunkName); - fs.writeFileSync(this.file, JSON.stringify(assetsByChunkName)); -}; - -WriteVersionsPlugin.prototype.apply = function(compiler) { - compiler.plugin("done", this.writeStats.bind(this, compiler)); -}; - -module.exports = WriteVersionsPlugin; \ No newline at end of file diff --git a/modules/localStorage.js b/modules/localStorage.js deleted file mode 100644 index 45141c3..0000000 --- a/modules/localStorage.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = class LocalStorage { - constructor() { - this.storage = Object.create(null); - } - - set(key, value) { - this.storage[key] = value; - } - - get(key) { - return this.storage[key]; - } - - has(key) { - return (key in this.storage); - } - - - static instance() { - if (!this._instance) { - this._instance = new LocalStorage(); - } - return this._instance; - } - - async getOrGenerate(key, func, skipCache) { - if (skipCache) return await func(); - - if (!this.has(key)) { - this.set(key, await func()); - } - return this.get(key); - } -}; \ No newline at end of file diff --git a/modules/log/browser.js b/modules/log/browser.js deleted file mode 100755 index 96e6029..0000000 --- a/modules/log/browser.js +++ /dev/null @@ -1,21 +0,0 @@ - -// browserify-version -// supports standard methods, but no settings -module.exports = function() { - - let logger = { - info: function() { - this.isDebug && console.info.apply(console, arguments); - }, - debug: function() { - this.isDebug && console.debug.apply(console, arguments); - }, - error: function() { - this.isDebug && console.error.apply(console, arguments); - } - }; - - return logger; - - -}; diff --git a/modules/log/bunyan.js b/modules/log/bunyan.js deleted file mode 100755 index 0a7314f..0000000 --- a/modules/log/bunyan.js +++ /dev/null @@ -1,3 +0,0 @@ -let bunyan = require('bunyan'); - -module.exports = bunyan; diff --git a/modules/log/errSerializer.js b/modules/log/errSerializer.js deleted file mode 100755 index e3cd904..0000000 --- a/modules/log/errSerializer.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = function(err) { - if (!err || !err.stack) - return err; - let obj = { - message: err.message, - name: err.name, - stack: getFullErrorStack(err), - code: err.code, - signal: err.signal - }; - return obj; -}; - -/* - * This function dumps long stack traces for exceptions having a cause() - * method. The error classes from - * [verror](https://github.com/davepacheco/node-verror) and - * [restify v2.0](https://github.com/mcavage/node-restify) are examples. - * - * Based on `dumpException` in - * https://github.com/davepacheco/node-extsprintf/blob/master/lib/extsprintf.js - */ -function getFullErrorStack(ex) { - let ret = ex.stack || ex.toString(); - if (ex.cause) { - let cex = typeof (ex.cause) === 'function' ? ex.cause() : ex.cause; - if (cex) { - ret += '\nCaused by: ' + getFullErrorStack(cex); - } - } - return ret; -} - diff --git a/modules/log/httpErrorSerializer.js b/modules/log/httpErrorSerializer.js deleted file mode 100755 index 54590e4..0000000 --- a/modules/log/httpErrorSerializer.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = function(httpError) { - if (!httpError.status) { - return httpError; - } - - return { - status: httpError.status, - message: httpError.message - }; -}; diff --git a/modules/log/index.js b/modules/log/index.js deleted file mode 100755 index ca22b1a..0000000 --- a/modules/log/index.js +++ /dev/null @@ -1,45 +0,0 @@ -// Usage: require('log')() -// NB: this file is RELOADED for EVERY REQUIRE -// (cleared from cache, to get parent filename every time) - -let bunyan = require('./bunyan'); -let requestSerializer = require('./requestSerializer'); -let requestVerboseSerializer = require('./requestVerboseSerializer'); -let resSerializer = require('./resSerializer'); -let errSerializer = require('./errSerializer'); -let httpErrorSerializer = require('./httpErrorSerializer'); -let path = require('path'); - - -// log.debug({req: ...}) -// exported => new serializers can be added by other modules -let serializers = exports.serializers = { - requestVerbose: requestVerboseSerializer, - request: requestSerializer, - res: resSerializer, - err: errSerializer, - httpError: httpErrorSerializer -}; - -let streams = require('./streams'); - -// if no name, then name is a parent module filename (or it's directory if index) -module.exports = function(name) { - if (!name) { - name = path.basename(module.parent.filename, '.js'); - if (name == 'index') { - name = path.basename(path.dirname(module.parent.filename)) + '/index'; - } - } - - let logger = bunyan.createLogger({ - name: name, - streams: streams, - serializers: serializers - }); - - return logger; -}; - - -delete require.cache[__filename]; diff --git a/modules/log/requestCaptureStream.js b/modules/log/requestCaptureStream.js deleted file mode 100755 index 81cebff..0000000 --- a/modules/log/requestCaptureStream.js +++ /dev/null @@ -1,94 +0,0 @@ -"use strict"; - -// Adapted and rewritten, from restify by Ilya Kantor -// initial Copyright 2012 Mark Cavage, Inc. All rights reserved. -let Stream = require('stream').Stream; -let util = require('util'); - -let bunyan = require('bunyan'); -let LRU = require('lru-cache'); -let os = require('os'); - -///--- API - -/** - * A Bunyan stream to capture records in a ring buffer and only pass through - * on a higher-level record. E.g. buffer up all records but only dump when - * getting a WARN or above. - * - * @param {Object} options contains the parameters: - * - {Object} stream The stream to which to write when dumping captured - * records. One of `stream` or `streams` must be specified. - * - {Array} streams One of `stream` or `streams` must be specified. - * - {Number|String} level The level at which to trigger dumping captured - * records. Defaults to bunyan.WARN. - * - {Number} maxRecords Number of records to capture. Default 100. - * - {Number} maxRequestIds Number of simultaneous request id capturing - * buckets to maintain. Default 1000. - */ -class RequestCaptureStream extends Stream { - constructor(opts) { - super(); - - this.level = opts.level ? bunyan.resolveLevel(opts.level) : bunyan.WARN; - this.limit = opts.maxRecords || 100; - this.maxRequestIds = opts.maxRequestIds || 1000; - - this.requestMap = LRU({ - max: this.maxRequestIds - }); - - this._offset = -1; - this._rings = []; - - this.stream = opts.stream; - } - - - write(record) { - // only request records - if (!record.requestId) return; - - let reqId = record.requestId; - let ring; - let self = this; - - if (!(ring = this.requestMap.get(reqId))) { - if (++this._offset > this.maxRequestIds) - this._offset = 0; - - if (this._rings.length <= this._offset) { - this._rings.push(new bunyan.RingBuffer({ - limit: self.limit - })); - } - - ring = this._rings[this._offset]; - ring.records.length = 0; - this.requestMap.set(reqId, ring); - } - - ring.write(record); - - if (record.level >= this.level && !(record.status && record.status < 500) ) { - this.dump(ring); - } - } - - dump(ring) { - - let i, r; - for (i = 0; i < ring.records.length; i++) { - r = ring.records[i]; - this.stream.write(this.stream.raw ? r : JSON.stringify(r, bunyan.safeCycles()) + '\n'); - } - ring.records.length = 0; - - } - - - -} - - -module.exports = RequestCaptureStream; diff --git a/modules/log/requestSerializer.js b/modules/log/requestSerializer.js deleted file mode 100755 index e1a7828..0000000 --- a/modules/log/requestSerializer.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function(request) { - if (!request || !request.method) { - return request; - } - return { - method: request.method, - url: request.originalUrl - }; -}; diff --git a/modules/log/requestVerboseSerializer.js b/modules/log/requestVerboseSerializer.js deleted file mode 100755 index b63fbfa..0000000 --- a/modules/log/requestVerboseSerializer.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = function(request) { - if (!request || !request.method) - return request; - return { - method: request.method, - url: request.originalUrl, - headers: request.headers, - body: request.body, - ip: request.ip - }; -}; diff --git a/modules/log/resSerializer.js b/modules/log/resSerializer.js deleted file mode 100755 index 8b8a0b2..0000000 --- a/modules/log/resSerializer.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function(res) { - if (!res || !res.statusCode) - return res; - return { - statusCode: res.statusCode, - header: res._header - }; -}; - diff --git a/modules/log/streams.js b/modules/log/streams.js deleted file mode 100755 index 720d1bd..0000000 --- a/modules/log/streams.js +++ /dev/null @@ -1,45 +0,0 @@ -const RequestCaptureStream = require('./requestCaptureStream'); - -let streams; - -if (process.env.LOG_LEVEL) { - streams = [{ - level: process.env.LOG_LEVEL, - stream: process.stdout - }]; -} else { - - switch (process.env.NODE_ENV) { - case 'development': - streams = [{ - level: 'debug', - stream: process.stdout - }]; - break; - case 'test': - streams = [/* empty, don't log anything, set LOG_LEVEL if want to see errors */]; - break; - case 'ebook': - case 'production': - - // normally I see only info, but look in error in case of problems - streams = [ - { - level: 'info', - stream: process.stdout - }, - { - level: 'debug', - type: 'raw', - stream: new RequestCaptureStream({ - level: 'error', - maxRecords: 150, - maxRequestIds: 2000, - stream: process.stderr - }) - } - ]; - } -} - -module.exports = streams; diff --git a/modules/markit/basicParser.js b/modules/markit/basicParser.js deleted file mode 100755 index 3a9e89d..0000000 --- a/modules/markit/basicParser.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - - -const LANG = require('config').lang; - -const MarkdownIt = require('markdown-it'); - -const charTypographyPlugin = require('./plugins/charTypography'); -const extendedCodePlugin = require('./plugins/extendedCode'); -const outlinedBlocksPlugin = require('./plugins/outlinedBlocks'); -const sourceBlocksPlugin = require('./plugins/sourceBlocks'); - -const imgDescToAttrsPlugin = require('./plugins/imgDescToAttrs'); - -const markdownErrorPlugin = require('./plugins/markdownError'); -const blockTagsPlugin = require('./plugins/blockTags/plugin'); -const cutPlugin = require('./plugins/blockTags/cut'); -const deflistPlugin = require('markdown-it-deflist'); -const getPrismLanguage = require('./getPrismLanguage'); - -module.exports = class BasicParser { - - constructor(options) { - options = options || {}; - this.options = options; - - this.env = options.env || {}; - this.md = new MarkdownIt(Object.assign({ - typographer: true, - blockTags: ['cut'].concat(getPrismLanguage.allSupported), - linkHeaderTag: false, - html: false, - quotes: LANG == 'ru' ? '«»„“' : '“”‘’' - }, options)); - - extendedCodePlugin(this.md); - outlinedBlocksPlugin(this.md); - sourceBlocksPlugin(this.md); - imgDescToAttrsPlugin(this.md); - markdownErrorPlugin(this.md); - blockTagsPlugin(this.md); - cutPlugin(this.md); - charTypographyPlugin(this.md); - deflistPlugin(this.md); - } - - parse(text) { - return this.md.parse(text, this.env); - } - parseInline(text) { - return this.md.parseInline(text, this.env); - } - - render(text) { - return this.md.renderer.render(this.parse(text), this.md.options, this.env); - } - - renderInline(text) { - let tokens = this.parseInline(text); - let result = this.md.renderer.render(tokens, this.md.options, this.env); - return result; - } - - renderTokens(tokens) { - return this.md.renderer.render(tokens, this.md.options, this.env); - } - -}; diff --git a/modules/markit/getPrismLanguage.js b/modules/markit/getPrismLanguage.js deleted file mode 100755 index 26ac75b..0000000 --- a/modules/markit/getPrismLanguage.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -let ext2language = { - html: 'markup', - js: 'javascript', - coffee: 'coffeescript', - '': 'none' -}; - -let languages = 'none markup javascript css coffeescript php http java ruby scss sql'.split(' '); - -let allSupported = Object.keys(ext2language).concat(languages); - -function getPrismLanguage(language) { - language = ext2language[language] || language; - if (languages.indexOf(language) == -1) language = 'none'; - - return language; -} - -// all supported programming languages -getPrismLanguage.languages = languages; - -// all supported programming languages and extensions -getPrismLanguage.allSupported = allSupported; - -module.exports = getPrismLanguage; diff --git a/modules/markit/index.js b/modules/markit/index.js deleted file mode 100755 index c2e9292..0000000 --- a/modules/markit/index.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -exports.BasicParser = require('./basicParser'); -exports.ServerParser = require('./serverParser'); - -exports.Token = require('markdown-it/lib/token'); -exports.tokenUtils = require('./utils/token'); - -exports.stripTitle = require('./stripTitle'); -exports.stripYamlMetadata = require('./stripYamlMetadata'); - diff --git a/modules/markit/loadImgSizeAsync.js b/modules/markit/loadImgSizeAsync.js deleted file mode 100755 index 3cefd97..0000000 --- a/modules/markit/loadImgSizeAsync.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -assert(typeof IS_CLIENT === 'undefined'); - - -const imageSize = require('image-size'); - -const path = require('path'); -const tokenUtils = require('./utils/token'); -const t = require('i18n'); -const fs = require('mz/fs'); -const gm = require('gm'); - -t.requirePhrase('markit', 'error'); - -class SrcError extends Error { -} - -module.exports = async function(tokens, options) { - - - for (let idx = 0; idx < tokens.length; idx++) { - let token = tokens[idx]; - - if (token.type == 'figure') { - - await processImageOrFigure(token); - continue; - } - - if (token.type != 'inline') continue; - - for (let i = 0; i < token.children.length; i++) { - let inlineToken = token.children[i]; - //
gives figure inside inline token - if (inlineToken.type != 'image' && inlineToken.type != 'figure') continue; - - await processImageOrFigure(inlineToken); - } - - } - - async function processImageOrFigure(token) { - - if (token.attrIndex('height') != -1 || token.attrIndex('width') != -1) return; - - try { - await doProcessImageOrFigure(token); - } catch (error) { - if (error instanceof SrcError) { - // replace image with error text - token.type = (token.type == 'image') ? 'markdown_error_inline' : 'markdown_error_block'; - token.tag = ''; - token.children = null; - token.attrs = null; - token.content = error.message; - } else { - throw error; - } - - } - } - - function srcUnderRoot(root, src) { - let absolutePath = path.join(root, src); - - if (absolutePath.slice(0, root.length + 1) != root + path.sep) { - throw new SrcError(t('markit.error.src_out_of_root', {src})); - } - - return absolutePath; - } - - async function getImageInfo(src) { - - let sourcePath = srcUnderRoot( - options.publicRoot, - src - ); - - // check readability - let stat; - - try { - stat = await fs.stat(sourcePath); - } catch (e) { - throw new SrcError(t('markit.error.image_not_found', {src})); - } - - if (!stat.isFile()) { - throw new SrcError(t('markit.error.image_not_found', {src})); - } - - if (/\.svg$/i.test(sourcePath)) { - try { - let size = await function(callback) { - // GraphicsMagick fails with `gm identify my.svg` - gm(sourcePath).options({imageMagick: true}).identify('{"width":%w,"height":%h}', callback); - }; - - size = JSON.parse(size); // warning: no error processing - - return size; - } catch (e) { - throw new SrcError(`${src}: ${e.message}`); - } - } - - - try { - return await new Promise((resolve, reject) => { - imageSize(sourcePath, (err, res) => err ? reject(err) : resolve(res)); - }); - - } catch (e) { - if (e instanceof TypeError) { - throw new SrcError(t('markit.error.image_invalid', {src})); - } - - throw new SrcError(`${src}: ${e.message}`); - } - } - - async function doProcessImageOrFigure(token) { - let src = tokenUtils.attrGet(token, 'src'); - if (!src) return; - - let imageInfo = await getImageInfo(src); - - tokenUtils.attrReplace(token, 'width', imageInfo.width); - tokenUtils.attrReplace(token, 'height', imageInfo.height); - } - - -}; - - diff --git a/modules/markit/loadSrcAsync.js b/modules/markit/loadSrcAsync.js deleted file mode 100755 index 1d0c81a..0000000 --- a/modules/markit/loadSrcAsync.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -/** - * Loads info from external sources for - * sandbox: links - * codetabs - * edit - * iframe edit - * [js src...] (for editing) - * - * @type {ok|exports|module.exports} - */ - -const assert = require('assert'); -const path = require('path'); -const fs = require('mz/fs'); -const t = require('i18n'); -const tokenUtils = require('./utils/token'); - -t.requirePhrase('markit', 'error'); - - -class SrcError extends Error { -} - -function srcUnderRoot(root, src) { - let absolutePath = path.join(root, src); - - if (absolutePath.slice(0, root.length + 1) !== root + path.sep) { - throw new SrcError(t('markit.error.src_out_of_root', {src})); - } - - return absolutePath; -} - -let storage; - -module.exports = async function (tokens, options) { - - if (!storage) { - storage = require('tutorial').TutorialViewStorage.instance(); - } - - let methods = { - blocktag_codetabs: src2plunk, - blocktag_edit: src2plunk, - blocktag_iframe, - blocktag_source, - link_open - }; - - async function src2plunk(token) { - - let src = path.join(options.resourceWebRoot, token.blockTagAttrs.src); - - let plunk = storage.get(src); - - if (!plunk) { - throw new SrcError(t('markit.error.no_such_plunk', {src})); - } - - token.plunk = plunk; - } - - async function link_open(token) { - let href = tokenUtils.attrGet(token, 'href'); - if (!href.startsWith('sandbox:')) return; - - let src = path.join(options.resourceWebRoot, href.slice('sandbox:'.length)); - - let plunk = storage.get(src); - - if (!plunk) { - throw new SrcError(t('markit.error.no_such_plunk', {src: href})); - } - - tokenUtils.attrReplace(token, 'href', plunk.getUrl()); - } - - async function blocktag_iframe(token) { - if (token.blockTagAttrs.edit || token.blockTagAttrs.zip) { - await src2plunk(token); - } - } - - async function blocktag_source(token) { - - if (!token.blockTagAttrs.src) return; - - let sourcePath = srcUnderRoot( - options.publicRoot, - path.join(options.resourceWebRoot, token.blockTagAttrs.src) - ); - - let content; - - try { - content = await fs.readFile(sourcePath, 'utf-8'); - } catch (e) { - throw new SrcError( - t('markit.error.read_file', {src: token.blockTagAttrs.src}) + - (process.env.NODE_ENV == 'development' ? ` [${sourcePath}]` : '') - ); - } - - token.content = content; - } - - async function walk(tokens, isInline) { - - for (let idx = 0; idx < tokens.length; idx++) { - let token = tokens[idx]; - let process = methods[token.type]; - if (process) { - try { - await process(token); - } catch (err) { - if (err instanceof SrcError) { - token.type = isInline ? 'markdown_error_inline' : 'markdown_error_block'; - token.content = err.message; - } else { - throw err; - } - } - } - - if (token.children) { - await walk(token.children, true); - } - - } - - } - - - await walk(tokens); -}; - - diff --git a/modules/markit/locales/code/en.yml b/modules/markit/locales/code/en.yml deleted file mode 100755 index ffed170..0000000 --- a/modules/markit/locales/code/en.yml +++ /dev/null @@ -1,4 +0,0 @@ -run: run -show: show -open: - sandbox: open in sandbox diff --git a/modules/markit/locales/code/ru.yml b/modules/markit/locales/code/ru.yml deleted file mode 100755 index 636532b..0000000 --- a/modules/markit/locales/code/ru.yml +++ /dev/null @@ -1,4 +0,0 @@ -run: выполнить -show: показать -open: - sandbox: открыть в песочнице diff --git a/modules/markit/locales/codeTabs/en.yml b/modules/markit/locales/codeTabs/en.yml deleted file mode 100755 index 103b49a..0000000 --- a/modules/markit/locales/codeTabs/en.yml +++ /dev/null @@ -1,4 +0,0 @@ - -result: Result -open_in_window: open in a new window -edit_in_sandbox: edit in the sandbox diff --git a/modules/markit/locales/codeTabs/ru.yml b/modules/markit/locales/codeTabs/ru.yml deleted file mode 100755 index f4070f9..0000000 --- a/modules/markit/locales/codeTabs/ru.yml +++ /dev/null @@ -1,4 +0,0 @@ - -result: Результат -open_in_window: открыть в отдельном окне -edit_in_sandbox: редактировать в песочнице diff --git a/modules/markit/locales/compare/en.yml b/modules/markit/locales/compare/en.yml deleted file mode 100755 index 65a4290..0000000 --- a/modules/markit/locales/compare/en.yml +++ /dev/null @@ -1,2 +0,0 @@ -merits: Merits -demerits: Demerits diff --git a/modules/markit/locales/compare/ru.yml b/modules/markit/locales/compare/ru.yml deleted file mode 100755 index 7851634..0000000 --- a/modules/markit/locales/compare/ru.yml +++ /dev/null @@ -1,2 +0,0 @@ -merits: Достоинства -demerits: Недостатки diff --git a/modules/markit/locales/demo/en.yml b/modules/markit/locales/demo/en.yml deleted file mode 100755 index 9b03557..0000000 --- a/modules/markit/locales/demo/en.yml +++ /dev/null @@ -1,2 +0,0 @@ -window: Demo in new window -run: Run the demo diff --git a/modules/markit/locales/demo/ru.yml b/modules/markit/locales/demo/ru.yml deleted file mode 100755 index 5179728..0000000 --- a/modules/markit/locales/demo/ru.yml +++ /dev/null @@ -1,2 +0,0 @@ -window: Демо в новом окне -run: Запустить демо diff --git a/modules/markit/locales/edit/en.yml b/modules/markit/locales/edit/en.yml deleted file mode 100755 index efbd26c..0000000 --- a/modules/markit/locales/edit/en.yml +++ /dev/null @@ -1,2 +0,0 @@ -open: - sandbox: open in sandbox diff --git a/modules/markit/locales/edit/ru.yml b/modules/markit/locales/edit/ru.yml deleted file mode 100755 index 1717afe..0000000 --- a/modules/markit/locales/edit/ru.yml +++ /dev/null @@ -1,2 +0,0 @@ -open: - sandbox: открыть в песочнице diff --git a/modules/markit/locales/error/en.yml b/modules/markit/locales/error/en.yml deleted file mode 100755 index 462f949..0000000 --- a/modules/markit/locales/error/en.yml +++ /dev/null @@ -1,7 +0,0 @@ -image_not_found: "Image \"#{src}\" not found" -image_invalid: "Image \"#{src}\" is corrupted" -attr_required: "Attribute \"#{attr}\" is required" -src_out_of_root: "The source path \"#{src}\" is outside of the root" -read_file: "Cannot read file \"#{src}\"" -no_such_plunk: "No such plunk \"#{src}\"" -anchor_exits: "Anchor exists already: \"#{anchor}\"" diff --git a/modules/markit/locales/error/ru.yml b/modules/markit/locales/error/ru.yml deleted file mode 100755 index 50f0054..0000000 --- a/modules/markit/locales/error/ru.yml +++ /dev/null @@ -1,7 +0,0 @@ -image_not_found: "Изображение \"#{src}\" не найдено" -image_invalid: "Изображение \"#{src}\" повреждено" -attr_required: "Отсутствует параметр \"#{attr}\"" -src_out_of_root: "Исходный путь \"#{src}\" находится вне корня" -read_file: "Не могу прочитать файл \"#{src}\"" -no_such_plunk: "Не найдена песочница \"#{src}\"" -anchor_exits: "Такая метка уже есть: \"#{anchor}\"" diff --git a/modules/markit/locales/iframe/en.yml b/modules/markit/locales/iframe/en.yml deleted file mode 100755 index e21bcfe..0000000 --- a/modules/markit/locales/iframe/en.yml +++ /dev/null @@ -1,4 +0,0 @@ -open: - sandbox: open in sandbox - window: open in new window - download: download as zip diff --git a/modules/markit/locales/iframe/ru.yml b/modules/markit/locales/iframe/ru.yml deleted file mode 100755 index 030256d..0000000 --- a/modules/markit/locales/iframe/ru.yml +++ /dev/null @@ -1,4 +0,0 @@ -open: - sandbox: открыть в песочнице - window: открыть в новом окне - download: скачать архив diff --git a/modules/markit/locales/outlined/en.yml b/modules/markit/locales/outlined/en.yml deleted file mode 100755 index 921b94f..0000000 --- a/modules/markit/locales/outlined/en.yml +++ /dev/null @@ -1,3 +0,0 @@ -smart: "Please note:" -warn: "Important:" -ponder: "How do you think?" diff --git a/modules/markit/locales/outlined/ru.yml b/modules/markit/locales/outlined/ru.yml deleted file mode 100755 index 7b453f2..0000000 --- a/modules/markit/locales/outlined/ru.yml +++ /dev/null @@ -1,3 +0,0 @@ -smart: "На заметку:" -warn: "Важно:" -ponder: "Как вы думаете?" diff --git a/modules/markit/migrate.js b/modules/markit/migrate.js deleted file mode 100755 index 80c2886..0000000 --- a/modules/markit/migrate.js +++ /dev/null @@ -1,228 +0,0 @@ -'use strict'; - -const parseAttrs = require('./utils/parseAttrs'); -const yaml = require('js-yaml'); -const stripIndents = require('textUtil/stripIndents'); - -module.exports = function(text) { - - text = text.replace(/[ \t]+$/gim, ''); // remove spaces - - text = text.replace(/\[(edit.*?)\](.*?)\[\/edit\]/g, '[$1 title="$2"]'); - text = text.replace(/\[(edit.*?)\/\]/g, '[$1]'); - - // remove [pre no-typography]...[/pre] - text = text.replace(/^\[pre.*?\]/gim, ''); - text = text.replace(/^\[\/pre\]/gim, ''); - - text = text.replace(/\[\]\(\/(.*?)\)/g, ''); - text = text.replace(/\[\]\((http.*?)\)/g, '<$1>'); - - text = text.replace(/\[(https?:.*?)\]\(\)/g, '<$1>'); - - text = text.replace(/\[key (.*?)\]/g, '`key:$1`'); - - text = text.replace(/\[demo([^\]]*?)\s*\/\]/g, '[demo$1]'); - - text = text.replace(/^```(\w+)[ ]*\n([\s\S]*?)^```/gim, function(match, lang, code) { - let comment; - - code = code.trim(); - - comment = code.match(/^\/\/\+(.*?)(\n|$)/); - - if (comment) { - comment = comment[1]; - code = code.replace(/^\/\/\+(.*?)(\n|$)/, ''); - } else { - comment = code.match(/^(\n|$)/); - if (comment) { - comment = comment[1]; - code = code.replace(/^(\n|$)/, ''); - } - } - - code = stripIndents(code); - - if (comment) comment = ' ' + comment.replace(/ /g, ' ').trim(); - else comment = ''; - - if (!code) return `[${lang}${comment}]`; - else return '```' + lang + comment + '\n' + code + '\n' + '```'; - }); - - // EXTRACT META... - let meta = {}; - // importance -> meta - - text = text.replace(/\n\[importance (\d)\]\n/, function(match, importance) { - meta.importance = +importance; - return '\n'; - }); - - text = text.replace(/\n\[libs\]([\s\S]*?)\[\/libs\]/, function(match, libs) { - meta.libs = libs.trim().split('\n'); - return '\n'; - }); - - let head = ''; - - text = text.replace(/\n\[head\]([\s\S]*?)\[\/head\]/, function(match, headContent) { - head = headContent.trim(); - return '\n'; - }); - - // ...EXTRACT META... - - // WITH LABELS... - - let codeBlockLabels = {}; - let codeInlineLabels = {}; - - text = text.replace(/\n```[\s\S]*?^```/gim, function(code) { - let label = '~CODELABEL:' + (Math.random() * 1e8 ^ 0) + '\n'; - codeBlockLabels[label] = code; - - return label; - }); - - let r = new RegExp('`[^`\n]+`', 'gim'); - text = text.replace(r, function(code) { - let label = '~INLINECODE:' + (Math.random() * 1e8 ^ 0); - codeInlineLabels[label] = code; - return label; - }); - - text = text.replace(/^(\[.*?\])$/gim, '\n$1\n'); // ensure that all block tags are in paragraphs - - - function fixListItem(listItem) { - - listItem = listItem.replace(/\n\n([.<*!а-яёa-z0-9])/gim, '\n\n $1'); - - let codeLabels = listItem.match(/~CODELABEL:\d+(\n|$)/g) || []; - for (let i = 0; i < codeLabels.length; i++) { - let label = codeLabels[i]; - if (label[label.length - 1] != '\n') label += '\n'; - codeBlockLabels[label] = indent(codeBlockLabels[label]); - } - - return listItem; - } - - text = text.replace(/
    ([\s\S]+?)<\/ul>/gim, function(match, list) { - - list = list.replace(/<\/li>
  • /g, '
  • \n
  • '); - - list = list.replace(/
  • \s*([\s\S]*?)\s*<\/li>/gim, function(m, listItem) { - - listItem = fixListItem(listItem); - - return '- ' + listItem; - }); - - return list; - }); - - text = text.replace(/
      ([\s\S]+?)<\/ol>/gim, function(match, list) { - - list = list.replace(/<\/li>
    1. /g, '
    2. \n
    3. '); - let i = 0; - list = list.replace(/
    4. \s*([\s\S]*?)\s*<\/li>/gim, function(m, listItem) { - - listItem = fixListItem(listItem); - - return ++i + '. ' + listItem; - }); - - return list; - }); - - text = text.replace(/
      ([\s\S]+?)<\/dl>/gim, function(match, list) { - - - list = list.replace(/<\/dt>
      /g, '\n
      '); - list = list.replace(/
      \s*([\s\S]*?)\s*<\/dt>/gim, '$1'); - - list = list.replace(/
      \s*([\s\S]*?)\s*<\/dd>/gim, function(m, listItem) { - - //console.log("\n--------------\nLISTITEM FROM\n", listItem); - listItem = fixListItem(listItem); - - //console.log("LISTITEM TO\n", listItem); - return ': ' + listItem.replace(/^\s+/, '') + '\n'; - }); - - return list; - }); - - - text = text.replace(//g, function(match, attrs) { - attrs = parseAttrs(attrs); - - let src = attrs.src; - if (!src) throw new Error('No src in match ' + match); - - delete attrs.src; - let attrString = []; - for (let name in attrs) { - attrString.push(`${name}="${attrs[name]}"`); - } - attrString = attrString.join(' '); - - if (attrString) attrString = '|' + attrString; - return `![${attrString}](${src})`; - }); - - - for (let label in codeInlineLabels) { - text = text.replace(label, escapeRegReplace(codeInlineLabels[label])); - } - - // code inside non-md table is not supported any more - text = text.replace(/<(th|td)>(.*?)<\/\1>/gim, function(match, td, content) { - return `<${td}>` + content.replace(/`(.*?)`/gim, '$1') + ``; - }); - - for (let label in codeBlockLabels) { - text = text.replace(label, escapeRegReplace(codeBlockLabels[label]) + '\n'); - } - - // ...WITH LABELS - - text = text.replace(/[ \t]+$/gim, ''); // trailing spaces - text = text.replace(/\n{3,}/gim, '\n\n'); // many line breaks into 2 - - text = text.replace(/\n\[compare\]([\s\S]*?)\[\/compare\]/g, function(match, content) { - // -List -> - List - return '\n```compare\n' + content.trim().replace(/^([-+])/gim, '$1 ') + '\n```'; - }); - - - text = text.replace(/\n\[(smart|warn|ponder|summary|online|offline|quote)(.*?)\]([\s\S]*?)\[\/\1\]/g, function(match, name, attrs, content) { - content = content.trim(); - - let delim = '```'; - - if (content.indexOf('```') > -1) delim = '````'; - - return '\n' + delim + name + (attrs ? ' ' + attrs.trim() : '') + '\n' + content + '\n' + delim; - }); - - - if (Object.keys(meta).length) { - text = yaml.safeDump(meta) + '\n---\n\n' + text.replace(/^\s*/, ''); - } - - return {meta, head, text}; -}; - -function indent(code) { - return ' ' + code.replace(/\n/gim, '\n '); -} - -// escape a string to use as a safe replace -// otherwise $1 becomes backreference -function escapeRegReplace(string) { - return string.replace(/\$/g, '$$$$'); // $ -> $$ -} diff --git a/modules/markit/plugins/blockTags/codetabs.js b/modules/markit/plugins/blockTags/codetabs.js deleted file mode 100755 index 0d3af35..0000000 --- a/modules/markit/plugins/blockTags/codetabs.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -assert(typeof IS_CLIENT === 'undefined'); - -const path = require('path'); -const getPrismLanguage = require('../../getPrismLanguage'); - -require('lib/serverPug'); - -const codeTabsTemplate = require('../../templates/codeTabs.pug'); - -module.exports = function(md) { - - md.renderer.rules.blocktag_codetabs = function(tokens, idx, options, env, slf) { - let token = tokens[idx]; - - if (options.ebookType) { - return `

      `; - } - - let files = token.plunk.files; - - let tabs = []; - - let hasServerJs = false; - - for (let i = 0; i < files.length; i++) { - let file = files[i]; - - let ext = path.extname(file.filename).slice(1); - - let prismLanguage = getPrismLanguage(ext); - - let languageClass = 'language-' + prismLanguage + ' line-numbers'; - - tabs.push({ - title: file.filename, - class: languageClass, - content: file.content - }); - - if (file.filename == 'server.js') { - hasServerJs = true; - } - } - - let height = +token.blockTagAttrs.height || 200; - - if (!options.html) { - height = Math.min(height, 800); - } - - - let locals = { - tabs, - height, - src: path.join(options.resourceWebRoot, token.blockTagAttrs.src) + '/' - }; - - if (hasServerJs) { - locals.zip = { - href: '/tutorial/zipview/' + path.basename(locals.src) + '.zip?plunkId=' + token.plunk.plunkId - }; - } else { - locals.edit = { - href: 'http://plnkr.co/edit/' + token.plunk.plunkId + '?p=preview', - plunkId: token.plunk.plunkId - }; - } - - locals.external = { - href: locals.src - }; - - return codeTabsTemplate(locals); - }; -}; - diff --git a/modules/markit/plugins/blockTags/cut.js b/modules/markit/plugins/blockTags/cut.js deleted file mode 100755 index 1fd2de6..0000000 --- a/modules/markit/plugins/blockTags/cut.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - - -function removeCut(state) { - for (let idx = state.tokens.length - 1; idx >= 0; idx--) { - - if (state.tokens[idx].type !== 'inline') continue; - - doReplacementsInToken(state.tokens[idx].children); - } -} - -function doReplacementsInToken(inlineTokens) { - let i, token; - - for (i = 0; i < inlineTokens.length; i++) { - token = inlineTokens[i]; - if (token.type === 'text') { - token.content = token.content.replace('[cut]', ''); - } - } -} - -module.exports = function smartArrows_plugin(md, scheme) { - // must come before the built-in m-dash and n-dash support - md.core.ruler.before('replacements', 'remove_cut', removeCut); -}; diff --git a/modules/markit/plugins/blockTags/demo.js b/modules/markit/plugins/blockTags/demo.js deleted file mode 100755 index bac6267..0000000 --- a/modules/markit/plugins/blockTags/demo.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -const t = require('i18n'); - -t.requirePhrase('markit', 'demo'); - -module.exports = function(md) { - - md.renderer.rules.blocktag_demo = function(tokens, idx, options, env, slf) { - - let src = tokens[idx].blockTagAttrs.src; - - if (src) { - let href = (src[0] == '/') ? src : options.staticHost + options.resourceWebRoot + '/' + src; - href += '/'; - - return `${t('markit.demo.window')}`; - } - - return `${t('markit.demo.run')}`; - - }; - -}; diff --git a/modules/markit/plugins/blockTags/edit.js b/modules/markit/plugins/blockTags/edit.js deleted file mode 100755 index dc8da90..0000000 --- a/modules/markit/plugins/blockTags/edit.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -/** - * Client/server plugin - */ - -const t = require('i18n'); - -t.requirePhrase('markit', 'edit'); -t.requirePhrase('markit', 'error'); - -module.exports = function(md) { - - md.renderer.rules.blocktag_edit = function(tokens, idx, options, env, slf) { - - let token = tokens[idx]; - - let text = token.blockTagAttrs.title || t('markit.edit.open.sandbox'); - - let href = `http://plnkr.co/edit/${token.plunk.plunkId}?p=preview`; - return `${md.utils.escapeHtml(text)}`; - }; - -}; diff --git a/modules/markit/plugins/blockTags/iframe.js b/modules/markit/plugins/blockTags/iframe.js deleted file mode 100755 index b844a5a..0000000 --- a/modules/markit/plugins/blockTags/iframe.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -/** - * Client/server plugin - */ - -const t = require('i18n'); - -t.requirePhrase('markit', 'iframe'); -t.requirePhrase('markit', 'error'); - -module.exports = function(md) { - - md.renderer.rules.blocktag_iframe = function(tokens, idx, options, env, slf) { - - let token = tokens[idx]; - - let trusted = options.html && !token.blockTagAttrs.untrusted ? 1 : 0; - - let height = +token.blockTagAttrs.height || 300; - - if (!options.html) { - height = Math.min(height, 800); - } - - let src = token.blockTagAttrs.src; - - let iframeId = options.html && token.blockTagAttrs.id ? ` id="${token.blockTagAttrs.id}"` : ''; - - if (!src) { - return `
      ${t('markit.error.attr_required', {attr: 'src'})}
      `; - } - - // relative url w/o domain means we want static host - // [iframe src="dir"] - // otherwise we want a dynamic service e.g - // [iframe src="/ajax/service"] - if (src[0] != '/' && !~src.indexOf('://')) { - if (!~src.indexOf('.')) src += '/'; - // samedomain means we keep iframe on current domain - // for using js between it and the main window (see travel/ in tutorial) - src = (options.html && token.blockTagAttrs.samedomain ? '' : options.staticHost) + options.resourceWebRoot + '/' + src; - } - - let toolbarHtml = ''; - if (token.blockTagAttrs.link) { - toolbarHtml += `
      - -
      - `; - } - - if (token.plunk && token.blockTagAttrs.edit) { - toolbarHtml += `
      - -
      - `; - } - - if (token.plunk && token.blockTagAttrs.zip) { - let zipname = src.split('/').filter(Boolean).reverse()[0]; - let href = `/tutorial/zipview/${zipname}.zip?plunkId=${token.plunk.plunkId}`; - toolbarHtml += `
      - -
      - `; - } - - return `
      -
      ${toolbarHtml}
      - -
      `; - - }; -}; diff --git a/modules/markit/plugins/blockTags/plugin.js b/modules/markit/plugins/blockTags/plugin.js deleted file mode 100755 index 969edba..0000000 --- a/modules/markit/plugins/blockTags/plugin.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -const parseAttrs = require('../../utils/parseAttrs'); -const getPrismLanguage = require('../../getPrismLanguage'); - -function rewriteInlineToBlockTags(state) { - for (let idx = 1; idx < state.tokens.length - 1; idx++) { - if (state.tokens[idx - 1].type == 'paragraph_open' && - state.tokens[idx + 1].type == 'paragraph_close' && - state.tokens[idx].type == 'inline') { - - let blockTagMatch = state.tokens[idx].content.trim().match(/^\[(\w+\s*[^\]]*)\]$/); - if (!blockTagMatch) continue; - - let blockTagAttrs = parseAttrs(blockTagMatch[1], true); - - let blockName = blockTagAttrs.blockName; - - // if not supported - if (!state.md.options.blockTags || state.md.options.blockTags.indexOf(blockName) == -1) continue; - - let tokenType = getPrismLanguage.allSupported.indexOf(blockName) == -1 ? 'blocktag_' + blockName : 'blocktag_source'; - - let blockTagToken = new state.Token(tokenType, blockName, state.tokens[idx].nesting); - - blockTagToken.blockTagAttrs = blockTagAttrs; - blockTagToken.map = state.tokens[idx].map.slice(); - blockTagToken.block = true; - blockTagToken.level = state.tokens[idx].level; - - state.tokens.splice(idx - 1, 3, blockTagToken); - // no need to move idx back, because - // p ! p p ! p - // 0 1 2 - // ^ if match here, we have this after move - // B p ! p - // ^ idx position ok - - } - } -} - - -module.exports = function(md) { - - md.core.ruler.push('rewrite_inline_to_block_tags', rewriteInlineToBlockTags); - -}; - - diff --git a/modules/markit/plugins/blockTags/todo.js b/modules/markit/plugins/blockTags/todo.js deleted file mode 100755 index 348e211..0000000 --- a/modules/markit/plugins/blockTags/todo.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = function(md) { - - md.renderer.rules.blocktag_todo = function(tokens, idx, options, env, slf) { - return ''; - }; - -}; diff --git a/modules/markit/plugins/charTypography.js b/modules/markit/plugins/charTypography.js deleted file mode 100755 index d2ecade..0000000 --- a/modules/markit/plugins/charTypography.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -function charTypography(state) { - for (let idx = state.tokens.length - 1; idx >= 0; idx--) { - - if (state.tokens[idx].type !== 'inline') continue; - - doReplacementsInToken(state.tokens[idx].children); - } -} - -function doReplacementsInToken(inlineTokens) { - let i, token; - - for (i = 0; i < inlineTokens.length; i++) { - token = inlineTokens[i]; - if (token.type === 'text') { - token.content = token.content - .replace(/([^+])\+\-/gi, '$1±') - .replace(/\.\.\./mg, '…') - .replace(/\([сСcC]\)(?=[^\.\,\;\:])/ig, '©') - .replace(/\(r\)/ig, '®') - .replace(/\(tm\)/ig, '™') - .replace(/(\s|;)\-(\s)/gi, '$1–$2') - .replace(/<->/gi, '↔').replace(/<-/gi, '←').replace(/(\s)->/gi, '$1→') - .replace(/\s-(\w)/gim, '‑$1'); // non-breaking hyphen: -Infinity won't get line-broken - } - } -} - -module.exports = function smartArrows_plugin(md, scheme) { - // must come before the built-in m-dash and n-dash support - md.core.ruler.before('replacements', 'char_typography', charTypography); -}; diff --git a/modules/markit/plugins/compare.js b/modules/markit/plugins/compare.js deleted file mode 100755 index 8489923..0000000 --- a/modules/markit/plugins/compare.js +++ /dev/null @@ -1,160 +0,0 @@ -'use strict'; - -/** - * Single: -
      compare_single_open -
      bullet_list_open -> compare_single_list_plus_open -
      -
        -
      • ...
      • add class to every li -
      • ...
      • -
      -
      -
      compare_list_plus_close -
      compare_close - - Double: -
      compare_double_open -
      bullet_list_open ->compare_double_list_plus_open -
      -
        -
        Достоинства
        -
      • ...
      • -
      • ...
      • -
      -
      -
      -
      bullet_list_open -> compare_double_list_plus_open -
      -
        -
        Недостатки
        -
      • ...
      • add class to every li -
      • ...
      • -
      -
      -
      compare_list_minus_close -
      - */ - -const parseAttrs = require('../utils/parseAttrs'); -const t = require('i18n'); -const markdownItContainer = require('markdown-it-container'); - -t.requirePhrase('markit', 'compare'); - -module.exports = function(md) { - - md.use(markdownItContainer, 'compare', { - marker: '`' - }); - - - // rewrite lists inside compare to compare_list_plus_open/close - md.core.ruler.push('rewrite_compare_list', function rewriteCompareList(state) { - - let tokens = state.tokens; - for (let idx = 0; idx < tokens.length; idx++) { - - if (tokens[idx].type != 'container_compare_open') continue; - - let listCount = 0; - let i; - for (i = idx + 1; i < tokens.length; i++) { - if (tokens[i].type == 'bullet_list_open') listCount++; - if (tokens[i].type == 'container_compare_close') break; - } - - if (listCount != 1 && listCount != 2) { - // do nothing if not a list, rewind the loop forward to close - idx = i; - continue; - } - - let subType = listCount == 2 ? 'double' : 'single'; - - tokens[idx].type = `compare_${subType}_open`; - - while (tokens[idx].type != 'container_compare_close') { - if (tokens[idx].type == 'bullet_list_open') { - if (tokens[idx].markup == '+') { - tokens[idx].type = `compare_${subType}_list_plus_open`; - } else { - tokens[idx].type = `compare_${subType}_list_minus_open`; - } - } - - if (tokens[idx].type == 'bullet_list_close') { - if (tokens[idx].markup == '+') { - tokens[idx].type = `compare_${subType}_list_plus_close`; - } else { - tokens[idx].type = `compare_${subType}_list_minus_close`; - } - } - - idx++; - } - - tokens[idx].type = `compare_${subType}_close`; - - } - - }); - - - // Single - - md.renderer.rules.compare_single_open = function(tokens, idx, options, env, slf) { - return '
      '; - }; - - md.renderer.rules.compare_single_close = function() { - return '
      '; - }; - - md.renderer.rules.compare_single_list_plus_open = function(tokens, idx, options, env, slf) { - return '
        '; - }; - - md.renderer.rules.compare_single_list_plus_close = function(tokens, idx, options, env, slf) { - return '
      '; - }; - - md.renderer.rules.compare_single_list_minus_open = function(tokens, idx, options, env, slf) { - return '
        '; - }; - - md.renderer.rules.compare_single_list_minus_close = function(tokens, idx, options, env, slf) { - return '
      '; - }; - - // Double - - - md.renderer.rules.compare_double_open = function(tokens, idx, options, env, slf) { - return '
      '; - }; - - md.renderer.rules.compare_double_close = function() { - return '
      '; - }; - - md.renderer.rules.compare_double_list_plus_open = function(tokens, idx, options, env, slf) { - return `
      -
      -
      ${t('markit.compare.merits')}
        `; - }; - - md.renderer.rules.compare_double_list_plus_close = function(tokens, idx, options, env, slf) { - return '
      '; - }; - - md.renderer.rules.compare_double_list_minus_open = function(tokens, idx, options, env, slf) { - return `
      -
      -
      ${t('markit.compare.demerits')}
        `; - }; - - md.renderer.rules.compare_double_list_minus_close = function(tokens, idx, options, env, slf) { - return '
      '; - }; -}; diff --git a/modules/markit/plugins/extendedCode.js b/modules/markit/plugins/extendedCode.js deleted file mode 100755 index 21ff026..0000000 --- a/modules/markit/plugins/extendedCode.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -/** - * Adds `key:Ctrl+B` support to code_inline - * @param md - */ - -module.exports = function(md) { - - md.renderer.rules.code_inline = function(tokens, idx, options, env, slf) { - - let token = tokens[idx]; - let content = token.content.trim(); - - if (content.indexOf('key:') == 0) { - return renderKey(content.slice(4)); - } else { - let codePrefixes = ['pattern', 'match', 'subject']; - for (let i = 0; i < codePrefixes.length; i++) { - let prefix = codePrefixes[i]; - if (content.startsWith(prefix + ':')) { - return `${md.utils.escapeHtml(content.slice(prefix.length + 1))}`; - } - } - } - - return '' + md.utils.escapeHtml(content) + ''; - }; - - function renderKey(keys) { - - let results = []; - - if (keys === '+') { - return `+`; - } - - let plusLabel = Math.random(); - keys = keys.replace(/\+\+/g, '+' + plusLabel); - keys = keys.split('+'); - - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - results.push((key == plusLabel) ? '+' : md.utils.escapeHtml(key)); - if (i < keys.length - 1) { - results.push('+'); - } - } - - return `${results.join('')}`; - } - -}; diff --git a/modules/markit/plugins/headerAnchor.js b/modules/markit/plugins/headerAnchor.js deleted file mode 100755 index 833f115..0000000 --- a/modules/markit/plugins/headerAnchor.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - - -/** - * Reads ## Heading [#anchor] - * Transliterates heading text if no anchor - * writes that to `headingToken.anchor` - * - * Renders as link if `options.linkHeaderTag` or header id - */ - -const parseAttrs = require('../utils/parseAttrs'); -const makeAnchor = require('textUtil/makeAnchor'); - -const t = require('i18n'); - -t.requirePhrase('markit', 'error'); - -// add headingToken.achor -// not "id" attr, because rendering uses `.anchor` for the extra link OR id -function readHeadingAnchor(state) { - - let env = state.env; - if (!env.headingsMap) env.headingsMap = new Map(); - - let tokens = state.tokens; - for (let idx = 0; idx < state.tokens.length; idx++) { - let headingToken = state.tokens[idx]; - - if (headingToken.type !== 'heading_open') continue; - - idx++; - - let inlineToken = tokens[idx]; - if (inlineToken.type != 'inline') continue; - - let anchorReg = /\s+\[#(.*?)\]$/; - let anchor; - if (inlineToken.content.match(anchorReg)) { - anchor = inlineToken.content.match(anchorReg)[1]; - - // strip [#...] from token content - inlineToken.content = inlineToken.content.replace(anchorReg, ''); - let lastTextToken = inlineToken.children[inlineToken.children.length - 1]; - lastTextToken.content = inlineToken.content.replace(anchorReg, ''); - - if (env.headingsMap.has(anchor)) { - // fixed anchor, cannot change, add -1 -2 - lastTextToken.type = 'markdown_error_inline'; - lastTextToken.content = t('markit.error.anchor_exists', {anchor: 'anchor'}); - } else { - env.headingsMap.set(anchor, 1); - } - - } else { - anchor = makeAnchor(inlineToken.content, state.md.options.translitAnchors); - - if (env.headingsMap.has(anchor)) { - // иначе просто добавляю -2, -3 ... - env.headingsMap.set(anchor, env.headingsMap.get(anchor) + 1); - anchor = anchor + '-' + env.headingsMap.get(anchor); - } else { - env.headingsMap.set(anchor, 1); - } - - } - - - headingToken.anchor = anchor; - - } - -} - -module.exports = function(md) { - - md.core.ruler.push('read_heading_anchor', readHeadingAnchor); - - md.renderer.rules.heading_open = function(tokens, idx, options, env, slf) { - let token = tokens[idx]; - let anchor = token.anchor; - if (options.linkHeaderTag) { - return `<${token.tag}${slf.renderAttrs(token)}>`; - } else { - // for ebook need id - return `<${token.tag} id="${anchor}">`; - } - }; - - md.renderer.rules.heading_close = function(tokens, idx, options, env, slf) { - let token = tokens[idx]; - if (options.linkHeaderTag) { - return ``; - } else { - return ``; - } - }; - -}; diff --git a/modules/markit/plugins/headerLevelShift.js b/modules/markit/plugins/headerLevelShift.js deleted file mode 100755 index 9f5351f..0000000 --- a/modules/markit/plugins/headerLevelShift.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -module.exports = function(md) { - - md.core.ruler.push('header_level_shift', function(state) { - - let headerLevelShift = state.md.options.headerLevelShift; - - if (!headerLevelShift) return; - - let tokens = state.tokens; - for (let idx = 0; idx < state.tokens.length; idx++) { - let token = state.tokens[idx]; - - if (token.type == 'heading_open' || token.type == 'heading_close') { - let newLevel = +tokens[idx].tag.slice(1) + headerLevelShift; - token.tag = 'h' + newLevel; - } - - } - }); - - -}; - - diff --git a/modules/markit/plugins/imgDescToAttrs.js b/modules/markit/plugins/imgDescToAttrs.js deleted file mode 100755 index d7517fe..0000000 --- a/modules/markit/plugins/imgDescToAttrs.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -/** - * Reads attrs from ![alt|height=100 width=200](...) into image token - * - * P.S. Plugins that work like ![...](/url =100x150) require special parser, not markdown-compatible markup - */ - -const parseAttrs = require('../utils/parseAttrs'); -const tokenUtils = require('../utils/token'); - -function readImgAttrs(state) { - - for (let idx = 0; idx < state.tokens.length; idx++) { - let token = state.tokens[idx]; - - if (token.type !== 'inline') continue; - - for (let i = 0; i < token.children.length; i++) { - let inlineToken = token.children[i]; - - if (inlineToken.type == 'image') { - processImg(inlineToken); - } - } - } - - - // doesn't work for ![desc *me*|height="*hi*"](fig.png) - // works for ![desc *me*|height="hi"](fig.png) - function processImg(imgToken) { - if (!imgToken.children.length) return; // ![](..) empty image - - // last always textToken - let lastTextToken = imgToken.children[imgToken.children.length - 1]; - - let parts = lastTextToken.content.split('|'); - if (parts.length != 2) { // no | or many || (invalid) - // try ', ' for tables - parts = lastTextToken.content.split(', '); - if (parts.length != 2) { - // still invalid - return; - } - } - - lastTextToken.content = parts[0]; - - let attrs = parseAttrs(parts[1]); - - for (let name in attrs) { - if (!state.md.options.html && ['height', 'width'].indexOf(name) == -1) continue; - tokenUtils.attrReplace(imgToken, name, attrs[name]); - } - } - - -} - -module.exports = function(md) { - - md.core.ruler.push('read_img_attrs', readImgAttrs); - -}; diff --git a/modules/markit/plugins/imgFigures.js b/modules/markit/plugins/imgFigures.js deleted file mode 100755 index 7e55e25..0000000 --- a/modules/markit/plugins/imgFigures.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -/** - * Reads attrs from ![alt|height=100 width=200](...) into image token - * - * P.S. Plugins that work like ![...](/url =100x150) require special parser, not markdown-compatible markup - */ - -const parseAttrs = require('../utils/parseAttrs'); -const tokenUtils = require('../utils/token'); - -function imgFigures(state) { - - for (let idx = 1; idx < state.tokens.length - 1; idx++) { - let token = state.tokens[idx]; - - if (token.type !== 'inline') continue; - - // inline token must have 1 child - if (!token.children || token.children.length !== 1) continue; - // child is image - if (token.children[0].type !== 'image') continue; - // prev token is paragraph open - - let isInParagraph = state.tokens[idx - 1].type == 'paragraph_open' && - state.tokens[idx + 1].type == 'paragraph_close'; - - let hasFigureAttr = tokenUtils.attrGet(token.children[0], 'figure'); - - tokenUtils.attrDel(token.children[0], 'figure'); // this attr is not needed any more - - if (!isInParagraph && !hasFigureAttr) continue; - - // we have a figure! - // replace

      with figure - let figureToken = token.children[0]; - figureToken.type = 'figure'; - figureToken.tag = 'figure'; - - if (isInParagraph) { - state.tokens.splice(idx - 1, 3, figureToken); - } - } - -} - -module.exports = function(md) { - - md.core.ruler.push('img_figures', imgFigures); - - md.renderer.rules.figure = function(tokens, idx, options, env, slf) { - let token = tokens[idx]; - let width = tokenUtils.attrGet(token, 'width'); - let height = tokenUtils.attrGet(token, 'height'); - - if (options.ebookType || !width || !height) { - return `
      `; - } - - // here we assume a figure has no "class" attribute - // so we put our own class on it - // (if it had, it would refer to figure?) - - return `
      -
      - -
      `; - - }; -}; diff --git a/modules/markit/plugins/markdownError.js b/modules/markit/plugins/markdownError.js deleted file mode 100755 index e107090..0000000 --- a/modules/markit/plugins/markdownError.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports = function(md) { - - md.renderer.rules.markdown_error_block = function(tokens, idx, options, env, slf) { - return '
      ' + md.utils.escapeHtml(tokens[idx].content) + '
      '; - }; - - md.renderer.rules.markdown_error_inline = function(tokens, idx, options, env, slf) { - return '' + md.utils.escapeHtml(tokens[idx].content) + ''; - }; - -}; diff --git a/modules/markit/plugins/onlineOffline.js b/modules/markit/plugins/onlineOffline.js deleted file mode 100755 index 4f6391e..0000000 --- a/modules/markit/plugins/onlineOffline.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -const markdownItContainer = require('markdown-it-container'); -const parseAttrs = require('../utils/parseAttrs'); - -module.exports = function(md) { - - md.use(markdownItContainer, 'online', { - marker: '`' - }); - md.use(markdownItContainer, 'offline', { - marker: '`' - }); - - - md.core.ruler.push('remove_online_offline', function(state) { - - let isEbook = Boolean(state.md.options.ebookType); - - let tokens = state.tokens; - - let remove = isEbook ? 'online' : 'offline'; - - // remove online/offline what's appropriate - for (let idx = 0; idx < state.tokens.length; idx++) { - let token = state.tokens[idx]; - - if (token.type == `container_${remove}_open`) { - let i = idx + 1; - while(tokens[i].type != `container_${remove}_close`) i++; - - tokens.splice(idx, i - idx + 1); - idx --; - } - } - }); - - function renderEmpty() { - return ''; - } - - md.renderer.rules.container_online_open = renderEmpty; - md.renderer.rules.container_online_close = renderEmpty; - md.renderer.rules.container_offline_open = renderEmpty; - md.renderer.rules.container_offline_close = renderEmpty; - -}; diff --git a/modules/markit/plugins/outlinedBlocks.js b/modules/markit/plugins/outlinedBlocks.js deleted file mode 100755 index 7c74cb3..0000000 --- a/modules/markit/plugins/outlinedBlocks.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -/** - * Client/server plugin - */ - -const markdownItContainer = require('markdown-it-container'); -const parseAttrs = require('../utils/parseAttrs'); -const t = require('i18n'); - -t.requirePhrase('markit', 'outlined'); - -module.exports = function(md) { - - ['warn', 'smart', 'ponder'].forEach(name => { - md.use(markdownItContainer, name, { - marker: '`', - render(tokens, idx, options, env, slf) { - - if (tokens[idx].nesting === 1) { - let attrs = parseAttrs(tokens[idx].info, true); - let header = attrs.header; - if (header) { - //header = header.replace(/`(.*?)`/g, '$1'); - header = md.renderInline(header); - } else { - header = t(`markit.outlined.${name}`); - } - return `
      -
      ${header}
      -
      `; - - } else { - // closing tag - return '
      \n'; - } - } - }); - }); - - - -}; diff --git a/modules/markit/plugins/quote.js b/modules/markit/plugins/quote.js deleted file mode 100755 index fe36e53..0000000 --- a/modules/markit/plugins/quote.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/** - * ```quote author="Author" - */ - -const markdownItContainer = require('markdown-it-container'); -const parseAttrs = require('../utils/parseAttrs'); - -module.exports = function(md) { - - md.use(markdownItContainer, 'quote', { - marker: '`' - }); - - md.renderer.rules.container_quote_open = function(tokens, idx, options, env, slf) { - return `
      `; - }; - - md.renderer.rules.container_quote_close = function(tokens, idx, options, env, slf) { - let result = '
      '; - - let i = idx - 1; - while (tokens[i].type != 'container_quote_open') i--; - - let attrs = parseAttrs(tokens[i].info, true); - - if (attrs.author) { - result += `
      - ${md.utils.escapeHtml(attrs.author)} -
      `; - } - - return result + '
      '; - }; - - -}; diff --git a/modules/markit/plugins/resolveMdnSrc.js b/modules/markit/plugins/resolveMdnSrc.js deleted file mode 100755 index ffcee06..0000000 --- a/modules/markit/plugins/resolveMdnSrc.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -/** - * Replaces relative img src with resourceRoot/src - */ - -const tokenUtils = require('../utils/token'); -const lang = require('config').lang; - -module.exports = function(md) { - - md.core.ruler.push('resolve_mdn_src', function(state) { - - for (let idx = 0; idx < state.tokens.length; idx++) { - let token = state.tokens[idx]; - - if (token.type !== 'inline') continue; - - for (let i = 0; i < token.children.length; i++) { - let inlineToken = token.children[i]; - - if (inlineToken.type == 'link_open') { - let href = tokenUtils.attrGet(inlineToken, 'href'); - - if (href.startsWith('mdn:')) { - let parts = href.slice(4).split('/'); - - let locale = lang == 'en' ? 'en-US' : lang; - - let prefix = `https://developer.mozilla.org/${locale}/docs/Web`; - - if (parts[0] == 'js') { - prefix += '/JavaScript/Reference/Global_Objects/'; - } else if (parts[0] == 'api') { - prefix += '/API/'; - } else { - prefix += '/'; - } - - parts.shift(); - href = prefix + parts.join('/'); - - tokenUtils.attrReplace(inlineToken, 'href', href); - } - } - } - } - - }); - -}; diff --git a/modules/markit/plugins/resolveRelativeSrc.js b/modules/markit/plugins/resolveRelativeSrc.js deleted file mode 100755 index 333f7c8..0000000 --- a/modules/markit/plugins/resolveRelativeSrc.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -/** - * Replaces relative img src with resourceRoot/src - */ - -const tokenUtils = require('../utils/token'); - -module.exports = function(md) { - - md.core.ruler.push('resolve_relative_src', function(state) { - - let methods = { - link_open, - image - }; - - for (let idx = 0; idx < state.tokens.length; idx++) { - let token = state.tokens[idx]; - - if (token.type !== 'inline') continue; - - for (let i = 0; i < token.children.length; i++) { - let inlineToken = token.children[i]; - - if (methods[inlineToken.type]) { - methods[inlineToken.type](inlineToken); - } - } - } - - function image(imgToken) { - let src = tokenUtils.attrGet(imgToken, 'src'); - - if (src.indexOf('://') == -1 && src[0] != '/') { - src = state.md.options.resourceWebRoot + '/' + src; - tokenUtils.attrReplace(imgToken, 'src', src); - } - } - - function link_open(token) { - let href = tokenUtils.attrGet(token, 'href'); - - // don't touch info:tutorial link protocol - // don't touch absolute links - // don't touch in-page #hash - if (!href.match(/^\w+:/) && href[0] != '/' && href[0] != '#') { - href = state.md.options.resourceWebRoot + '/' + href; - tokenUtils.attrReplace(token, 'href', href); - } - - } - - }); - -}; diff --git a/modules/markit/plugins/sourceBlocks.js b/modules/markit/plugins/sourceBlocks.js deleted file mode 100755 index 01fb085..0000000 --- a/modules/markit/plugins/sourceBlocks.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -/** - * Client/server plugin - * Rewrites fenced blocks to blocktag_source - * adds the renderer for it - */ - -const parseAttrs = require('../utils/parseAttrs'); -const stripIndents = require('textUtil/stripIndents'); -const extractHighlight = require('../utils/source/extractHighlight'); -const t = require('i18n'); -const getPrismLanguage = require('../getPrismLanguage'); - -const LANG = require('config').lang; - -t.requirePhrase('markit', 'code'); - - -function rewriteFenceToSource(state) { - - for (let idx = 0; idx < state.tokens.length; idx++) { - - if (state.tokens[idx].type == 'fence') { - let attrs = parseAttrs(state.tokens[idx].info, true); - - let langOrExt = attrs.blockName || ''; - - if (getPrismLanguage.allSupported.indexOf(langOrExt) == -1) continue; - - state.tokens[idx].type = 'blocktag_source'; - state.tokens[idx].blockTagAttrs = attrs; - } - } - -} - - -module.exports = function(md) { - - md.core.ruler.push('rewrite_fence_to_source', rewriteFenceToSource); - - md.renderer.rules.blocktag_source = function(tokens, idx, options, env, slf) { - let token = tokens[idx]; - - let attrs = token.blockTagAttrs; - - let lang = attrs.blockName; - let prismLanguage = getPrismLanguage(lang); - - token.attrPush([ 'data-trusted', (options.html && !attrs.untrusted) ? 1 : 0]); - - if (attrs.global) { - token.attrPush(['data-global', 1]); - } - - token.attrPush([ 'class', 'code-example' ]); - - if (attrs['no-strict']) { - token.attrPush(['data-no-strict', 1]); - } - - let height; - // demo height of - if (+attrs.height) { - height = +attrs.height; - if (!options.html) height = Math.max(height, 800); - token.attrPush(['data-demo-height', height]); - } - - if (options.html && attrs.autorun) { - // autorun may have "no-epub" value meaning that it shouldn't run on epub (code not supported) - token.attrPush(['data-autorun', attrs.autorun]); - } - - if (attrs.refresh) { - token.attrPush(['data-refresh', '1']); - } - - if (options.html && attrs.demo) { - token.attrPush(['data-demo', '1']); - } - - // strip first empty lines - let content = stripIndents(token.content); - - let highlight = extractHighlight(content); - - if (highlight.block) { - token.attrPush(['data-highlight-block', highlight.block]); - } - if (highlight.inline) { - token.attrPush(['data-highlight-inline', highlight.inline]); - } - - content = highlight.text; - - let toolbarHtml = ''; - if (attrs.run) { - toolbarHtml = ` -
      -
      - -
      -
      - -
      -
      `; - } - - let codeResultHtml = ''; - - //- iframe must be in HTML with the right height - //- otherwise page sizes will be wrong and autorun leads to resizes/jumps - if (attrs.autorun && options.html && lang == 'html') { - //- iframes with about:html are saved to disk incorrectly by FF (1 iframe content for all) - //- @see https://bugzilla.mozilla.org/show_bug.cgi?id=1154167 - codeResultHtml = `
      - -
      `; - } - - return ` -
      - ${toolbarHtml} -
      -
      ${md.utils.escapeHtml(content)}
      -
      -
      - ${codeResultHtml} - `; - - }; - -}; diff --git a/modules/markit/plugins/summary.js b/modules/markit/plugins/summary.js deleted file mode 100755 index de4f86e..0000000 --- a/modules/markit/plugins/summary.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -/** - * Client/server plugin - */ - -const markdownItContainer = require('markdown-it-container'); -const parseAttrs = require('../utils/parseAttrs'); -const t = require('i18n'); - -const LANG = require('config').lang; - -module.exports = function(md) { - - md.use(markdownItContainer, 'summary', { - marker: '`', - render(tokens, idx) { - - if (tokens[idx].nesting === 1) { - return `
      `; - } else { - // closing tag - return '
      \n'; - } - } - }); - -}; diff --git a/modules/markit/readme.ru.md b/modules/markit/readme.ru.md deleted file mode 100755 index 587c727..0000000 --- a/modules/markit/readme.ru.md +++ /dev/null @@ -1,359 +0,0 @@ - -# Парсер для JavaScript.ru - - -Парсер для адаптированного формата Markdown, который используется на Javascript.ru. - -У него есть два режима работы: - 1. Доверенный -- для статей, задач и другого основного материала. Возможны любые теги и т.п. - 2. Безопасный -- для комментариев и другого user-generated content. Большинство тегов HTML можно использовать. - -## Вставка кода ```js - -Блок кода вставляется как в github: - -
      -```js
      -alert(1);
      -```
      -
      - -Или: -
      -```html
      -<!DOCTYPE HTML>
      -<title>Viva la HTML5!</title>
      -```
      -
      - -Поддерживаемые языки (список может быть расширен): - - html - - js - - css - - coffee - - php - - http - - java - - ruby - - scss - - sql - -### Выполняемый код `//+ run` и другие настройки - -Если хочется, чтобы посетитель мог запустить код -- добавьте первую строку `//+ run`: - -
      -```js
      -//+ run
      -alert(1);
      -```
      -
      - -Независимо от языка можно использовать любой стиль комментария: `//+ ... `, `/*+ ... */`, `#+ ...` или ``, -главное чтобы он был *первой строкой* и в начале был *плюс и пробел*. Этот комментарий не попадёт в итоговый вывод. - -Есть два языка, для которых это поддерживается: - 1. `js` - в доверенном режиме через `eval`, в безопасном -- в `iframe` с другого домена. - 2. `html` - в доверенном режиме показ будет в `iframe` с того же домена, в безопасном -- с другого домена. - -Прочие настройки, возможные в этой же строке: - - `height=100` -- высота (в пикселях) для `iframe`, в котором будет выполняться пример. Обычно для HTML она вычисляется автоматически по содержимому. - - `src="my.js"` -- код будет взят из файла `my.js` - - `autorun` -- пример будет запущен автоматически по загрузке страницы. - - `refresh` -- каждый запуск JS-кода будет осуществлён в "чистом" окружении. - Этот флаг актуален только для безопасного режима, т.к. обычно `iframe` с другого домена кешируется и используется многократно. - - `demo` - флаг актуален только для решений задач, он означает, что при нажатии на кнопку "Демо" в условии запустится этот код. - -Пример ниже возьмёт код из файла `my.js` и запускает его автоматически: -
      -```js
      -//+ src="my.js" autorun
      -```
      -
      - -Этот пример будет при запуске показан в ` - ` - ); - - }); - - -/* - it(``, function*() { - let result = await render(this.test.title); - result.trim().should.be.eql('

      Task 1

      '); - }); - - it(``, function*() { - let result = await render(this.test.title); - result.trim().should.be.eql('

      Article 1.2

      '); - }); - */ - - it(`notfigure ![desc|height=100 width=200](/url)`, function*() { - let result = await render(this.test.title); - result.trim().should.be.eql('

      notfigure desc

      '); - }); - - it(`notfigure ![desc](blank.png)`, function*() { - let result = await render(this.test.title); - result.trim().should.be.eql('

      notfigure desc

      '); - }); - - it(`notfigure ![desc](not-exists.png)`, function*() { - let result = await render(this.test.title); - result.trim().should.match(/^

      notfigure .*?<\/p>$/); - }); - - it(`notfigure ![desc](error.png)`, function*() { - let result = await render(this.test.title); - result.trim().should.match(/^

      notfigure .*?<\/p>$/); - }); - - it(`notfigure ![desc](1.js)`, function*() { - let result = await render(this.test.title); - result.trim().should.match(/^

      notfigure .*?<\/p>$/); - }); - - it(`![figure|height=100 width=200](/url)`, function*() { - let result = await render(this.test.title); - result.trim().should.be.html(`

      -
      - -
      `); - }); - - it(`## Header [#anchor]`, function*() { - let result = await render(this.test.title); - result.trim().should.be.html('

      Header

      '); - }); - - it(`## My header`, function*() { - let result = await render(this.test.title); - result.trim().should.be.html('

      My header

      '); - }); - - it(`\`\`\`compare -- Полная интеграция с HTML/CSS. -- Простые вещи делаются просто. -- Поддерживается всеми распространёнными браузерами и включён по умолчанию. -\`\`\``, function*() { - let result = await render(this.test.title); - result.trim().should.be.html(` -
      -
      -
      -
        -
      • Полная интеграция с HTML/CSS.
      • -
      • Простые вещи делаются просто.
      • -
      • Поддерживается всеми распространёнными браузерами и включён по умолчанию.
      • -
      -
      -
      -
      `); - }); - - it(`\`\`\`compare -+ one -- two -\`\`\``, function*() { - let result = await render(this.test.title); - result.trim().should.be.html(`
      -
      -
      -
      Достоинства
      -
        -
      • one
      • -
      -
      -
      -
      -
      -
      Недостатки
      -
        -
      • two
      • -
      -
      -
      -
      `); - }); - - -}); - - diff --git a/modules/markit/test/utils/parseAttrs.js b/modules/markit/test/utils/parseAttrs.js deleted file mode 100755 index 1ab0c94..0000000 --- a/modules/markit/test/utils/parseAttrs.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const parseAttrs = require('../../utils/parseAttrs'); - -describe('parseAttrs', function() { - - it(`bare my=value test="quoted value" me='single quoted value'`, function() { - let result = parseAttrs(this.test.title); - result.should.be.eql({bare: true, my: 'value', test: 'quoted value', me: 'single quoted value'}); - }); - - it(`multiple bare attrs`, function() { - let result = parseAttrs(this.test.title); - result.should.be.eql({multiple: true, bare: true, attrs: true}); - }); - - it(`str='single quote \\' inside'`, function() { - let result = parseAttrs(this.test.title); - result.should.be.eql({ str: "single quote ' inside" }); - }); - - it(`str="double quote \\" inside"`, function() { - let result = parseAttrs(this.test.title); - result.should.be.eql({ str: 'double quote " inside' }); - }); - -}); - diff --git a/modules/markit/test/utils/source.js b/modules/markit/test/utils/source.js deleted file mode 100755 index 5c7a6e8..0000000 --- a/modules/markit/test/utils/source.js +++ /dev/null @@ -1,91 +0,0 @@ -let extractHighlight = require('../../utils/source/extractHighlight'); -let stripIndents = require('textUtil/stripIndents'); - -let fs = require('fs'); -let path = require('path'); - -function readIn(name) { - return fs.readFileSync(path.join(__dirname, 'source/in', name), 'utf-8'); -} -function readOut(name) { - return fs.readFileSync(path.join(__dirname, 'source/out', name), 'utf-8'); -} - -describe("source", function() { - - describe("stripIndents", function() { - - beforeEach(function() { - this.currentTest.in = readIn(this.currentTest.title); - this.currentTest.result = stripIndents(this.currentTest.in); - this.currentTest.out = readOut(this.currentTest.title); - }); - - it("indented.txt", function() { - this.test.result.should.be.eql(this.test.out); - }); - }); - - describe("extractHighlight", function() { - - beforeEach(function() { - this.currentTest.in = readIn(this.currentTest.title); - this.currentTest.result = extractHighlight(this.currentTest.in); - this.currentTest.out = readOut(this.currentTest.title); - }); - - - it("block-whole.txt", function() { - this.test.result.inline.should.be.eql(''); - this.test.result.block.should.be.eql('0-1'); - this.test.result.text.should.be.eql(this.test.out); - }); - - it("block-one-line.txt", function() { - this.test.result.inline.should.be.eql(''); - this.test.result.block.should.be.eql('0-0'); - this.test.result.text.should.be.eql(this.test.out); - }); - - it("single-line.txt", function() { - this.test.result.inline.should.be.eql(''); - this.test.result.block.should.be.eql('1-1'); - this.test.result.text.should.be.eql(this.test.out); - }); - - it("blocks-two.txt", function() { - this.test.result.inline.should.be.eql(''); - this.test.result.block.should.be.eql('1-2,4-4'); - this.test.result.text.should.be.eql(this.test.out); - }); - - - it("inline.txt", function() { - this.test.result.inline.should.be.eql('0:8-18'); - this.test.result.block.should.be.eql(''); - this.test.result.text.should.be.eql(this.test.out); - }); - - it("inline-multi.txt", function() { - this.test.result.inline.should.be.eql('0:8-18,1:8-9,1:18-19'); - this.test.result.block.should.be.eql(''); - this.test.result.text.should.be.eql(this.test.out); - }); - - it("mixed.txt", function() { - this.test.result.inline.should.be.eql('2:8-18,3:8-9'); - this.test.result.block.should.be.eql('2-4'); - this.test.result.text.should.be.eql(this.test.out); - }); - - - it("big.txt", function() { - this.test.result.inline.should.be.eql('9:26-37,10:13-26'); - this.test.result.block.should.be.eql('13-13,23-24'); - this.test.result.text.should.be.eql(this.test.out); - }); - - - }); - -}); diff --git a/modules/markit/test/utils/source/in/big.txt b/modules/markit/test/utils/source/in/big.txt deleted file mode 100755 index cb78206..0000000 --- a/modules/markit/test/utils/source/in/big.txt +++ /dev/null @@ -1,29 +0,0 @@ -let Animal = Class.extend({ - init: function(name) { - this.name = name; - }, - run: function() { - alert(this.name + ' бежит!'); - } -}); - -// Объявить класс Rabbit, *!*наследующий*/!* от Animal -let Rabbit = *!*Animal.extend*/!*({ - - init: function(name) { -*!* - this._super(name); // вызвать родительский init(name) -*/!* - }, - - run: function() { - this._super(); // вызвать родительский run - alert('..и мощно прыгает за морковкой!'); - } - -}); - -*!* -let rabbit = new Rabbit("Кроль"); -rabbit.run(); // "Кроль бежит!", затем "..и мощно прыгает за морковкой!" -*/!* diff --git a/modules/markit/test/utils/source/in/block-one-line.txt b/modules/markit/test/utils/source/in/block-one-line.txt deleted file mode 100755 index 3be30c5..0000000 --- a/modules/markit/test/utils/source/in/block-one-line.txt +++ /dev/null @@ -1,4 +0,0 @@ -*!* -my; -*/!* -code; diff --git a/modules/markit/test/utils/source/in/block-whole.txt b/modules/markit/test/utils/source/in/block-whole.txt deleted file mode 100755 index cbe57be..0000000 --- a/modules/markit/test/utils/source/in/block-whole.txt +++ /dev/null @@ -1,4 +0,0 @@ -*!* -my; -code; -*/!* diff --git a/modules/markit/test/utils/source/in/blocks-two.txt b/modules/markit/test/utils/source/in/blocks-two.txt deleted file mode 100755 index 40304c8..0000000 --- a/modules/markit/test/utils/source/in/blocks-two.txt +++ /dev/null @@ -1,10 +0,0 @@ -first -*!* -my; -code; -*/!* - -*!* -test -*/!* -last diff --git a/modules/markit/test/utils/source/in/indented.txt b/modules/markit/test/utils/source/in/indented.txt deleted file mode 100755 index aeb08f8..0000000 --- a/modules/markit/test/utils/source/in/indented.txt +++ /dev/null @@ -1,6 +0,0 @@ - - lines above must be removed - they may contain spaces and tabs \t - text below is unindented - - diff --git a/modules/markit/test/utils/source/in/inline-multi.txt b/modules/markit/test/utils/source/in/inline-multi.txt deleted file mode 100755 index d469323..0000000 --- a/modules/markit/test/utils/source/in/inline-multi.txt +++ /dev/null @@ -1,3 +0,0 @@ -let f = *!*function()*/!* { - alert(*!*1*/!*); alert(*!*2*/!*); -} diff --git a/modules/markit/test/utils/source/in/inline.txt b/modules/markit/test/utils/source/in/inline.txt deleted file mode 100755 index d3c24cc..0000000 --- a/modules/markit/test/utils/source/in/inline.txt +++ /dev/null @@ -1,3 +0,0 @@ -let f = *!*function()*/!* { - alert(1); -} diff --git a/modules/markit/test/utils/source/in/mixed.txt b/modules/markit/test/utils/source/in/mixed.txt deleted file mode 100755 index 1a3006d..0000000 --- a/modules/markit/test/utils/source/in/mixed.txt +++ /dev/null @@ -1,8 +0,0 @@ -test; - -*!* -let f = *!*function()*/!* { - alert(*!*1*/!*); -} -*/!* -one more line; diff --git a/modules/markit/test/utils/source/in/single-line.txt b/modules/markit/test/utils/source/in/single-line.txt deleted file mode 100755 index 3dc28c1..0000000 --- a/modules/markit/test/utils/source/in/single-line.txt +++ /dev/null @@ -1,3 +0,0 @@ -one; -two;*!* -three; diff --git a/modules/markit/test/utils/source/in/space_after_exclamation.txt b/modules/markit/test/utils/source/in/space_after_exclamation.txt deleted file mode 100755 index c824f21..0000000 --- a/modules/markit/test/utils/source/in/space_after_exclamation.txt +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/modules/markit/test/utils/source/out/big.txt b/modules/markit/test/utils/source/out/big.txt deleted file mode 100755 index 0da6b32..0000000 --- a/modules/markit/test/utils/source/out/big.txt +++ /dev/null @@ -1,25 +0,0 @@ -let Animal = Class.extend({ - init: function(name) { - this.name = name; - }, - run: function() { - alert(this.name + ' бежит!'); - } -}); - -// Объявить класс Rabbit, наследующий от Animal -let Rabbit = Animal.extend({ - - init: function(name) { - this._super(name); // вызвать родительский init(name) - }, - - run: function() { - this._super(); // вызвать родительский run - alert('..и мощно прыгает за морковкой!'); - } - -}); - -let rabbit = new Rabbit("Кроль"); -rabbit.run(); // "Кроль бежит!", затем "..и мощно прыгает за морковкой!" \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/block-one-line.txt b/modules/markit/test/utils/source/out/block-one-line.txt deleted file mode 100755 index ce07c2b..0000000 --- a/modules/markit/test/utils/source/out/block-one-line.txt +++ /dev/null @@ -1,2 +0,0 @@ -my; -code; \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/block-whole.txt b/modules/markit/test/utils/source/out/block-whole.txt deleted file mode 100755 index ce07c2b..0000000 --- a/modules/markit/test/utils/source/out/block-whole.txt +++ /dev/null @@ -1,2 +0,0 @@ -my; -code; \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/blocks-two.txt b/modules/markit/test/utils/source/out/blocks-two.txt deleted file mode 100755 index 6c1217a..0000000 --- a/modules/markit/test/utils/source/out/blocks-two.txt +++ /dev/null @@ -1,6 +0,0 @@ -first -my; -code; - -test -last \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/indented.txt b/modules/markit/test/utils/source/out/indented.txt deleted file mode 100755 index e0399bb..0000000 --- a/modules/markit/test/utils/source/out/indented.txt +++ /dev/null @@ -1,3 +0,0 @@ -lines above must be removed -they may contain spaces and tabs \t - text below is unindented \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/inline-multi.txt b/modules/markit/test/utils/source/out/inline-multi.txt deleted file mode 100755 index a883c1d..0000000 --- a/modules/markit/test/utils/source/out/inline-multi.txt +++ /dev/null @@ -1,3 +0,0 @@ -let f = function() { - alert(1); alert(2); -} \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/inline.txt b/modules/markit/test/utils/source/out/inline.txt deleted file mode 100755 index 22c0395..0000000 --- a/modules/markit/test/utils/source/out/inline.txt +++ /dev/null @@ -1,3 +0,0 @@ -let f = function() { - alert(1); -} \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/mixed.txt b/modules/markit/test/utils/source/out/mixed.txt deleted file mode 100755 index f128026..0000000 --- a/modules/markit/test/utils/source/out/mixed.txt +++ /dev/null @@ -1,6 +0,0 @@ -test; - -let f = function() { - alert(1); -} -one more line; \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/single-line.txt b/modules/markit/test/utils/source/out/single-line.txt deleted file mode 100755 index f01a81b..0000000 --- a/modules/markit/test/utils/source/out/single-line.txt +++ /dev/null @@ -1,3 +0,0 @@ -one; -two; -three; \ No newline at end of file diff --git a/modules/markit/test/utils/source/out/space_after_exclamation.txt b/modules/markit/test/utils/source/out/space_after_exclamation.txt deleted file mode 100755 index 65e2146..0000000 --- a/modules/markit/test/utils/source/out/space_after_exclamation.txt +++ /dev/null @@ -1,8 +0,0 @@ -
      <style>
      -  #top {
      -    position: fixed;
      -    right: 10px;
      -    top: 10px;
      -    background: #fee;
      -  }
      -</style>
      diff --git a/modules/markit/utils/parseAttrs.js b/modules/markit/utils/parseAttrs.js deleted file mode 100755 index 7fc7df7..0000000 --- a/modules/markit/utils/parseAttrs.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -// 'my=5 test=3 bla="my "test"' -> my=5 test=3 bla="my " (test is not matched) -const attrsReg = /([\w-]+)(?:=(?:'((?:\\'|[^'])*)'|"((?:\\"|[^"])*)"|(\S+))|(?:\s|$))/g; - -module.exports = function(attrs, withBlockName) { - const attrsObject = {}; - - if (!attrs) { - return attrsObject; - } - - let blockName; - if (withBlockName) { - blockName = attrs.match(/^\w+/); - blockName = blockName && blockName[0]; - attrs = attrs.replace(/^\w+\s+/, ''); - } - - let match, name, value; - while ((match = attrsReg.exec(attrs)) !== null) { - name = match[1]; - value = match[2] !== undefined ? match[2].replace(/\\'/g, "'") : - match[3] !== undefined ? match[3].replace(/\\"/g, '"') : match[4]; - - attrsObject[name.toLowerCase()] = (value === undefined) ? true : value; - } - - if (blockName) { - attrsObject.blockName = blockName; - } - - return attrsObject; -}; diff --git a/modules/markit/utils/source/extractHighlight.js b/modules/markit/utils/source/extractHighlight.js deleted file mode 100755 index 0819df8..0000000 --- a/modules/markit/utils/source/extractHighlight.js +++ /dev/null @@ -1,88 +0,0 @@ - -function deTab(text) { - // attacklab: Detab's completely rewritten for speed. - // In perl we could fix it by anchoring the regexp with \G. - // In javascript we're less fortunate. - - // expand first n-1 tabs - text = text.replace(/\t(?=\t)/g, " "); // attacklab: g_tab_width - - // replace the nth with two sentinels - text = text.replace(/\t/g, "~A~B"); - - // use the sentinel to anchor our regex so it doesn't explode - text = text.replace(/~B(.+?)~A/g, - function(wholeMatch, m1) { - let leadingText = m1; - let numSpaces = 2 - leadingText.length % 2; // attacklab: g_tab_width - - // there *must* be a better way to do this: - for (let i = 0; i < numSpaces; i++) leadingText += " "; - - return leadingText; - } - ); - - // clean up sentinels - text = text.replace(/~A/g, " "); // attacklab: g_tab_width - text = text.replace(/~B/g, ""); - - return text; -} - -module.exports = function(text) { - text = deTab(text); - text += "\n"; - - let r = {block: [], inline: []}; - let last = null; - let newText = []; - - text.split("\n").forEach(function(line) { - if (/^\s*\*!\*\s*$/.test(line)) { // only *!* - if (last) { - newText.push(line); - } else { - last = newText.length; - } - } else if (/^\s*\*\/!\*\s*$/.test(line)) { // only */!* - if (last !== null) { - r.block.push(last + '-' + (newText.length-1)); - last = null; - } else { - newText.push(line); - } - } else if (/\s*\*!\*\s*$/.test(line)) { // ends with *!* - r.block.push(newText.length + '-' + newText.length); - line = line.replace(/\s*\*!\*\s*$/g, ''); - newText.push(line); - } else { - newText.push(""); - let offset = 0; - while(true) { - let fromPos = line.indexOf('*!*'); - let toPos = line.indexOf('*/!*'); - if (fromPos != -1 && toPos != -1) { - r.inline.push( (newText.length-1) + ':' + (offset+fromPos) + '-' + (offset+toPos-3) ); - newText[newText.length-1] += line.slice(0, toPos+4).replace(/\*\/?!\*/g, ''); - offset += toPos - 3; - line = line.slice(toPos+4); - } else { - newText[newText.length-1] += line; - break; - } - } - } - }); - - if (last) { - r.block.push( last + '-' + (newText.length-1) ); - } - - return { - block: r.block.join(','), - inline: r.inline.join(','), - text: newText.join("\n").replace(/\s+$/, '') - }; - -}; diff --git a/modules/markit/utils/token.js b/modules/markit/utils/token.js deleted file mode 100755 index 65abe63..0000000 --- a/modules/markit/utils/token.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -/** - * tokenAttrReplace(name, value) - * - * Replace all attributes with name `name` with one with the value `attrData` - **/ -function attrReplace(token, name, value) { - let found; - - let attrs = token.attrs; - - if (attrs) { - // modify the existing attr is possible - for (let i = 0; i < attrs.length; i++) { - if (attrs[i][0] === name) { - if (!found) { - attrs[i][1] = value; - found = true; - } else { - // remove extra attrs with same name - attrs.splice(i, 1); - i--; - } - } - } - - // add a new attribute with such name if none was found - if (!found) { - attrs.push([name, value]); - } - } else { - token.attrs = [ [name, value] ]; - } -} - -function addClass(token, value) { - let classAttr = attrGet(token, 'class'); - if (classAttr.match(new RegExp(`\b${value}\b`))) return; - - if (classAttr) { - classAttr += ' ' + value; - } else { - classAttr = value; - } - attrReplace(token, 'class', classAttr); -} - -function attrGet(token, name) { - let idx = token.attrIndex(name); - if (idx == -1) return null; - return token.attrs[idx][1]; -} - -function attrDel(token, name) { - let idx = token.attrIndex(name); - if (idx == -1) return null; - token.attrs.splice(idx, 1); -} - -exports.attrReplace = attrReplace; - -exports.attrGet = attrGet; -exports.attrDel = attrDel; -exports.addClass = addClass; - diff --git a/modules/momentWithLocale.js b/modules/momentWithLocale.js deleted file mode 100755 index 07eff3b..0000000 --- a/modules/momentWithLocale.js +++ /dev/null @@ -1,3 +0,0 @@ -require('moment/locale/' + process.env.NODE_LANG); - -module.exports = require('moment'); diff --git a/modules/pathListCheck.js b/modules/pathListCheck.js deleted file mode 100755 index f4e3f83..0000000 --- a/modules/pathListCheck.js +++ /dev/null @@ -1,31 +0,0 @@ -const log = require('log')(); -const pathToRegexp = require('path-to-regexp'); - -function PathListCheck() { - this.paths = []; -} - -PathListCheck.prototype.add = function(path) { - if (path instanceof RegExp) { - this.paths.push(path); - } else if (typeof path == 'string') { - this.paths.push(pathToRegexp(path)); - } else { - throw new Error("unsupported path type: " + path); - } -}; - -PathListCheck.prototype.check = function(path) { - - for (let i = 0; i < this.paths.length; i++) { - log.trace("path test " + path + " against " + this.paths[i]); - if (this.paths[i].test(path)) { - log.trace("path match found"); - return true; - } - } - - return false; -}; - -module.exports = PathListCheck; diff --git a/modules/pugResolve.js b/modules/pugResolve.js deleted file mode 100644 index bd0c194..0000000 --- a/modules/pugResolve.js +++ /dev/null @@ -1,62 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const lang = require('config').lang; -const config = require('config'); - -function tryPaths(roots, filename) { - let paths = [ - `${filename}.${lang}.pug`, - `${filename}.pug`, - `${filename}/index.${lang}.pug`, - `${filename}/index.pug`, - ]; - - for(let root of roots) { - for (let tryPath of paths) { - if (fs.existsSync(path.join(root, tryPath))) { - return path.join(root, tryPath); - } - } - } - - return null; -} - -function pugResolve(filename, source, loadOptions) { - filename = filename.replace(/\.pug$/, ''); - - // console.log("RESOLVE", filename, source); - if (filename[0] === '~') { - filename = filename.slice(1); - let paths = require.resolve.paths(filename); - let resolved = tryPaths(paths, filename); - if (!resolved) { - throw new Error(`Pug file ${filename} from ${source} not resolved`); - } - return resolved; - } - - if (filename[0] === '/') { - let result = tryPaths([loadOptions.useAbsoluteTemplatePath ? '/' : loadOptions.basedir], filename); - if (!result) { - throw new Error(`Pug file ${filename} not resolved`); - } - return result; - } - - - let roots = []; - if (source) { - roots.push(path.dirname(source)); - } else { - roots.push(config.projectRoot); - } - if (loadOptions && loadOptions.roots) roots.push(...loadOptions.roots); - let result = tryPaths(roots, filename); - if (!result) { - throw new Error(`Pug file ${filename} from ${source || 'code'} not resolved`); - } - return result; -} - -module.exports = pugResolve; \ No newline at end of file diff --git a/modules/render.js b/modules/render.js new file mode 100644 index 0000000..16afd21 --- /dev/null +++ b/modules/render.js @@ -0,0 +1,24 @@ +let Renderer = require('jsengine/koa/renderer'); + +// (!) this.render does not assign this.body to the result +// that's because render can be used for different purposes, e.g to send emails +exports.init = function(app) { + app.use(async function(ctx, next) { + + let renderer = new Renderer(ctx); + /** + * Render template + * Find the file: + * if locals.useAbsoluteTemplatePath => use templatePath + * else if templatePath starts with / => lookup in locals.basedir + * otherwise => lookup in ctx.templateDir (MW should set it) + * @param templatePath file to find (see the logic above) + * @param locals + * @returns {String} + */ + ctx.render = (templatePath, locals) => renderer.render(templatePath, locals); + + await next(); + }); + +}; diff --git a/handlers/static/index.js b/modules/static/index.js similarity index 100% rename from handlers/static/index.js rename to modules/static/index.js diff --git a/styles/blocks/00-variables/icons.styl b/modules/styles/blocks/00-variables/icons.styl similarity index 100% rename from styles/blocks/00-variables/icons.styl rename to modules/styles/blocks/00-variables/icons.styl diff --git a/styles/blocks/00-variables/variables.styl b/modules/styles/blocks/00-variables/variables.styl similarity index 51% rename from styles/blocks/00-variables/variables.styl rename to modules/styles/blocks/00-variables/variables.styl index 6b42246..c02eac5 100755 --- a/styles/blocks/00-variables/variables.styl +++ b/modules/styles/blocks/00-variables/variables.styl @@ -16,18 +16,25 @@ color_red = #B80000 color_green = #469269 color_black = #000 color_orange = #F8AB47 +color_orange_light = #FDC073 link_color = #0059B2 light_link_color = #3B86C4 alternate_link_color = #2974BB // there are some a bit lighter links that need separate color +link_color_new = #3A8AED link_hover_color = #BA1000 link_visited_color = #551A8B navigation_link_color = #696664 +text_yellow = #FFFED7 +text_grey_dark = #373636 +text_courses_dark = #404040 + color = #333 code_color = #333 gray_color = #666 light_gray_color = #999 +ultralight_gray_color = #E1E1E1 separator_color = #DFDFD0 secondary_color = #B20600 hover_color_correction = 25% @@ -38,15 +45,66 @@ background_blocks = #F5F2F0 background_yellow_light = #F6F4EB background_yellow_dark = #EDE9D8 -font = 'Open Sans', Arial, Helvetica, sans-serif size = 14px -lineheight = 18px +lineheight = 20px + +// System variables -secondary_font = 'Open Sans Condensed', Arial, Helvetica, sans-serif -// "Source Code Pro" has no Cyrillic and on Windows it looks like Courier -// "Consolas" is not installed on Linux/old Macs, it is paid per page-views if @font-face -// We need 1 font for all, because SVGs use this font, and rectangles depend on its width +// Typefaces +font = BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Helvetica,Arial, sans-serif +secondary_font = BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Helvetica,Arial, sans-serif fixed_width_font = 'Consolas', 'Lucida Console', 'Menlo', 'Monaco', monospace +font_en = -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif + +// Font sizes +font_size_xs = 12px +font_size_s = 14px +font_size_m = 16px +font_size_l = 20px +font_size_xl = 24px +font_size_xxl = 24px + +// Line heights +line_height_xs = 18px +line_height_s = 20px +line_height_m = 22px +line_height_l = 24px +line_height_xl = 32px +line_height_xxl = 40px + +// Spacing Outer Vertical (mostly margins buttom/top to keep the vertical rythm) +outer_vertical_xs = 8px +outer_vertical_s = 12px +outer_vertical_m = 16px +outer_vertical_l = 24px +outer_vertical_xl = 32px +outer_vertical_xxl = 64px + +// Spacing Outer Horizontal (mostly margins right/left to keep the horizontal rythm) +outer_horizontal_xxs = 2px +outer_horizontal_xs = 4px +outer_horizontal_s = 8px +outer_horizontal_m = 16px +outer_horizontal_l = 24px +outer_horizontal_xl = 32px +outer_horizontal_xxl = 64px + +// Spacing Inner Square (good for cards, message boxes, footers etc) +inner_square_s = 8px +inner_square_m = 16px +inner_square_l = 24px +inner_square_xl = 32px +inner_square_xxl = 64px + +// Spacing Inner Landscape (good for buttons, pills, menus, data tables etc) +inner_landscape_s = 12px 8px +inner_landscape_m = 16px 12px +inner_landscape_l = 24px 16px + +// Spacing Inner Portrait (mostly used for forms) +inner_portrait_s = 4px 8px +inner_portrait_m = 8px 16px +inner_portrait_l = 16px 32px sidebar_width = 250px @@ -63,6 +121,7 @@ max_width_wide = 1000px max_width_extra_wide = 1200px +margin_block = 56px media_step_1 = 1120px media_step_2 = 1170px diff --git a/styles/blocks/01-reset/reset.styl b/modules/styles/blocks/01-reset/reset.styl similarity index 100% rename from styles/blocks/01-reset/reset.styl rename to modules/styles/blocks/01-reset/reset.styl diff --git a/styles/blocks/02-placeholders/button-reset.styl b/modules/styles/blocks/02-placeholders/button-reset.styl similarity index 100% rename from styles/blocks/02-placeholders/button-reset.styl rename to modules/styles/blocks/02-placeholders/button-reset.styl diff --git a/styles/blocks/02-placeholders/clearfix.styl b/modules/styles/blocks/02-placeholders/clearfix.styl similarity index 100% rename from styles/blocks/02-placeholders/clearfix.styl rename to modules/styles/blocks/02-placeholders/clearfix.styl diff --git a/styles/blocks/02-placeholders/link-button.styl b/modules/styles/blocks/02-placeholders/link-button.styl similarity index 100% rename from styles/blocks/02-placeholders/link-button.styl rename to modules/styles/blocks/02-placeholders/link-button.styl diff --git a/styles/blocks/04-links/link-types/doc.png b/modules/styles/blocks/04-links/link-types/doc.png similarity index 100% rename from styles/blocks/04-links/link-types/doc.png rename to modules/styles/blocks/04-links/link-types/doc.png diff --git a/styles/blocks/04-links/link-types/ecma.png b/modules/styles/blocks/04-links/link-types/ecma.png similarity index 100% rename from styles/blocks/04-links/link-types/ecma.png rename to modules/styles/blocks/04-links/link-types/ecma.png diff --git a/styles/blocks/04-links/link-types/mailto.png b/modules/styles/blocks/04-links/link-types/mailto.png similarity index 100% rename from styles/blocks/04-links/link-types/mailto.png rename to modules/styles/blocks/04-links/link-types/mailto.png diff --git a/styles/blocks/04-links/link-types/mdn.png b/modules/styles/blocks/04-links/link-types/mdn.png similarity index 100% rename from styles/blocks/04-links/link-types/mdn.png rename to modules/styles/blocks/04-links/link-types/mdn.png diff --git a/styles/blocks/04-links/link-types/msdn.png b/modules/styles/blocks/04-links/link-types/msdn.png similarity index 100% rename from styles/blocks/04-links/link-types/msdn.png rename to modules/styles/blocks/04-links/link-types/msdn.png diff --git a/styles/blocks/04-links/link-types/newwindow.png b/modules/styles/blocks/04-links/link-types/newwindow.png similarity index 100% rename from styles/blocks/04-links/link-types/newwindow.png rename to modules/styles/blocks/04-links/link-types/newwindow.png diff --git a/styles/blocks/04-links/link-types/pdf.png b/modules/styles/blocks/04-links/link-types/pdf.png similarity index 100% rename from styles/blocks/04-links/link-types/pdf.png rename to modules/styles/blocks/04-links/link-types/pdf.png diff --git a/styles/blocks/04-links/link-types/sandbox.png b/modules/styles/blocks/04-links/link-types/sandbox.png similarity index 100% rename from styles/blocks/04-links/link-types/sandbox.png rename to modules/styles/blocks/04-links/link-types/sandbox.png diff --git a/styles/blocks/04-links/link-types/w3c.png b/modules/styles/blocks/04-links/link-types/w3c.png similarity index 100% rename from styles/blocks/04-links/link-types/w3c.png rename to modules/styles/blocks/04-links/link-types/w3c.png diff --git a/styles/blocks/04-links/link-types/wiki.png b/modules/styles/blocks/04-links/link-types/wiki.png similarity index 100% rename from styles/blocks/04-links/link-types/wiki.png rename to modules/styles/blocks/04-links/link-types/wiki.png diff --git a/styles/blocks/04-links/link-types/xls.png b/modules/styles/blocks/04-links/link-types/xls.png similarity index 100% rename from styles/blocks/04-links/link-types/xls.png rename to modules/styles/blocks/04-links/link-types/xls.png diff --git a/styles/blocks/04-links/link-types/zip.png b/modules/styles/blocks/04-links/link-types/zip.png similarity index 100% rename from styles/blocks/04-links/link-types/zip.png rename to modules/styles/blocks/04-links/link-types/zip.png diff --git a/styles/blocks/04-links/links.styl b/modules/styles/blocks/04-links/links.styl similarity index 100% rename from styles/blocks/04-links/links.styl rename to modules/styles/blocks/04-links/links.styl diff --git a/styles/blocks/balance/balance.styl b/modules/styles/blocks/balance/balance.styl similarity index 100% rename from styles/blocks/balance/balance.styl rename to modules/styles/blocks/balance/balance.styl diff --git a/styles/blocks/banner-bottom/banner-bottom.styl b/modules/styles/blocks/banner-bottom/banner-bottom.styl similarity index 100% rename from styles/blocks/banner-bottom/banner-bottom.styl rename to modules/styles/blocks/banner-bottom/banner-bottom.styl diff --git a/styles/blocks/body/body.styl b/modules/styles/blocks/body/body.styl similarity index 100% rename from styles/blocks/body/body.styl rename to modules/styles/blocks/body/body.styl diff --git a/styles/blocks/breadcrumbs/breadcrumbs.styl b/modules/styles/blocks/breadcrumbs/breadcrumbs.styl similarity index 100% rename from styles/blocks/breadcrumbs/breadcrumbs.styl rename to modules/styles/blocks/breadcrumbs/breadcrumbs.styl diff --git a/styles/blocks/close-button/close-button.styl b/modules/styles/blocks/close-button/close-button.styl similarity index 100% rename from styles/blocks/close-button/close-button.styl rename to modules/styles/blocks/close-button/close-button.styl diff --git a/styles/blocks/code-example/code-example.styl b/modules/styles/blocks/code-example/code-example.styl similarity index 100% rename from styles/blocks/code-example/code-example.styl rename to modules/styles/blocks/code-example/code-example.styl diff --git a/styles/blocks/code-result/code-result.styl b/modules/styles/blocks/code-result/code-result.styl similarity index 100% rename from styles/blocks/code-result/code-result.styl rename to modules/styles/blocks/code-result/code-result.styl diff --git a/styles/blocks/code-tabs/code-tabs.styl b/modules/styles/blocks/code-tabs/code-tabs.styl similarity index 100% rename from styles/blocks/code-tabs/code-tabs.styl rename to modules/styles/blocks/code-tabs/code-tabs.styl diff --git a/styles/blocks/codebox/codebox.styl b/modules/styles/blocks/codebox/codebox.styl similarity index 100% rename from styles/blocks/codebox/codebox.styl rename to modules/styles/blocks/codebox/codebox.styl diff --git a/styles/blocks/columns/columns.styl b/modules/styles/blocks/columns/columns.styl similarity index 100% rename from styles/blocks/columns/columns.styl rename to modules/styles/blocks/columns/columns.styl diff --git a/styles/blocks/comments/comments.styl b/modules/styles/blocks/comments/comments.styl similarity index 100% rename from styles/blocks/comments/comments.styl rename to modules/styles/blocks/comments/comments.styl diff --git a/styles/blocks/content/content.styl b/modules/styles/blocks/content/content.styl similarity index 100% rename from styles/blocks/content/content.styl rename to modules/styles/blocks/content/content.styl diff --git a/styles/blocks/corrector/corrector.styl b/modules/styles/blocks/corrector/corrector.styl similarity index 100% rename from styles/blocks/corrector/corrector.styl rename to modules/styles/blocks/corrector/corrector.styl diff --git a/styles/blocks/domtree/domtree.styl b/modules/styles/blocks/domtree/domtree.styl similarity index 100% rename from styles/blocks/domtree/domtree.styl rename to modules/styles/blocks/domtree/domtree.styl diff --git a/styles/blocks/error/error.styl b/modules/styles/blocks/error/error.styl similarity index 100% rename from styles/blocks/error/error.styl rename to modules/styles/blocks/error/error.styl diff --git a/styles/blocks/extract/extract.styl b/modules/styles/blocks/extract/extract.styl similarity index 100% rename from styles/blocks/extract/extract.styl rename to modules/styles/blocks/extract/extract.styl diff --git a/styles/blocks/faq-cite/faq-cite.styl b/modules/styles/blocks/faq-cite/faq-cite.styl similarity index 100% rename from styles/blocks/faq-cite/faq-cite.styl rename to modules/styles/blocks/faq-cite/faq-cite.styl diff --git a/styles/blocks/fixed-tab/fixed-tab.styl b/modules/styles/blocks/fixed-tab/fixed-tab.styl similarity index 100% rename from styles/blocks/fixed-tab/fixed-tab.styl rename to modules/styles/blocks/fixed-tab/fixed-tab.styl diff --git a/styles/blocks/flex-column/flex-column.styl b/modules/styles/blocks/flex-column/flex-column.styl similarity index 100% rename from styles/blocks/flex-column/flex-column.styl rename to modules/styles/blocks/flex-column/flex-column.styl diff --git a/styles/blocks/font-test/font-test.styl b/modules/styles/blocks/font-test/font-test.styl similarity index 100% rename from styles/blocks/font-test/font-test.styl rename to modules/styles/blocks/font-test/font-test.styl diff --git a/styles/blocks/font/font-icons.styl b/modules/styles/blocks/font/font-icons.styl similarity index 100% rename from styles/blocks/font/font-icons.styl rename to modules/styles/blocks/font/font-icons.styl diff --git a/styles/blocks/font/icons.otf b/modules/styles/blocks/font/icons.otf similarity index 100% rename from styles/blocks/font/icons.otf rename to modules/styles/blocks/font/icons.otf diff --git a/styles/blocks/font/icons.woff b/modules/styles/blocks/font/icons.woff similarity index 100% rename from styles/blocks/font/icons.woff rename to modules/styles/blocks/font/icons.woff diff --git a/styles/blocks/full-phone/full-phone.styl b/modules/styles/blocks/full-phone/full-phone.styl similarity index 100% rename from styles/blocks/full-phone/full-phone.styl rename to modules/styles/blocks/full-phone/full-phone.styl diff --git a/styles/blocks/image-with-text/image-with-text.styl b/modules/styles/blocks/image-with-text/image-with-text.styl similarity index 100% rename from styles/blocks/image-with-text/image-with-text.styl rename to modules/styles/blocks/image-with-text/image-with-text.styl diff --git a/styles/blocks/image/image.styl b/modules/styles/blocks/image/image.styl similarity index 100% rename from styles/blocks/image/image.styl rename to modules/styles/blocks/image/image.styl diff --git a/styles/blocks/important/important.sprite/info.png b/modules/styles/blocks/important/important.sprite/info.png similarity index 100% rename from styles/blocks/important/important.sprite/info.png rename to modules/styles/blocks/important/important.sprite/info.png diff --git a/styles/blocks/important/important.sprite/ok.png b/modules/styles/blocks/important/important.sprite/ok.png similarity index 100% rename from styles/blocks/important/important.sprite/ok.png rename to modules/styles/blocks/important/important.sprite/ok.png diff --git a/styles/blocks/important/important.sprite/question.png b/modules/styles/blocks/important/important.sprite/question.png similarity index 100% rename from styles/blocks/important/important.sprite/question.png rename to modules/styles/blocks/important/important.sprite/question.png diff --git a/styles/blocks/important/important.sprite/warning.png b/modules/styles/blocks/important/important.sprite/warning.png similarity index 100% rename from styles/blocks/important/important.sprite/warning.png rename to modules/styles/blocks/important/important.sprite/warning.png diff --git a/styles/blocks/important/important.styl b/modules/styles/blocks/important/important.styl similarity index 100% rename from styles/blocks/important/important.styl rename to modules/styles/blocks/important/important.styl diff --git a/styles/blocks/intro/intro.styl b/modules/styles/blocks/intro/intro.styl similarity index 100% rename from styles/blocks/intro/intro.styl rename to modules/styles/blocks/intro/intro.styl diff --git a/styles/blocks/lessons-list/lessons-list.styl b/modules/styles/blocks/lessons-list/lessons-list.styl similarity index 100% rename from styles/blocks/lessons-list/lessons-list.styl rename to modules/styles/blocks/lessons-list/lessons-list.styl diff --git a/styles/blocks/main/main.styl b/modules/styles/blocks/main/main.styl similarity index 88% rename from styles/blocks/main/main.styl rename to modules/styles/blocks/main/main.styl index be9288f..ad3f10d 100755 --- a/styles/blocks/main/main.styl +++ b/modules/styles/blocks/main/main.styl @@ -16,17 +16,18 @@ $main-loud &_width-limit-wide max-width max_width_wide + &::before, &__header::before content "" display table // убираем margin collapse &__header .breadcrumbs - margin 0 + margin 0 0 12px 0 padding 0 &__header - margin 25px 0 15px 0 + margin 40px 0 16px 0 &__header_center border 0 @@ -36,10 +37,13 @@ $main-loud text-align center & &__header-title - font 700 286%/150% secondary_font - margin 15px 0 0 0 + font 700 32px/40px secondary_font + margin 0 auto 12px auto padding 0 + @media screen and (max-width 1180px) + max-width 430px + & &__header-title:not(::first-line) color green @@ -103,17 +107,18 @@ $main-loud content "●" float left // not position: absolute because the latter doesn't show in iBooks (epub) margin-left -20px - color #B2C1C1 + color #000 + font-size 8px // TODO: h1 вне блока .main__header ??! Проверить, встречается ли h1 margin-bottom .5em - /* FIXME пока некрасиво с этим: page-break-before: always; */ + /* FIXME пока некрасиво с этим: page-break-before: always; */ h2 - margin 36px 0 22px - font-size 185% - line-height 150% + margin 24px 0 12px + font-size 24px + line-height 32px font-family font position relative @@ -145,14 +150,14 @@ $main-loud position relative h2 &__anchor, - h3 &__anchor, - h4 &__anchor - transition unquote('color') animation_duration + h3 &__anchor, + h4 &__anchor + transition unquote('color') animation_duration h2 &__anchor:hover, - h3 &__anchor:hover, - h4 &__anchor:hover - color hoverize(color) + h3 &__anchor:hover, + h4 &__anchor:hover + color hoverize(color) h1 code, h1 a, @@ -220,6 +225,9 @@ $main-loud /* did it break something? fix it there! */ /* color code_color */ font-family fixed_width_font + padding 2px 4px + background #f5f2f0 + border-radius 2px /* for regexps */ code.pattern @@ -241,7 +249,7 @@ $main-loud margin 1px 0 p - margin 22px 0 + margin 0 0 12px // Is it really used? dl @@ -325,7 +333,7 @@ $main-loud line-height 20px @media (min-width: media_step_3) - line-height 22px + line-height line_height_m @media (min-width: largescreen) font-size 16px diff --git a/styles/blocks/map/map.styl b/modules/styles/blocks/map/map.styl similarity index 100% rename from styles/blocks/map/map.styl rename to modules/styles/blocks/map/map.styl diff --git a/styles/blocks/mixins/hover.styl b/modules/styles/blocks/mixins/hover.styl similarity index 100% rename from styles/blocks/mixins/hover.styl rename to modules/styles/blocks/mixins/hover.styl diff --git a/styles/blocks/mixins/imagesize.styl b/modules/styles/blocks/mixins/imagesize.styl similarity index 100% rename from styles/blocks/mixins/imagesize.styl rename to modules/styles/blocks/mixins/imagesize.styl diff --git a/styles/blocks/modal/modal.styl b/modules/styles/blocks/modal/modal.styl similarity index 100% rename from styles/blocks/modal/modal.styl rename to modules/styles/blocks/modal/modal.styl diff --git a/styles/blocks/notify/notify.styl b/modules/styles/blocks/notify/notify.styl similarity index 100% rename from styles/blocks/notify/notify.styl rename to modules/styles/blocks/notify/notify.styl diff --git a/styles/blocks/number-input/number-input.styl b/modules/styles/blocks/number-input/number-input.styl similarity index 100% rename from styles/blocks/number-input/number-input.styl rename to modules/styles/blocks/number-input/number-input.styl diff --git a/styles/blocks/page-footer/page-footer.png b/modules/styles/blocks/page-footer/page-footer.png similarity index 100% rename from styles/blocks/page-footer/page-footer.png rename to modules/styles/blocks/page-footer/page-footer.png diff --git a/styles/blocks/page-footer/page-footer.styl b/modules/styles/blocks/page-footer/page-footer.styl similarity index 100% rename from styles/blocks/page-footer/page-footer.styl rename to modules/styles/blocks/page-footer/page-footer.styl diff --git a/styles/blocks/page-footer/slack.svg b/modules/styles/blocks/page-footer/slack.svg similarity index 100% rename from styles/blocks/page-footer/slack.svg rename to modules/styles/blocks/page-footer/slack.svg diff --git a/styles/blocks/page-loader/page-loader.styl b/modules/styles/blocks/page-loader/page-loader.styl similarity index 100% rename from styles/blocks/page-loader/page-loader.styl rename to modules/styles/blocks/page-loader/page-loader.styl diff --git a/styles/blocks/page-wrapper/page-wrapper.styl b/modules/styles/blocks/page-wrapper/page-wrapper.styl similarity index 100% rename from styles/blocks/page-wrapper/page-wrapper.styl rename to modules/styles/blocks/page-wrapper/page-wrapper.styl diff --git a/styles/blocks/page/page.styl b/modules/styles/blocks/page/page.styl similarity index 100% rename from styles/blocks/page/page.styl rename to modules/styles/blocks/page/page.styl diff --git a/styles/blocks/prism/prism.styl b/modules/styles/blocks/prism/01-prism.styl similarity index 100% rename from styles/blocks/prism/prism.styl rename to modules/styles/blocks/prism/01-prism.styl diff --git a/modules/styles/blocks/prism/02-prism-line-highlight.styl b/modules/styles/blocks/prism/02-prism-line-highlight.styl new file mode 100644 index 0000000..64f0735 --- /dev/null +++ b/modules/styles/blocks/prism/02-prism-line-highlight.styl @@ -0,0 +1,39 @@ +.main /* Used this parent class to be able to overwrite some generic rules from main.styl */ + + .inline-highlight + position absolute + pointer-events none + line-height inherit + white-space pre + left 0 + top -2px + z-index -1 + padding 0 + + .mask + padding 0 + background #F5E7C6 + outline 2px solid #F5E7C6 + + .block-highlight + display block + position absolute + left 0 + right 0 + top -1px + margin-top 1em /* Same as .prism’s padding-top */ + padding 0 + + pointer-events none + + line-height inherit + white-space pre + + .mask + display block + background #F5E7C6 + outline 1px solid #F5E7C6 + left 0 + right 0 + position absolute + padding 0 diff --git a/modules/styles/blocks/prism/03-prism-line-numbers.styl b/modules/styles/blocks/prism/03-prism-line-numbers.styl new file mode 100644 index 0000000..e8b4828 --- /dev/null +++ b/modules/styles/blocks/prism/03-prism-line-numbers.styl @@ -0,0 +1,27 @@ +pre.line-numbers + position relative + padding-left 3.8em + counter-reset linenumber + +.line-numbers .line-numbers-rows + position absolute + pointer-events none + top 0 + font-size 100% + left -3.8em + width 3em /* works for line-numbers below 1000 lines */ + letter-spacing -1px + border-right 1px solid light_gray_color + user-select none + +.line-numbers-rows > span + pointer-events none + display block + counter-increment linenumber + +.line-numbers-rows > span:before + content counter(linenumber) + color light_gray_color + display block + padding-right 0.8em + text-align right diff --git a/modules/styles/blocks/prism/04-my-prism.styl b/modules/styles/blocks/prism/04-my-prism.styl new file mode 100644 index 0000000..1b9be30 --- /dev/null +++ b/modules/styles/blocks/prism/04-my-prism.styl @@ -0,0 +1,51 @@ +pre[class*="language-"], +code[class*="language-"] + font 14px/17px fixed_width_font + z-index 0 + text-shadow none + margin 0 + +pre[class*="language-"] + position relative + > code.language-markup + color inherit + position relative + + > code[class*="language-"] + background none + padding 0 + +pre.line-numbers + padding-left 3.5em + +// span with line numbers is moved from to the outer
      ,
      +// because we need to handle many ... inside single 
      +// (this we need for highlighting *!*...* /!* inline
      +.line-numbers .line-numbers-rows
      +  left 0
      +  top 0
      +  padding 1em 0
      +  border 0
      +  background #e7e5e3
      +  width auto
      +
      +.line-numbers .line-numbers-rows:after
      +  width auto
      +  display block
      +  visibility hidden
      +  margin-top -1.2em // fitted value
      +  content: '222' // stretch line up to 3 digits
      +
      +.line-numbers-rows > span:before,
      +.line-numbers .line-numbers-rows:after
      +  padding 0 .7em 0 .8em
      +  background #e7e5e3 // #146 https://github.com/iliakan/javascript-nodejs/issues/146#issuecomment-72321753
      +  text-shadow none
      +
      +/* not sure if larger code is better
      +@media (min-width: largescreen)
      +  pre[class*="language-"],
      +  code[class*="language-"]
      +    font-size font_size_m
      +    line-height 19px
      +*/
      \ No newline at end of file
      diff --git a/styles/blocks/progress/progress.styl b/modules/styles/blocks/progress/progress.styl
      similarity index 100%
      rename from styles/blocks/progress/progress.styl
      rename to modules/styles/blocks/progress/progress.styl
      diff --git a/styles/blocks/questionnaire/questionnaire.styl b/modules/styles/blocks/questionnaire/questionnaire.styl
      similarity index 100%
      rename from styles/blocks/questionnaire/questionnaire.styl
      rename to modules/styles/blocks/questionnaire/questionnaire.styl
      diff --git a/styles/blocks/quote/quote.styl b/modules/styles/blocks/quote/quote.styl
      similarity index 100%
      rename from styles/blocks/quote/quote.styl
      rename to modules/styles/blocks/quote/quote.styl
      diff --git a/styles/blocks/rating-chooser/rating-chooser.styl b/modules/styles/blocks/rating-chooser/rating-chooser.styl
      similarity index 100%
      rename from styles/blocks/rating-chooser/rating-chooser.styl
      rename to modules/styles/blocks/rating-chooser/rating-chooser.styl
      diff --git a/styles/blocks/rating/rating.styl b/modules/styles/blocks/rating/rating.styl
      similarity index 100%
      rename from styles/blocks/rating/rating.styl
      rename to modules/styles/blocks/rating/rating.styl
      diff --git a/styles/blocks/share-icons/share-icons.styl b/modules/styles/blocks/share-icons/share-icons.styl
      similarity index 100%
      rename from styles/blocks/share-icons/share-icons.styl
      rename to modules/styles/blocks/share-icons/share-icons.styl
      diff --git a/styles/blocks/share/share.styl b/modules/styles/blocks/share/share.styl
      similarity index 100%
      rename from styles/blocks/share/share.styl
      rename to modules/styles/blocks/share/share.styl
      diff --git a/styles/blocks/shortcut/shortcut.styl b/modules/styles/blocks/shortcut/shortcut.styl
      similarity index 100%
      rename from styles/blocks/shortcut/shortcut.styl
      rename to modules/styles/blocks/shortcut/shortcut.styl
      diff --git a/styles/blocks/sidebar/sidebar.styl b/modules/styles/blocks/sidebar/sidebar.styl
      similarity index 100%
      rename from styles/blocks/sidebar/sidebar.styl
      rename to modules/styles/blocks/sidebar/sidebar.styl
      diff --git a/styles/blocks/simple-button/simple-button.styl b/modules/styles/blocks/simple-button/simple-button.styl
      similarity index 100%
      rename from styles/blocks/simple-button/simple-button.styl
      rename to modules/styles/blocks/simple-button/simple-button.styl
      diff --git a/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl b/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl
      similarity index 100%
      rename from styles/blocks/sitetoolbar-light/sitetoolbar-light.styl
      rename to modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl
      diff --git a/styles/blocks/sitetoolbar/sitetoolbar.styl b/modules/styles/blocks/sitetoolbar/sitetoolbar.styl
      similarity index 100%
      rename from styles/blocks/sitetoolbar/sitetoolbar.styl
      rename to modules/styles/blocks/sitetoolbar/sitetoolbar.styl
      diff --git a/styles/blocks/special-links-list/special-links-list.styl b/modules/styles/blocks/special-links-list/special-links-list.styl
      similarity index 100%
      rename from styles/blocks/special-links-list/special-links-list.styl
      rename to modules/styles/blocks/special-links-list/special-links-list.styl
      diff --git a/styles/blocks/spinner/spinner.styl b/modules/styles/blocks/spinner/spinner.styl
      similarity index 100%
      rename from styles/blocks/spinner/spinner.styl
      rename to modules/styles/blocks/spinner/spinner.styl
      diff --git a/styles/blocks/spoiler/spoiler.styl b/modules/styles/blocks/spoiler/spoiler.styl
      similarity index 100%
      rename from styles/blocks/spoiler/spoiler.styl
      rename to modules/styles/blocks/spoiler/spoiler.styl
      diff --git a/styles/blocks/standard-table/standard-table.styl b/modules/styles/blocks/standard-table/standard-table.styl
      similarity index 100%
      rename from styles/blocks/standard-table/standard-table.styl
      rename to modules/styles/blocks/standard-table/standard-table.styl
      diff --git a/styles/blocks/submit-button/submit-button.styl b/modules/styles/blocks/submit-button/submit-button.styl
      similarity index 100%
      rename from styles/blocks/submit-button/submit-button.styl
      rename to modules/styles/blocks/submit-button/submit-button.styl
      diff --git a/styles/blocks/subscribe/subscribe.styl b/modules/styles/blocks/subscribe/subscribe.styl
      similarity index 100%
      rename from styles/blocks/subscribe/subscribe.styl
      rename to modules/styles/blocks/subscribe/subscribe.styl
      diff --git a/styles/blocks/summary/summary.styl b/modules/styles/blocks/summary/summary.styl
      similarity index 100%
      rename from styles/blocks/summary/summary.styl
      rename to modules/styles/blocks/summary/summary.styl
      diff --git a/styles/blocks/switch-input/switch-input.styl b/modules/styles/blocks/switch-input/switch-input.styl
      similarity index 100%
      rename from styles/blocks/switch-input/switch-input.styl
      rename to modules/styles/blocks/switch-input/switch-input.styl
      diff --git a/styles/blocks/switch/switch.styl b/modules/styles/blocks/switch/switch.styl
      similarity index 100%
      rename from styles/blocks/switch/switch.styl
      rename to modules/styles/blocks/switch/switch.styl
      diff --git a/styles/blocks/tabbed-pane/tabbed-pane.styl b/modules/styles/blocks/tabbed-pane/tabbed-pane.styl
      similarity index 100%
      rename from styles/blocks/tabbed-pane/tabbed-pane.styl
      rename to modules/styles/blocks/tabbed-pane/tabbed-pane.styl
      diff --git a/styles/blocks/tablet-ebook/tablet-ebook.styl b/modules/styles/blocks/tablet-ebook/tablet-ebook.styl
      similarity index 100%
      rename from styles/blocks/tablet-ebook/tablet-ebook.styl
      rename to modules/styles/blocks/tablet-ebook/tablet-ebook.styl
      diff --git a/styles/blocks/tablet-menu/tablet-menu.styl b/modules/styles/blocks/tablet-menu/tablet-menu.styl
      similarity index 100%
      rename from styles/blocks/tablet-menu/tablet-menu.styl
      rename to modules/styles/blocks/tablet-menu/tablet-menu.styl
      diff --git a/styles/blocks/task-single/task-single.styl b/modules/styles/blocks/task-single/task-single.styl
      similarity index 100%
      rename from styles/blocks/task-single/task-single.styl
      rename to modules/styles/blocks/task-single/task-single.styl
      diff --git a/styles/blocks/task/task.styl b/modules/styles/blocks/task/task.styl
      similarity index 100%
      rename from styles/blocks/task/task.styl
      rename to modules/styles/blocks/task/task.styl
      diff --git a/styles/blocks/tasks/tasks.styl b/modules/styles/blocks/tasks/tasks.styl
      similarity index 100%
      rename from styles/blocks/tasks/tasks.styl
      rename to modules/styles/blocks/tasks/tasks.styl
      diff --git a/styles/blocks/text-input-button/text-input-button.styl b/modules/styles/blocks/text-input-button/text-input-button.styl
      similarity index 100%
      rename from styles/blocks/text-input-button/text-input-button.styl
      rename to modules/styles/blocks/text-input-button/text-input-button.styl
      diff --git a/styles/blocks/toolbar/toolbar.styl b/modules/styles/blocks/toolbar/toolbar.styl
      similarity index 100%
      rename from styles/blocks/toolbar/toolbar.styl
      rename to modules/styles/blocks/toolbar/toolbar.styl
      diff --git a/styles/blocks/tutorial-progress/tutorial-progress.styl b/modules/styles/blocks/tutorial-progress/tutorial-progress.styl
      similarity index 100%
      rename from styles/blocks/tutorial-progress/tutorial-progress.styl
      rename to modules/styles/blocks/tutorial-progress/tutorial-progress.styl
      diff --git a/styles/blocks/upload-userpic/upload-userpic.styl b/modules/styles/blocks/upload-userpic/upload-userpic.styl
      similarity index 100%
      rename from styles/blocks/upload-userpic/upload-userpic.styl
      rename to modules/styles/blocks/upload-userpic/upload-userpic.styl
      diff --git a/modules/textUtil/cut.js b/modules/textUtil/cut.js
      deleted file mode 100755
      index ac4497c..0000000
      --- a/modules/textUtil/cut.js
      +++ /dev/null
      @@ -1,47 +0,0 @@
      -//// cuts a piece from text of given length or shorter
      -//// preferrable at paragraph
      -//// ported from Drupal8 text_summary
      -//module.exports = function(text, maxLength) {
      -//
      -//  if (text.length <= maxLength) return text;
      -//
      -//  // The summary may not be longer than maximum length specified. Initial slice.
      -//  let summary = text.slice(0, maxLength);
      -//
      -//  // Store the actual length of the UTF8 string -- which might not be the same
      -//  // as $size.
      -//  let maxRpos = summary.length;
      -//
      -//  // How much to cut off the end of the summary so that it doesn't end in the
      -//  // middle of a paragraph, sentence, or word.
      -//  // Initialize it to maximum in order to find the minimum.
      -//  let minRpos = maxRpos;
      -//
      -//  // Build an array of arrays of break points grouped by preference.
      -//  let breakPoints = ["\n", '. ', '! ',  '? ', ', ', ')', ']'];
      -//  // Iterate over the groups of break points until a break point is found.
      -//  foreach ($break_points as $points) {
      -//    // Look for each break point, starting at the end of the summary.
      -//    foreach ($points as $point => $offset) {
      -//      // The summary is already reversed, but the break point isn't.
      -//      $rpos = strpos($reversed, strrev($point));
      -//      if ($rpos !== FALSE) {
      -//        $min_rpos = min($rpos + $offset, $min_rpos);
      -//      }
      -//    }
      -//
      -//    // If a break point was found in this group, slice and stop searching.
      -//    if ($min_rpos !== $max_rpos) {
      -//      // Don't slice with length 0. Length must be <0 to slice from RHS.
      -//      $summary = ($min_rpos === 0) ? $summary : substr($summary, 0, 0 - $min_rpos);
      -//      break;
      -//    }
      -//  }
      -//
      -//  // If the htmlcorrector filter is present, apply it to the generated summary.
      -//  if (isset($format) && $filters->has('filter_htmlcorrector') && $filters->get('filter_htmlcorrector')->status) {
      -//    $summary = Html::normalize($summary);
      -//  }
      -//
      -//  return $summary;
      -//}
      diff --git a/modules/textUtil/escapeHtmlAttr.js b/modules/textUtil/escapeHtmlAttr.js
      deleted file mode 100755
      index 39dbcc2..0000000
      --- a/modules/textUtil/escapeHtmlAttr.js
      +++ /dev/null
      @@ -1,6 +0,0 @@
      -
      -
      -module.exports = function(text) {
      -  return text.toString().replace(/"/g, '"').replace(/'/g, ''');
      -};
      -
      diff --git a/modules/textUtil/escapeHtmlText.js b/modules/textUtil/escapeHtmlText.js
      deleted file mode 100755
      index 9199604..0000000
      --- a/modules/textUtil/escapeHtmlText.js
      +++ /dev/null
      @@ -1,4 +0,0 @@
      -module.exports = function(text) {
      -  // need toString to escape numbers
      -  return String(text).replace(/&([^#]|#[^0-9]?|#x[^0-9]?|$)/g, '&$1').replace(//g, '>');
      -};
      diff --git a/modules/textUtil/makeAnchor.js b/modules/textUtil/makeAnchor.js
      deleted file mode 100755
      index 94e7c33..0000000
      --- a/modules/textUtil/makeAnchor.js
      +++ /dev/null
      @@ -1,18 +0,0 @@
      -let transliterate = require('./transliterate');
      -
      -module.exports = function(title, translitAnchors) {
      -  let anchor = title.trim()
      -    .replace(/<\/?[a-z].*?>/gim, '')  // strip tags, leave /?@[\\\]^_`{|}~]/g, '-') // пунктуация, пробелы -> дефис
      -    .replace(/[^a-zа-яё0-9-]/gi, '') // убрать любые символы, кроме [слов цифр дефиса])
      -    .replace(/-+/gi, '-') // слить дефисы вместе
      -    .replace(/^-|-$/g, ''); // убрать дефисы с концов
      -
      -  if (translitAnchors) {
      -    anchor = transliterate(anchor);
      -  }
      -  anchor = anchor.toLowerCase();
      -
      -  return anchor;
      -};
      -
      diff --git a/modules/textUtil/priceInWords.js b/modules/textUtil/priceInWords.js
      deleted file mode 100755
      index 8654bee..0000000
      --- a/modules/textUtil/priceInWords.js
      +++ /dev/null
      @@ -1,250 +0,0 @@
      -// borrowed from http://javascript.ru/forum/misc/40642-summa-propisyu-2.html
      -
      -module.exports = function(number, locale) {
      -  locale = locale || 'ru';
      -
      -  return locale == 'ru' ? ru(number) : ua(number);
      -};
      -
      -function ru(_number) {
      -  let _arr_numbers = new Array();
      -  _arr_numbers[1] = new Array('', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять', 'десять', 'одиннадцать', 'двенадцать', 'тринадцать', 'четырнадцать', 'пятнадцать', 'шестнадцать', 'семнадцать', 'восемнадцать', 'девятнадцать');
      -  _arr_numbers[2] = new Array('', '', 'двадцать', 'тридцать', 'сорок', 'пятьдесят', 'шестьдесят', 'семьдесят', 'восемьдесят', 'девяносто');
      -  _arr_numbers[3] = new Array('', 'сто', 'двести', 'триста', 'четыреста', 'пятьсот', 'шестьсот', 'семьсот', 'восемьсот', 'девятьсот');
      -  function number_parser(_num, _desc) {
      -    let _string = '';
      -    let _num_hundred = '';
      -    if (_num.length == 3) {
      -      _num_hundred = _num.substr(0, 1);
      -      _num = _num.substr(1, 3);
      -      _string = _arr_numbers[3][_num_hundred] + ' ';
      -    }
      -    if (_num < 20) _string += _arr_numbers[1][parseFloat(_num)] + ' ';
      -    else {
      -      let _first_num = _num.substr(0, 1);
      -      let _second_num = _num.substr(1, 2);
      -      _string += _arr_numbers[2][_first_num] + ' ' + _arr_numbers[1][_second_num] + ' ';
      -    }
      -    switch (_desc) {
      -    case 0:
      -      let last = _num.length - 1;
      -      let _last_num = parseFloat(_num.charAt(last));
      -      let _slice_num = _num.slice(0, -1);
      -      let _pre_last_num = parseFloat(_slice_num);
      -      if (_last_num == 1 && 1 != _pre_last_num) _string += 'рубль';
      -      else if (_last_num > 1 && _last_num < 5 && 1 != _pre_last_num)  _string += 'рубля';
      -      else if ("" != _slice_num) _string += 'рублей';
      -      else if (1 == _pre_last_num) _string += 'рублей';
      -      else if ("" != _slice_num && _last_num > 4) _string += 'рублей';
      -      else if ("" == _slice_num && _last_num > 4) _string += 'рублей';
      -      else if ("" == _slice_num && 0 == _last_num) _string += 'Ноль рублей';
      -      else _string += 'рубль';
      -      break;
      -
      -    case 1:
      -      let last = _num.length - 1;
      -      let _last_num = parseFloat(_num.charAt(last));
      -      let _slice_num = _num.slice(0, -1);
      -      let _pre_last_num = parseFloat(_slice_num);
      -      if (_last_num == 1 && 1 != _pre_last_num) _string += 'тысяча ';
      -      else if (_last_num == 1 && 1 == _pre_last_num.toString().length) _string += 'тысяча ';
      -      else if (_last_num > 1 && _last_num < 5 && 1 != _pre_last_num) _string += 'тысячи ';
      -      else if (parseFloat(_num) != 0) _string += 'тысяч ';
      -      _string = _string.replace('один ', 'одна ');
      -      _string = _string.replace('два ', 'две ');
      -      break;
      -
      -    case 2:
      -      let _last_num = parseFloat(_num.substr(-1));
      -      let last = _num.length - 1;
      -      let _last_num = parseFloat(_num.charAt(last));
      -      let _slice_num = _num.slice(0, -1);
      -      let _pre_last_num = parseFloat(_slice_num);
      -      if (_last_num == 1 && 1 != _pre_last_num) _string += 'миллион ';
      -      else if (_last_num == 1 && 1 == _pre_last_num.toString().length) _string += 'миллион ';
      -      else if (_last_num > 1 && _last_num < 5 && 1 != _pre_last_num) _string += 'миллиона ';
      -      else _string += 'миллионов ';
      -      break;
      -    case 3:
      -      let _last_num = parseFloat(_num.substr(-1));
      -      let last = _num.length - 1;
      -      let _last_num = parseFloat(_num.charAt(last));
      -      let _slice_num = _num.slice(0, -1);
      -      let _pre_last_num = parseFloat(_slice_num);
      -      if (_last_num == 1 && 1 != _pre_last_num) _string += 'миллиард ';
      -      else if (_last_num == 1 && 1 == _pre_last_num.toString().length) _string += 'миллиард ';
      -      else if (_last_num > 1 && _last_num < 5 && 1 != _pre_last_num) _string += 'миллиарда ';
      -      else _string += 'миллиардов ';
      -      break;
      -    }
      -    _string = _string.replace('  ', ' ');
      -    return _string;
      -  }
      -
      -  function decimals_parser(_num) {
      -    let _first_num = _num.substr(0, 1);
      -    let _second_num = parseFloat(_num.substr(1, 2));
      -    let _string = ' ' + _first_num + _second_num;
      -    if (_second_num == 1 && 1 != _first_num) _string += ' копейка';
      -    else if (_second_num > 1 && _second_num < 5 && 1 != _first_num) _string += ' копейки';
      -    else _string += ' копеек';
      -    return _string;
      -  }
      -
      -  if (!_number || _number == 0) return 'Ноль рублей';
      -  if (typeof _number !== 'number') {
      -    _number = _number.replace(',', '.');
      -    _number = parseFloat(_number);
      -    if (isNaN(_number)) return 'Ноль рублей';
      -  }
      -  _number = _number.toFixed(2);
      -  if (_number.indexOf('.') != -1) {
      -    let _number_arr = _number.split('.');
      -    let _number = _number_arr[0];
      -    let _number_decimals = _number_arr[1];
      -  }
      -  let _number_length = _number.length;
      -  let _string = '';
      -  let _num_parser = '';
      -  let _count = 0;
      -  for (let _p = (_number_length - 1); _p >= 0; _p--) {
      -    let _num_digit = _number.substr(_p, 1);
      -    _num_parser = _num_digit + _num_parser;
      -    if ((_num_parser.length == 3 || _p == 0) && !isNaN(parseFloat(_num_parser))) {
      -      _string = number_parser(_num_parser, _count) + _string;
      -      _num_parser = '';
      -      _count++;
      -    }
      -  }
      -  if (_number_decimals) _string += decimals_parser(_number_decimals);
      -  _string = _string.charAt(0).toUpperCase() + _string.substr(1).toLowerCase();
      -  return _string;
      -};
      -
      -// https://rsdn.ru/forum/src/2899783.flat
      -function ua(number) {
      -
      -  let mapNumbers = {
      -    0:   [2, 1, "нуль"],
      -    1:   [0, 2, "один", "одна"],
      -    2:   [1, 2, "два", "дві"],
      -    3:   [1, 1, "три"],
      -    4:   [1, 1, "чотири"],
      -    5:   [2, 1, "п'ять"],
      -    6:   [2, 1, "шість"],
      -    7:   [2, 1, "сім"],
      -    8:   [2, 1, "вісім"],
      -    9:   [2, 1, "дев'ять"],
      -    10:  [2, 1, "десять"],
      -    11:  [2, 1, "одинадцять"],
      -    12:  [2, 1, "дванадцять"],
      -    13:  [2, 1, "тринадцять"],
      -    14:  [2, 1, "чотирнадцять"],
      -    15:  [2, 1, "п'ятнадцять"],
      -    16:  [2, 1, "шістнадцять"],
      -    17:  [2, 1, "сімнадцять"],
      -    18:  [2, 1, "вісімнадцять"],
      -    19:  [2, 1, "дев'ятнадцять"],
      -    20:  [2, 1, "двадцять"],
      -    30:  [2, 1, "тридцять"],
      -    40:  [2, 1, "сорок"],
      -    50:  [2, 1, "п'ятдесят"],
      -    60:  [2, 1, "шістдесят"],
      -    70:  [2, 1, "сімдесят"],
      -    80:  [2, 1, "вісімдесят"],
      -    90:  [2, 1, "дев'яносто"],
      -    100: [2, 1, "сто"],
      -    200: [2, 1, "двісті"],
      -    300: [2, 1, "триста"],
      -    400: [2, 1, "чотириста"],
      -    500: [2, 1, "п'ятсот"],
      -    600: [2, 1, "шістсот"],
      -    700: [2, 1, "сімсот"],
      -    800: [2, 1, "вісімсот"],
      -    900: [2, 1, "дев'ятсот"]
      -  };
      -
      -  let mapOrders = [
      -    {_Gender: false, _arrStates: ["гривня", "гривні", "гривень"], _bAddZeroWord: true},
      -    {_Gender: false, _arrStates: ["тисяча", "тисячі", "тисяч"]},
      -    {_Gender: true, _arrStates: ["мільйон", "мільйона", "мільйонів"]},
      -    {_Gender: true, _arrStates: ["мільярд", "мільярда", "мільярдів"]},
      -    {_Gender: true, _arrStates: ["триліон", "триліона", "триліонів"]}
      -  ];
      -
      -  let objKop = {_Gender: false, _arrStates: ["копійка", "копійки", "копійок"]};
      -
      -  function Value(dVal, bGender) {
      -    let xVal = mapNumbers[dVal];
      -    if (xVal[1] == 1) {
      -      return xVal[2];
      -    } else {
      -      return xVal[2 + (bGender ? 0 : 1)];
      -    }
      -  }
      -
      -  function From0To999(fValue, oObjDesc, fnAddNum, fnAddDesc) {
      -    let nCurrState = 2;
      -    if (Math.floor(fValue / 100) > 0) {
      -      let fCurr = Math.floor(fValue / 100) * 100;
      -      fnAddNum(Value(fCurr, oObjDesc._Gender));
      -      nCurrState = mapNumbers[fCurr][0];
      -      fValue -= fCurr;
      -    }
      -
      -    if (fValue < 20) {
      -      if (Math.floor(fValue) > 0 || (oObjDesc._bAddZeroWord)) {
      -        fnAddNum(Value(fValue, oObjDesc._Gender));
      -        nCurrState = mapNumbers[fValue][0];
      -      }
      -    } else {
      -      let fCurr = Math.floor(fValue / 10) * 10;
      -      fnAddNum(Value(fCurr, oObjDesc._Gender));
      -      nCurrState = mapNumbers[fCurr][0];
      -      fValue -= fCurr;
      -
      -      if (Math.floor(fValue) > 0) {
      -        fnAddNum(Value(fValue, oObjDesc._Gender));
      -        nCurrState = mapNumbers[fValue][0];
      -      }
      -    }
      -
      -    fnAddDesc(oObjDesc._arrStates[nCurrState]);
      -  }
      -
      -  function FloatToSamplesInWordsUkr(fAmount) {
      -    let fInt = Math.floor(fAmount + 0.005);
      -    let fDec = Math.floor(((fAmount - fInt) * 100) + 0.5);
      -
      -    let arrRet = [];
      -    let iOrder = 0;
      -    let arrSouthands = [];
      -    for (; fInt > 0.9999; fInt /= 1000) {
      -      arrSouthands.push(Math.floor(fInt % 1000));
      -    }
      -    if (arrSouthands.length == 0) {
      -      arrSouthands.push(0);
      -    }
      -
      -    function PushToRes(strVal) {
      -      arrRet.push(strVal);
      -    }
      -
      -    for (let iSouth = arrSouthands.length - 1; iSouth >= 0; --iSouth) {
      -      From0To999(arrSouthands[iSouth], mapOrders[iSouth], PushToRes, PushToRes);
      -    }
      -
      -    if (arrRet.length > 0) {
      -      // Capitalize first letter
      -      arrRet[0] = arrRet[0].match(/^(.)/)[1].toLocaleUpperCase() + arrRet[0].match(/^.(.*)$/)[1];
      -    }
      -
      -    arrRet.push((fDec < 10) ? ("0" + fDec) : ("" + fDec));
      -    From0To999(fDec, objKop, function() {
      -    }, PushToRes);
      -
      -    return arrRet.join(" ");
      -  }
      -
      -  return FloatToSamplesInWordsUkr(number);
      -}
      diff --git a/modules/textUtil/stripIndents.js b/modules/textUtil/stripIndents.js
      deleted file mode 100755
      index 104eb10..0000000
      --- a/modules/textUtil/stripIndents.js
      +++ /dev/null
      @@ -1,53 +0,0 @@
      -
      -function stripFirstEmptyLines(text) {
      -  return text.replace(/^\n+/, ''); // no 'm' flag!
      -}
      -// strip first empty lines
      -function rtrim(text) {
      -  return text.replace(/\s+$/, '');  // no 'm' flag!
      -}
      -
      -function rtrimLines(text) {
      -  return text.replace(/[ \t]+$/gim, '');
      -}
      -
      -function stripSpaceIndent(text) {
      -
      -  if (!text) return text;
      -
      -  let stripPattern = /^ *(?=\S+)/gm;
      -
      -  let indentLen = text.match(stripPattern)
      -    .reduce(function (min, line) {
      -      return Math.min(min, line.length);
      -    }, Infinity);
      -
      -  let indent = new RegExp('^ {' + indentLen + '}', 'gm');
      -  return indentLen > 0 ? text.replace(indent, '') : text;
      -}
      -
      -function stripTabIndent(text) {
      -  if (!text) return text;
      -
      -  let stripPattern = /^\t*(?=\S+)/gm;
      -
      -  let indentLen = text.match(stripPattern)
      -    .reduce(function (min, line) {
      -      return Math.min(min, line.length);
      -    }, Infinity);
      -
      -  let indent = new RegExp('^\t{' + indentLen + '}', 'gm');
      -  return indentLen > 0 ? text.replace(indent, '') : text;
      -}
      -
      -// same as Ruby strip_heredoc + rtrim every line + strip first lines and rtrim
      -module.exports = function(text) {
      -  text = rtrim(text);
      -  text = rtrimLines(text);
      -  text = stripFirstEmptyLines(text);
      -
      -  text = stripSpaceIndent(text);
      -  text = stripTabIndent(text);
      -
      -  return text;
      -};
      diff --git a/modules/textUtil/stripTags.js b/modules/textUtil/stripTags.js
      deleted file mode 100755
      index db77a37..0000000
      --- a/modules/textUtil/stripTags.js
      +++ /dev/null
      @@ -1,3 +0,0 @@
      -module.exports = function(text) {
      -  return text.replace(/<\/?[a-z].*?>/gim, '');
      -};
      diff --git a/modules/textUtil/transliterate.js b/modules/textUtil/transliterate.js
      deleted file mode 100755
      index 678530b..0000000
      --- a/modules/textUtil/transliterate.js
      +++ /dev/null
      @@ -1,66 +0,0 @@
      -// Transliteration ported from https://github.com/yaroslav/russian/blob/master/lib/russian/transliteration.rb
      -
      -let LOWER_SINGLE = {
      -  "і": "i", "ґ": "g", "ё": "yo", "№": "#", "є": "e",
      -  "ї": "yi", "а": "a", "б": "b",
      -  "в": "v", "г": "g", "д": "d", "е": "e", "ж": "zh",
      -  "з": "z", "и": "i", "й": "y", "к": "k", "л": "l",
      -  "м": "m", "н": "n", "о": "o", "п": "p", "р": "r",
      -  "с": "s", "т": "t", "у": "u", "ф": "f", "х": "h",
      -  "ц": "ts", "ч": "ch", "ш": "sh", "щ": "sch", "ъ": "",
      -  "ы": "y", "ь": "", "э": "e", "ю": "yu", "я": "ya"
      -};
      -
      -let LOWER_MULTI = {
      -  "ье": "ie",
      -  "ьё": "ie"
      -};
      -
      -let UPPER_SINGLE = {
      -  "Ґ": "G", "Ё": "YO", "Є": "E", "Ї": "YI", "І": "I",
      -  "А": "A", "Б": "B", "В": "V", "Г": "G",
      -  "Д": "D", "Е": "E", "Ж": "ZH", "З": "Z", "И": "I",
      -  "Й": "Y", "К": "K", "Л": "L", "М": "M", "Н": "N",
      -  "О": "O", "П": "P", "Р": "R", "С": "S", "Т": "T",
      -  "У": "U", "Ф": "F", "Х": "H", "Ц": "TS", "Ч": "CH",
      -  "Ш": "SH", "Щ": "SCH", "Ъ": "", "Ы": "Y", "Ь": "",
      -  "Э": "E", "Ю": "YU", "Я": "YA"
      -};
      -let UPPER_MULTI = {
      -  "ЬЕ": "IE",
      -  "ЬЁ": "IE"
      -};
      -
      -let LOWER = Object.assign({}, LOWER_SINGLE, LOWER_MULTI);
      -
      -let UPPER = Object.assign({}, UPPER_SINGLE, UPPER_MULTI);
      -
      -let MULTI_KEYS = Object.keys(Object.assign({}, LOWER_MULTI, UPPER_MULTI)).sort(function(a, b) {
      -  return a.length > b.length;
      -});
      -
      -
      -// Transliterate a string with russian/ukrainian characters
      -function transliterate(str) {
      -  let reg = new RegExp(MULTI_KEYS.join('|') + '|\\w|.', 'g');
      -
      -  let result = "";
      -  let chars = str.match(reg);
      -  for (let i = 0; i < chars.length; i++) {
      -    if (chars[i] in UPPER && chars[i + 1] in LOWER) {
      -      // combined case
      -      let r = UPPER[chars[i]].toLowerCase();
      -      result += r[0].toUpperCase() + r.slice(1);
      -    } else if (chars[i] in UPPER) {
      -      result += UPPER[chars[i]];
      -    } else if (chars[i] in LOWER) {
      -      result += LOWER[chars[i]];
      -    } else {
      -      result += chars[i];
      -    }
      -  }
      -
      -  return result;
      -}
      -
      -module.exports = transliterate;
      diff --git a/modules/textUtil/ucWordStart.js b/modules/textUtil/ucWordStart.js
      deleted file mode 100755
      index f49ca83..0000000
      --- a/modules/textUtil/ucWordStart.js
      +++ /dev/null
      @@ -1,9 +0,0 @@
      -'use strict';
      -
      -module.exports = function ucWordStart(cityOrCountry) {
      -  if (!cityOrCountry) return cityOrCountry;
      -
      -  return cityOrCountry.trim().toLowerCase().replace(/(^| |-)./g, function(match) {
      -    return match.toUpperCase();
      -  });
      -};
      diff --git a/package.json b/package.json
      index 3f2d413..19a51b0 100755
      --- a/package.json
      +++ b/package.json
      @@ -3,12 +3,12 @@
         "version": "1.0.0",
         "private": true,
         "scripts": {
      -    "prod": "NODE_ENV=production NODE_PATH=./handlers:./modules node ./bin/server.js",
      +    "prod": "NODE_ENV=production NODE_PATH=./modules node ./bin/server.js",
           "fixperms": "sudo chown -R `id -u` .* * ~/.n*",
           "//": "test must exit with status 1 if fails, don't use | or ensure the right exit code if you do",
      -    "test": "SELENIUM_LOCAL=1 NODE_ENV=test NODE_PATH=./handlers:./modules ./node_modules/.bin/gulp test",
      -    "build": "NODE_PATH=./handlers:./modules ./node_modules/.bin/gulp build",
      -    "gulp": "NODE_PATH=./handlers:./modules ./node_modules/.bin/gulp"
      +    "test": "SELENIUM_LOCAL=1 NODE_ENV=test NODE_PATH=./modules ./node_modules/.bin/gulp test",
      +    "build": "NODE_PATH=./modules ./node_modules/.bin/gulp build",
      +    "gulp": "NODE_PATH=./modules ./node_modules/.bin/gulp"
         },
         "precommit": "NODE_ENV=development node `which gulp` pre-commit",
         "//": "node-xmpp-client installs for linux only",
      @@ -20,7 +20,7 @@
           "babel-plugin-transform-runtime": "*",
           "babel-preset-env": "*",
           "babelfish": "^1.1.2",
      -    "bemto.pug": "github:kizu/bemto",
      +    "bemto.pug": "iliakan/bemto",
           "bunyan": "*",
           "chokidar": "^2.0.4",
           "clarify": "^2.1.0",
      @@ -72,7 +72,7 @@
           "style-loader": "^0",
           "stylus-loader": "^3",
           "trace": "^3.1.0",
      -    "uglify-js": "^3",
      +    "uglify-es": "^3",
           "uglifyjs-webpack-plugin": "^1",
           "webpack": "^4",
           "yaml-loader": "^0.5.0"
      diff --git a/replace.php b/replace.php
      deleted file mode 100644
      index 6149fde..0000000
      --- a/replace.php
      +++ /dev/null
      @@ -1,29 +0,0 @@
      - code.language-markup
      -        color inherit
      -        position relative
      -
      -    > code[class*="language-"]
      -        background none
      -        padding 0
      -
      -pre.line-numbers
      -    padding-left 3.2em
      -
      -// span with line numbers is moved from  to the outer 
      ,
      -// because we need to handle many ... inside single 
      -// (this we need for highlighting *!*...* /!* inline
      -.line-numbers .line-numbers-rows
      -    left 0
      -    top 0
      -    padding 1em 0
      -    border 0
      -    background #e7e5e3
      -    width auto
      -
      -.line-numbers .line-numbers-rows:after
      -    width auto
      -    display block
      -    visibility hidden
      -    margin-top -1.2em // fitted value
      -    content: '222' // stretch line up to 3 digits
      -
      -.line-numbers-rows > span:before,
      -.line-numbers .line-numbers-rows:after
      -    padding 0 .7em 0 .8em
      -    background #e7e5e3 // #146 https://github.com/iliakan/javascript-nodejs/issues/146#issuecomment-72321753
      -    text-shadow none
      -
      -@media (min-width: largescreen)
      -    pre[class*="language-"],
      -    code[class*="language-"]
      -        font-size font_size_m
      -        line-height 19px
      diff --git a/styles/blocks/prism/prism-line-highlight.styl b/styles/blocks/prism/prism-line-highlight.styl
      deleted file mode 100755
      index 2c55b05..0000000
      --- a/styles/blocks/prism/prism-line-highlight.styl
      +++ /dev/null
      @@ -1,39 +0,0 @@
      -.main /* Used this parent class to be able to overwrite some generic rules from main.styl */
      -
      -    .inline-highlight
      -        position absolute
      -        pointer-events none
      -        line-height inherit
      -        white-space pre
      -        left 0
      -        top -2px
      -        z-index -1
      -        padding 0
      -
      -        .mask
      -            padding 0
      -            background #F5E7C6
      -            outline 2px solid #F5E7C6
      -
      -    .block-highlight
      -        display block
      -        position absolute
      -        left 0
      -        right 0
      -        top -1px
      -        margin-top 1em /* Same as .prism’s padding-top */
      -        padding 0
      -
      -        pointer-events none
      -
      -        line-height inherit
      -        white-space pre
      -
      -        .mask
      -            display block
      -            background #F5E7C6
      -            outline 1px solid #F5E7C6
      -            left 0
      -            right 0
      -            position absolute
      -            padding 0
      diff --git a/styles/blocks/prism/prism-line-numbers.styl b/styles/blocks/prism/prism-line-numbers.styl
      deleted file mode 100755
      index e437394..0000000
      --- a/styles/blocks/prism/prism-line-numbers.styl
      +++ /dev/null
      @@ -1,27 +0,0 @@
      -pre.line-numbers
      -    position relative
      -    padding-left 3.8em
      -    counter-reset linenumber
      -
      -.line-numbers .line-numbers-rows
      -    position absolute
      -    pointer-events none
      -    top 0
      -    font-size 100%
      -    left -3.8em
      -    width 3em /* works for line-numbers below 1000 lines */
      -    letter-spacing -1px
      -    border-right 1px solid light_gray_color
      -    user-select none
      -
      -.line-numbers-rows > span
      -    pointer-events none
      -    display block
      -    counter-increment linenumber
      -
      -.line-numbers-rows > span:before
      -    content counter(linenumber)
      -    color light_gray_color
      -    display block
      -    padding-right 0.8em
      -    text-align right
      diff --git a/tasks/server.js b/tasks/server.js
      index 2b23eaf..184e7d0 100755
      --- a/tasks/server.js
      +++ b/tasks/server.js
      @@ -1,4 +1,4 @@
      -let app = require('app');
      +let app = require('jsengine/koa/app');
       let config = require('config');
       
       module.exports = function() {
      diff --git a/templates/blocks/head.pug b/templates/blocks/head.pug
      index 90c5150..d05f54f 100755
      --- a/templates/blocks/head.pug
      +++ b/templates/blocks/head.pug
      @@ -17,7 +17,7 @@ meta(name="msapplication-TileImage" content="/img/favicon/tileicon.png")
       // favicon the rest
       link(rel="icon" href="/img/favicon/favicon.png")
       
      -meta(itemprop="image", content=(schema.titleImage || urlBase.main + "/img/logo_square.png"))
      +meta(itemprop="image", content=(schema.titleImage || urlBase.main.href + "/img/logo_square.png"))
       
       !=css("styles")
       
      diff --git a/templates/notification.pug b/templates/notification.pug
      deleted file mode 100755
      index d92663a..0000000
      --- a/templates/notification.pug
      +++ /dev/null
      @@ -1,10 +0,0 @@
      -extends /layouts/main
      -
      -block append variables
      -  - var layout_header_class = "main__header_center"
      -  - var sitetoolbar = true
      -  - var layout_main_class = "main_width-limit"
      -
      -block content
      -  +b(class=["notification", "_message", "_" + message.type])
      -    +e.content!= message.html
      
      From 3c924869151aac4bea2f65c839f7ef1ee6370a6a Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 5 Sep 2018 10:50:40 +0300
      Subject: [PATCH 036/218] jsengine to symlinks optionally
      
      ---
       .gitmodules             |  3 ---
       dev                     |  2 +-
       edit                    | 11 +++++++----
       edit.cmd                |  3 ++-
       modules/config/index.js | 12 +++++++++++-
       modules/jsengine        |  1 -
       6 files changed, 21 insertions(+), 11 deletions(-)
       delete mode 100644 .gitmodules
       delete mode 160000 modules/jsengine
      
      diff --git a/.gitmodules b/.gitmodules
      deleted file mode 100644
      index 29011c8..0000000
      --- a/.gitmodules
      +++ /dev/null
      @@ -1,3 +0,0 @@
      -[submodule "modules/jsengine"]
      -	path = modules/jsengine
      -	url = https://github.com/iliakan/jsengine
      diff --git a/dev b/dev
      index 38d3ee3..62a5092 100755
      --- a/dev
      +++ b/dev
      @@ -1,6 +1,6 @@
       #!/bin/bash
       
      -: ${1?"Usage: $0 "}
      +: ${1?"Usage: $0 "}
       
       set -e
       
      diff --git a/edit b/edit
      index 42ca73e..1644f4f 100755
      --- a/edit
      +++ b/edit
      @@ -1,10 +1,13 @@
      -#!/bin/bash
      +#!/usr/bin/env bash
       
      -: ${1?"Usage: $0 "}
      +# ./edit ru <- tutorial language: RU, server: EN
      +# ./edit ru ru <- tutorial language: RU, server: RU
       
      -BRANCH="${1:-$NODE_LANG}"
      +: ${1?"Usage: $0  []"}
       
      -export NODE_LANG=$BRANCH
      +
      +export TUTORIAL_ROOT="../javascript-tutorial-$1"
      +export NODE_LANG="${2:-en}"
       export NODE_ENV=production
       export TUTORIAL_EDIT=1
       export NODE_PRESERVE_SYMLINKS=1
      diff --git a/edit.cmd b/edit.cmd
      index b54ef86..012a5dc 100644
      --- a/edit.cmd
      +++ b/edit.cmd
      @@ -1,5 +1,6 @@
       @if "%~1"=="" goto usage
       
      +export TUTORIAL_ROOT="../javascript-tutorial-%1"
       @set NODE_LANG=%1
       @set NODE_ENV=production
       @set TUTORIAL_EDIT=1
      @@ -14,4 +15,4 @@ goto :eof
       
       :usage
       echo Usage: %0 
      -exit /B 1
      \ No newline at end of file
      +exit /B 1
      diff --git a/modules/config/index.js b/modules/config/index.js
      index f9c9b9f..1a77cca 100755
      --- a/modules/config/index.js
      +++ b/modules/config/index.js
      @@ -60,11 +60,21 @@ let config = module.exports = {
         tmpRoot:               path.join(process.cwd(), 'tmp'),
         // js/css build versions
         cacheRoot:          path.join(process.cwd(), 'cache'),
      -  tutorialGithubBaseUrl: 'https://github.com/iliakan/javascript-tutorial-' + lang + '/tree/master',
       
         handlers: require('./handlers')
       };
       
      +let repos = require('jsengine/koa/tutorial/repos');
      +for(let repo in repos) {
      +  if (repos[repo].lang === lang) {
      +    config.tutorialRepo = {
      +      github: repo,
      +      branch: repos[repo].branch || 'master',
      +      url: new URL('https://github.com/' + repo + '/tree/' + (repos[repo].branch || 'master'))
      +    }
      +  }
      +}
      +
       require.extensions['.yml'] = function(module, filename) {
         module.exports = yaml.safeLoad(fs.readFileSync(filename, 'utf-8'));
       };
      diff --git a/modules/jsengine b/modules/jsengine
      deleted file mode 160000
      index 34aeab9..0000000
      --- a/modules/jsengine
      +++ /dev/null
      @@ -1 +0,0 @@
      -Subproject commit 34aeab95f13fc5345adc7a392841a19dd277c2bd
      
      From 76d34071f6851adb2bb37d067f2836a3350f362f Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 5 Sep 2018 10:51:35 +0300
      Subject: [PATCH 037/218] jsengine to symlinks optionally
      
      ---
       .gitignore | 1 +
       1 file changed, 1 insertion(+)
      
      diff --git a/.gitignore b/.gitignore
      index 00f373b..10f3f4e 100755
      --- a/.gitignore
      +++ b/.gitignore
      @@ -39,3 +39,4 @@ out/*
       public/*
       package-lock.json
       
      +/modules/jsengine
      
      From 2a00a2a6ef3295a6fe26c7c18a153fa1190d9ef6 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 5 Sep 2018 11:08:27 +0300
      Subject: [PATCH 038/218] WIP
      
      ---
       edit.cmd | 12 +++++++++---
       1 file changed, 9 insertions(+), 3 deletions(-)
      
      diff --git a/edit.cmd b/edit.cmd
      index 012a5dc..52839ff 100644
      --- a/edit.cmd
      +++ b/edit.cmd
      @@ -1,7 +1,13 @@
       @if "%~1"=="" goto usage
       
      -export TUTORIAL_ROOT="../javascript-tutorial-%1"
      -@set NODE_LANG=%1
      +@set TUTORIAL_ROOT="../javascript-tutorial-%1"
      +
      +@if "%~2"=="" (
      +    @set NODE_LANG=en
      +) else (
      +    @set NODE_LANG=%2
      +)
      +
       @set NODE_ENV=production
       @set TUTORIAL_EDIT=1
       @set ASSET_VERSIONING=query
      @@ -14,5 +20,5 @@ call gulp edit
       goto :eof
       
       :usage
      -echo Usage: %0 
      +echo Usage: %0  []
       exit /B 1
      
      From 18bc8222992500769679fbcd8e9cfff03804d8a5 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 5 Sep 2018 13:44:52 +0300
      Subject: [PATCH 039/218] WIP
      
      ---
       dev.cmd                   |  3 +--
       edit.cmd                  | 13 ++++++-------
       modules/config/webpack.js |  4 ----
       package.json              |  2 +-
       4 files changed, 8 insertions(+), 14 deletions(-)
      
      diff --git a/dev.cmd b/dev.cmd
      index cb7a15a..0b142f4 100644
      --- a/dev.cmd
      +++ b/dev.cmd
      @@ -3,11 +3,10 @@
       set NODE_LANG=%1
       @set NODE_ENV=development
       @set ASSET_VERSIONING=query
      -@set WATCH=1
       @set NODE_PRESERVE_SYMLINKS=1
       @set NODE_PATH=./modules
       
      -call gulp dev
      +call gulp dev | bunyan
       
       goto :eof
       
      diff --git a/edit.cmd b/edit.cmd
      index 52839ff..7d802c6 100644
      --- a/edit.cmd
      +++ b/edit.cmd
      @@ -1,24 +1,23 @@
       @if "%~1"=="" goto usage
       
      -@set TUTORIAL_ROOT="../javascript-tutorial-%1"
      +@set TUTORIAL_ROOT=../javascript-tutorial-%1
       
       @if "%~2"=="" (
      -    @set NODE_LANG=en
      +    set NODE_LANG=en
       ) else (
      -    @set NODE_LANG=%2
      +    set NODE_LANG=%2
       )
       
       @set NODE_ENV=production
       @set TUTORIAL_EDIT=1
       @set ASSET_VERSIONING=query
      -@set WATCH=1
       @set NODE_PRESERVE_SYMLINKS=1
       @set NODE_PATH=./modules
       
      -call gulp edit
      +call gulp edit | bunyan
       
       goto :eof
       
       :usage
      -echo Usage: %0  []
      -exit /B 1
      +@echo "Usage: %0  []"
      +@exit /B 1
      diff --git a/modules/config/webpack.js b/modules/config/webpack.js
      index 99bc336..fd03aa8 100755
      --- a/modules/config/webpack.js
      +++ b/modules/config/webpack.js
      @@ -133,10 +133,6 @@ module.exports = function (config) {
       
           module: {
             rules:   [
      -        {
      -          test: /\.json$/,
      -          use:  'json-loader'
      -        },
               {
                 test: /\.yml$/,
                 use:  ['json-loader', 'yaml-loader']
      diff --git a/package.json b/package.json
      index 19a51b0..6d86ee9 100755
      --- a/package.json
      +++ b/package.json
      @@ -74,7 +74,7 @@
           "trace": "^3.1.0",
           "uglify-es": "^3",
           "uglifyjs-webpack-plugin": "^1",
      -    "webpack": "^4",
      +    "webpack": ">=4.17.2",
           "yaml-loader": "^0.5.0"
         },
         "engineStrict": true,
      
      From 38e87242606d73830596766d5193c39063362302 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 5 Sep 2018 18:52:07 +0300
      Subject: [PATCH 040/218] WIP
      
      ---
       .gitignore                  |  1 +
       gulpfile.js                 | 19 +++++++---------
       modules/config/index.js     |  2 +-
       modules/config/webpack.js   | 36 ++++++++++++++++--------------
       package.json                |  4 +---
       tasks/livereload.js         |  3 +--
       tasks/minify.js             | 44 -------------------------------------
       tasks/nodemon.js            | 27 +++++++++++++++++++++--
       tasks/resizeRetinaImages.js | 40 ---------------------------------
       tasks/syncResources.js      |  1 -
       tasks/webpack.js            | 14 +++++-------
       11 files changed, 61 insertions(+), 130 deletions(-)
       mode change 100755 => 100644 tasks/livereload.js
       delete mode 100755 tasks/minify.js
       delete mode 100755 tasks/resizeRetinaImages.js
      
      diff --git a/.gitignore b/.gitignore
      index 10f3f4e..4daeacd 100755
      --- a/.gitignore
      +++ b/.gitignore
      @@ -40,3 +40,4 @@ public/*
       package-lock.json
       
       /modules/jsengine
      +.gitmodules
      diff --git a/gulpfile.js b/gulpfile.js
      index c74a2d5..de28af1 100755
      --- a/gulpfile.js
      +++ b/gulpfile.js
      @@ -30,7 +30,7 @@ gulp.task("nodemon", lazyRequireTask('./tasks/nodemon', {
         watch:  ["modules"]
       }));
       
      -gulp.task("client:livereload", lazyRequireTask("./tasks/livereload", {
      +gulp.task("livereload", lazyRequireTask("./tasks/livereload", {
         // watch files *.*, not directories, no need to reload for new/removed files,
         // we're only interested in changes
       
      @@ -65,7 +65,7 @@ gulp.task('watch', lazyRequireTask('./tasks/watch', {
         taskMapping: [
           {
             watch: 'assets/**',
      -      task:  'client:sync-resources'
      +      task:  'sync-resources'
           }
         ]
       }));
      @@ -74,29 +74,26 @@ gulp.task('deploy', function(callback) {
         runSequence("deploy:build", "deploy:update", callback);
       });
       
      -gulp.task("client:sync-resources", lazyRequireTask('./tasks/syncResources', {
      +gulp.task("sync-resources", lazyRequireTask('./tasks/syncResources', {
         assets: 'public'
       }));
       
       
      -gulp.task('client:minify', lazyRequireTask('./tasks/minify'));
      -gulp.task('client:resize-retina-images', lazyRequireTask('./tasks/resizeRetinaImages'));
      -
      -gulp.task('client:webpack', lazyRequireTask('./tasks/webpack'));
      -// gulp.task('client:webpack-dev-server', lazyRequireTask('./tasks/webpackDevServer'));
      +gulp.task('webpack', lazyRequireTask('./tasks/webpack'));
      +// gulp.task('webpack-dev-server', lazyRequireTask('./tasks/webpackDevServer'));
       
       
       gulp.task('build', function(callback) {
      -  runSequence("client:sync-resources", 'client:webpack', callback);
      +  runSequence("sync-resources", 'webpack', callback);
       });
       
       gulp.task('server', lazyRequireTask('./tasks/server'));
       
      -gulp.task('edit', ['client:webpack', 'jsengine:koa:tutorial:importWatch', "client:sync-resources", 'client:livereload', 'server']);
      +gulp.task('edit', ['webpack', 'jsengine:koa:tutorial:importWatch', "sync-resources", 'livereload', 'server']);
       
       
       gulp.task('dev', function(callback) {
      -  runSequence("client:sync-resources", ['nodemon', 'client:livereload', 'client:webpack', 'watch'], callback);
      +  runSequence("sync-resources", ['nodemon', 'livereload', 'webpack', 'watch'], callback);
       });
       
       gulp.on('err', function(gulpErr) {
      diff --git a/modules/config/index.js b/modules/config/index.js
      index 1a77cca..2fa3f2d 100755
      --- a/modules/config/index.js
      +++ b/modules/config/index.js
      @@ -86,5 +86,5 @@ t.requireHandlerLocales();
       
       // webpack config uses general config
       // we have a loop dep here
      -config.webpack = require('./webpack')(config);
      +config.webpack = require('./webpack');
       
      diff --git a/modules/config/webpack.js b/modules/config/webpack.js
      index fd03aa8..1e65d2f 100755
      --- a/modules/config/webpack.js
      +++ b/modules/config/webpack.js
      @@ -1,29 +1,31 @@
       let fs = require('fs');
      -let nib = require('nib');
      -let rupture = require('rupture');
      -let path = require('path');
      -let chokidar = require('chokidar');
       let config = require('config');
      -let webpack = require('webpack');
      -let WriteVersionsPlugin = require('jsengine/webpack/writeVersionsPlugin');
      -let CssWatchRebuildPlugin = require('jsengine/webpack/cssWatchRebuildPlugin');
      -const CopyWebpackPlugin = require('copy-webpack-plugin')
      -const MiniCssExtractPlugin = require("mini-css-extract-plugin");
      -const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
      -const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
      -const fse = require('fs-extra');
      -const glob = require('glob');
      +let path = require('path');
       
       // 3rd party / slow to build modules
       // no webpack dependencies inside
       // no es6 (for 6to5 processing) inside
       // NB: includes angular-*
      -let noProcessModulesRegExp = new RegExp("node_modules" + (path.sep === '/' ? path.sep : '\\\\')  + "(angular|prismjs|sanitize-html|i18n-iso-countries)");
      +let noProcessModulesRegExp = new RegExp("node_modules" + (path.sep === '/' ? path.sep : '\\\\') + "(angular|prismjs|sanitize-html|i18n-iso-countries)");
       
       let devMode = process.env.NODE_ENV == 'development';
       
       
      -module.exports = function (config) {
      +module.exports = function () {
      +
      +  let nib = require('nib');
      +  let rupture = require('rupture');
      +  let chokidar = require('chokidar');
      +  let webpack = require('webpack');
      +  let WriteVersionsPlugin = require('jsengine/webpack/writeVersionsPlugin');
      +  let CssWatchRebuildPlugin = require('jsengine/webpack/cssWatchRebuildPlugin');
      +  const CopyWebpackPlugin = require('copy-webpack-plugin')
      +  const MiniCssExtractPlugin = require("mini-css-extract-plugin");
      +  const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
      +  const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
      +  const fse = require('fs-extra');
      +
      +
       // tutorial.js?hash
       // tutorial.hash.js
         function extHash(name, ext, hash) {
      @@ -223,7 +225,7 @@ module.exports = function (config) {
             extensions: ['.js', '.styl'],
             alias:      {
               'entities/maps/entities.json': 'jsengine/markit/emptyEntities',
      -        config: 'client/config'
      +        config:                        'client/config'
             },
             modules:    modulesDirectories
           },
      @@ -271,7 +273,7 @@ module.exports = function (config) {
             new webpack.IgnorePlugin({
               checkResource: (arg) => arg.endsWith('.yml') && arg !== './' + config.lang + '.yml',
               // under dirs like: ../locales/..
      -        checkContext: arg => /\/locales(\/|$)/.test(arg)
      +        checkContext:  arg => /\/locales(\/|$)/.test(arg)
             }),
       
             new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')),
      diff --git a/package.json b/package.json
      index 6d86ee9..1a7b898 100755
      --- a/package.json
      +++ b/package.json
      @@ -32,9 +32,6 @@
           "gulp": "^3",
           "gulp-livereload": "*",
           "gulp-ll": "*",
      -    "gulp-load-plugins": "*",
      -    "gulp-nodemon": "*",
      -    "gulp-util": "*",
           "image-size": "*",
           "js-yaml": "*",
           "json-loader": "^0.5.7",
      @@ -58,6 +55,7 @@
           "node-notifier": "*",
           "node-uuid": "*",
           "node-zip": "*",
      +    "nodemon": "^1.18.4",
           "optimize-css-assets-webpack-plugin": "^4.0.3",
           "path-to-regexp": "*",
           "postcss-loader": "*",
      diff --git a/tasks/livereload.js b/tasks/livereload.js
      old mode 100755
      new mode 100644
      index cb0b59f..ff06dad
      --- a/tasks/livereload.js
      +++ b/tasks/livereload.js
      @@ -1,6 +1,5 @@
       let livereload = require('gulp-livereload');
       let gulp = require('gulp');
      -let gutil = require('gulp-util');
       let throttle = require('lodash/throttle');
       let chokidar = require('chokidar');
       
      @@ -18,7 +17,7 @@ module.exports = function(options) {
           //livereload.changedVerySoon = _.throttle(livereload.changed, 100, {leading: false});
       
           setTimeout(function() {
      -      gutil.log("livereload: listen on change " + options.watch);
      +      console.log("livereload: listen on change " + options.watch);
       
             chokidar.watch(options.watch, {
               awaitWriteFinish: {
      diff --git a/tasks/minify.js b/tasks/minify.js
      deleted file mode 100755
      index 2923efe..0000000
      --- a/tasks/minify.js
      +++ /dev/null
      @@ -1,44 +0,0 @@
      -const pngquant = require('imagemin-pngquant');
      -const path = require('path');
      -const gulp = require('gulp');
      -const gp = require('gulp-load-plugins')();
      -const gutil = require('gulp-util');
      -const fs = require('fs');
      -
      -/**
      - *
      - * @param options
      - *  options.root => the root to import from
      - * @returns {Function}
      - */
      -module.exports = function(options) {
      -  options = options || {};
      -
      -  const root = options.root || require('yargs').argv.root;
      -
      -  if (!root) {
      -    throw new Error("Root not set");
      -  }
      -
      -  return function(callback) {
      -
      -    gutil.log("minify " + root);
      -
      -    // When enable: CHECK demo.svg (!!!)
      -    // it has JS inside!!! svgo breaks it by removing t-template-path
      -    return gulp.src(root + '/**/*.{png,jpg,gif}')
      -    //return gulp.src(root + '/**/*.{svg,png,jpg,gif}')
      -      .pipe(gp.debug())
      -
      -      .pipe(gp.imagemin({
      -        verbose: true,
      -        progressive: true,
      -      //  svgoPlugins: [{removeViewBox: false}],
      -        use:         [pngquant()]
      -      }))
      -      .pipe(gulp.dest(root));
      -  };
      -
      -
      -};
      -
      diff --git a/tasks/nodemon.js b/tasks/nodemon.js
      index b921d22..ffb8c39 100755
      --- a/tasks/nodemon.js
      +++ b/tasks/nodemon.js
      @@ -1,9 +1,32 @@
      -const gp = require('gulp-load-plugins')();
      +const nodemon = require('nodemon');
       
       module.exports = function(options) {
       
         return function(callback) {
      -    gp.nodemon(options);
      +
      +    nodemon({
      +      // shared client/server code has require('template.pug) which precompiles template on run
      +      // so I have to restart server to pickup the template change
      +      ext:      "js",
      +      verbose:  true,
      +      delay: 10,
      +      nodeArgs: process.env.NODE_DEBUG ? ['--inspect'] : [],
      +      script:   "./bin/server.js",
      +      //ignoreRoot: ['.git', 'node_modules'].concat(glob.sync('{handlers,modules}/**/client')), // ignore handlers' client code
      +      ignore:   ['**/client/', '**/photoCut/', 'public'], // ignore handlers' client code
      +      watch:    ["modules"],
      +      watchOptions: {
      +        awaitWriteFinish: {
      +          stabilityThreshold: 300,
      +          pollInterval: 100
      +        }
      +      }
      +    })
      +      .on('log', function (log){
      +        console.log(log.colour);
      +      });
      +
         };
       
      +
       };
      diff --git a/tasks/resizeRetinaImages.js b/tasks/resizeRetinaImages.js
      deleted file mode 100755
      index 17a4eb1..0000000
      --- a/tasks/resizeRetinaImages.js
      +++ /dev/null
      @@ -1,40 +0,0 @@
      -const es = require('event-stream');
      -const path = require('path');
      -const gulp = require('gulp');
      -const gp = require('gulp-load-plugins')();
      -const gutil = require('gulp-util');
      -const fs = require('fs');
      -const gm = require('gm');
      -
      -/**
      - * Resize all @2x. images to normal resolution
      - * @param options
      - *  options.root => the root to resize from
      - * @returns {Function}
      - */
      -module.exports = function(options) {
      -  options = options || {};
      -
      -  const root = options.root || require('yargs').argv.root;
      -
      -  if (!root) {
      -    throw new Error("Root not set");
      -  }
      -
      -  return function(callback) {
      -
      -    gutil.log("resize retina images " + root);
      -
      -    return gulp.src(root + '/**/*@2x.{png,jpg,gif}')
      -      .pipe(gp.debug())
      -
      -      .pipe(es.map(function(file, cb) {
      -        let normalResolutionPath = file.path.replace(/@2x(?=\.[^.]+$)/, '');
      -        gutil.log(file.path + ' -> ' + normalResolutionPath);
      -        gm(file.path).resize("50%").write(normalResolutionPath, cb);
      -      }));
      -  };
      -
      -
      -};
      -
      diff --git a/tasks/syncResources.js b/tasks/syncResources.js
      index 9292a62..ee69009 100755
      --- a/tasks/syncResources.js
      +++ b/tasks/syncResources.js
      @@ -1,6 +1,5 @@
       const fs = require('fs');
       const fse = require('fs-extra');
      -const gp = require('gulp-load-plugins')();
       const glob = require('glob');
       
       module.exports = function(resources) {
      diff --git a/tasks/webpack.js b/tasks/webpack.js
      index 73583a4..8fc3476 100755
      --- a/tasks/webpack.js
      +++ b/tasks/webpack.js
      @@ -1,15 +1,13 @@
       let webpack = require('webpack');
      -let gp = require('gulp-load-plugins')();
       let notifier = require('node-notifier');
       
       module.exports = function() {
       
         return function(callback) {
       
      -    let config = require('config').webpack;
      +    let config = require('config').webpack();
       
           webpack(config, function(err, stats) {
      -
             if (!err) {
               // errors in files do not stop webpack watch
               // instead, they are gathered, so I get the first one here (if no other)
      @@ -19,13 +17,11 @@ module.exports = function() {
       
             if (err) {
       
      -        let message = err.replace(/.*!/, '');
      -
               notifier.notify({
      -          message
      +          message: err
               });
       
      -        gp.util.log(err);
      +        console.log(err);
       
               if (!config.watch) callback(err);
               return;
      @@ -47,7 +43,7 @@ module.exports = function() {
             require('fs').writeFileSync('/tmp/webpack.json', JSON.stringify(stats.toJson()));
             */
       
      -      gp.util.log('[webpack]', stats.toString({
      +      console.log('[webpack]', stats.toString({
               hash: false,
               version: false,
               timings: true,
      @@ -60,7 +56,7 @@ module.exports = function() {
       
             /*
             Log profile and all details
      -       gp.util.log('[webpack]', stats.toString({
      +       console.log('[webpack]', stats.toString({
              hash: false,
              version: false,
              timings: true,
      
      From 4b7f46953d9dc8b0f85ac98fd15c20e29915f39b Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 5 Sep 2018 19:13:01 +0300
      Subject: [PATCH 041/218] readme
      
      ---
       README.md | 50 ++++++++++++++++++++++++++++++++++++++------------
       1 file changed, 38 insertions(+), 12 deletions(-)
      
      diff --git a/README.md b/README.md
      index 4940dcd..985a52a 100755
      --- a/README.md
      +++ b/README.md
      @@ -33,13 +33,16 @@ You can use it to run the tutorial locally and translate it into your language.
           ```
           cd /js
           git clone https://github.com/iliakan/javascript-tutorial-server
      +    git clone https://github.com/iliakan/jsengine modules/jsengine
           ```
       
      +    Please note, there are two clone commands. That's not a typo: `modules/jsengine` is cloned from another repository.
      +
       5. Clone the tutorial text into it.
       
           The text repository has `"-language"` postfix at the end, e.g for the French version `fr`, for Russian – `ru` etc.
           
      -    For the Russian version:
      +    E.g. for the Russian version:
           ```
           cd /js
           git clone https://github.com/iliakan/javascript-tutorial-ru
      @@ -47,14 +50,15 @@ You can use it to run the tutorial locally and translate it into your language.
       
       6. Run the site
       
      -    Run the site with the same language:
      +    Run the site with the same language. Above we cloned `ru` tutorial, so:
      +
           ```
           cd /js/javascript-tutorial-server
           ./edit ru
           ```
       
      -    Please note that the argument of `edit` is exactly the language you cloned at step 5.
      -    
      +    This will import the tutorial from `/js/javascript-tutorial-ru` and start the server.
      +
           Wait a bit while it reads the tutorial from disk and builds static assets.
       
           Then access the site at `http://127.0.0.1:3000`.
      @@ -63,7 +67,22 @@ You can use it to run the tutorial locally and translate it into your language.
       
           As you edit text files in the tutorial text repository (cloned at step 5), 
           the webpage gets reloaded automatically. 
      + 
           
      +# Change server language
      +
      +The server is using English by default for navigation and other non-tutorial messages.
      +
      +You can set another language it with the second argument of `edit`.
      +
      +E.g. import `ru` tutorial and use `ru` locale for the server
      +
      +```
      +cd /js/javascript-tutorial-server
      +./edit ru ru
      +```
      +
      +Please note, the code must have corresponding `.yml` files with that language. As of now, `ru` and `en` are fully supported.
           
       # Dev mode
       
      @@ -71,20 +90,24 @@ If you'd like to edit the server code, *not* the tutorial text (assuming you're
       
       ```
       // import and cache the "ru" version of the tutorial from /js/javascript-tutorial-ru
      -// (can be any language)
      +// NODE_LANG sets server language
      +// TUTORIAL_ROOT is the full path to tutorial repo
      +
       cd /js/javascript-tutorial-server
      -NODE_LANG=ru npm run gulp tutorial:import
      +NODE_LANG=en TUTORIAL_ROOT=/js/javascript-tutorial-ru npm run gulp jsengine:koa:tutorial:import
       ``` 
      -    
      -And then:
      +        
      +And then `./dev ` runs the server:
       
       ```
       cd /js/javascript-tutorial-server
      -./dev ru
      +./dev en
       ```
       
      -Running `./dev` uses the cached version of the tutorial, it does not watch tutorial text.
      -But it reloads the server after code changes. 
      +Running `./dev` uses the tutorial imported and cached by the previous command. 
      +
      +It does not watch tutorial text, but it reloads the server after code changes.
      + 
       Again, that's for developing the server code itself, not writing the tutorial.
           
       # TroubleShooting
      @@ -93,6 +116,9 @@ If something doesn't work – [file an issue](https://github.com/iliakan/javascr
       
       Please mention OS and Node.js version.
       
      +Also please pull the very latest git code and install latest Node.js modules before publishing an issue.
      +
       --  
       Yours,  
      -Ilya Kantor 
      \ No newline at end of file
      +Ilya Kantor 
      +iliakan@javascript.info
      
      From 44b2681164ac166c139948674f05ff4c62189ef9 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Fri, 7 Sep 2018 10:43:41 +0300
      Subject: [PATCH 042/218] minor
      
      ---
       README.md                      | 2 +-
       edit                           | 2 +-
       modules/client/clientRender.js | 1 -
       3 files changed, 2 insertions(+), 3 deletions(-)
      
      diff --git a/README.md b/README.md
      index 985a52a..15325e4 100755
      --- a/README.md
      +++ b/README.md
      @@ -91,7 +91,7 @@ If you'd like to edit the server code, *not* the tutorial text (assuming you're
       ```
       // import and cache the "ru" version of the tutorial from /js/javascript-tutorial-ru
       // NODE_LANG sets server language
      -// TUTORIAL_ROOT is the full path to tutorial repo
      +// TUTORIAL_ROOT is the full path to tutorial repo, by default is /js/javascript-tutorial-$NODE_LANG
       
       cd /js/javascript-tutorial-server
       NODE_LANG=en TUTORIAL_ROOT=/js/javascript-tutorial-ru npm run gulp jsengine:koa:tutorial:import
      diff --git a/edit b/edit
      index 1644f4f..9d1fdb7 100755
      --- a/edit
      +++ b/edit
      @@ -12,5 +12,5 @@ export NODE_ENV=production
       export TUTORIAL_EDIT=1
       export NODE_PRESERVE_SYMLINKS=1
       
      -npm --silent run -- gulp edit
      +npm --silent run -- gulp edit | bunyan -o short -l debug
       
      diff --git a/modules/client/clientRender.js b/modules/client/clientRender.js
      index 06e53ee..6d764b3 100755
      --- a/modules/client/clientRender.js
      +++ b/modules/client/clientRender.js
      @@ -1,4 +1,3 @@
      -// const bem = require('bemPug')();
       const LANG = require('config').lang;
       
       const t = require('jsengine/i18n/t');
      
      From f8c8204cceac0e14648dbd202066ffb285a9359f Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Fri, 14 Sep 2018 10:42:41 +0300
      Subject: [PATCH 043/218] Update README.md
      
      ---
       README.md | 4 ++++
       1 file changed, 4 insertions(+)
      
      diff --git a/README.md b/README.md
      index 15325e4..6d9184d 100755
      --- a/README.md
      +++ b/README.md
      @@ -16,6 +16,10 @@ You can use it to run the tutorial locally and translate it into your language.
           These are required to update and run the project.
           For Windows just download and install, otherwise use standard OS install tools (packages or whatever convenient).
           
      +    Please use Node.JS 10. 
      +    
      +    If you're using Node.JS 8, then default NPM package manager is buggy, please update it with `npm up -g` command.
      +    
           (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/).
       
       2. Install global Node modules:
      
      From 174154fd090db0def137237ebb738fd44317a834 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Fri, 14 Sep 2018 10:43:14 +0300
      Subject: [PATCH 044/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 6d9184d..61dc671 100755
      --- a/README.md
      +++ b/README.md
      @@ -18,7 +18,7 @@ You can use it to run the tutorial locally and translate it into your language.
           
           Please use Node.JS 10. 
           
      -    If you're using Node.JS 8, then default NPM package manager is buggy, please update it with `npm up -g` command.
      +    If you're using Node.JS 8, then the default NPM package manager is buggy, please update it with `npm up -g` command before you proceed.
           
           (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/).
       
      
      From a1e2e89dc682906097bebad0df9c4b2edbbef8ae Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Fri, 14 Sep 2018 10:45:19 +0300
      Subject: [PATCH 045/218] Update README.md
      
      ---
       README.md | 4 ++--
       1 file changed, 2 insertions(+), 2 deletions(-)
      
      diff --git a/README.md b/README.md
      index 61dc671..8754902 100755
      --- a/README.md
      +++ b/README.md
      @@ -75,7 +75,7 @@ You can use it to run the tutorial locally and translate it into your language.
           
       # Change server language
       
      -The server is using English by default for navigation and other non-tutorial messages.
      +The server uses English by default for navigation and other non-tutorial messages.
       
       You can set another language it with the second argument of `edit`.
       
      @@ -86,7 +86,7 @@ cd /js/javascript-tutorial-server
       ./edit ru ru
       ```
       
      -Please note, the code must have corresponding `.yml` files with that language. As of now, `ru` and `en` are fully supported.
      +Please note, the server must support that language. That is: server code must have corresponding locale files for that language, otherwise it exists with an error. As of now, `ru` and `en` are fully supported.
           
       # Dev mode
       
      
      From 44e6bba9f9351cfeba259baa5ecedebf64e34864 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Fri, 14 Sep 2018 10:46:27 +0300
      Subject: [PATCH 046/218] Update README.md
      
      ---
       README.md | 5 +++--
       1 file changed, 3 insertions(+), 2 deletions(-)
      
      diff --git a/README.md b/README.md
      index 8754902..337b8a9 100755
      --- a/README.md
      +++ b/README.md
      @@ -90,10 +90,11 @@ Please note, the server must support that language. That is: server code must ha
           
       # Dev mode
       
      -If you'd like to edit the server code, *not* the tutorial text (assuming you're familiar with Node.js), run this once:
      +If you'd like to edit the server code, *not* the tutorial text (assuming you're familiar with Node.js), then there are two steps.
      +
      +First, import and cache the tutorial:
       
       ```
      -// import and cache the "ru" version of the tutorial from /js/javascript-tutorial-ru
       // NODE_LANG sets server language
       // TUTORIAL_ROOT is the full path to tutorial repo, by default is /js/javascript-tutorial-$NODE_LANG
       
      
      From f794322ff5b823d783e9a76bc518d7698b4f8512 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Fri, 14 Sep 2018 10:46:59 +0300
      Subject: [PATCH 047/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 337b8a9..ce325ef 100755
      --- a/README.md
      +++ b/README.md
      @@ -92,7 +92,7 @@ Please note, the server must support that language. That is: server code must ha
       
       If you'd like to edit the server code, *not* the tutorial text (assuming you're familiar with Node.js), then there are two steps.
       
      -First, import and cache the tutorial:
      +First, run the command that imports (and caches) the tutorial:
       
       ```
       // NODE_LANG sets server language
      
      From 6607993ef01f0608c3a3a9a2633dd1e5204e43e9 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Tue, 25 Sep 2018 15:55:43 +0300
      Subject: [PATCH 048/218] misc
      
      ---
       locales/en.yml             | 76 ++++++++++++++++++++++++++++++++-----
       locales/ru.yml             | 78 +++++++++++++++++++++++++++++++++-----
       modules/config/handlers.js |  2 +-
       modules/config/webpack.js  |  4 +-
       modules/static/index.js    |  9 -----
       5 files changed, 140 insertions(+), 29 deletions(-)
       delete mode 100755 modules/static/index.js
      
      diff --git a/locales/en.yml b/locales/en.yml
      index d4a6437..6e98fe7 100755
      --- a/locales/en.yml
      +++ b/locales/en.yml
      @@ -1,15 +1,73 @@
       site:
      -  comments: Comments
      +  privacy_policy: Privacy policy
       
      -  tutorial: Tutorial
      -  tutorial_map: Tutorial map
      -  additional_articles: Additional articles
      +  gdpr_dialog:
      +    title: This website uses cookies
      +    text: We use browser technologies such as cookies and local storage to store your preferences. You need to accept our Privacy Policy and Terms of Use for us to do so.
      +    accept: Accept
      +    cancel: Cancel
       
      -  edit_on_github: Edit on Github
      +  toolbar:
      +    lang_switcher:
      +      cta_text: We want to make this open-source project available for people all around the world. Please help us to translate the content of this tutorial to the language you know
      +      footer_text: how much content is translated to the corresponding language
      +      old_version: Old version is published, needs backporting.
      +    logo:
      +      normal:
      +        svg: sitetoolbar__logo_en.svg
      +        width: 200 # 171
      +      normal-white:
      +        svg: sitetoolbar__logo_en-white.svg
      +      small:
      +        svg: sitetoolbar__logo_small_en.svg
      +        width: 70
      +      small-white:
      +        svg: sitetoolbar__logo_small_en-white.svg
      +    sections:
      +    - slug: ''
      +      url: '/'
      +      title: 'Tutorial'
      +    - slug: 'courses'
      +      title: 'Courses'
      +    buy_ebook_extra: 'Buy'
      +    buy_ebook: 'EPUB/PDF'
      +    search_placeholder: 'Search on Javascript.info'
      +    search_button: 'Search'
      +
      +    public_profile: Public profile
      +    account: Account
      +    notifications: Notifications
      +    admin: Admin
      +    logout: Logout
      +
      +  sorry_old_browser: Sorry, IE<10 is not supported, please use a newer browser.
      +  contact_us: contact us
      +  about_the_project: about the project
      +  ilya_kantor: Ilya Kantor
      +  comments: Comments
      +  loading: Loading...
      +  search: Search
      +  share: Share
      +  read_before_commenting: read this before commenting…
       
         meta:
           description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.'
      -  title: The Modern JavaScript Tutorial
      -  subtitle: "How it's done now. From the basics to advanced topics with simple, but detailed explanations."
      -  toc: 'Table of contents'
      -  description: 'Main course contains 2 parts which cover JavaScript as a programming language and working with a browser. There are also additional series of thematic articles.'
      \ No newline at end of file
      +
      +  tablet-menu:
      +    choose_section: Choose section
      +    search_placeholder: Search in the tutorial
      +    search_button: Search
      +
      +  comment:
      +    help:
      +    - You're welcome to post additions, questions to the articles and answers to them.
      +    - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
      +    - If you can't understand something in the article – please elaborate.
      +
      +  edit_on_github: Edit on Github
      +  error: error
      +  close: close
      +
      +  hide_forever: hide permanently
      +  hidden_forever: This information will not show up any more.
      +
      diff --git a/locales/ru.yml b/locales/ru.yml
      index 92ab994..62cb221 100755
      --- a/locales/ru.yml
      +++ b/locales/ru.yml
      @@ -1,15 +1,75 @@
       site:
      +  privacy_policy: Privacy policy
      +
      +  toolbar:
      +    lang_switcher:
      +      cta_text: Мы хотим сделать этот проект с открытым исходным кодом доступным для людей во всем мире. Пожалуйста, помогите нам перевести это руководство на свой язык
      +      footer_text: количество контента, переведенное на соотвествующий язык
      +      old_version: Опубликована полная, но предыдущая версия учебника.
      +    logo:
      +      normal:
      +        svg: sitetoolbar__logo_ru.svg
      +        width: 171
      +      normal-white:
      +        svg: sitetoolbar__logo_ru-white.svg
      +      small:
      +        svg: sitetoolbar__logo_small_ru.svg
      +        width: 80
      +      small-white:
      +        svg: sitetoolbar__logo_small_ru-white.svg
      +    sections:
      +    - slug: 'tutorial'
      +      url: '/'
      +      title: 'Учебник'
      +    - slug: 'courses'
      +      title: 'Курсы'
      +    - url: 'https://javascript.ru/forum/'
      +      title: 'Форум'
      +    - url: 'https://es5.javascript.ru'
      +      title: 'ES5'
      +    - slug: 'quiz'
      +      title: 'Тесты знаний'
      +    - slug: 'jobs'
      +      title: 'Стажировки'
      +    buy_ebook_extra: 'Купить'
      +    buy_ebook: 'EPUB/PDF'
      +    search_placeholder: 'Искать на Javascript.ru'
      +    search_button: 'Найти'
      +
      +    public_profile: Публичный профиль
      +    account: Аккаунт
      +    notifications: Уведомления
      +    admin: Админ
      +    logout: Выйти
      +
      +  sorry_old_browser: Извините, IE<10 не поддерживается, пожалуйста используйте более новый браузер.
      +  contact_us: связаться с нами
      +  about_the_project: о проекте
      +  ilya_kantor: Илья Кантор
         comments: Комментарии
      +  loading: Загружается...
      +  search: Искать
      +  share: Поделиться
      +  read_before_commenting: перед тем как писать…
       
      -  tutorial: Учебник
      -  tutorial_map: Карта учебника
      -  additional_articles: Дополнительно
      +  tablet-menu:
      +    choose_section: Выберите раздел
      +    search_placeholder: Поиск в учебнике
      +    search_button: Поиск
       
      -  edit_on_github: Редактировать на Github
      +  comment:
      +    help:
      +    - Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
      +    - Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
      +    - Если что-то непонятно в статье — пишите, что именно и с какого места.
       
         meta:
      -    description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.'
      -  title: The Modern JavaScript Tutorial
      -  subtitle: "How it's done now. From the basics to advanced topics with simple, but detailed explanations."
      -  toc: 'Table of contents'
      -  description: 'Main course contains 2 parts which cover JavaScript as a programming language and working with a browser. There are also additional series of thematic articles.'
      +    description: 'Современный учебник JavaScript, начиная с основ, включающий в себя много тонкостей и фишек JavaScript/DOM.'
      +
      +  edit_on_github: Редактировать на Github
      +  error: ошибка
      +  close: закрыть
      +
      +  hide_forever: не показывать
      +  hidden_forever: Эта информация больше не будет выводиться.
      +
      diff --git a/modules/config/handlers.js b/modules/config/handlers.js
      index cc00041..9995820 100755
      --- a/modules/config/handlers.js
      +++ b/modules/config/handlers.js
      @@ -4,7 +4,7 @@ const path = require('path');
       const fs = require('fs');
       
       let handlerNames = [
      -  'static',
      +  'jsengine/koa/static',
         'jsengine/koa/requestId',
         'jsengine/koa/requestLog',
         'jsengine/koa/nocache',
      diff --git a/modules/config/webpack.js b/modules/config/webpack.js
      index 1e65d2f..2c99d6c 100755
      --- a/modules/config/webpack.js
      +++ b/modules/config/webpack.js
      @@ -42,6 +42,7 @@ module.exports = function () {
       
         //console.log("MODULE DIRS", modulesDirectories);
       
      +
         /**
          * handler/client/assets/* goes to public/assets/
          */
      @@ -54,6 +55,7 @@ module.exports = function () {
             assetPaths.push(from);
           }
         }
      +
         //console.log("ASSET PATHS", assetPaths);
       
         /**
      @@ -141,7 +143,7 @@ module.exports = function () {
               },
               {
                 test: /\.pug$/,
      -          use:  'pug-loader?root=' + config.projectRoot + '/templates'
      +          use:  'pug-loader?root=' + config.projectRoot + '/templates&globals=__'
               },
               {
                 test:    /\.js$/,
      diff --git a/modules/static/index.js b/modules/static/index.js
      deleted file mode 100755
      index 7deb8c0..0000000
      --- a/modules/static/index.js
      +++ /dev/null
      @@ -1,9 +0,0 @@
      -const serve = require('koa-static');
      -const config = require('config');
      -
      -exports.init = function(app) {
      -  app.use(serve(config.publicRoot, {
      -    maxage: 86400 * 1e3 // 1 day cache static
      -  }));
      -};
      -
      
      From 0f67254d6bb28f2bbb64f8ece17303b27b980ccd Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 26 Sep 2018 16:52:53 +0300
      Subject: [PATCH 049/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index ce325ef..bd47d44 100755
      --- a/README.md
      +++ b/README.md
      @@ -37,7 +37,7 @@ You can use it to run the tutorial locally and translate it into your language.
           ```
           cd /js
           git clone https://github.com/iliakan/javascript-tutorial-server
      -    git clone https://github.com/iliakan/jsengine modules/jsengine
      +    git clone https://github.com/iliakan/jsengine javascript-tutorial-server/modules/jsengine
           ```
       
           Please note, there are two clone commands. That's not a typo: `modules/jsengine` is cloned from another repository.
      
      From 50f524c9bdc20c70a32290be6fda8b825695e9b5 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 26 Sep 2018 20:35:35 +0300
      Subject: [PATCH 050/218] sidebar highlight
      
      ---
       modules/client/head/sidebar.js             | 55 ++++++++++++++++++++--
       modules/styles/blocks/sidebar/sidebar.styl | 43 ++++++++++++++---
       2 files changed, 87 insertions(+), 11 deletions(-)
      
      diff --git a/modules/client/head/sidebar.js b/modules/client/head/sidebar.js
      index 7eb88a5..460b08e 100755
      --- a/modules/client/head/sidebar.js
      +++ b/modules/client/head/sidebar.js
      @@ -3,6 +3,8 @@ document.addEventListener('click', onClick);
       
       document.addEventListener('keydown', onKeyDown);
       
      +initSidebarHighlight();
      +
       function toggle() {
       
         let pageWrapper = document.querySelector('.page-wrapper');
      @@ -11,11 +13,15 @@ function toggle() {
       
         pageWrapper && pageWrapper.classList.toggle('page-wrapper_sidebar_on');
       
      -  if (document.querySelector('.page').classList.contains('page_sidebar_on')) {
      -    delete localStorage.noSidebar;
      -  } else {
      -    localStorage.noSidebar = 1;
      -  }
      +  window.acceptGdpr(accepted => {
      +    if (document.querySelector('.page').classList.contains('page_sidebar_on')) {
      +      delete localStorage.noSidebar;
      +    } else {
      +      if (accepted) {
      +        localStorage.noSidebar = 1;
      +      }
      +    }
      +  });
       
       }
       
      @@ -45,3 +51,42 @@ function onKeyDown(event) {
         event.preventDefault();
       
       }
      +
      +function initSidebarHighlight() {
      +
      +  function highlight() {
      +
      +    let current = document.getElementsByClassName('sidebar__navigation-link_active');
      +    if (current[0]) current[0].classList.remove('sidebar__navigation-link_active');
      +
      +    //debugger;
      +    let h2s = document.getElementsByTagName('h2');
      +    let i;
      +    for (i = 0; i < h2s.length; i++) {
      +      let h2 = h2s[i];
      +      // first in-page header
      +      // >1, because when visiting http://javascript.local/native-prototypes#native-prototype-change,
      +      // top may be 0.375 or kind of...
      +      if (h2.getBoundingClientRect().top > 1) break;
      +    }
      +    i--; // we need the one before it (currently reading)
      +
      +    if (i >= 0) {
      +      let href = h2s[i].firstElementChild && h2s[i].firstElementChild.getAttribute('href');
      +      let li = document.querySelector('.sidebar__navigation-link a[href="' + href + '"]');
      +      if (href && li) {
      +        li.classList.add('sidebar__navigation-link_active');
      +      }
      +    }
      +
      +  }
      +
      +  document.addEventListener('DOMContentLoaded', function() {
      +    highlight();
      +
      +    window.addEventListener('scroll', highlight);
      +  });
      +
      +
      +}
      +
      diff --git a/modules/styles/blocks/sidebar/sidebar.styl b/modules/styles/blocks/sidebar/sidebar.styl
      index 73f6626..60a71de 100755
      --- a/modules/styles/blocks/sidebar/sidebar.styl
      +++ b/modules/styles/blocks/sidebar/sidebar.styl
      @@ -11,6 +11,9 @@
               margin 23px 30px 0
               position relative
       
      +    &_profile &__content
      +        margin-top 30px
      +
           //- модификатор _sticky-footer делает последнюю секцию всегда прижатой к низу сайдбара
           &_sticky-footer &__content
               min-height calc(100% - 85px)
      @@ -20,6 +23,10 @@
               position absolute
               bottom 0
       
      +    &_profile
      +        background #f7f7f7
      +        border-right 0
      +
           & &__toggle
               @extend $button-reset
       
      @@ -59,7 +66,6 @@
           &__toggle::before
               @extend $font-menu
       
      -
           &__section
               margin 22px 0
               padding 0 2px
      @@ -86,6 +92,9 @@
           &__link
               color navigation_link_color
       
      +    &_profile &__link
      +        color #000
      +
           &__link:hover
               text-decoration none
       
      @@ -96,14 +105,13 @@
               &_active
                   color link_hover_color
       
      +    &_profile &__navigation-link
      +        &_active
      +            color link_hover_color
      +
           &__share
               margin 8px 20px 0 0
       
      -
      -    @media tablet
      -        &__toggle
      -            display none
      -
           &_compact &__section
               margin 15px 0
       
      @@ -122,6 +130,29 @@
               line-height 1.1
               margin 8px 0
       
      +    @media tablet
      +        &__toggle
      +            display none
      +
      +        &_profile &__content
      +            margin 24px 16px 0
      +
      +        &_profile &__navigation-links
      +            display flex
      +            flex-wrap wrap
      +            justify-content center
      +            margin 0 -10px
      +
      +        &_profile &__navigation-link
      +            padding 0 10px
      +            margin 5px 0
      +
      +        &_profile &__navigation-link:last-child
      +            margin-right 0
      +
      +        &_profile &__navigation
      +            display none
      +
           @media short_window
               &_sticky-footer &__content
                   padding-bottom 38px
      
      From 0c9a4aa65d11e24fb7e5e3e4d43279ee93f5bd61 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Thu, 27 Sep 2018 09:31:04 +0300
      Subject: [PATCH 051/218] move sidebar to jsengine
      
      ---
       modules/client/head/sidebar.js             |  92 ------------
       modules/styles/blocks/sidebar/sidebar.styl | 164 +--------------------
       templates/blocks/sidebar.pug               |   9 --
       3 files changed, 1 insertion(+), 264 deletions(-)
       delete mode 100755 modules/client/head/sidebar.js
       delete mode 100755 templates/blocks/sidebar.pug
      
      diff --git a/modules/client/head/sidebar.js b/modules/client/head/sidebar.js
      deleted file mode 100755
      index 460b08e..0000000
      --- a/modules/client/head/sidebar.js
      +++ /dev/null
      @@ -1,92 +0,0 @@
      -
      -document.addEventListener('click', onClick);
      -
      -document.addEventListener('keydown', onKeyDown);
      -
      -initSidebarHighlight();
      -
      -function toggle() {
      -
      -  let pageWrapper = document.querySelector('.page-wrapper');
      -
      -  document.querySelector('.page').classList.toggle('page_sidebar_on');
      -
      -  pageWrapper && pageWrapper.classList.toggle('page-wrapper_sidebar_on');
      -
      -  window.acceptGdpr(accepted => {
      -    if (document.querySelector('.page').classList.contains('page_sidebar_on')) {
      -      delete localStorage.noSidebar;
      -    } else {
      -      if (accepted) {
      -        localStorage.noSidebar = 1;
      -      }
      -    }
      -  });
      -
      -}
      -
      -function onClick(event) {
      -
      -  if (!event.target.hasAttribute('data-sidebar-toggle')) return;
      -
      -  toggle();
      -}
      -
      -
      -function onKeyDown(event) {
      -  // don't react on Ctrl-> <- if in text
      -  if (document.activeElement) {
      -    if (~['INPUT', 'TEXTAREA', 'SELECT'].indexOf(document.activeElement.tagName)) return;
      -  }
      -
      -  if (event.keyCode != "S".charCodeAt(0)) return;
      -
      -  if (~navigator.userAgent.toLowerCase().indexOf("mac os x")) {
      -    if (!event.metaKey || !event.altKey) return;
      -  } else {
      -    if (!event.altKey) return;
      -  }
      -
      -  toggle();
      -  event.preventDefault();
      -
      -}
      -
      -function initSidebarHighlight() {
      -
      -  function highlight() {
      -
      -    let current = document.getElementsByClassName('sidebar__navigation-link_active');
      -    if (current[0]) current[0].classList.remove('sidebar__navigation-link_active');
      -
      -    //debugger;
      -    let h2s = document.getElementsByTagName('h2');
      -    let i;
      -    for (i = 0; i < h2s.length; i++) {
      -      let h2 = h2s[i];
      -      // first in-page header
      -      // >1, because when visiting http://javascript.local/native-prototypes#native-prototype-change,
      -      // top may be 0.375 or kind of...
      -      if (h2.getBoundingClientRect().top > 1) break;
      -    }
      -    i--; // we need the one before it (currently reading)
      -
      -    if (i >= 0) {
      -      let href = h2s[i].firstElementChild && h2s[i].firstElementChild.getAttribute('href');
      -      let li = document.querySelector('.sidebar__navigation-link a[href="' + href + '"]');
      -      if (href && li) {
      -        li.classList.add('sidebar__navigation-link_active');
      -      }
      -    }
      -
      -  }
      -
      -  document.addEventListener('DOMContentLoaded', function() {
      -    highlight();
      -
      -    window.addEventListener('scroll', highlight);
      -  });
      -
      -
      -}
      -
      diff --git a/modules/styles/blocks/sidebar/sidebar.styl b/modules/styles/blocks/sidebar/sidebar.styl
      index 60a71de..65419f4 100755
      --- a/modules/styles/blocks/sidebar/sidebar.styl
      +++ b/modules/styles/blocks/sidebar/sidebar.styl
      @@ -1,163 +1 @@
      -.sidebar
      -    width sidebar_width - 1
      -    background #F5F2F0
      -    border-right 1px solid #d5d2d0
      -
      -    &__inner
      -        overflow auto
      -        height 100%
      -
      -    &__content
      -        margin 23px 30px 0
      -        position relative
      -
      -    &_profile &__content
      -        margin-top 30px
      -
      -    //- модификатор _sticky-footer делает последнюю секцию всегда прижатой к низу сайдбара
      -    &_sticky-footer &__content
      -        min-height calc(100% - 85px)
      -        padding-bottom 62px // резервируем место под «редактировать на github»
      -
      -    &_sticky-footer &__section:last-child
      -        position absolute
      -        bottom 0
      -
      -    &_profile
      -        background #f7f7f7
      -        border-right 0
      -
      -    & &__toggle
      -        @extend $button-reset
      -
      -    & &__toggle,
      -    & .map
      -        position absolute
      -        top 12px
      -        left 100%
      -        margin-left 19px
      -        width 44px
      -        height 44px
      -        border-radius 50%
      -        cursor pointer
      -
      -    & .map
      -        text-align center
      -        line-height 44px
      -        top 80px
      -        transition transform animation_duration animation_duration
      -
      -    & &__toggle:active
      -        position absolute
      -
      -    &__toggle:focus,
      -    & .map:focus
      -        outline 0
      -
      -    //- класс вместо псевдокласса для мобильных
      -    &__toggle:hover,
      -    & .map:hover
      -        background #f4f4f4
      -
      -    &__toggle::before
      -        font-size 22px
      -        color #8B8987
      -
      -    &__toggle::before
      -        @extend $font-menu
      -
      -    &__section
      -        margin 22px 0
      -        padding 0 2px
      -        position relative
      -
      -    &__section_separator_before
      -        margin-top 29px
      -
      -    &__section_separator_before::before
      -        content ""
      -        position absolute
      -        top -12px
      -        width 35px
      -        border-top 1px solid #d7d7d4
      -
      -    &__section_share
      -        margin-top 18px
      -
      -    &__section-title
      -        color #C4C2C0
      -        font-weight 400
      -        margin 12px 0
      -
      -    &__link
      -        color navigation_link_color
      -
      -    &_profile &__link
      -        color #000
      -
      -    &__link:hover
      -        text-decoration none
      -
      -    &__navigation-link
      -        margin 12px 0
      -        line-height 1.25
      -
      -        &_active
      -            color link_hover_color
      -
      -    &_profile &__navigation-link
      -        &_active
      -            color link_hover_color
      -
      -    &__share
      -        margin 8px 20px 0 0
      -
      -    &_compact &__section
      -        margin 15px 0
      -
      -    // в компактном виде сохраняем у футера отступы по умолчанию
      -    // для более удобного выравнивания с нижними хлебными крошками
      -    &_compact&_sticky-footer &__section:last-child
      -        margin 22px 0
      -
      -    &_compact &__section_separator_before
      -        margin-top 24px
      -
      -    &_compact &__section-title
      -        margin 8px 0
      -
      -    &_compact &__navigation-link
      -        line-height 1.1
      -        margin 8px 0
      -
      -    @media tablet
      -        &__toggle
      -            display none
      -
      -        &_profile &__content
      -            margin 24px 16px 0
      -
      -        &_profile &__navigation-links
      -            display flex
      -            flex-wrap wrap
      -            justify-content center
      -            margin 0 -10px
      -
      -        &_profile &__navigation-link
      -            padding 0 10px
      -            margin 5px 0
      -
      -        &_profile &__navigation-link:last-child
      -            margin-right 0
      -
      -        &_profile &__navigation
      -            display none
      -
      -    @media short_window
      -        &_sticky-footer &__content
      -            padding-bottom 38px
      -            min-height calc(100% - 62px)
      -
      -        &_compact&_sticky-footer &__section:last-child,
      -        &_sticky-footer &__section:last-child
      -            margin 10px 0
      +@require '~jsengine/sidebar/templates/sidebar.styl'
      diff --git a/templates/blocks/sidebar.pug b/templates/blocks/sidebar.pug
      deleted file mode 100755
      index 6017649..0000000
      --- a/templates/blocks/sidebar.pug
      +++ /dev/null
      @@ -1,9 +0,0 @@
      -each section in sidebar.sections
      -  +e.section
      -    if section.title
      -      +e('h4').section-title!= section.title
      -    +e('nav').navigation
      -      +e('ul').navigation-links
      -        each link in section.links
      -          +e('li').navigation-link
      -            +e('a').link(href=link.url)!= link.title
      
      From 45821b45038b2af3406b87b3a0796142017461f3 Mon Sep 17 00:00:00 2001
      From: =?UTF-8?q?=E7=9F=B3=E4=BA=95=E8=B3=A2=E4=BA=8C?= 
      Date: Sat, 29 Sep 2018 15:09:54 +0900
      Subject: [PATCH 052/218] Add Japanese translations
      
      ---
       locales/ja.yml                   | 73 ++++++++++++++++++++++++++++++++
       modules/frontpage/locales/ja.yml |  8 ++++
       2 files changed, 81 insertions(+)
       create mode 100755 locales/ja.yml
       create mode 100644 modules/frontpage/locales/ja.yml
      
      diff --git a/locales/ja.yml b/locales/ja.yml
      new file mode 100755
      index 0000000..da4b6f2
      --- /dev/null
      +++ b/locales/ja.yml
      @@ -0,0 +1,73 @@
      +site:
      +  privacy_policy: プライバシーポリシー
      +
      +  gdpr_dialog:
      +    title: このウェブサイトはクッキーを使用します
      +    text: 私たちはあなたの選択を保存するために、クッキーやローカルストレージなどのブラウザ技術を使用しています。プライバシーポリシー利用規約 に同意する必要があります。
      +    accept: 受け入れる
      +    cancel: キャンセル
      +
      +  toolbar:
      +    lang_switcher:
      +      cta_text: 私たちはこのオープンソースプロジェクトを世界中の人々に提供したいと考えています。このチュートリアルの内容をあなたが知っている言語に翻訳するのを手伝ってください。
      +      footer_text: 対応する言語に翻訳されているコンテンツの量
      +      old_version: 古いバージョンが公開されており、バックポートが必要です。
      +    logo:
      +      normal:
      +        svg: sitetoolbar__logo_en.svg
      +        width: 200 # 171
      +      normal-white:
      +        svg: sitetoolbar__logo_en-white.svg
      +      small:
      +        svg: sitetoolbar__logo_small_en.svg
      +        width: 70
      +      small-white:
      +        svg: sitetoolbar__logo_small_en-white.svg
      +    sections:
      +    - slug: ''
      +      url: '/'
      +      title: 'チュートリアル'
      +    - slug: 'courses'
      +      title: 'コース'
      +    buy_ebook_extra: '購入する'
      +    buy_ebook: 'EPUB/PDF'
      +    search_placeholder: 'Search on Javascript.info'
      +    search_button: '検索'
      +
      +    public_profile: Public profile
      +    account: Account
      +    notifications: Notifications
      +    admin: Admin
      +    logout: Logout
      +
      +  sorry_old_browser: すみません、IE<10 はサポートしていないため新しいブラウザを使用してください。
      +  contact_us: コンタクトをとる
      +  about_the_project: プロジェクトについて
      +  ilya_kantor: Ilya Kantor
      +  comments: コメント
      +  loading: ロード中...
      +  search: 検索
      +  share: シェア
      +  read_before_commenting: コメントをする前にこれを読んでください…
      +
      +  meta:
      +    description: '現代の JavaScript チュートリアル: クロージャ、ドキュメント、イベント、オブジェクト指向プログラミングなどを含む、サンプルとタスクを使ったシンプルで詳細な説明です。'
      +
      +  tablet-menu:
      +    choose_section: セクションを選択
      +    search_placeholder: チュートリアル内を検索
      +    search_button: 検索
      +
      +  comment:
      +    help:
      +    - 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
      +    - 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
      +    - あなたが記事の中で理解できないことがあれば、詳しく説明してください。
      +
      +  edit_on_github: Edit on Github
      +  error: エラー
      +  close: 閉じる
      +
      +  hide_forever: 永久に隠す
      +  hidden_forever: この情報はこれ以上表示されることはありません。
      +
      diff --git a/modules/frontpage/locales/ja.yml b/modules/frontpage/locales/ja.yml
      new file mode 100644
      index 0000000..24aea14
      --- /dev/null
      +++ b/modules/frontpage/locales/ja.yml
      @@ -0,0 +1,8 @@
      +modern_javascript_tutorial: "現代の Javascript チュートリアル"
      +subtitle: "基本から高度なテーマまで、シンプルで詳細な説明をします。"
      +part: "パート #{num}"
      +more: "もっと…"
      +
      +part3:
      +  title: "その他の記事"
      +  content: "チュートリアルの最初の2つの部分でカバーされていない追加のトピックのリストです。 ここには明確な階層はなく、必要な順序で記事にアクセスできます。"
      \ No newline at end of file
      
      From 8484a3ca6f9d82d3223d5152db37e6c46db856a1 Mon Sep 17 00:00:00 2001
      From: =?UTF-8?q?=E7=9F=B3=E4=BA=95=E8=B3=A2=E4=BA=8C?= 
      Date: Sat, 29 Sep 2018 15:39:03 +0900
      Subject: [PATCH 053/218] Remove unnecessary 'require'
      
      ---
       modules/client/head/index.js | 1 -
       1 file changed, 1 deletion(-)
      
      diff --git a/modules/client/head/index.js b/modules/client/head/index.js
      index c6484ca..b02125a 100755
      --- a/modules/client/head/index.js
      +++ b/modules/client/head/index.js
      @@ -18,7 +18,6 @@ exports.Modal = require('./modal');
       exports.fontTest = require('./fontTest');
       exports.resizeOnload = require('./resizeOnload');
       require('./layout');
      -require('./sidebar');
       require('./navigationArrows');
       require('./hover');
       require('./trackLinks');
      
      From 8f3246f39ff2810ea38923c0ad35b9c7bffa8db0 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sat, 29 Sep 2018 09:55:06 +0300
      Subject: [PATCH 054/218] sidebar
      
      ---
       modules/client/head/index.js | 1 +
       1 file changed, 1 insertion(+)
      
      diff --git a/modules/client/head/index.js b/modules/client/head/index.js
      index b02125a..760ada4 100755
      --- a/modules/client/head/index.js
      +++ b/modules/client/head/index.js
      @@ -18,6 +18,7 @@ exports.Modal = require('./modal');
       exports.fontTest = require('./fontTest');
       exports.resizeOnload = require('./resizeOnload');
       require('./layout');
      +require('jsengine/sidebar/client');
       require('./navigationArrows');
       require('./hover');
       require('./trackLinks');
      
      From 37598d11390031d10bb62b4cd4bba7bed6525d8e Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 30 Sep 2018 13:26:28 +0300
      Subject: [PATCH 055/218] cleanup
      
      ---
       modules/config/index.js                              |  3 ++-
       modules/frontpage/locales/en.yml                     | 12 ++++--------
       modules/frontpage/locales/ja.yml                     |  8 +++++++-
       modules/frontpage/locales/ru.yml                     | 12 ++++--------
       .../templates/blocks/_frontpage-content/index.pug    | 12 ++++++------
       modules/frontpage/templates/frontpage.pug            |  2 +-
       6 files changed, 24 insertions(+), 25 deletions(-)
      
      diff --git a/modules/config/index.js b/modules/config/index.js
      index 2fa3f2d..31dd552 100755
      --- a/modules/config/index.js
      +++ b/modules/config/index.js
      @@ -70,7 +70,8 @@ for(let repo in repos) {
           config.tutorialRepo = {
             github: repo,
             branch: repos[repo].branch || 'master',
      -      url: new URL('https://github.com/' + repo + '/tree/' + (repos[repo].branch || 'master'))
      +      tree: new URL('https://github.com/' + repo + '/tree/' + (repos[repo].branch || 'master')),
      +      blob: new URL('https://github.com/' + repo + '/blob/' + (repos[repo].branch || 'master'))
           }
         }
       }
      diff --git a/modules/frontpage/locales/en.yml b/modules/frontpage/locales/en.yml
      index ebc6427..ce93689 100644
      --- a/modules/frontpage/locales/en.yml
      +++ b/modules/frontpage/locales/en.yml
      @@ -1,8 +1,4 @@
      -modern_javascript_tutorial: "The Modern Javascript Tutorial"
      -subtitle: "From the basics to advanced topics, using simple, but detailed explanations."
      -part: "Part #{num}"
      -more: "More…"
      -
      -part3:
      -  title: "Additional articles"
      -  content: "List of extra topics that are not covered by first two parts of tutorial. There is no clear hierarchy here, you can access articles in the order you want."
      \ No newline at end of file
      +view_github: "view on Github"
      +search_placeholder: "Search in the tutorial"
      +search_button: "Search"
      +share_text: "Share"
      diff --git a/modules/frontpage/locales/ja.yml b/modules/frontpage/locales/ja.yml
      index 24aea14..ec6fb4e 100644
      --- a/modules/frontpage/locales/ja.yml
      +++ b/modules/frontpage/locales/ja.yml
      @@ -1,3 +1,9 @@
      +view_github: "смотреть на Github"
      +search_placeholder: "Поиск по учебнику"
      +search_button: "Найти"
      +share_text: "Поделиться"
      +
      +
       modern_javascript_tutorial: "現代の Javascript チュートリアル"
       subtitle: "基本から高度なテーマまで、シンプルで詳細な説明をします。"
       part: "パート #{num}"
      @@ -5,4 +11,4 @@ more: "もっと…"
       
       part3:
         title: "その他の記事"
      -  content: "チュートリアルの最初の2つの部分でカバーされていない追加のトピックのリストです。 ここには明確な階層はなく、必要な順序で記事にアクセスできます。"
      \ No newline at end of file
      +  content: "チュートリアルの最初の2つの部分でカバーされていない追加のトピックのリストです。 ここには明確な階層はなく、必要な順序で記事にアクセスできます。"
      diff --git a/modules/frontpage/locales/ru.yml b/modules/frontpage/locales/ru.yml
      index 2152402..aae7213 100644
      --- a/modules/frontpage/locales/ru.yml
      +++ b/modules/frontpage/locales/ru.yml
      @@ -1,9 +1,5 @@
      -modern_javascript_tutorial: "Современный учебник Javascript"
      -subtitle: "Перед вами учебник по JavaScript, начиная с основ, включающий в себя много тонкостей и фишек JavaScript/DOM."
      -part: "Часть #{num}"
      -more: "Далее…"
      +view_github: "смотреть на Github"
      +search_placeholder: "Поиск по учебнику"
      +search_button: "Найти"
      +share_text: "Поделиться"
       
      -
      -part3:
      -  title: "Additional articles"
      -  content: "List of extra topics that are not covered by first two parts of tutorial. There is no clear hierarchy here, you can access articles in the order you want."
      \ No newline at end of file
      diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.pug b/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      index 1ede8cf..56a5374 100755
      --- a/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      +++ b/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      @@ -14,13 +14,13 @@ mixin frontpage-content(data)
                     +e.title
                       +e('a').link(href=subTopic.getUrl())= subTopic.title
                 if (topic.children.length > 6)
      -            +e('li').more= t('frontpage.more')
      +            +e('li').more= t('tutorial.more')
       
         +b.frontpage-content
           +e.container
             - let folder = tutorialTree.bySlug('js')
             +e.inner
      -        +e.part= t('frontpage.part', {num: 1})
      +        +e.part= t('tutorial.part', {num: 1})
               +e.title= folder.title
               +e.description!= topArticlesRendered.js.content
               +b.list
      @@ -29,7 +29,7 @@ mixin frontpage-content(data)
           +e.container
             - let folder = tutorialTree.bySlug('ui')
             +e.inner
      -        +e.part= t('frontpage.part', {num: 2})
      +        +e.part= t('tutorial.part', {num: 2})
               +e.title= folder.title
               +e.description!= topArticlesRendered.ui.content
               +b.list
      @@ -38,9 +38,9 @@ mixin frontpage-content(data)
       
           +e.container
             +e.inner
      -        +e.part= t('frontpage.part', {num: 3})
      -        +e.title= t('frontpage.part3.title')
      -        +e.description!= t('frontpage.part3.content')
      +        +e.part= t('tutorial.part', {num: 3})
      +        +e.title= t('tutorial.part3.title')
      +        +e.description!= t('tutorial.part3.content')
               +b.list
                 +partList(tutorialTree.tree.slice(2))
       
      diff --git a/modules/frontpage/templates/frontpage.pug b/modules/frontpage/templates/frontpage.pug
      index 674cbf8..07a8b28 100755
      --- a/modules/frontpage/templates/frontpage.pug
      +++ b/modules/frontpage/templates/frontpage.pug
      @@ -3,7 +3,7 @@ extends /layouts/main
       include blocks/_frontpage-content
       
       block append variables
      -  - var headTitle = t('frontpage.modern_javascript_tutorial');
      +  - var headTitle = t('tutorial.modern_javascript_tutorial');
         - var title = false
         - var header = false
         - var layout_main_class = 'main_width-limit-wide'
      
      From 8b3f5c860be3356334be7bd492fc1dbe2c0983fd Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 30 Sep 2018 13:27:59 +0300
      Subject: [PATCH 056/218] fix
      
      ---
       modules/frontpage/locales/ja.yml | 18 ++++--------------
       1 file changed, 4 insertions(+), 14 deletions(-)
      
      diff --git a/modules/frontpage/locales/ja.yml b/modules/frontpage/locales/ja.yml
      index ec6fb4e..ce93689 100644
      --- a/modules/frontpage/locales/ja.yml
      +++ b/modules/frontpage/locales/ja.yml
      @@ -1,14 +1,4 @@
      -view_github: "смотреть на Github"
      -search_placeholder: "Поиск по учебнику"
      -search_button: "Найти"
      -share_text: "Поделиться"
      -
      -
      -modern_javascript_tutorial: "現代の Javascript チュートリアル"
      -subtitle: "基本から高度なテーマまで、シンプルで詳細な説明をします。"
      -part: "パート #{num}"
      -more: "もっと…"
      -
      -part3:
      -  title: "その他の記事"
      -  content: "チュートリアルの最初の2つの部分でカバーされていない追加のトピックのリストです。 ここには明確な階層はなく、必要な順序で記事にアクセスできます。"
      +view_github: "view on Github"
      +search_placeholder: "Search in the tutorial"
      +search_button: "Search"
      +share_text: "Share"
      
      From a9d3343eab97b245ffa0bad852cfe4580ea58057 Mon Sep 17 00:00:00 2001
      From: =?UTF-8?q?=E7=9F=B3=E4=BA=95=E8=B3=A2=E4=BA=8C?= 
      Date: Sun, 30 Sep 2018 20:24:52 +0900
      Subject: [PATCH 057/218] Additional Japanese translation
      
      ---
       locales/ja.yml                   | 2 +-
       modules/frontpage/locales/ja.yml | 8 ++++----
       2 files changed, 5 insertions(+), 5 deletions(-)
      
      diff --git a/locales/ja.yml b/locales/ja.yml
      index da4b6f2..2b07d47 100755
      --- a/locales/ja.yml
      +++ b/locales/ja.yml
      @@ -64,7 +64,7 @@ site:
           - 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
           - あなたが記事の中で理解できないことがあれば、詳しく説明してください。
       
      -  edit_on_github: Edit on Github
      +  edit_on_github: Githubで編集
         error: エラー
         close: 閉じる
       
      diff --git a/modules/frontpage/locales/ja.yml b/modules/frontpage/locales/ja.yml
      index ce93689..76fbaae 100644
      --- a/modules/frontpage/locales/ja.yml
      +++ b/modules/frontpage/locales/ja.yml
      @@ -1,4 +1,4 @@
      -view_github: "view on Github"
      -search_placeholder: "Search in the tutorial"
      -search_button: "Search"
      -share_text: "Share"
      +view_github: "Githubで見る"
      +search_placeholder: "チュートリアル内を検索"
      +search_button: "検索"
      +share_text: "シェア"
      
      From cf31a3239a1f55d2142cf076838c968fdde725c3 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 30 Sep 2018 16:03:41 +0300
      Subject: [PATCH 058/218] locale build fix
      
      ---
       modules/config/webpack.js | 27 +++++++++++++++++++--------
       1 file changed, 19 insertions(+), 8 deletions(-)
      
      diff --git a/modules/config/webpack.js b/modules/config/webpack.js
      index 2c99d6c..2bdffef 100755
      --- a/modules/config/webpack.js
      +++ b/modules/config/webpack.js
      @@ -253,30 +253,41 @@ module.exports = function () {
               _: 'lodash'
             }),
       
      -      // ignore all locales (will require manually from moment-with-locale
      +      // ignore all locales except current lang
             new webpack.IgnorePlugin({
      -        checkResource: (arg) => {
      +        checkResource(arg) {
                 // locale requires that file back from it, need to keep it
      -          if (arg === '../moment') return false;
      -          tmp = arg;
      +          if (arg === '../moment') return false; // don't ignore this
      +          if (arg === './' + config.lang || arg === './' + config.lang + '.js') return false; // don't ignore current locale
      +          tmp = arg; // for logging only
                 return true;
               },
               // under dirs like: ../locales/..
      -        checkContext:  arg => {
      +        checkContext(arg) {
                 let ignore = arg.endsWith(path.join('node_modules', 'moment', 'locale'));
                 if (ignore) {
      -            //console.log("ignore moment locale", tmp, arg);
      +            // console.log("ignore moment locale", tmp, arg);
                   return true;
                 }
               }
             }),
       
      +
             // ignore site locale files except the lang
             new webpack.IgnorePlugin({
      -        checkResource: (arg) => arg.endsWith('.yml') && arg !== './' + config.lang + '.yml',
      +        checkResource(arg) {
      +          let result = arg.endsWith('.yml') && !arg.endsWith('/' + config.lang + '.yml');
      +          tmp = arg; // for logging
      +          return result;
      +        },
               // under dirs like: ../locales/..
      -        checkContext:  arg => /\/locales(\/|$)/.test(arg)
      +        checkContext(arg) {
      +          let ignore = /\/locales(\/|$)/.test(arg);
      +          // console.log("ignore yml", tmp, arg);
      +          return ignore;
      +        }
             }),
      +      
       
             new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')),
       
      
      From 71090b5cef11e1e9c869b375478997ff690b106e Mon Sep 17 00:00:00 2001
      From: Ding Xue-wen 
      Date: Thu, 4 Oct 2018 16:04:24 +0800
      Subject: [PATCH 059/218] add zh-yml
      
      ---
       locales/zh.yml                   | 73 ++++++++++++++++++++++++++++++++
       modules/frontpage/locales/zh.yml |  4 ++
       2 files changed, 77 insertions(+)
       create mode 100644 locales/zh.yml
       create mode 100644 modules/frontpage/locales/zh.yml
      
      diff --git a/locales/zh.yml b/locales/zh.yml
      new file mode 100644
      index 0000000..6e98fe7
      --- /dev/null
      +++ b/locales/zh.yml
      @@ -0,0 +1,73 @@
      +site:
      +  privacy_policy: Privacy policy
      +
      +  gdpr_dialog:
      +    title: This website uses cookies
      +    text: We use browser technologies such as cookies and local storage to store your preferences. You need to accept our Privacy Policy and Terms of Use for us to do so.
      +    accept: Accept
      +    cancel: Cancel
      +
      +  toolbar:
      +    lang_switcher:
      +      cta_text: We want to make this open-source project available for people all around the world. Please help us to translate the content of this tutorial to the language you know
      +      footer_text: how much content is translated to the corresponding language
      +      old_version: Old version is published, needs backporting.
      +    logo:
      +      normal:
      +        svg: sitetoolbar__logo_en.svg
      +        width: 200 # 171
      +      normal-white:
      +        svg: sitetoolbar__logo_en-white.svg
      +      small:
      +        svg: sitetoolbar__logo_small_en.svg
      +        width: 70
      +      small-white:
      +        svg: sitetoolbar__logo_small_en-white.svg
      +    sections:
      +    - slug: ''
      +      url: '/'
      +      title: 'Tutorial'
      +    - slug: 'courses'
      +      title: 'Courses'
      +    buy_ebook_extra: 'Buy'
      +    buy_ebook: 'EPUB/PDF'
      +    search_placeholder: 'Search on Javascript.info'
      +    search_button: 'Search'
      +
      +    public_profile: Public profile
      +    account: Account
      +    notifications: Notifications
      +    admin: Admin
      +    logout: Logout
      +
      +  sorry_old_browser: Sorry, IE<10 is not supported, please use a newer browser.
      +  contact_us: contact us
      +  about_the_project: about the project
      +  ilya_kantor: Ilya Kantor
      +  comments: Comments
      +  loading: Loading...
      +  search: Search
      +  share: Share
      +  read_before_commenting: read this before commenting…
      +
      +  meta:
      +    description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.'
      +
      +  tablet-menu:
      +    choose_section: Choose section
      +    search_placeholder: Search in the tutorial
      +    search_button: Search
      +
      +  comment:
      +    help:
      +    - You're welcome to post additions, questions to the articles and answers to them.
      +    - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
      +    - If you can't understand something in the article – please elaborate.
      +
      +  edit_on_github: Edit on Github
      +  error: error
      +  close: close
      +
      +  hide_forever: hide permanently
      +  hidden_forever: This information will not show up any more.
      +
      diff --git a/modules/frontpage/locales/zh.yml b/modules/frontpage/locales/zh.yml
      new file mode 100644
      index 0000000..ce93689
      --- /dev/null
      +++ b/modules/frontpage/locales/zh.yml
      @@ -0,0 +1,4 @@
      +view_github: "view on Github"
      +search_placeholder: "Search in the tutorial"
      +search_button: "Search"
      +share_text: "Share"
      
      From e43592367a09f5acc45aeda2a4c3f78d45ad5ee1 Mon Sep 17 00:00:00 2001
      From: LeviDing 
      Date: Thu, 4 Oct 2018 16:07:39 +0800
      Subject: [PATCH 060/218] Translate to Chinese
      
      ---
       modules/frontpage/locales/zh.yml | 8 ++++----
       1 file changed, 4 insertions(+), 4 deletions(-)
      
      diff --git a/modules/frontpage/locales/zh.yml b/modules/frontpage/locales/zh.yml
      index ce93689..d45b36e 100644
      --- a/modules/frontpage/locales/zh.yml
      +++ b/modules/frontpage/locales/zh.yml
      @@ -1,4 +1,4 @@
      -view_github: "view on Github"
      -search_placeholder: "Search in the tutorial"
      -search_button: "Search"
      -share_text: "Share"
      +view_github: "在 Github 上查看"
      +search_placeholder: "在教程中搜索"
      +search_button: "搜索"
      +share_text: "分享"
      
      From 3e943c9a5702b581fd855262be29998f24cd725c Mon Sep 17 00:00:00 2001
      From: LeviDing 
      Date: Thu, 4 Oct 2018 18:16:12 +0800
      Subject: [PATCH 061/218] add translation
      
      ---
       locales/zh.yml | 78 +++++++++++++++++++++++++-------------------------
       1 file changed, 39 insertions(+), 39 deletions(-)
      
      diff --git a/locales/zh.yml b/locales/zh.yml
      index 6e98fe7..4b94849 100644
      --- a/locales/zh.yml
      +++ b/locales/zh.yml
      @@ -1,17 +1,17 @@
       site:
      -  privacy_policy: Privacy policy
      +  privacy_policy: 隐私政策
       
         gdpr_dialog:
      -    title: This website uses cookies
      -    text: We use browser technologies such as cookies and local storage to store your preferences. You need to accept our Privacy Policy and Terms of Use for us to do so.
      -    accept: Accept
      -    cancel: Cancel
      +    title: 本网站使用 cookie
      +    text: 我们使用 cookie 和本地存储等浏览器技术来存储你的偏好设置。你需要接受我们的 隐私政策 和本网站的 其他条款。
      +    accept: 接受
      +    cancel: 取消
       
         toolbar:
           lang_switcher:
      -      cta_text: We want to make this open-source project available for people all around the world. Please help us to translate the content of this tutorial to the language you know
      -      footer_text: how much content is translated to the corresponding language
      -      old_version: Old version is published, needs backporting.
      +      cta_text: 我们希望将这个开源项目提供给全世界的人。请帮助我们将教程的内容 翻译为你所掌握的语言 对应的版本。
      +      footer_text: 多少比重的内容已经被翻译成了相应的语言。
      +      old_version: 旧版本已发布,需要向后移植。
           logo:
             normal:
               svg: sitetoolbar__logo_en.svg
      @@ -26,48 +26,48 @@ site:
           sections:
           - slug: ''
             url: '/'
      -      title: 'Tutorial'
      -    - slug: 'courses'
      -      title: 'Courses'
      -    buy_ebook_extra: 'Buy'
      +      title: '教程'
      +    - slug: '课程'
      +      title: '课程'
      +    buy_ebook_extra: '购买'
           buy_ebook: 'EPUB/PDF'
      -    search_placeholder: 'Search on Javascript.info'
      -    search_button: 'Search'
      +    search_placeholder: '在 Javascript.info 网站中搜索'
      +    search_button: '搜索'
       
      -    public_profile: Public profile
      -    account: Account
      -    notifications: Notifications
      -    admin: Admin
      -    logout: Logout
      +    public_profile: 公开资料
      +    account: 账号
      +    notifications: 通知
      +    admin: 管理员
      +    logout: 登出
       
      -  sorry_old_browser: Sorry, IE<10 is not supported, please use a newer browser.
      -  contact_us: contact us
      -  about_the_project: about the project
      +  sorry_old_browser: 很抱歉,我们不支持 IE<10 等浏览器,请使用一个更新版本的浏览器。
      +  contact_us: 联系我们
      +  about_the_project: 关于本项目
         ilya_kantor: Ilya Kantor
      -  comments: Comments
      -  loading: Loading...
      -  search: Search
      -  share: Share
      -  read_before_commenting: read this before commenting…
      +  comments: 评论
      +  loading: 加载中...
      +  search: 搜索
      +  share: 分享
      +  read_before_commenting: 在评论之前先阅读本内容…
       
         meta:
      -    description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.'
      +    description: '现代 JavaScript 教程:有关示例和任务的简单但详细的解释包括:闭包、文档和事件,以及面向对象编程等。'
       
         tablet-menu:
      -    choose_section: Choose section
      -    search_placeholder: Search in the tutorial
      -    search_button: Search
      +    choose_section: 选择章节
      +    search_placeholder: 在教程中搜索
      +    search_button: 搜索
       
         comment:
           help:
      -    - You're welcome to post additions, questions to the articles and answers to them.
      -    - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
      -    - If you can't understand something in the article – please elaborate.
      +    - 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
      +    - 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 使用 sandbox(plnkrJSBincodepen 等)。
      +    - 如果你无法理解文章中的内容 — 请详细说明。
       
      -  edit_on_github: Edit on Github
      -  error: error
      -  close: close
      +  edit_on_github: 在 Github 上编辑
      +  error: 错误
      +  close: 关闭
       
      -  hide_forever: hide permanently
      -  hidden_forever: This information will not show up any more.
      +  hide_forever: 永久隐藏
      +  hidden_forever: 此信息将不再显示。
       
      
      From 1e3163b08583f6c48e14efe2461bf336342c6b69 Mon Sep 17 00:00:00 2001
      From: LeviDing 
      Date: Thu, 4 Oct 2018 19:08:14 +0800
      Subject: [PATCH 062/218] =?UTF-8?q?translate=20sandbox=20to=20=E6=B2=99?=
       =?UTF-8?q?=E7=AE=B1?=
      MIME-Version: 1.0
      Content-Type: text/plain; charset=UTF-8
      Content-Transfer-Encoding: 8bit
      
      ---
       locales/zh.yml | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/locales/zh.yml b/locales/zh.yml
      index 4b94849..710446f 100644
      --- a/locales/zh.yml
      +++ b/locales/zh.yml
      @@ -61,7 +61,7 @@ site:
         comment:
           help:
           - 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
      -    - 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 使用 sandbox(plnkrJSBincodepen 等)。
      +    - 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 使用沙箱(plnkrJSBincodepen 等)。
           - 如果你无法理解文章中的内容 — 请详细说明。
       
         edit_on_github: 在 Github 上编辑
      
      From 1b6c345913b5b7c9d49b9b28ff612b6d99407387 Mon Sep 17 00:00:00 2001
      From: LeviDing 
      Date: Thu, 4 Oct 2018 19:09:38 +0800
      Subject: [PATCH 063/218] Update zh.yml
      
      ---
       locales/zh.yml | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/locales/zh.yml b/locales/zh.yml
      index 710446f..1b13ce6 100644
      --- a/locales/zh.yml
      +++ b/locales/zh.yml
      @@ -61,7 +61,7 @@ site:
         comment:
           help:
           - 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
      -    - 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 使用沙箱(plnkrJSBincodepen 等)。
      +    - 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
           - 如果你无法理解文章中的内容 — 请详细说明。
       
         edit_on_github: 在 Github 上编辑
      
      From dd2ecedb70f3c1acdbb947156c47b27b2d3dcda0 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Thu, 4 Oct 2018 21:28:43 +0300
      Subject: [PATCH 064/218] up
      
      ---
       .gitignore                |  6 +++---
       cache/.gitkeep            |  0
       gulpfile.js               | 26 +++-----------------------
       modules/config/index.js   | 23 +++++++++++++++++++----
       modules/config/webpack.js |  2 +-
       tmp/.gitkeep              |  0
       6 files changed, 26 insertions(+), 31 deletions(-)
       delete mode 100644 cache/.gitkeep
       delete mode 100644 tmp/.gitkeep
      
      diff --git a/.gitignore b/.gitignore
      index 4daeacd..156e7e4 100755
      --- a/.gitignore
      +++ b/.gitignore
      @@ -21,16 +21,16 @@ sftp-config.json
       Thumbs.db
       
       # database dump for tutorial export
      -dump/
      +/dump
       
       # NPM packages folder.
       node_modules/
       
       # TMP folder (run-time tmp)
      -tmp/
      +/tmp
       
       # Manifest (build-generated content, versions)
      -cache/
      +/cache
       
       # contains v8 executable for linux-tick-processor (run from project root)
       out/*
      diff --git a/cache/.gitkeep b/cache/.gitkeep
      deleted file mode 100644
      index e69de29..0000000
      diff --git a/gulpfile.js b/gulpfile.js
      index de28af1..089b87c 100755
      --- a/gulpfile.js
      +++ b/gulpfile.js
      @@ -58,43 +58,23 @@ gulp.task("test", lazyRequireTask('./tasks/test', {
       }));
       
       
      -gulp.task('watch', lazyRequireTask('./tasks/watch', {
      -  root:        __dirname,
      -  // for performance, watch only these dirs under root
      -  dirs: ['assets', 'styles'],
      -  taskMapping: [
      -    {
      -      watch: 'assets/**',
      -      task:  'sync-resources'
      -    }
      -  ]
      -}));
      -
       gulp.task('deploy', function(callback) {
         runSequence("deploy:build", "deploy:update", callback);
       });
       
      -gulp.task("sync-resources", lazyRequireTask('./tasks/syncResources', {
      -  assets: 'public'
      -}));
      -
       
       gulp.task('webpack', lazyRequireTask('./tasks/webpack'));
       // gulp.task('webpack-dev-server', lazyRequireTask('./tasks/webpackDevServer'));
       
       
      -gulp.task('build', function(callback) {
      -  runSequence("sync-resources", 'webpack', callback);
      -});
      +gulp.task('build', ['webpack']);
       
       gulp.task('server', lazyRequireTask('./tasks/server'));
       
      -gulp.task('edit', ['webpack', 'jsengine:koa:tutorial:importWatch', "sync-resources", 'livereload', 'server']);
      +gulp.task('edit', ['webpack', 'jsengine:koa:tutorial:importWatch', 'livereload', 'server']);
       
       
      -gulp.task('dev', function(callback) {
      -  runSequence("sync-resources", ['nodemon', 'livereload', 'webpack', 'watch'], callback);
      -});
      +gulp.task('dev', ['nodemon', 'livereload', 'webpack']);
       
       gulp.on('err', function(gulpErr) {
         if (gulpErr.err) {
      diff --git a/modules/config/index.js b/modules/config/index.js
      index 31dd552..5051c48 100755
      --- a/modules/config/index.js
      +++ b/modules/config/index.js
      @@ -1,5 +1,5 @@
       let path = require('path');
      -let fs = require('fs');
      +let fs = require('fs-extra');
       let yaml = require('js-yaml');
       let env = process.env;
       
      @@ -54,12 +54,13 @@ let config = module.exports = {
       
         projectRoot:           process.cwd(),
         // public files, served by nginx
      -  publicRoot:            path.join(process.cwd(), 'public'),
      +  publicRoot:            path.join(process.cwd(), 'public', lang),
         // private files, for expiring links, not directly accessible
         tutorialRoot:          env.TUTORIAL_ROOT || path.join(process.cwd(), '..', 'javascript-tutorial-' + lang),
      -  tmpRoot:               path.join(process.cwd(), 'tmp'),
      +  tmpRoot:               path.join(process.cwd(), 'tmp', lang),
         // js/css build versions
      -  cacheRoot:          path.join(process.cwd(), 'cache'),
      +  cacheRoot:          path.join(process.cwd(), 'cache', lang),
      +  assetsRoot:            path.join(process.cwd(), 'assets'),
       
         handlers: require('./handlers')
       };
      @@ -89,3 +90,17 @@ t.requireHandlerLocales();
       // we have a loop dep here
       config.webpack = require('./webpack');
       
      +
      +createRoot(config.publicRoot);
      +createRoot(config.cacheRoot);
      +createRoot(config.tmpRoot);
      +
      +function createRoot(root) {
      +  // may be existing symlink
      +  if (fs.existsSync(root) && fs.statSync(root).isFile()) {
      +    fs.unlinkSync(root);
      +  }
      +  if (!fs.existsSync(root)) {
      +    fs.ensureDirSync(root);
      +  }
      +}
      diff --git a/modules/config/webpack.js b/modules/config/webpack.js
      index 2bdffef..f0dcf39 100755
      --- a/modules/config/webpack.js
      +++ b/modules/config/webpack.js
      @@ -46,7 +46,7 @@ module.exports = function () {
         /**
          * handler/client/assets/* goes to public/assets/
          */
      -  let assetPaths = [];
      +  let assetPaths = [config.assetsRoot];
         for (let handlerName in config.handlers) {
           let handlerPath = config.handlers[handlerName].path;
           let from = `${handlerPath}/client/assets`;
      diff --git a/tmp/.gitkeep b/tmp/.gitkeep
      deleted file mode 100644
      index e69de29..0000000
      
      From 16798707353fed91efa8ef476603e6f8598a820c Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Thu, 4 Oct 2018 21:37:20 +0300
      Subject: [PATCH 065/218] multi-lang dirs, fixes
      
      ---
       gulpfile.js                |  2 +-
       modules/config/webpack.js  | 13 +++++++++++++
       templates/layouts/main.pug |  2 ++
       3 files changed, 16 insertions(+), 1 deletion(-)
      
      diff --git a/gulpfile.js b/gulpfile.js
      index 089b87c..54c191e 100755
      --- a/gulpfile.js
      +++ b/gulpfile.js
      @@ -26,7 +26,7 @@ gulp.task("nodemon", lazyRequireTask('./tasks/nodemon', {
         nodeArgs: process.env.NODE_DEBUG  ? ['--debug'] : [],
         script: "./bin/server.js",
         //ignoreRoot: ['.git', 'node_modules'].concat(glob.sync('{handlers,modules}/**/client')), // ignore handlers' client code
      -  ignore: ['**/client/'], // ignore handlers' client code
      +  ignore: ['**/client/', 'public'], // ignore handlers' client code
         watch:  ["modules"]
       }));
       
      diff --git a/modules/config/webpack.js b/modules/config/webpack.js
      index f0dcf39..efca7a5 100755
      --- a/modules/config/webpack.js
      +++ b/modules/config/webpack.js
      @@ -242,6 +242,19 @@ module.exports = function () {
             fs: 'empty'
           },
       
      +    performance: {
      +      maxEntrypointSize: 350000,
      +      maxAssetSize: 350000, // warning if asset is bigger than 300k
      +      assetFilter(assetFilename) {  // only check js/css
      +        // ignore assets copied by CopyWebpackPlugin
      +        if (assetFilename.startsWith('..')) { // they look like ../courses/achievements/course-complete.svg
      +          // built assets do not have ..
      +          return false;
      +        }
      +        return assetFilename.endsWith('.js') || assetFilename.endsWith('.css');
      +      }
      +    },
      +
           plugins: [
             new webpack.DefinePlugin({
               LANG:      JSON.stringify(config.lang),
      diff --git a/templates/layouts/main.pug b/templates/layouts/main.pug
      index be704c1..85abec3 100755
      --- a/templates/layouts/main.pug
      +++ b/templates/layouts/main.pug
      @@ -8,6 +8,8 @@ block main
               +b("ol").breadcrumbs
                 include ../blocks/breadcrumbs
       
      +      block over-title
      +        
             if title
               - var parts = title.split('\n');
               h1.main__header-title
      
      From 18d1670f177ebeeb1c8697d83b1a3bb32759dbe3 Mon Sep 17 00:00:00 2001
      From: =?UTF-8?q?=E7=9F=B3=E4=BA=95=E8=B3=A2=E4=BA=8C?= 
      Date: Sat, 6 Oct 2018 17:33:23 +0900
      Subject: [PATCH 066/218] Fix missing translation
      
      ---
       locales/ja.yml | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/locales/ja.yml b/locales/ja.yml
      index 2b07d47..2c49661 100755
      --- a/locales/ja.yml
      +++ b/locales/ja.yml
      @@ -31,7 +31,7 @@ site:
             title: 'コース'
           buy_ebook_extra: '購入する'
           buy_ebook: 'EPUB/PDF'
      -    search_placeholder: 'Search on Javascript.info'
      +    search_placeholder: 'チュートリアル内を検索'
           search_button: '検索'
       
           public_profile: Public profile
      
      From e386697b8a2d2792bf55928cbebf52d84d99f82f Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 22 Oct 2018 20:32:07 +0300
      Subject: [PATCH 067/218] fix
      
      ---
       templates/blocks/profile-brief/index.pug | 0
       1 file changed, 0 insertions(+), 0 deletions(-)
       create mode 100644 templates/blocks/profile-brief/index.pug
      
      diff --git a/templates/blocks/profile-brief/index.pug b/templates/blocks/profile-brief/index.pug
      new file mode 100644
      index 0000000..e69de29
      
      From e127b8510b6892f1a141f5d501e8a87b3fd44ce5 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 9 Jan 2019 10:59:50 +0300
      Subject: [PATCH 068/218] Update README.md
      
      ---
       README.md | 29 ++++++++++++++---------------
       1 file changed, 14 insertions(+), 15 deletions(-)
      
      diff --git a/README.md b/README.md
      index bd47d44..5b84ee4 100755
      --- a/README.md
      +++ b/README.md
      @@ -6,6 +6,8 @@ This is a standalone server for the javascript tutorial https://javascript.info.
       
       You can use it to run the tutorial locally and translate it into your language.
       
      +Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call scripts with ".cmd" extension, that are present in the code alongside with Unix versions. 
      +
       # Installation
       
       (If you have an old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`).
      @@ -16,9 +18,7 @@ You can use it to run the tutorial locally and translate it into your language.
           These are required to update and run the project.
           For Windows just download and install, otherwise use standard OS install tools (packages or whatever convenient).
           
      -    Please use Node.JS 10. 
      -    
      -    If you're using Node.JS 8, then the default NPM package manager is buggy, please update it with `npm up -g` command before you proceed.
      +    Please use Node.JS 10+. 
           
           (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/).
       
      @@ -30,7 +30,7 @@ You can use it to run the tutorial locally and translate it into your language.
       
       3. Create the root folder.
       
      -    Create a folder `/js` for the project. You can use any other directory as well, just adjust the paths below.
      +    Create a folder `/js` for the project. If you use another directory as the root, adjust the paths below.
       
       4. Clone the tutorial server into it:
       
      @@ -44,7 +44,7 @@ You can use it to run the tutorial locally and translate it into your language.
       
       5. Clone the tutorial text into it.
       
      -    The text repository has `"-language"` postfix at the end, e.g for the French version `fr`, for Russian – `ru` etc.
      +    The text repository ends with the language code, e.g for the French version `...-fr`, for Russian – `...-ru` etc.
           
           E.g. for the Russian version:
           ```
      @@ -75,7 +75,7 @@ You can use it to run the tutorial locally and translate it into your language.
           
       # Change server language
       
      -The server uses English by default for navigation and other non-tutorial messages.
      +The server uses English by default for navigation and design.
       
       You can set another language it with the second argument of `edit`.
       
      @@ -86,32 +86,31 @@ cd /js/javascript-tutorial-server
       ./edit ru ru
       ```
       
      -Please note, the server must support that language. That is: server code must have corresponding locale files for that language, otherwise it exists with an error. As of now, `ru` and `en` are fully supported.
      +Please note, the server must support that language. There must be corresponding locale files for that language in the code of the server, otherwise it exists with an error. As of now, `ru`, `en`, `zh` and `ja` are fully supported.
           
       # Dev mode
       
      -If you'd like to edit the server code, *not* the tutorial text (assuming you're familiar with Node.js), then there are two steps.
      +If you'd like to edit the server code, *not* the tutorial text (assuming you're familiar with Node.js), then there are two steps to make that easy.
       
       First, run the command that imports (and caches) the tutorial:
       
       ```
      -// NODE_LANG sets server language
      -// TUTORIAL_ROOT is the full path to tutorial repo, by default is /js/javascript-tutorial-$NODE_LANG
      -
       cd /js/javascript-tutorial-server
       NODE_LANG=en TUTORIAL_ROOT=/js/javascript-tutorial-ru npm run gulp jsengine:koa:tutorial:import
       ``` 
      -        
      -And then `./dev ` runs the server:
      +
      +In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/javascript-tutorial-$NODE_LANG`.
      +
      +Afterwards, call `./dev ` to run the server:
       
       ```
       cd /js/javascript-tutorial-server
       ./dev en
       ```
       
      -Running `./dev` uses the tutorial imported and cached by the previous command. 
      +Running `./dev` uses the tutorial that was imported and cached by the previous command. 
       
      -It does not watch tutorial text, but it reloads the server after code changes.
      +It does not "watch" tutorial text, but it reloads the server after code changes.
        
       Again, that's for developing the server code itself, not writing the tutorial.
           
      
      From 036df4521ce9692699bcf7244e6133bf2be705a7 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 9 Jan 2019 11:01:25 +0300
      Subject: [PATCH 069/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 5b84ee4..5f4f78e 100755
      --- a/README.md
      +++ b/README.md
      @@ -90,7 +90,7 @@ Please note, the server must support that language. There must be corresponding
           
       # Dev mode
       
      -If you'd like to edit the server code, *not* the tutorial text (assuming you're familiar with Node.js), then there are two steps to make that easy.
      +If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to make that easy.
       
       First, run the command that imports (and caches) the tutorial:
       
      
      From cb4df08d2fddd4f6d94a4dc4c0772c29f647c3b9 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Thu, 21 Feb 2019 20:27:40 +0300
      Subject: [PATCH 070/218] Update README.md
      
      ---
       README.md | 29 +++++++++++++++++------------
       1 file changed, 17 insertions(+), 12 deletions(-)
      
      diff --git a/README.md b/README.md
      index 5f4f78e..46407f0 100755
      --- a/README.md
      +++ b/README.md
      @@ -46,32 +46,38 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
       
           The text repository ends with the language code, e.g for the French version `...-fr`, for Russian – `...-ru` etc.
           
      -    E.g. for the Russian version:
      +    E.g. for the English version:
           ```
           cd /js
      -    git clone https://github.com/iliakan/javascript-tutorial-ru
      +    git clone https://github.com/iliakan/javascript-tutorial-en
           ```
       
       6. Run the site
       
      -    Run the site with the same language. Above we cloned `ru` tutorial, so:
      +    Install local modules:
       
           ```
           cd /js/javascript-tutorial-server
      -    ./edit ru
      +    npm install
      +    ```
      +    
      +    Run the site with the same language. Above we cloned `en` tutorial, so:
      +
      +    ```
      +    ./edit en
           ```
       
      -    This will import the tutorial from `/js/javascript-tutorial-ru` and start the server.
      +    This will import the tutorial from `/js/javascript-tutorial-en` and start the server.
       
      -    Wait a bit while it reads the tutorial from disk and builds static assets.
      +    Wait a bit while it reads the tutorial from the disk and builds static assets.
       
           Then access the site at `http://127.0.0.1:3000`.
       
       7. Edit the tutorial
       
           As you edit text files in the tutorial text repository (cloned at step 5), 
      -    the webpage gets reloaded automatically. 
      - 
      +    the webpage will reload automatically. 
      +
           
       # Change server language
       
      @@ -79,7 +85,7 @@ The server uses English by default for navigation and design.
       
       You can set another language it with the second argument of `edit`.
       
      -E.g. import `ru` tutorial and use `ru` locale for the server
      +E.g. if you cloned `ru` tutorial, it makes sense to use `ru` locale for the server as well:
       
       ```
       cd /js/javascript-tutorial-server
      @@ -90,13 +96,13 @@ Please note, the server must support that language. There must be corresponding
           
       # Dev mode
       
      -If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to make that easy.
      +If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to do.
       
       First, run the command that imports (and caches) the tutorial:
       
       ```
       cd /js/javascript-tutorial-server
      -NODE_LANG=en TUTORIAL_ROOT=/js/javascript-tutorial-ru npm run gulp jsengine:koa:tutorial:import
      +NODE_LANG=en TUTORIAL_ROOT=/js/javascript-tutorial-en npm run gulp jsengine:koa:tutorial:import
       ``` 
       
       In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/javascript-tutorial-$NODE_LANG`.
      @@ -123,6 +129,5 @@ Please mention OS and Node.js version.
       Also please pull the very latest git code and install latest Node.js modules before publishing an issue.
       
       --  
      -Yours,  
       Ilya Kantor 
       iliakan@javascript.info
      
      From 7ba729b1c683fd077d087236433cd40f7f2b3e3a Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 10 Apr 2019 10:56:41 +0300
      Subject: [PATCH 071/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 46407f0..4890cbf 100755
      --- a/README.md
      +++ b/README.md
      @@ -10,7 +10,7 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
       
       # Installation
       
      -(If you have an old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`).
      +(If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`).
       
       
       1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org).
      
      From bca8b50e1232e5b29bb894f482323134bb47fcd8 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 10 Apr 2019 10:59:14 +0300
      Subject: [PATCH 072/218] Update README.md
      
      ---
       README.md | 13 ++++++-------
       1 file changed, 6 insertions(+), 7 deletions(-)
      
      diff --git a/README.md b/README.md
      index 4890cbf..6822d6f 100755
      --- a/README.md
      +++ b/README.md
      @@ -10,9 +10,6 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
       
       # Installation
       
      -(If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`).
      -
      -
       1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org).
       
           These are required to update and run the project.
      @@ -120,13 +117,15 @@ It does not "watch" tutorial text, but it reloads the server after code changes.
        
       Again, that's for developing the server code itself, not writing the tutorial.
           
      -# TroubleShooting
      +# Troubleshooting
      +
      +If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.
       
      -If something doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new).
      +Please ensure you have Node.js version 10+ (`node -v` shows the version).
       
      -Please mention OS and Node.js version.
      +If it still doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). Please mention OS and Node.js version, 
       
      -Also please pull the very latest git code and install latest Node.js modules before publishing an issue.
      +Please pull the very latest git code and install latest NPM modules before publishing an issue.
       
       --  
       Ilya Kantor 
      
      From 03b1cebac6e168e2fd6e8adaf486aabe07099596 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 10 Apr 2019 11:02:46 +0300
      Subject: [PATCH 073/218] Update README.md
      
      ---
       README.md | 3 ++-
       1 file changed, 2 insertions(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 6822d6f..c041305 100755
      --- a/README.md
      +++ b/README.md
      @@ -128,5 +128,6 @@ If it still doesn't work – [file an issue](https://github.com/iliakan/javascri
       Please pull the very latest git code and install latest NPM modules before publishing an issue.
       
       --  
      -Ilya Kantor 
      +Yours,  
      +Ilya Kantor  
       iliakan@javascript.info
      
      From 9d45b96768f116defcc8e81abf618852a32c31eb Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sat, 13 Apr 2019 23:54:58 +0300
      Subject: [PATCH 074/218] Update README.md
      
      ---
       README.md | 37 +++++++++++++++++++------------------
       1 file changed, 19 insertions(+), 18 deletions(-)
      
      diff --git a/README.md b/README.md
      index c041305..9712f39 100755
      --- a/README.md
      +++ b/README.md
      @@ -1,4 +1,4 @@
      -# Tutorial server
      +# Tutorial Server
       
       Hi everyone!
       
      @@ -10,12 +10,12 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
       
       # Installation
       
      -1. Install [Git](https://git-scm.com/downloads) and [Node.JS](https://nodejs.org).
      +1. Install [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org).
       
           These are required to update and run the project.
           For Windows just download and install, otherwise use standard OS install tools (packages or whatever convenient).
           
      -    Please use Node.JS 10+. 
      +    Please use Node.js 10+. 
           
           (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/).
       
      @@ -33,20 +33,21 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
       
           ```
           cd /js
      -    git clone https://github.com/iliakan/javascript-tutorial-server
      -    git clone https://github.com/iliakan/jsengine javascript-tutorial-server/modules/jsengine
      +    git clone https://github.com/javascript-tutorial/server
      +    git clone https://github.com/javascript-tutorial/engine server/modules/engine
           ```
       
      -    Please note, there are two clone commands. That's not a typo: `modules/jsengine` is cloned from another repository.
      +    Please note, there are two clone commands. That's not a typo: `modules/engine` is cloned from another repository.
       
       5. Clone the tutorial text into it.
       
      -    The text repository ends with the language code, e.g for the French version `...-fr`, for Russian – `...-ru` etc.
      -    
      -    E.g. for the English version:
      +    The repository starts with the language code, e.g for the French version `fr.javascript.info`, for Russian – `ru.javascript.info` etc.
      +
      +    The English version is `en.javascript.info`.
      +
           ```
           cd /js
      -    git clone https://github.com/iliakan/javascript-tutorial-en
      +    git clone https://github.com/iliakan/en.javascript.info
           ```
       
       6. Run the site
      @@ -54,7 +55,7 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
           Install local modules:
       
           ```
      -    cd /js/javascript-tutorial-server
      +    cd /js/server
           npm install
           ```
           
      @@ -64,7 +65,7 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
           ./edit en
           ```
       
      -    This will import the tutorial from `/js/javascript-tutorial-en` and start the server.
      +    This will import the tutorial from `/js/en.javascript.info` and start the server.
       
           Wait a bit while it reads the tutorial from the disk and builds static assets.
       
      @@ -85,7 +86,7 @@ You can set another language it with the second argument of `edit`.
       E.g. if you cloned `ru` tutorial, it makes sense to use `ru` locale for the server as well:
       
       ```
      -cd /js/javascript-tutorial-server
      +cd /js/server
       ./edit ru ru
       ```
       
      @@ -98,16 +99,16 @@ If you'd like to edit the server code (assuming you're familiar with Node.js), *
       First, run the command that imports (and caches) the tutorial:
       
       ```
      -cd /js/javascript-tutorial-server
      -NODE_LANG=en TUTORIAL_ROOT=/js/javascript-tutorial-en npm run gulp jsengine:koa:tutorial:import
      +cd /js/server
      +NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
       ``` 
       
      -In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/javascript-tutorial-$NODE_LANG`.
      +In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
       
       Afterwards, call `./dev ` to run the server:
       
       ```
      -cd /js/javascript-tutorial-server
      +cd /js/server
       ./dev en
       ```
       
      @@ -123,7 +124,7 @@ If you have a very old copy of the English tutorial, please rename `1-js/05-data
       
       Please ensure you have Node.js version 10+ (`node -v` shows the version).
       
      -If it still doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). Please mention OS and Node.js version, 
      +If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). Please mention OS and Node.js version, 
       
       Please pull the very latest git code and install latest NPM modules before publishing an issue.
       
      
      From c0ba2d0f12fdbc008393ddee3f2876acc17b48c6 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 14 Apr 2019 08:22:25 +0300
      Subject: [PATCH 075/218] jsengine -> engine
      
      ---
       .gitignore                                 |  2 +-
       bin/server.js                              |  4 ++--
       gulpfile.js                                |  6 +++---
       modules/client/clientRender.js             |  2 +-
       modules/client/head/index.js               |  2 +-
       modules/client/localeExample.js            |  4 ++--
       modules/config/handlers.js                 | 24 +++++++++++-----------
       modules/config/index.js                    |  4 ++--
       modules/config/webpack.js                  | 14 ++++++-------
       modules/dev/index.js                       |  2 +-
       modules/frontpage/controller/frontpage.js  | 12 +++++------
       modules/frontpage/index.js                 |  2 +-
       modules/render.js                          |  2 +-
       modules/styles/blocks/sidebar/sidebar.styl |  2 +-
       tasks/server.js                            |  2 +-
       15 files changed, 42 insertions(+), 42 deletions(-)
      
      diff --git a/.gitignore b/.gitignore
      index 156e7e4..6e12431 100755
      --- a/.gitignore
      +++ b/.gitignore
      @@ -39,5 +39,5 @@ out/*
       public/*
       package-lock.json
       
      -/modules/jsengine
      +/modules/engine
       .gitmodules
      diff --git a/bin/server.js b/bin/server.js
      index 95a8685..34d6c27 100755
      --- a/bin/server.js
      +++ b/bin/server.js
      @@ -1,8 +1,8 @@
       #!/usr/bin/env node
       
       const config = require('config');
      -const app = require('jsengine/koa/app');
      -const log = require('jsengine/log')();
      +const app = require('engine/koa/app');
      +const log = require('engine/log')();
       
       app.waitBootAndListen(config.server.host, config.server.port).then(() => {
         log.info("App is listening");
      diff --git a/gulpfile.js b/gulpfile.js
      index 54c191e..53361be 100755
      --- a/gulpfile.js
      +++ b/gulpfile.js
      @@ -7,7 +7,7 @@ const gulp = require('gulp');
       const glob = require('glob');
       const path = require('path');
       const fs = require('fs');
      -const {lazyRequireTask, requireModuleTasks} = require('jsengine/gulp/requireModuleTasks');
      +const {lazyRequireTask, requireModuleTasks} = require('engine/gulp/requireModuleTasks');
       const runSequence = require('run-sequence');
       
       const config = require('config');
      @@ -42,7 +42,7 @@ gulp.task("livereload", lazyRequireTask("./tasks/livereload", {
         ]
       }));
       
      -requireModuleTasks('jsengine/koa/tutorial');
      +requireModuleTasks('engine/koa/tutorial');
       
       let testSrcs = ['modules/**/test/**/*.js'];
       // on Travis, keys are required for E2E Selenium tests
      @@ -71,7 +71,7 @@ gulp.task('build', ['webpack']);
       
       gulp.task('server', lazyRequireTask('./tasks/server'));
       
      -gulp.task('edit', ['webpack', 'jsengine:koa:tutorial:importWatch', 'livereload', 'server']);
      +gulp.task('edit', ['webpack', 'engine:koa:tutorial:importWatch', 'livereload', 'server']);
       
       
       gulp.task('dev', ['nodemon', 'livereload', 'webpack']);
      diff --git a/modules/client/clientRender.js b/modules/client/clientRender.js
      index 6d764b3..c8b605f 100755
      --- a/modules/client/clientRender.js
      +++ b/modules/client/clientRender.js
      @@ -1,6 +1,6 @@
       const LANG = require('config').lang;
       
      -const t = require('jsengine/i18n/t');
      +const t = require('engine/i18n/t');
       
       module.exports = function(template, locals) {
         locals = locals ? Object.create(locals) : {};
      diff --git a/modules/client/head/index.js b/modules/client/head/index.js
      index 760ada4..29e4e91 100755
      --- a/modules/client/head/index.js
      +++ b/modules/client/head/index.js
      @@ -18,7 +18,7 @@ exports.Modal = require('./modal');
       exports.fontTest = require('./fontTest');
       exports.resizeOnload = require('./resizeOnload');
       require('./layout');
      -require('jsengine/sidebar/client');
      +require('engine/sidebar/client');
       require('./navigationArrows');
       require('./hover');
       require('./trackLinks');
      diff --git a/modules/client/localeExample.js b/modules/client/localeExample.js
      index 8296e27..57c4190 100644
      --- a/modules/client/localeExample.js
      +++ b/modules/client/localeExample.js
      @@ -1,8 +1,8 @@
       // client-side locale example
       // can be used as require('client/test') from server-side
       // and from client side too
      -const t = require('jsengine/i18n/t');
      +const t = require('engine/i18n/t');
       
       t.i18n.add('test', require('../../locales/' + require('config').lang + '.yml'));
       
      -console.log(t('test.ilya_kantor'));
      \ No newline at end of file
      +console.log(t('test.ilya_kantor'));
      diff --git a/modules/config/handlers.js b/modules/config/handlers.js
      index 9995820..5c4d839 100755
      --- a/modules/config/handlers.js
      +++ b/modules/config/handlers.js
      @@ -4,43 +4,43 @@ const path = require('path');
       const fs = require('fs');
       
       let handlerNames = [
      -  'jsengine/koa/static',
      -  'jsengine/koa/requestId',
      -  'jsengine/koa/requestLog',
      -  'jsengine/koa/nocache',
      +  'engine/koa/static',
      +  'engine/koa/requestId',
      +  'engine/koa/requestLog',
      +  'engine/koa/nocache',
       
         // this middleware adds this.render method
         // it is *before error*, because errors need this.render
         'render',
       
         // errors wrap everything
      -  'jsengine/koa/error',
      +  'engine/koa/error',
       
         // this logger only logs HTTP status and URL
         // before everything to make sure it log all
      -  'jsengine/koa/accessLogger',
      +  'engine/koa/accessLogger',
       
         // pure node.js examples from tutorial
         // before session
         // before form parsing, csrf checking or whatever, bare node
      -  'jsengine/koa/nodeExample',
      +  'engine/koa/nodeExample',
       
         // before anything that may deal with body
         // it parses JSON & URLENCODED FORMS,
         // it does not parse form/multipart
      -  'jsengine/koa/bodyParser',
      +  'engine/koa/bodyParser',
       
         // parse FORM/MULTIPART
         // (many tweaks possible, lets the middleware decide how to parse it)
      -  'jsengine/koa/multipartParser',
      +  'engine/koa/multipartParser',
       
         // right after parsing body, make sure we logged for development
      -  'jsengine/koa/verboseLogger',
      +  'engine/koa/verboseLogger',
       
      -  'jsengine/koa/conditional',
      +  'engine/koa/conditional',
       
         process.env.NODE_ENV === 'development' && 'dev',
      -  'jsengine/koa/tutorial',
      +  'engine/koa/tutorial',
         'frontpage'
       ].filter(Boolean);
       
      diff --git a/modules/config/index.js b/modules/config/index.js
      index 5051c48..53bf510 100755
      --- a/modules/config/index.js
      +++ b/modules/config/index.js
      @@ -65,7 +65,7 @@ let config = module.exports = {
         handlers: require('./handlers')
       };
       
      -let repos = require('jsengine/koa/tutorial/repos');
      +let repos = require('engine/koa/tutorial/repos');
       for(let repo in repos) {
         if (repos[repo].lang === lang) {
           config.tutorialRepo = {
      @@ -83,7 +83,7 @@ require.extensions['.yml'] = function(module, filename) {
       
       
       // after module.exports for circle dep w/ config
      -const t = require('jsengine/i18n');
      +const t = require('engine/i18n');
       t.requireHandlerLocales();
       
       // webpack config uses general config
      diff --git a/modules/config/webpack.js b/modules/config/webpack.js
      index efca7a5..49c76b8 100755
      --- a/modules/config/webpack.js
      +++ b/modules/config/webpack.js
      @@ -17,8 +17,8 @@ module.exports = function () {
         let rupture = require('rupture');
         let chokidar = require('chokidar');
         let webpack = require('webpack');
      -  let WriteVersionsPlugin = require('jsengine/webpack/writeVersionsPlugin');
      -  let CssWatchRebuildPlugin = require('jsengine/webpack/cssWatchRebuildPlugin');
      +  let WriteVersionsPlugin = require('engine/webpack/writeVersionsPlugin');
      +  let CssWatchRebuildPlugin = require('engine/webpack/cssWatchRebuildPlugin');
         const CopyWebpackPlugin = require('copy-webpack-plugin')
         const MiniCssExtractPlugin = require("mini-css-extract-plugin");
         const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
      @@ -65,7 +65,7 @@ module.exports = function () {
         let entries = {
           head:      'client/head',
           footer:    'client/footer',
      -    tutorial:  'jsengine/koa/tutorial/client',
      +    tutorial:  'engine/koa/tutorial/client',
           styles:    config.tmpRoot + '/styles.styl',
           frontpage: config.tmpRoot + '/frontpage.styl'
         };
      @@ -131,7 +131,7 @@ module.exports = function () {
               entry: {
                 styles: config.tmpRoot + '/styles.styl',
                 head: 'client/head',
      -          tutorial: 'jsengine/koa/tutorial/client',
      +          tutorial: 'engine/koa/tutorial/client',
                 footer: 'client/footer',
               },*/
       
      @@ -188,7 +188,7 @@ module.exports = function () {
                       ]
                     }
                   },
      -            'jsengine/webpack/hover-loader',
      +            'engine/webpack/hover-loader',
                   {
                     loader:  'stylus-loader',
                     options: {
      @@ -226,7 +226,7 @@ module.exports = function () {
             // allow require('styles') which looks for styles/index.styl
             extensions: ['.js', '.styl'],
             alias:      {
      -        'entities/maps/entities.json': 'jsengine/markit/emptyEntities',
      +        'entities/maps/entities.json': 'engine/markit/emptyEntities',
               config:                        'client/config'
             },
             modules:    modulesDirectories
      @@ -300,7 +300,7 @@ module.exports = function () {
                 return ignore;
               }
             }),
      -      
      +
       
             new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')),
       
      diff --git a/modules/dev/index.js b/modules/dev/index.js
      index 5317fe0..1204624 100755
      --- a/modules/dev/index.js
      +++ b/modules/dev/index.js
      @@ -1,4 +1,4 @@
      -let mountHandlerMiddleware = require('jsengine/koa/mountHandlerMiddleware');
      +let mountHandlerMiddleware = require('engine/koa/mountHandlerMiddleware');
       
       exports.init = function(app) {
         app.use( mountHandlerMiddleware('/dev', __dirname) );
      diff --git a/modules/frontpage/controller/frontpage.js b/modules/frontpage/controller/frontpage.js
      index 58e2c29..4f4a358 100755
      --- a/modules/frontpage/controller/frontpage.js
      +++ b/modules/frontpage/controller/frontpage.js
      @@ -1,11 +1,11 @@
       
      -const Article = require('jsengine/koa/tutorial').Article;
      -const TutorialTree = require('jsengine/koa/tutorial').TutorialTree;
      -const Task = require('jsengine/koa/tutorial').Task;
      +const Article = require('engine/koa/tutorial').Article;
      +const TutorialTree = require('engine/koa/tutorial').TutorialTree;
      +const Task = require('engine/koa/tutorial').Task;
       const _ = require('lodash');
      -const ArticleRenderer = require('jsengine/koa/tutorial').ArticleRenderer;
      -const localStorage = require('jsengine/local-storage').instance();
      -const t = require('jsengine/i18n');
      +const ArticleRenderer = require('engine/koa/tutorial').ArticleRenderer;
      +const localStorage = require('engine/local-storage').instance();
      +const t = require('engine/i18n');
       
       t.requirePhrase('frontpage');
       
      diff --git a/modules/frontpage/index.js b/modules/frontpage/index.js
      index b3639bf..6ae99e4 100755
      --- a/modules/frontpage/index.js
      +++ b/modules/frontpage/index.js
      @@ -1,4 +1,4 @@
      -const mountHandlerMiddleware = require('jsengine/koa/mountHandlerMiddleware');
      +const mountHandlerMiddleware = require('engine/koa/mountHandlerMiddleware');
       
       exports.init = function(app) {
         app.use(mountHandlerMiddleware('/', __dirname));
      diff --git a/modules/render.js b/modules/render.js
      index 16afd21..e75db2c 100644
      --- a/modules/render.js
      +++ b/modules/render.js
      @@ -1,4 +1,4 @@
      -let Renderer = require('jsengine/koa/renderer');
      +let Renderer = require('engine/koa/renderer');
       
       // (!) this.render does not assign this.body to the result
       // that's because render can be used for different purposes, e.g to send emails
      diff --git a/modules/styles/blocks/sidebar/sidebar.styl b/modules/styles/blocks/sidebar/sidebar.styl
      index 65419f4..41c8e0f 100755
      --- a/modules/styles/blocks/sidebar/sidebar.styl
      +++ b/modules/styles/blocks/sidebar/sidebar.styl
      @@ -1 +1 @@
      -@require '~jsengine/sidebar/templates/sidebar.styl'
      +@require '~engine/sidebar/templates/sidebar.styl'
      diff --git a/tasks/server.js b/tasks/server.js
      index 184e7d0..0333f36 100755
      --- a/tasks/server.js
      +++ b/tasks/server.js
      @@ -1,4 +1,4 @@
      -let app = require('jsengine/koa/app');
      +let app = require('engine/koa/app');
       let config = require('config');
       
       module.exports = function() {
      
      From 9e53da64da6343852cb3f153256b3bf1807ed5e9 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 14 Apr 2019 08:39:30 +0300
      Subject: [PATCH 076/218] move to org
      
      ---
       edit | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/edit b/edit
      index 9d1fdb7..d38a3ea 100755
      --- a/edit
      +++ b/edit
      @@ -6,7 +6,7 @@
       : ${1?"Usage: $0  []"}
       
       
      -export TUTORIAL_ROOT="../javascript-tutorial-$1"
      +export TUTORIAL_ROOT="../$1.javascript.info"
       export NODE_LANG="${2:-en}"
       export NODE_ENV=production
       export TUTORIAL_EDIT=1
      
      From 08c119b5e0d1667dd5400056fb9335c13ad369cb Mon Sep 17 00:00:00 2001
      From: Alexey Pyltsyn 
      Date: Sun, 14 Apr 2019 17:59:05 +0300
      Subject: [PATCH 077/218] Improve README.md
      
      ---
       README.md | 6 +++---
       1 file changed, 3 insertions(+), 3 deletions(-)
      
      diff --git a/README.md b/README.md
      index 9712f39..338688d 100755
      --- a/README.md
      +++ b/README.md
      @@ -6,7 +6,7 @@ This is a standalone server for the javascript tutorial https://javascript.info.
       
       You can use it to run the tutorial locally and translate it into your language.
       
      -Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call scripts with ".cmd" extension, that are present in the code alongside with Unix versions. 
      +Windows, Unix systems and macOS are supported. For Windows, you'll need to call scripts with ".cmd" extension, that are present in the code alongside with Unix versions. 
       
       # Installation
       
      @@ -47,7 +47,7 @@ Windows, Unix-systems and MacOS are supported. For Windows, you'll need to call
       
           ```
           cd /js
      -    git clone https://github.com/iliakan/en.javascript.info
      +    git clone https://github.com/javascript-tutorial/en.javascript.info
           ```
       
       6. Run the site
      @@ -124,7 +124,7 @@ If you have a very old copy of the English tutorial, please rename `1-js/05-data
       
       Please ensure you have Node.js version 10+ (`node -v` shows the version).
       
      -If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). Please mention OS and Node.js version, 
      +If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). Please mention OS and Node.js version.
       
       Please pull the very latest git code and install latest NPM modules before publishing an issue.
       
      
      From f7f57b54b6b3a370a45c6d0fdaf8264720091c16 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 14 Apr 2019 18:08:31 +0300
      Subject: [PATCH 078/218] Cache optional
      
      ---
       package.json | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/package.json b/package.json
      index 1a7b898..4566bd1 100755
      --- a/package.json
      +++ b/package.json
      @@ -8,7 +8,7 @@
           "//": "test must exit with status 1 if fails, don't use | or ensure the right exit code if you do",
           "test": "SELENIUM_LOCAL=1 NODE_ENV=test NODE_PATH=./modules ./node_modules/.bin/gulp test",
           "build": "NODE_PATH=./modules ./node_modules/.bin/gulp build",
      -    "gulp": "NODE_PATH=./modules ./node_modules/.bin/gulp"
      +    "gulp": "NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules ./node_modules/.bin/gulp"
         },
         "precommit": "NODE_ENV=development node `which gulp` pre-commit",
         "//": "node-xmpp-client installs for linux only",
      
      From 3a60516014ec05caf1fcbd4c0c8fd58f6fecad49 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 14 Apr 2019 18:12:23 +0300
      Subject: [PATCH 079/218] fix
      
      ---
       modules/frontpage/locales/en.yml | 1 +
       modules/frontpage/locales/ja.yml | 1 +
       modules/frontpage/locales/ru.yml | 1 +
       modules/frontpage/locales/zh.yml | 1 +
       4 files changed, 4 insertions(+)
      
      diff --git a/modules/frontpage/locales/en.yml b/modules/frontpage/locales/en.yml
      index ce93689..3c1b2dd 100644
      --- a/modules/frontpage/locales/en.yml
      +++ b/modules/frontpage/locales/en.yml
      @@ -1,3 +1,4 @@
      +modern_javascript_tutorial: "The Modern JavaScript Tutorial"
       view_github: "view on Github"
       search_placeholder: "Search in the tutorial"
       search_button: "Search"
      diff --git a/modules/frontpage/locales/ja.yml b/modules/frontpage/locales/ja.yml
      index 76fbaae..a857c2b 100644
      --- a/modules/frontpage/locales/ja.yml
      +++ b/modules/frontpage/locales/ja.yml
      @@ -1,3 +1,4 @@
      +modern_javascript_tutorial: "現代の JavaScript チュートリアル"
       view_github: "Githubで見る"
       search_placeholder: "チュートリアル内を検索"
       search_button: "検索"
      diff --git a/modules/frontpage/locales/ru.yml b/modules/frontpage/locales/ru.yml
      index aae7213..41a9dbb 100644
      --- a/modules/frontpage/locales/ru.yml
      +++ b/modules/frontpage/locales/ru.yml
      @@ -1,3 +1,4 @@
      +modern_javascript_tutorial: "Современный учебник JavaScript"
       view_github: "смотреть на Github"
       search_placeholder: "Поиск по учебнику"
       search_button: "Найти"
      diff --git a/modules/frontpage/locales/zh.yml b/modules/frontpage/locales/zh.yml
      index d45b36e..a51c554 100644
      --- a/modules/frontpage/locales/zh.yml
      +++ b/modules/frontpage/locales/zh.yml
      @@ -1,3 +1,4 @@
      +modern_javascript_tutorial: "现代 JavaScript 教程"
       view_github: "在 Github 上查看"
       search_placeholder: "在教程中搜索"
       search_button: "搜索"
      
      From e7f31f1c651c27ce0b357c1aad59366a4a2fa83c Mon Sep 17 00:00:00 2001
      From: Alexey Pyltsyn 
      Date: Sun, 14 Apr 2019 18:15:23 +0300
      Subject: [PATCH 080/218] Update links
      
      ---
       locales/en.yml | 2 +-
       locales/ja.yml | 2 +-
       locales/ru.yml | 2 +-
       locales/zh.yml | 2 +-
       package.json   | 4 ++--
       5 files changed, 6 insertions(+), 6 deletions(-)
      
      diff --git a/locales/en.yml b/locales/en.yml
      index 6e98fe7..7b01de3 100755
      --- a/locales/en.yml
      +++ b/locales/en.yml
      @@ -9,7 +9,7 @@ site:
       
         toolbar:
           lang_switcher:
      -      cta_text: We want to make this open-source project available for people all around the world. Please help us to translate the content of this tutorial to the language you know
      +      cta_text: We want to make this open-source project available for people all around the world. Please help us to translate the content of this tutorial to the language you know
             footer_text: how much content is translated to the corresponding language
             old_version: Old version is published, needs backporting.
           logo:
      diff --git a/locales/ja.yml b/locales/ja.yml
      index 2c49661..dda0c39 100755
      --- a/locales/ja.yml
      +++ b/locales/ja.yml
      @@ -9,7 +9,7 @@ site:
       
         toolbar:
           lang_switcher:
      -      cta_text: 私たちはこのオープンソースプロジェクトを世界中の人々に提供したいと考えています。このチュートリアルの内容をあなたが知っている言語に翻訳するのを手伝ってください。
      +      cta_text: 私たちはこのオープンソースプロジェクトを世界中の人々に提供したいと考えています。このチュートリアルの内容をあなたが知っている言語に翻訳するのを手伝ってください。
             footer_text: 対応する言語に翻訳されているコンテンツの量
             old_version: 古いバージョンが公開されており、バックポートが必要です。
           logo:
      diff --git a/locales/ru.yml b/locales/ru.yml
      index 62cb221..8db0253 100755
      --- a/locales/ru.yml
      +++ b/locales/ru.yml
      @@ -3,7 +3,7 @@ site:
       
         toolbar:
           lang_switcher:
      -      cta_text: Мы хотим сделать этот проект с открытым исходным кодом доступным для людей во всем мире. Пожалуйста, помогите нам перевести это руководство на свой язык
      +      cta_text: Мы хотим сделать этот проект с открытым исходным кодом доступным для людей во всем мире. Пожалуйста, помогите нам перевести это руководство на свой язык
             footer_text: количество контента, переведенное на соотвествующий язык
             old_version: Опубликована полная, но предыдущая версия учебника.
           logo:
      diff --git a/locales/zh.yml b/locales/zh.yml
      index 1b13ce6..bcee06e 100644
      --- a/locales/zh.yml
      +++ b/locales/zh.yml
      @@ -9,7 +9,7 @@ site:
       
         toolbar:
           lang_switcher:
      -      cta_text: 我们希望将这个开源项目提供给全世界的人。请帮助我们将教程的内容 翻译为你所掌握的语言 对应的版本。
      +      cta_text: 我们希望将这个开源项目提供给全世界的人。请帮助我们将教程的内容 翻译为你所掌握的语言 对应的版本。
             footer_text: 多少比重的内容已经被翻译成了相应的语言。
             old_version: 旧版本已发布,需要向后移植。
           logo:
      diff --git a/package.json b/package.json
      index 4566bd1..796562f 100755
      --- a/package.json
      +++ b/package.json
      @@ -78,11 +78,11 @@
         "engineStrict": true,
         "repository": {
           "type": "git",
      -    "url": "https://github.com/iliakan/javascript-tutorial-server.git"
      +    "url": "https://github.com/javascript-tutorial/javascript-tutorial-server.git"
         },
         "author": "Ilya Kantor",
         "license": "CC BY-NC-SA 3.0",
         "bugs": {
      -    "url": "https://github.com/iliakan/javascript-tutorial-server/issues"
      +    "url": "https://github.com/javascript-tutorial/javascript-tutorial-server/issues"
         }
       }
      
      From cdd887cdda1b4aace2a1811758eb0100911938cf Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 14 Apr 2019 18:19:46 +0300
      Subject: [PATCH 081/218] header
      
      ---
       .../frontpage/templates/blocks/_frontpage-content/index.pug   | 1 +
       .../frontpage/templates/blocks/_frontpage-content/index.styl  | 2 +-
       modules/frontpage/templates/frontpage.pug                     | 4 ++--
       modules/styles/blocks/main/main.styl                          | 2 +-
       4 files changed, 5 insertions(+), 4 deletions(-)
      
      diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.pug b/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      index 56a5374..6562f63 100755
      --- a/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      +++ b/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      @@ -17,6 +17,7 @@ mixin frontpage-content(data)
                   +e('li').more= t('tutorial.more')
       
         +b.frontpage-content
      +
           +e.container
             - let folder = tutorialTree.bySlug('js')
             +e.inner
      diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.styl b/modules/frontpage/templates/blocks/_frontpage-content/index.styl
      index c25ef4d..7a9436d 100755
      --- a/modules/frontpage/templates/blocks/_frontpage-content/index.styl
      +++ b/modules/frontpage/templates/blocks/_frontpage-content/index.styl
      @@ -39,7 +39,7 @@
       
         &__inner
           max-width 920px
      -    margin 0 auto
      +    margin 0 // 0 auto
           padding-top 48px
           padding-bottom 64px
       
      diff --git a/modules/frontpage/templates/frontpage.pug b/modules/frontpage/templates/frontpage.pug
      index 07a8b28..2368344 100755
      --- a/modules/frontpage/templates/frontpage.pug
      +++ b/modules/frontpage/templates/frontpage.pug
      @@ -4,8 +4,8 @@ include blocks/_frontpage-content
       
       block append variables
         - var headTitle = t('tutorial.modern_javascript_tutorial');
      -  - var title = false
      -  - var header = false
      +  - var title = t('tutorial.modern_javascript_tutorial')
      +  - var header = t('tutorial.modern_javascript_tutorial')
         - var layout_main_class = 'main_width-limit-wide'
         - var content_class = 'frontpage'
       
      diff --git a/modules/styles/blocks/main/main.styl b/modules/styles/blocks/main/main.styl
      index ad3f10d..0147dc4 100755
      --- a/modules/styles/blocks/main/main.styl
      +++ b/modules/styles/blocks/main/main.styl
      @@ -27,7 +27,7 @@ $main-loud
               padding 0
       
           &__header
      -        margin 40px 0 16px 0
      +        margin 20px 0 -30px 0
       
           &__header_center
               border 0
      
      From b8bf03f5ec9917d3748cb091058995d85a192525 Mon Sep 17 00:00:00 2001
      From: Alexey Pyltsyn 
      Date: Sun, 14 Apr 2019 18:30:39 +0300
      Subject: [PATCH 082/218] Fix name of GitHub
      
      ---
       locales/en.yml                   | 2 +-
       locales/ja.yml                   | 2 +-
       locales/ru.yml                   | 2 +-
       locales/zh.yml                   | 2 +-
       modules/frontpage/locales/en.yml | 2 +-
       modules/frontpage/locales/ja.yml | 2 +-
       modules/frontpage/locales/ru.yml | 2 +-
       modules/frontpage/locales/zh.yml | 2 +-
       8 files changed, 8 insertions(+), 8 deletions(-)
      
      diff --git a/locales/en.yml b/locales/en.yml
      index 7b01de3..0fd63ad 100755
      --- a/locales/en.yml
      +++ b/locales/en.yml
      @@ -64,7 +64,7 @@ site:
           - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
           - If you can't understand something in the article – please elaborate.
       
      -  edit_on_github: Edit on Github
      +  edit_on_github: Edit on GitHub
         error: error
         close: close
       
      diff --git a/locales/ja.yml b/locales/ja.yml
      index dda0c39..ca2ef32 100755
      --- a/locales/ja.yml
      +++ b/locales/ja.yml
      @@ -64,7 +64,7 @@ site:
           - 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
           - あなたが記事の中で理解できないことがあれば、詳しく説明してください。
       
      -  edit_on_github: Githubで編集
      +  edit_on_github: GitHubで編集
         error: エラー
         close: 閉じる
       
      diff --git a/locales/ru.yml b/locales/ru.yml
      index 8db0253..e9d499a 100755
      --- a/locales/ru.yml
      +++ b/locales/ru.yml
      @@ -66,7 +66,7 @@ site:
         meta:
           description: 'Современный учебник JavaScript, начиная с основ, включающий в себя много тонкостей и фишек JavaScript/DOM.'
       
      -  edit_on_github: Редактировать на Github
      +  edit_on_github: Редактировать на GitHub
         error: ошибка
         close: закрыть
       
      diff --git a/locales/zh.yml b/locales/zh.yml
      index bcee06e..5daa8bf 100644
      --- a/locales/zh.yml
      +++ b/locales/zh.yml
      @@ -64,7 +64,7 @@ site:
           - 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
           - 如果你无法理解文章中的内容 — 请详细说明。
       
      -  edit_on_github: 在 Github 上编辑
      +  edit_on_github: 在 GitHub 上编辑
         error: 错误
         close: 关闭
       
      diff --git a/modules/frontpage/locales/en.yml b/modules/frontpage/locales/en.yml
      index 3c1b2dd..d722324 100644
      --- a/modules/frontpage/locales/en.yml
      +++ b/modules/frontpage/locales/en.yml
      @@ -1,5 +1,5 @@
       modern_javascript_tutorial: "The Modern JavaScript Tutorial"
      -view_github: "view on Github"
      +view_github: "view on GitHub"
       search_placeholder: "Search in the tutorial"
       search_button: "Search"
       share_text: "Share"
      diff --git a/modules/frontpage/locales/ja.yml b/modules/frontpage/locales/ja.yml
      index a857c2b..6b86ada 100644
      --- a/modules/frontpage/locales/ja.yml
      +++ b/modules/frontpage/locales/ja.yml
      @@ -1,5 +1,5 @@
       modern_javascript_tutorial: "現代の JavaScript チュートリアル"
      -view_github: "Githubで見る"
      +view_github: "GitHubで見る"
       search_placeholder: "チュートリアル内を検索"
       search_button: "検索"
       share_text: "シェア"
      diff --git a/modules/frontpage/locales/ru.yml b/modules/frontpage/locales/ru.yml
      index 41a9dbb..c9f46dd 100644
      --- a/modules/frontpage/locales/ru.yml
      +++ b/modules/frontpage/locales/ru.yml
      @@ -1,5 +1,5 @@
       modern_javascript_tutorial: "Современный учебник JavaScript"
      -view_github: "смотреть на Github"
      +view_github: "смотреть на GitHub"
       search_placeholder: "Поиск по учебнику"
       search_button: "Найти"
       share_text: "Поделиться"
      diff --git a/modules/frontpage/locales/zh.yml b/modules/frontpage/locales/zh.yml
      index a51c554..a4cdec4 100644
      --- a/modules/frontpage/locales/zh.yml
      +++ b/modules/frontpage/locales/zh.yml
      @@ -1,5 +1,5 @@
       modern_javascript_tutorial: "现代 JavaScript 教程"
      -view_github: "在 Github 上查看"
      +view_github: "在 GitHub 上查看"
       search_placeholder: "在教程中搜索"
       search_button: "搜索"
       share_text: "分享"
      
      From 8f78afbae28808c12062e0c6a2d2277ae0a9a9ba Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 14 Apr 2019 23:09:22 +0300
      Subject: [PATCH 083/218] .tree -> .roots
      
      ---
       modules/frontpage/controller/frontpage.js                     | 4 ++--
       .../frontpage/templates/blocks/_frontpage-content/index.pug   | 2 +-
       2 files changed, 3 insertions(+), 3 deletions(-)
      
      diff --git a/modules/frontpage/controller/frontpage.js b/modules/frontpage/controller/frontpage.js
      index 4f4a358..0f436bc 100755
      --- a/modules/frontpage/controller/frontpage.js
      +++ b/modules/frontpage/controller/frontpage.js
      @@ -40,12 +40,12 @@ exports.get = async function (ctx, next) {
       // path
       // siblings
       async function renderTop() {
      -  const tree = TutorialTree.instance().tree;
      +  const roots = TutorialTree.instance().roots;
       
         let articles = {};
       
         // render top-level content
      -  for (let slug of tree) {
      +  for (let slug of roots) {
           let article = TutorialTree.instance().bySlug(slug);
       
           let renderer = new ArticleRenderer();
      diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.pug b/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      index 6562f63..a7c43ea 100755
      --- a/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      +++ b/modules/frontpage/templates/blocks/_frontpage-content/index.pug
      @@ -43,5 +43,5 @@ mixin frontpage-content(data)
               +e.title= t('tutorial.part3.title')
               +e.description!= t('tutorial.part3.content')
               +b.list
      -          +partList(tutorialTree.tree.slice(2))
      +          +partList(tutorialTree.roots.slice(2))
       
      
      From dd005e5dbae30b6effffb7d72d2f7a45e2642604 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Tue, 16 Apr 2019 12:32:45 +0300
      Subject: [PATCH 084/218] fix headers
      
      ---
       modules/frontpage/templates/frontpage.pug | 1 +
       modules/styles/blocks/main/main.styl      | 2 +-
       modules/styles/blocks/page/page.styl      | 4 ++++
       3 files changed, 6 insertions(+), 1 deletion(-)
      
      diff --git a/modules/frontpage/templates/frontpage.pug b/modules/frontpage/templates/frontpage.pug
      index 2368344..6bdcb0a 100755
      --- a/modules/frontpage/templates/frontpage.pug
      +++ b/modules/frontpage/templates/frontpage.pug
      @@ -7,6 +7,7 @@ block append variables
         - var title = t('tutorial.modern_javascript_tutorial')
         - var header = t('tutorial.modern_javascript_tutorial')
         - var layout_main_class = 'main_width-limit-wide'
      +  - var layout_page_class = 'page_frontpage'
         - var content_class = 'frontpage'
       
       
      diff --git a/modules/styles/blocks/main/main.styl b/modules/styles/blocks/main/main.styl
      index 0147dc4..ad3f10d 100755
      --- a/modules/styles/blocks/main/main.styl
      +++ b/modules/styles/blocks/main/main.styl
      @@ -27,7 +27,7 @@ $main-loud
               padding 0
       
           &__header
      -        margin 20px 0 -30px 0
      +        margin 40px 0 16px 0
       
           &__header_center
               border 0
      diff --git a/modules/styles/blocks/page/page.styl b/modules/styles/blocks/page/page.styl
      index 07a2bd7..924d501 100755
      --- a/modules/styles/blocks/page/page.styl
      +++ b/modules/styles/blocks/page/page.styl
      @@ -33,6 +33,10 @@
           &_sidebar_on &__sidebar
               transform translateX(0)
       
      +    &_frontpage
      +        .main__header
      +            margin 20px 0 -30px 0
      +
           &__nav
               position fixed
               top 50%
      
      From 3c3c5557e64463edfca446c64e8ee426e1c95920 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 17 Apr 2019 00:08:57 +0300
      Subject: [PATCH 085/218] Update index.js
      
      ---
       modules/config/index.js | 20 +++++++++-----------
       1 file changed, 9 insertions(+), 11 deletions(-)
      
      diff --git a/modules/config/index.js b/modules/config/index.js
      index 53bf510..fe46e0a 100755
      --- a/modules/config/index.js
      +++ b/modules/config/index.js
      @@ -65,17 +65,15 @@ let config = module.exports = {
         handlers: require('./handlers')
       };
       
      -let repos = require('engine/koa/tutorial/repos');
      -for(let repo in repos) {
      -  if (repos[repo].lang === lang) {
      -    config.tutorialRepo = {
      -      github: repo,
      -      branch: repos[repo].branch || 'master',
      -      tree: new URL('https://github.com/' + repo + '/tree/' + (repos[repo].branch || 'master')),
      -      blob: new URL('https://github.com/' + repo + '/blob/' + (repos[repo].branch || 'master'))
      -    }
      -  }
      -}
      +let repo = `javascript-tutorial/${config.lang}.javascript.info`;
      +
      +config.tutorialRepo = {
      +  github: repo,
      +  url:    new URL('https://github.com/' + repo),
      +  tree:   new URL('https://github.com/' + repo + '/tree/master'),
      +  blob:   new URL('https://github.com/' + repo + '/blob/master')
      +};
      +
       
       require.extensions['.yml'] = function(module, filename) {
         module.exports = yaml.safeLoad(fs.readFileSync(filename, 'utf-8'));
      
      From 4748105f13ad160305f0beaa7f9456a32a060833 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Wed, 17 Apr 2019 09:51:04 +0300
      Subject: [PATCH 086/218] fix task solution
      
      ---
       modules/styles/blocks/04-links/links.styl |  3 ++
       modules/styles/blocks/task/task.styl      | 60 +++++++++++------------
       2 files changed, 33 insertions(+), 30 deletions(-)
      
      diff --git a/modules/styles/blocks/04-links/links.styl b/modules/styles/blocks/04-links/links.styl
      index f1e5c79..0713e7c 100755
      --- a/modules/styles/blocks/04-links/links.styl
      +++ b/modules/styles/blocks/04-links/links.styl
      @@ -38,6 +38,9 @@ $link-type
       :visited
           text-decoration none
       
      +a[href*="_[stub]_"]
      +    color gray !important
      +
       a:hover,
       a:active
           color link_hover_color
      diff --git a/modules/styles/blocks/task/task.styl b/modules/styles/blocks/task/task.styl
      index 3da8383..67fd19f 100755
      --- a/modules/styles/blocks/task/task.styl
      +++ b/modules/styles/blocks/task/task.styl
      @@ -1,15 +1,15 @@
       .task
           &__header
      -        margin 40px 0 12px
      +        margin 16px 0 16px
       
           &__title-wrap
               padding-right 45px
       
           & &__title
               margin 0
      -        line-height 1
      +        line-height 28px
               display inline
      -        font-size 128%
      +        font-size 20px
               font-weight bold
       
           & &__title a
      @@ -39,32 +39,29 @@
               margin-right 30px
       
           &__solution
      -        @extend $link-button
      -
      -    &__answer_open &__solution
      -        position relative
      -        background #568DCA
      -        color #fff
      -        text-decoration none
      +        @extend $button-reset
      +        display inline-block
      +        margin 18px 0
      +        padding 4px 12px
      +        border-radius 15px
      +        border 2px solid #568DCA
      +        font inherit
               line-height 18px
      -        padding 4px 15px
      -        margin -4px -15px
      -        border-radius 12px
      +        color #568DCA
      +        text-decoration none
       
      -    &__answer_open &__solution:hover
      -        color #fff
      +        &:hover
      +            color link_hover_color
      +            border-color link_hover_color
      +            text-decoration none
       
      -    &__answer_open &__solution::after
      -        content ""
      -        position absolute
      -        border-bottom 14px solid #F7F6EA
      -        border-right 9px solid transparent
      -        border-left 9px solid transparent
      -        width 0
      -        height 0
      -        top 100%
      -        left 50%
      -        margin 4px 0 0 -7px
      +        &[disabled]
      +        &[disabled]:hover
      +            cursor default
      +            opacity 0.5
      +            color #568DCA
      +            border-color #568DCA
      +            text-decoration none
       
           &__content,
           &__content p,
      @@ -95,7 +92,10 @@
               top -17px
               right -17px
       
      -    &__answer_open &__answer
      +        @media phone
      +            right -13px
      +
      +    &_answer_open &__answer
               display block
       
           &__step-show
      @@ -119,16 +119,16 @@
               border-top 1px solid #edece2
       
           &__step:first-child &__step-show::before,
      -    &__step_open + &__step &__step-show::before
      +    &_step_open + &__step &__step-show::before
               display none
       
           &__step &__answer-content
               display none
       
      -    &__step_open &__answer-content
      +    &_step_open &__answer-content
               display block
       
      -    &__step_open &__step-show
      +    &_step_open &__step-show
               display none
       
           & &__step-title
      
      From 8230cdfe11a96b8f31cb6a0ca9b22d2aedbb222a Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Tue, 23 Apr 2019 17:32:25 +0200
      Subject: [PATCH 087/218] fix
      
      ---
       edit.cmd                | 2 +-
       modules/config/index.js | 2 +-
       package.json            | 4 ++--
       3 files changed, 4 insertions(+), 4 deletions(-)
      
      diff --git a/edit.cmd b/edit.cmd
      index 7d802c6..bd1a5e1 100644
      --- a/edit.cmd
      +++ b/edit.cmd
      @@ -1,6 +1,6 @@
       @if "%~1"=="" goto usage
       
      -@set TUTORIAL_ROOT=../javascript-tutorial-%1
      +@set TUTORIAL_ROOT=../%1.javascript.info
       
       @if "%~2"=="" (
           set NODE_LANG=en
      diff --git a/modules/config/index.js b/modules/config/index.js
      index fe46e0a..445d3c2 100755
      --- a/modules/config/index.js
      +++ b/modules/config/index.js
      @@ -56,7 +56,7 @@ let config = module.exports = {
         // public files, served by nginx
         publicRoot:            path.join(process.cwd(), 'public', lang),
         // private files, for expiring links, not directly accessible
      -  tutorialRoot:          env.TUTORIAL_ROOT || path.join(process.cwd(), '..', 'javascript-tutorial-' + lang),
      +  tutorialRoot:          env.TUTORIAL_ROOT || path.join(process.cwd(), '..', lang + '.javascript.info'),
         tmpRoot:               path.join(process.cwd(), 'tmp', lang),
         // js/css build versions
         cacheRoot:          path.join(process.cwd(), 'cache', lang),
      diff --git a/package.json b/package.json
      index 796562f..b88c315 100755
      --- a/package.json
      +++ b/package.json
      @@ -78,11 +78,11 @@
         "engineStrict": true,
         "repository": {
           "type": "git",
      -    "url": "https://github.com/javascript-tutorial/javascript-tutorial-server.git"
      +    "url": "https://github.com/javascript-tutorial/server.git"
         },
         "author": "Ilya Kantor",
         "license": "CC BY-NC-SA 3.0",
         "bugs": {
      -    "url": "https://github.com/javascript-tutorial/javascript-tutorial-server/issues"
      +    "url": "https://github.com/javascript-tutorial/server/issues"
         }
       }
      
      From da55b93677d2106d6b0f3584abd783a32f2f37a9 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 12 May 2019 10:23:13 +0300
      Subject: [PATCH 088/218] translate stub
      
      ---
       modules/translate.js | 10 ++++++++++
       1 file changed, 10 insertions(+)
       create mode 100644 modules/translate.js
      
      diff --git a/modules/translate.js b/modules/translate.js
      new file mode 100644
      index 0000000..1aca2d9
      --- /dev/null
      +++ b/modules/translate.js
      @@ -0,0 +1,10 @@
      +// stub for translate
      +exports.TutorialStats = class Translate {
      +  instance() {
      +    return new Translate();
      +  }
      +
      +  isTranslated() {
      +    return true;
      +  }
      +};
      
      From 4330cae26aba9f0736263e3abc89311014269961 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 12 May 2019 10:26:47 +0300
      Subject: [PATCH 089/218] translate stub
      
      ---
       modules/translate.js | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/modules/translate.js b/modules/translate.js
      index 1aca2d9..a2c335b 100644
      --- a/modules/translate.js
      +++ b/modules/translate.js
      @@ -1,6 +1,6 @@
       // stub for translate
       exports.TutorialStats = class Translate {
      -  instance() {
      +  static instance() {
           return new Translate();
         }
       
      
      From 8d0516d03c1e20f59d1f690a58976d8e24c21e80 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Fri, 31 May 2019 09:24:16 +0300
      Subject: [PATCH 090/218] gulp3 -> 4
      
      ---
       gulpfile.js            | 25 ++++++++-----------
       package.json           |  8 +++----
       tasks/livereload.js    | 54 +++++++++++++++++++++---------------------
       tasks/nodemon.js       | 48 +++++++++++++++++--------------------
       tasks/server.js        |  8 +++----
       tasks/syncResources.js | 46 ++++++++++++++++-------------------
       tasks/test.js          | 16 ++++++-------
       tasks/watch.js         | 34 ++++++++++++++------------
       tasks/webpack.js       | 23 +++++++++---------
       9 files changed, 125 insertions(+), 137 deletions(-)
      
      diff --git a/gulpfile.js b/gulpfile.js
      index 53361be..d3e7bd7 100755
      --- a/gulpfile.js
      +++ b/gulpfile.js
      @@ -8,7 +8,7 @@ const glob = require('glob');
       const path = require('path');
       const fs = require('fs');
       const {lazyRequireTask, requireModuleTasks} = require('engine/gulp/requireModuleTasks');
      -const runSequence = require('run-sequence');
      +const {task, series, parallel} = require('gulp');
       
       const config = require('config');
       
      @@ -18,7 +18,7 @@ process.on('uncaughtException', function(err) {
       });
       
       
      -gulp.task("nodemon", lazyRequireTask('./tasks/nodemon', {
      +task("nodemon", lazyRequireTask('./tasks/nodemon', {
         // shared client/server code has require('template.jade) which precompiles template on run
         // so I have to restart server to pickup the template change
         ext:    "js,yml",
      @@ -30,7 +30,7 @@ gulp.task("nodemon", lazyRequireTask('./tasks/nodemon', {
         watch:  ["modules"]
       }));
       
      -gulp.task("livereload", lazyRequireTask("./tasks/livereload", {
      +task("livereload", lazyRequireTask("./tasks/livereload", {
         // watch files *.*, not directories, no need to reload for new/removed files,
         // we're only interested in changes
       
      @@ -51,35 +51,30 @@ if (!process.env.TEST_E2E || process.env.CI && process.env.TRAVIS_SECURE_ENV_VAR
         testSrcs.push('!modules/**/test/e2e/*.js');
       }
       
      -gulp.task("test", lazyRequireTask('./tasks/test', {
      +task("test", lazyRequireTask('./tasks/test', {
         src: testSrcs,
         reporter: 'spec',
         timeout: 100000 // big timeout for webdriver e2e tests
       }));
       
       
      -gulp.task('deploy', function(callback) {
      -  runSequence("deploy:build", "deploy:update", callback);
      -});
      -
      -
      -gulp.task('webpack', lazyRequireTask('./tasks/webpack'));
      +task('webpack', lazyRequireTask('./tasks/webpack'));
       // gulp.task('webpack-dev-server', lazyRequireTask('./tasks/webpackDevServer'));
       
       
      -gulp.task('build', ['webpack']);
      +task('build', series('webpack'));
       
      -gulp.task('server', lazyRequireTask('./tasks/server'));
      +task('server', lazyRequireTask('./tasks/server'));
       
      -gulp.task('edit', ['webpack', 'engine:koa:tutorial:importWatch', 'livereload', 'server']);
      +task('edit', parallel('webpack', 'engine:koa:tutorial:importWatch', 'livereload', 'server'));
       
       
      -gulp.task('dev', ['nodemon', 'livereload', 'webpack']);
      +task('dev', parallel('nodemon', 'livereload', 'webpack'));
       
       gulp.on('err', function(gulpErr) {
         if (gulpErr.err) {
           // cause
           console.error("Gulp error details", [gulpErr.err.message, gulpErr.err.stack, gulpErr.err.errors].filter(Boolean));
         }
      +  mongoose.disconnect();
       });
      -
      diff --git a/package.json b/package.json
      index b88c315..897f89d 100755
      --- a/package.json
      +++ b/package.json
      @@ -1,6 +1,6 @@
       {
         "name": "javascript-nodejs",
      -  "version": "1.0.0",
      +  "version": "2.0.0",
         "private": true,
         "scripts": {
           "prod": "NODE_ENV=production NODE_PATH=./modules node ./bin/server.js",
      @@ -29,9 +29,8 @@
           "file-loader": "^1.1",
           "fs-extra": "*",
           "gm": "*",
      -    "gulp": "^3",
      -    "gulp-livereload": "*",
      -    "gulp-ll": "*",
      +    "gulp": "^4",
      +    "gulp-livereload": "^4",
           "image-size": "*",
           "js-yaml": "*",
           "json-loader": "^0.5.7",
      @@ -65,7 +64,6 @@
           "pug-runtime": "^2.0.4",
           "request": "*",
           "request-promise": "*",
      -    "run-sequence": "*",
           "rupture": "*",
           "style-loader": "^0",
           "stylus-loader": "^3",
      diff --git a/tasks/livereload.js b/tasks/livereload.js
      index ff06dad..658ad63 100644
      --- a/tasks/livereload.js
      +++ b/tasks/livereload.js
      @@ -5,36 +5,36 @@ let chokidar = require('chokidar');
       
       // options.watch must NOT be www/**, because that breaks (why?!?) supervisor reloading
       // www/**/*.* is fine
      -module.exports = function(options) {
      +module.exports = async function(options) {
       
         // listen to changes after the file events finish to arrive
         // no one is going to livereload right now anyway
      -  return function(callback) {
      -    livereload.listen();
      -
      -    // reload once after all scripts are rebuit
      -    livereload.changedSoon = throttle(livereload.changed, 1000, {leading: false});
      -    //livereload.changedVerySoon = _.throttle(livereload.changed, 100, {leading: false});
      -
      -    setTimeout(function() {
      -      console.log("livereload: listen on change " + options.watch);
      -
      -      chokidar.watch(options.watch, {
      -        awaitWriteFinish: {
      -          stabilityThreshold: 300,
      -          pollInterval: 100
      -        }
      -      }).on('change', function(changed) {
      -        if (changed.match(/\.(js|map)/)) {
      -          // full page reload
      -          livereload.changedSoon(changed);
      -        } else {
      -          livereload.changed(changed);
      -        }
      -      });
      -
      -    }, 1000);
      -  };
      +  livereload.listen();
      +
      +  // reload once after all scripts are rebuit
      +  livereload.changedSoon = throttle(livereload.changed, 1000, {leading: false});
      +  //livereload.changedVerySoon = _.throttle(livereload.changed, 100, {leading: false});
      +
      +  setTimeout(function() {
      +    console.log("livereload: listen on change " + options.watch);
      +
      +    chokidar.watch(options.watch, {
      +      awaitWriteFinish: {
      +        stabilityThreshold: 300,
      +        pollInterval: 100
      +      }
      +    }).on('change', function(changed) {
      +      if (changed.match(/\.(js|map)/)) {
      +        // full page reload
      +        livereload.changedSoon(changed);
      +      } else {
      +        livereload.changed(changed);
      +      }
      +    });
      +
      +  }, 1000);
      +
      +  await new Promise(resolve => {});
       };
       
       
      diff --git a/tasks/nodemon.js b/tasks/nodemon.js
      index ffb8c39..6b81569 100755
      --- a/tasks/nodemon.js
      +++ b/tasks/nodemon.js
      @@ -1,32 +1,28 @@
       const nodemon = require('nodemon');
       
      -module.exports = function(options) {
      +module.exports = async function(options) {
       
      -  return function(callback) {
      -
      -    nodemon({
      -      // shared client/server code has require('template.pug) which precompiles template on run
      -      // so I have to restart server to pickup the template change
      -      ext:      "js",
      -      verbose:  true,
      -      delay: 10,
      -      nodeArgs: process.env.NODE_DEBUG ? ['--inspect'] : [],
      -      script:   "./bin/server.js",
      -      //ignoreRoot: ['.git', 'node_modules'].concat(glob.sync('{handlers,modules}/**/client')), // ignore handlers' client code
      -      ignore:   ['**/client/', '**/photoCut/', 'public'], // ignore handlers' client code
      -      watch:    ["modules"],
      -      watchOptions: {
      -        awaitWriteFinish: {
      -          stabilityThreshold: 300,
      -          pollInterval: 100
      -        }
      +  nodemon({
      +    // shared client/server code has require('template.pug) which precompiles template on run
      +    // so I have to restart server to pickup the template change
      +    ext:          "js",
      +    verbose:      true,
      +    delay:        10,
      +    nodeArgs:     process.env.NODE_DEBUG ? ['--inspect'] : [],
      +    script:       "./bin/server.js",
      +    //ignoreRoot: ['.git', 'node_modules'].concat(glob.sync('{handlers,modules}/**/client')), // ignore handlers' client code
      +    ignore:       ['**/client/', '**/photoCut/', 'public'], // ignore handlers' client code
      +    watch:        ["modules"],
      +    watchOptions: {
      +      awaitWriteFinish: {
      +        stabilityThreshold: 300,
      +        pollInterval:       100
             }
      -    })
      -      .on('log', function (log){
      -        console.log(log.colour);
      -      });
      -
      -  };
      -
      +    }
      +  })
      +    .on('log', function(log) {
      +      console.log(log.colour);
      +    });
       
      +  await new Promise(resolve => {});
       };
      diff --git a/tasks/server.js b/tasks/server.js
      index 0333f36..bea2aae 100755
      --- a/tasks/server.js
      +++ b/tasks/server.js
      @@ -1,9 +1,7 @@
       let app = require('engine/koa/app');
       let config = require('config');
       
      -module.exports = function() {
      -  return function() {
      -    return app.waitBootAndListen(config.server.host, config.server.port);
      -  }
      +module.exports = async function() {
      +  // this tasks never ends, because it runs the server
      +  await app.waitBootAndListen(config.server.host, config.server.port);
       };
      -
      diff --git a/tasks/syncResources.js b/tasks/syncResources.js
      index ee69009..7bb64b3 100755
      --- a/tasks/syncResources.js
      +++ b/tasks/syncResources.js
      @@ -2,38 +2,34 @@ const fs = require('fs');
       const fse = require('fs-extra');
       const glob = require('glob');
       
      -module.exports = function(resources) {
      +module.exports = async function(resources) {
       
      -  return function(callback) {
      +  for (let src in resources) {
      +    let dst = resources[src];
       
      -    for (let src in resources) {
      -      let dst = resources[src];
      +    let files = glob.sync(src + '/**');
      +    for (let i = 0; i < files.length; i++) {
      +      let srcPath = files[i];
       
      -      let files = glob.sync(src + '/**');
      -      for (let i = 0; i < files.length; i++) {
      -        let srcPath = files[i];
      +      let dstPath = srcPath.replace(src, dst);
      +      let srcStat = fs.statSync(srcPath);
       
      -        let dstPath = srcPath.replace(src, dst);
      -        let srcStat = fs.statSync(srcPath);
      -
      -        if (srcStat.isDirectory()) {
      -          fse.ensureDirSync(dstPath);
      -          continue;
      -        }
      -
      -        let dstMtime = 0;
      -        try {
      -          dstMtime = fs.statSync(dstPath).mtime;
      -        } catch(e) {}
      +      if (srcStat.isDirectory()) {
      +        fse.ensureDirSync(dstPath);
      +        continue;
      +      }
       
      -        if (srcStat.mtime > dstMtime) {
      -          fse.copySync(srcPath, dstPath);
      -        }
      +      let dstMtime = 0;
      +      try {
      +        dstMtime = fs.statSync(dstPath).mtime;
      +      } catch (e) {
      +      }
       
      +      if (srcStat.mtime > dstMtime) {
      +        fse.copySync(srcPath, dstPath);
             }
      -    }
       
      -    callback();
      +    }
      +  }
       
      -  };
       };
      diff --git a/tasks/test.js b/tasks/test.js
      index 1c78c58..2e3e4f6 100755
      --- a/tasks/test.js
      +++ b/tasks/test.js
      @@ -5,15 +5,15 @@ const assert = require('assert');
       require('should');
       require('co-mocha');
       
      -module.exports = function(options) {
      +module.exports = async function(options) {
       
      -  return function() {
      -    // config needs the right env
      -    assert(process.env.NODE_ENV == 'test', "NODE_ENV=test must be set");
      -
      -    return gulp.src(options.src, {read: false})
      -      .pipe(mocha(options));
      -  };
      +  // config needs the right env
      +  assert(process.env.NODE_ENV === 'test', "NODE_ENV=test must be set");
       
      +  return new Promise(resolve => {
      +    return gulp.src(options.src, {read: false, follow: true})
      +      .pipe(mocha(options))
      +      .on('finish', resolve);
      +  })
       };
       
      diff --git a/tasks/watch.js b/tasks/watch.js
      index 799d0a9..a8ab7b9 100755
      --- a/tasks/watch.js
      +++ b/tasks/watch.js
      @@ -29,7 +29,12 @@ function pushTaskQueue(task) {
           log("queue: already exists", task);
           return;
         }
      -
      +  /* (maybe the task should be moved into the end of the queue? couldn't find any practical difference)
      +   if (~taskQueue.indexOf(task)) {
      +   log("queue: already exists, removing", task);
      +   taskQueue.splice(taskQueue.indexOf(task), 1);
      +   }
      +   */
         taskQueue.push(task);
         log("push", taskQueue);
       
      @@ -96,23 +101,22 @@ function onModify(filePath) {
       
       // options.root - where to start watching
       // options.taskMapping - regexp -> task mappings
      -module.exports = function(options) {
      +module.exports = async function(options) {
       
      -  return function(callback) {
      -    let dirs = options.dirs.map(function(dir) {
      -      return path.join(options.root, dir);
      -    });
      +  let dirs = options.dirs.map(function(dir) {
      +    return path.join(options.root, dir);
      +  });
       
      -    let watcher = chokidar.watch(dirs, {ignoreInitial: true});
      +  let watcher = chokidar.watch(dirs, {ignoreInitial: true});
       
      -    watcher.root = options.root;
      -    watcher.taskMapping = options.taskMapping;
      +  watcher.root = options.root;
      +  watcher.taskMapping = options.taskMapping;
       
      -    watcher.on('add', onModify);
      -    watcher.on('change', onModify);
      -    watcher.on('unlink', onModify);
      -    watcher.on('unlinkDir', onModify);
      -    watcher.on('addDir', onModify);
      -  };
      +  watcher.on('add', onModify);
      +  watcher.on('change', onModify);
      +  watcher.on('unlink', onModify);
      +  watcher.on('unlinkDir', onModify);
      +  watcher.on('addDir', onModify);
       
      +  await new Promise(resolve => {});
       };
      diff --git a/tasks/webpack.js b/tasks/webpack.js
      index 8fc3476..fd5b95e 100755
      --- a/tasks/webpack.js
      +++ b/tasks/webpack.js
      @@ -1,10 +1,9 @@
       let webpack = require('webpack');
       let notifier = require('node-notifier');
       
      -module.exports = function() {
      -
      -  return function(callback) {
      +module.exports = async function() {
       
      +  await new Promise((resolve, reject) => {
           let config = require('config').webpack();
       
           webpack(config, function(err, stats) {
      @@ -23,7 +22,7 @@ module.exports = function() {
       
               console.log(err);
       
      -        if (!config.watch) callback(err);
      +        if (!config.watch) reject(err);
               return;
             }
       
      @@ -44,14 +43,14 @@ module.exports = function() {
             */
       
             console.log('[webpack]', stats.toString({
      -        hash: false,
      +        hash:    false,
               version: false,
               timings: true,
      -        assets: true,
      -        chunks: false,
      +        assets:  true,
      +        chunks:  false,
               modules: false,
      -        cached: true,
      -        colors: true
      +        cached:  true,
      +        colors:  true
             }));
       
             /*
      @@ -71,8 +70,10 @@ module.exports = function() {
       
              */
       
      -      if (!config.watch) callback();
      +      if (!config.watch) {
      +        resolve();
      +      }
           });
      +  });
       
      -  };
       };
      
      From 92d1a20c4a6d1603c0c26494136d2439f2ed14b8 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Tue, 4 Jun 2019 15:33:38 +0300
      Subject: [PATCH 091/218] cross-env
      
      ---
       README.md    |  2 +-
       package.json | 10 +++++-----
       2 files changed, 6 insertions(+), 6 deletions(-)
      
      diff --git a/README.md b/README.md
      index 338688d..08211c1 100755
      --- a/README.md
      +++ b/README.md
      @@ -100,7 +100,7 @@ First, run the command that imports (and caches) the tutorial:
       
       ```
       cd /js/server
      -NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
      +cross-env NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
       ``` 
       
       In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
      diff --git a/package.json b/package.json
      index 897f89d..8e13657 100755
      --- a/package.json
      +++ b/package.json
      @@ -1,14 +1,13 @@
       {
      -  "name": "javascript-nodejs",
      +  "name": "javascript-tutorial-server",
         "version": "2.0.0",
         "private": true,
         "scripts": {
      -    "prod": "NODE_ENV=production NODE_PATH=./modules node ./bin/server.js",
           "fixperms": "sudo chown -R `id -u` .* * ~/.n*",
           "//": "test must exit with status 1 if fails, don't use | or ensure the right exit code if you do",
      -    "test": "SELENIUM_LOCAL=1 NODE_ENV=test NODE_PATH=./modules ./node_modules/.bin/gulp test",
      -    "build": "NODE_PATH=./modules ./node_modules/.bin/gulp build",
      -    "gulp": "NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules ./node_modules/.bin/gulp"
      +    "test": "cross-env SELENIUM_LOCAL=1 NODE_ENV=test NODE_PATH=./modules ./node_modules/.bin/gulp test",
      +    "build": "cross-env NODE_PATH=./modules ./node_modules/.bin/gulp build",
      +    "gulp": "cross-env NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules ./node_modules/.bin/gulp"
         },
         "precommit": "NODE_ENV=development node `which gulp` pre-commit",
         "//": "node-xmpp-client installs for linux only",
      @@ -25,6 +24,7 @@
           "chokidar": "^2.0.4",
           "clarify": "^2.1.0",
           "copy-webpack-plugin": "^4.5.2",
      +    "cross-env": "^5.2.0",
           "css-loader": "^0",
           "file-loader": "^1.1",
           "fs-extra": "*",
      
      From 46fe0e16d24d98f1ab7594e7722e969123c1a61d Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Tue, 4 Jun 2019 15:44:59 +0300
      Subject: [PATCH 092/218] minor
      
      ---
       README.md | 11 +++++++++--
       1 file changed, 9 insertions(+), 2 deletions(-)
      
      diff --git a/README.md b/README.md
      index 08211c1..67bf93e 100755
      --- a/README.md
      +++ b/README.md
      @@ -22,7 +22,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
       2. Install global Node modules:
       
           ```
      -    npm install -g bunyan gulp
      +    npm install -g bunyan gulp@4
           ```
       
       3. Create the root folder.
      @@ -100,7 +100,14 @@ First, run the command that imports (and caches) the tutorial:
       
       ```
       cd /js/server
      -cross-env NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
      +NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
      +``` 
      +
      +For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
      +
      +```
      +cd /js/server
      +cross-env NODE_LANG=en...
       ``` 
       
       In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
      
      From db6bdcef042342aea6170e2f6280553908e560d5 Mon Sep 17 00:00:00 2001
      From: "Violet.Lee" 
      Date: Tue, 18 Jun 2019 15:56:00 +0900
      Subject: [PATCH 093/218] Fix symptom that blurry dragged text in code box
      
      ---
       modules/styles/blocks/prism/01-prism.styl | 1 -
       1 file changed, 1 deletion(-)
      
      diff --git a/modules/styles/blocks/prism/01-prism.styl b/modules/styles/blocks/prism/01-prism.styl
      index 3ad8d35..cac65c7 100755
      --- a/modules/styles/blocks/prism/01-prism.styl
      +++ b/modules/styles/blocks/prism/01-prism.styl
      @@ -7,7 +7,6 @@
       code[class*="language-"],
       pre[class*="language-"]
           color black
      -    text-shadow 0 1px white
           font-family fixed_width_font
           direction ltr
           text-align left
      
      From fee8a49268ea2ab0263e9b30e3c790f43f481899 Mon Sep 17 00:00:00 2001
      From: Alexander Ivanov 
      Date: Fri, 5 Jul 2019 13:17:59 +0300
      Subject: [PATCH 094/218] Fix Linux bunyan exec
      
      ---
       edit | 5 +++++
       1 file changed, 5 insertions(+)
      
      diff --git a/edit b/edit
      index d38a3ea..713fd87 100755
      --- a/edit
      +++ b/edit
      @@ -12,5 +12,10 @@ export NODE_ENV=production
       export TUTORIAL_EDIT=1
       export NODE_PRESERVE_SYMLINKS=1
       
      +# Use a local bunyan if no other is found in the current environment
      +if ! [ -x "$(command -v bunyan)" ]; then
      +  export PATH="$PATH:./node_modules/bunyan/bin"
      +fi
      +
       npm --silent run -- gulp edit | bunyan -o short -l debug
       
      
      From ae091adf6b7b3b7f95bae6e1755e2d2f8b2bcbce Mon Sep 17 00:00:00 2001
      From: Alexander Ivanov 
      Date: Fri, 5 Jul 2019 15:31:49 +0300
      Subject: [PATCH 095/218] Add technical details to README
      
      ---
       README.md | 25 ++++++++++++++++++++++++-
       1 file changed, 24 insertions(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 67bf93e..883578a 100755
      --- a/README.md
      +++ b/README.md
      @@ -124,7 +124,30 @@ Running `./dev` uses the tutorial that was imported and cached by the previous c
       It does not "watch" tutorial text, but it reloads the server after code changes.
        
       Again, that's for developing the server code itself, not writing the tutorial.
      -    
      +
      +# The technical details
      +
      +## For Linux users
      +
      +The server's tools use [inotify](https://en.wikipedia.org/wiki/Inotify) by default on Linux to monitor directories for changes. In some cases there may be too many items to monitor.
      +
      +You can get your current inotify files watch limit by:
      +
      +```sh
      +$> cat /proc/sys/fs/inotify/max_user_watches
      +```
      +
      +When this limit is not enough to monitor all files, you have to increase the limit for the server to work properly.
      +
      +You can set a new limit temporary by:
      +
      +```sh
      +$> sudo sysctl fs.inotify.max_user_watches=524288
      +$> sudo sysctl -p
      +```
      +
      +It is very important that you refer to the documentation for your operating system to change this parameter permanently.
      +
       # Troubleshooting
       
       If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.
      
      From c2d35feb9ba4f8ef283866de2eeaede69dfe02e7 Mon Sep 17 00:00:00 2001
      From: Alexander Ivanov 
      Date: Fri, 5 Jul 2019 15:54:16 +0300
      Subject: [PATCH 096/218] Remove extra spaces at the end of lines
      
      ---
       README.md                                     | 34 +++++++++----------
       modules/styles/blocks/01-reset/reset.styl     |  1 -
       .../blocks/02-placeholders/clearfix.styl      |  2 +-
       modules/styles/blocks/summary/summary.styl    |  2 +-
       templates/blocks/textarea-input/index.pug     |  2 +-
       templates/layouts/main.pug                    |  2 +-
       6 files changed, 21 insertions(+), 22 deletions(-)
      
      diff --git a/README.md b/README.md
      index 67bf93e..4eade59 100755
      --- a/README.md
      +++ b/README.md
      @@ -6,7 +6,7 @@ This is a standalone server for the javascript tutorial https://javascript.info.
       
       You can use it to run the tutorial locally and translate it into your language.
       
      -Windows, Unix systems and macOS are supported. For Windows, you'll need to call scripts with ".cmd" extension, that are present in the code alongside with Unix versions. 
      +Windows, Unix systems and macOS are supported. For Windows, you'll need to call scripts with ".cmd" extension, that are present in the code alongside with Unix versions.
       
       # Installation
       
      @@ -14,9 +14,9 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
       
           These are required to update and run the project.
           For Windows just download and install, otherwise use standard OS install tools (packages or whatever convenient).
      -    
      -    Please use Node.js 10+. 
      -    
      +
      +    Please use Node.js 10+.
      +
           (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/).
       
       2. Install global Node modules:
      @@ -58,7 +58,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
           cd /js/server
           npm install
           ```
      -    
      +
           Run the site with the same language. Above we cloned `en` tutorial, so:
       
           ```
      @@ -73,10 +73,10 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
       
       7. Edit the tutorial
       
      -    As you edit text files in the tutorial text repository (cloned at step 5), 
      -    the webpage will reload automatically. 
      +    As you edit text files in the tutorial text repository (cloned at step 5),
      +    the webpage will reload automatically.
      +
       
      -    
       # Change server language
       
       The server uses English by default for navigation and design.
      @@ -91,7 +91,7 @@ cd /js/server
       ```
       
       Please note, the server must support that language. There must be corresponding locale files for that language in the code of the server, otherwise it exists with an error. As of now, `ru`, `en`, `zh` and `ja` are fully supported.
      -    
      +
       # Dev mode
       
       If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to do.
      @@ -101,14 +101,14 @@ First, run the command that imports (and caches) the tutorial:
       ```
       cd /js/server
       NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
      -``` 
      +```
       
       For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
       
       ```
       cd /js/server
       cross-env NODE_LANG=en...
      -``` 
      +```
       
       In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
       
      @@ -119,12 +119,12 @@ cd /js/server
       ./dev en
       ```
       
      -Running `./dev` uses the tutorial that was imported and cached by the previous command. 
      +Running `./dev` uses the tutorial that was imported and cached by the previous command.
       
       It does not "watch" tutorial text, but it reloads the server after code changes.
      - 
      +
       Again, that's for developing the server code itself, not writing the tutorial.
      -    
      +
       # Troubleshooting
       
       If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.
      @@ -135,7 +135,7 @@ If it still doesn't work – [file an issue](https://github.com/javascript-tutor
       
       Please pull the very latest git code and install latest NPM modules before publishing an issue.
       
      ---  
      -Yours,  
      -Ilya Kantor  
      +--
      +Yours,
      +Ilya Kantor
       iliakan@javascript.info
      diff --git a/modules/styles/blocks/01-reset/reset.styl b/modules/styles/blocks/01-reset/reset.styl
      index 6ab8dae..d3fbb4f 100755
      --- a/modules/styles/blocks/01-reset/reset.styl
      +++ b/modules/styles/blocks/01-reset/reset.styl
      @@ -25,4 +25,3 @@ p
       
       .invisible
           visibility hidden
      -    
      \ No newline at end of file
      diff --git a/modules/styles/blocks/02-placeholders/clearfix.styl b/modules/styles/blocks/02-placeholders/clearfix.styl
      index c9e5913..c069666 100755
      --- a/modules/styles/blocks/02-placeholders/clearfix.styl
      +++ b/modules/styles/blocks/02-placeholders/clearfix.styl
      @@ -1,5 +1,5 @@
       $clearfix
      -    &::after 
      +    &::after
               content ""
               display block
               overflow hidden
      diff --git a/modules/styles/blocks/summary/summary.styl b/modules/styles/blocks/summary/summary.styl
      index bf86aa8..1e6be59 100755
      --- a/modules/styles/blocks/summary/summary.styl
      +++ b/modules/styles/blocks/summary/summary.styl
      @@ -11,5 +11,5 @@
               padding 1px 0
               border-radius 4px
       
      -    @media print 
      +    @media print
               page-break-inside: avoid;
      diff --git a/templates/blocks/textarea-input/index.pug b/templates/blocks/textarea-input/index.pug
      index a0ccf48..e5264c6 100755
      --- a/templates/blocks/textarea-input/index.pug
      +++ b/templates/blocks/textarea-input/index.pug
      @@ -1,6 +1,6 @@
       mixin textarea-input(data)
         - data = data || {}
      -  
      +
         +b('textarea').textarea-input(class={
           '_color_red': data.color === 'red'
         })&attributes(attributes)
      diff --git a/templates/layouts/main.pug b/templates/layouts/main.pug
      index 85abec3..de01cdb 100755
      --- a/templates/layouts/main.pug
      +++ b/templates/layouts/main.pug
      @@ -9,7 +9,7 @@ block main
                 include ../blocks/breadcrumbs
       
             block over-title
      -        
      +
             if title
               - var parts = title.split('\n');
               h1.main__header-title
      
      From 60696f32ba6c64acfced013fdbcf97e404678e68 Mon Sep 17 00:00:00 2001
      From: Alexander Ivanov 
      Date: Sun, 7 Jul 2019 12:11:58 +0300
      Subject: [PATCH 097/218] Moved to Trobleshooting
      
      Signed-off-by: Alexander Ivanov 
      ---
       README.md | 28 +++++++++++++++-------------
       1 file changed, 15 insertions(+), 13 deletions(-)
      
      diff --git a/README.md b/README.md
      index cc95b40..bcd9ade 100755
      --- a/README.md
      +++ b/README.md
      @@ -125,12 +125,24 @@ It does not "watch" tutorial text, but it reloads the server after code changes.
       
       Again, that's for developing the server code itself, not writing the tutorial.
       
      -# The technical details
      +# Troubleshooting
      +
      +If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.
       
      -## For Linux users
      +Please ensure you have Node.js version 10+ (`node -v` shows the version).
      +
      +If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). Please mention OS and Node.js version.
      +
      +Please pull the very latest git code and install latest NPM modules before publishing an issue.
      +
      +## Known Issues
      +
      +### For Linux users. inotify and monitored files
       
       The server's tools use [inotify](https://en.wikipedia.org/wiki/Inotify) by default on Linux to monitor directories for changes. In some cases there may be too many items to monitor.
       
      +_*!* Samples code below work correctly for Ubuntu_.
      +
       You can get your current inotify files watch limit by:
       
       ```sh
      @@ -146,17 +158,7 @@ $> sudo sysctl fs.inotify.max_user_watches=524288
       $> sudo sysctl -p
       ```
       
      -It is very important that you refer to the documentation for your operating system to change this parameter permanently.
      -
      -# Troubleshooting
      -
      -If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.
      -
      -Please ensure you have Node.js version 10+ (`node -v` shows the version).
      -
      -If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). Please mention OS and Node.js version.
      -
      -Please pull the very latest git code and install latest NPM modules before publishing an issue.
      +It is very important that you refer to the documentation for your operating system to change this parameter permanently
       
       --
       Yours,
      
      From 5f5daefa961941ec375f93206cce153ed4645b7a Mon Sep 17 00:00:00 2001
      From: Alexander Ivanov 
      Date: Sun, 7 Jul 2019 12:27:55 +0300
      Subject: [PATCH 098/218] Fix Linux bunyan exec (./dev)
      
      ---
       dev | 5 +++++
       1 file changed, 5 insertions(+)
      
      diff --git a/dev b/dev
      index 62a5092..9b3fc16 100755
      --- a/dev
      +++ b/dev
      @@ -12,5 +12,10 @@ export SITE_HOST=http://javascript.local
       export TUTORIAL_EDIT=
       export NODE_PRESERVE_SYMLINKS=1
       
      +# Use a local bunyan if no other is found in the current environment
      +if ! [ -x "$(command -v bunyan)" ]; then
      +  export PATH="$PATH:./node_modules/bunyan/bin"
      +fi
      +
       npm --silent run gulp dev | bunyan -o short -l debug
       
      
      From 5c0fcf06e9c80968a090d6a487dd8b03de5f9f13 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Sun, 28 Jul 2019 16:32:58 +0300
      Subject: [PATCH 099/218] Update README.md
      
      ---
       README.md | 19 +++++++++++++++++--
       1 file changed, 17 insertions(+), 2 deletions(-)
      
      diff --git a/README.md b/README.md
      index 4eade59..1d286ee 100755
      --- a/README.md
      +++ b/README.md
      @@ -125,9 +125,24 @@ It does not "watch" tutorial text, but it reloads the server after code changes.
       
       Again, that's for developing the server code itself, not writing the tutorial.
       
      -# Troubleshooting
      +# Importing images
      +
      +You only need to re-import images if you change them, or change their translations in `images.yml`.
      +
      +To do that, you need to have Mac and Sketch editor installed.
       
      -If you have a very old copy of the English tutorial, please rename `1-js/05-data-types/09-destructuring-assignment/1-destructuring-assignment` to `1-js/05-data-types/09-destructuring-assignment/1-destruct-user`.
      +1. The task to get YAML with strings in image (for translation, to add to `images.yml`):
      +    ```
      +    cd /js/server
      +    NODE_LANG=ru npm run gulp engine:koa:tutorial:imageYaml --image hello.svg
      +    ```
      +2. The task to import images from Sketch and apply translations:
      +    ```
      +    cd /js/server
      +    NODE_LANG=ru npm run gulp engine:koa:tutorial:figuresImport
      +    ```
      +    
      +# Troubleshooting
       
       Please ensure you have Node.js version 10+ (`node -v` shows the version).
       
      
      From b87e71170cd72821ac3cc1e770a9417541d29452 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:28:56 +0300
      Subject: [PATCH 100/218] Update README.md
      
      ---
       README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
       1 file changed, 50 insertions(+), 5 deletions(-)
      
      diff --git a/README.md b/README.md
      index 1d286ee..9ad7529 100755
      --- a/README.md
      +++ b/README.md
      @@ -125,22 +125,67 @@ It does not "watch" tutorial text, but it reloads the server after code changes.
       
       Again, that's for developing the server code itself, not writing the tutorial.
       
      -# Importing images
      +# Translating images
       
      -You only need to re-import images if you change them, or change their translations in `images.yml`.
      +Most pictures are in SVG format. Strings inside it are usually just text, it can be replaced.
       
      -To do that, you need to have Mac and Sketch editor installed.
      +That's great, as there are many strings in English in images, like tips, notes, etc. They look nice when translated.
      +
      +Image translations reside in `images.yml` file in the repository root, for example: 
      +
      +The file format is called "YAML", it's quite easy to understand:
      +
      +```yaml
      +code-style.svg:  # image file name
      +  "No space":    # English string
      +    text: "Без пробелов" # translation
      +    position: "center" # (optional) "center" or "right" - to position translated string.
      +```
      +
      +The translated string may become longer or shorter. If we have nice pictures, strings move around and need to be repositioned:
       
      -1. The task to get YAML with strings in image (for translation, to add to `images.yml`):
      +- `position: "right"` makes sure that after the translation the string right edge is at the same place, like this (below is the translated string):
      +    ```
      +    hello world
      +        你好世界
      +    ```
      +- `position: "center"` centers the translated string, good if you have a vertical diagram.
      +    ```
      +    hello world
      +      你好世界
      +    ```
      +- (default) the string left edge is the same:
      +    ```
      +    hello world
      +    你好世界
      +    ```
      +
      +
      +
      +## Extract strings
      +
      +The task to get all strings from an image (for translation, to add to `images.yml`):
           ```
           cd /js/server
           NODE_LANG=ru npm run gulp engine:koa:tutorial:imageYaml --image hello.svg
           ```
      -2. The task to import images from Sketch and apply translations:
      +
      +
      +# Importing images
      +
      +If you modify `figures.sketch` file with pictures (need Mac and Sketch editor installed for that), images are re-imported automatically by `./edit` script.
      +
      +To do that manually:
           ```
           cd /js/server
           NODE_LANG=ru npm run gulp engine:koa:tutorial:figuresImport
           ```
      +
      +You only need to re-import images if you change them, or change their translations in `images.yml`.
      +
      +To do that, you need to have Mac and Sketch editor installed.
      +
      +
           
       # Troubleshooting
       
      
      From 1af4e8de366bed3af9be8497444f60f70c6966ef Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:29:27 +0300
      Subject: [PATCH 101/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 9ad7529..9975de7 100755
      --- a/README.md
      +++ b/README.md
      @@ -127,7 +127,7 @@ Again, that's for developing the server code itself, not writing the tutorial.
       
       # Translating images
       
      -Most pictures are in SVG format. Strings inside it are usually just text, it can be replaced.
      +Most pictures are in SVG format. Strings inside it are usually just text, they can be replaced.
       
       That's great, as there are many strings in English in images, like tips, notes, etc. They look nice when translated.
       
      
      From 3a9c952f3c8f22759b303acfa67607538d457008 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:39:25 +0300
      Subject: [PATCH 102/218] Update README.md
      
      ---
       README.md | 105 +++++++++++++++++++++++++++---------------------------
       1 file changed, 53 insertions(+), 52 deletions(-)
      
      diff --git a/README.md b/README.md
      index 9975de7..3666a51 100755
      --- a/README.md
      +++ b/README.md
      @@ -21,7 +21,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
       
       2. Install global Node modules:
       
      -    ```
      +    ```bash
           npm install -g bunyan gulp@4
           ```
       
      @@ -31,7 +31,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
       
       4. Clone the tutorial server into it:
       
      -    ```
      +    ```bash
           cd /js
           git clone https://github.com/javascript-tutorial/server
           git clone https://github.com/javascript-tutorial/engine server/modules/engine
      @@ -45,7 +45,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
       
           The English version is `en.javascript.info`.
       
      -    ```
      +    ```bash
           cd /js
           git clone https://github.com/javascript-tutorial/en.javascript.info
           ```
      @@ -54,14 +54,14 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call
       
           Install local modules:
       
      -    ```
      +    ```bash
           cd /js/server
           npm install
           ```
       
           Run the site with the same language. Above we cloned `en` tutorial, so:
       
      -    ```
      +    ```bash
           ./edit en
           ```
       
      @@ -85,53 +85,20 @@ You can set another language it with the second argument of `edit`.
       
       E.g. if you cloned `ru` tutorial, it makes sense to use `ru` locale for the server as well:
       
      -```
      +```bash
       cd /js/server
       ./edit ru ru
       ```
       
       Please note, the server must support that language. There must be corresponding locale files for that language in the code of the server, otherwise it exists with an error. As of now, `ru`, `en`, `zh` and `ja` are fully supported.
       
      -# Dev mode
      -
      -If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to do.
      -
      -First, run the command that imports (and caches) the tutorial:
      -
      -```
      -cd /js/server
      -NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
      -```
      -
      -For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
      -
      -```
      -cd /js/server
      -cross-env NODE_LANG=en...
      -```
      -
      -In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
      -
      -Afterwards, call `./dev ` to run the server:
      -
      -```
      -cd /js/server
      -./dev en
      -```
      -
      -Running `./dev` uses the tutorial that was imported and cached by the previous command.
      -
      -It does not "watch" tutorial text, but it reloads the server after code changes.
      -
      -Again, that's for developing the server code itself, not writing the tutorial.
      -
       # Translating images
       
       Most pictures are in SVG format. Strings inside it are usually just text, they can be replaced.
       
       That's great, as there are many strings in English in images, like tips, notes, etc. They look nice when translated.
       
      -Image translations reside in `images.yml` file in the repository root, for example: 
      +Image translations reside in `images.yml` file in the repository root, for example: . Please, create it if needed.
       
       The file format is called "YAML", it's quite easy to understand:
       
      @@ -160,33 +127,67 @@ The translated string may become longer or shorter. If we have nice pictures, st
           你好世界
           ```
       
      +After `images.yaml` is ready, to apply translations:
       
      +1. Setup upstream (if you haven't yet) and pull latest changes:
      +    ```bash
      +    cd /js/zh.javascript.info 
      +    git remote add upstream https://github.com/javascript-tutorial/en.javascript.info
      +    git fetch upstream master
      +    ```
      +2. Run the translation task:
      +    ```bash
      +    cd /js/server
      +    # without --image it applies all translations (to all images)
      +    NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg
      +    ```
      +
      +The task takes upstream image (English version) and replaces strings to it.
       
      +In order for positioning to work, you need to have ImageMagick installed:  (or use packages for Linux/MacOS). 
      +    
       ## Extract strings
       
      -The task to get all strings from an image (for translation, to add to `images.yml`):
      +The task to get all strings from an image as YAML (for translation, to add to `images.yml`):
           ```
           cd /js/server
      -    NODE_LANG=ru npm run gulp engine:koa:tutorial:imageYaml --image hello.svg
      +    NODE_LANG=zh npm run gulp engine:koa:tutorial:imageYaml --image hello.svg
           ```
      +    
       
      +# Dev mode
       
      -# Importing images
      +If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to do.
       
      -If you modify `figures.sketch` file with pictures (need Mac and Sketch editor installed for that), images are re-imported automatically by `./edit` script.
      +First, run the command that imports (and caches) the tutorial:
       
      -To do that manually:
      -    ```
      -    cd /js/server
      -    NODE_LANG=ru npm run gulp engine:koa:tutorial:figuresImport
      -    ```
      +```bash
      +cd /js/server
      +NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
      +```
       
      -You only need to re-import images if you change them, or change their translations in `images.yml`.
      +For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
       
      -To do that, you need to have Mac and Sketch editor installed.
      +```bash
      +cd /js/server
      +cross-env NODE_LANG=en...
      +```
       
      +In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
      +
      +Afterwards, call `./dev ` to run the server:
      +
      +```bash
      +cd /js/server
      +./dev en
      +```
      +
      +Running `./dev` uses the tutorial that was imported and cached by the previous command.
      +
      +It does not "watch" tutorial text, but it reloads the server after code changes.
      +
      +Again, that's for developing the server code itself, not writing the tutorial.
       
      -    
       # Troubleshooting
       
       Please ensure you have Node.js version 10+ (`node -v` shows the version).
      
      From df4cbd0d33a1e845c809342624b271ec634acd65 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:44:01 +0300
      Subject: [PATCH 103/218] Update README.md
      
      ---
       README.md | 24 ++++++++++++++----------
       1 file changed, 14 insertions(+), 10 deletions(-)
      
      diff --git a/README.md b/README.md
      index 3666a51..e1c170b 100755
      --- a/README.md
      +++ b/README.md
      @@ -109,27 +109,31 @@ code-style.svg:  # image file name
           position: "center" # (optional) "center" or "right" - to position translated string.
       ```
       
      -The translated string may become longer or shorter. If we have nice pictures, strings move around and need to be repositioned:
      +The translated string may become longer or shorter. By default, the translated string starts at the same place:
       
      -- `position: "right"` makes sure that after the translation the string right edge is at the same place, like this (below is the translated string):
           ```
      -    hello world
      -        你好世界
      +    |hello world (before)
      +    | 你好世界  (after translation)
           ```
      +
      +Sometimes they need to be repositioned:
      +
       - `position: "center"` centers the translated string, good if you have a vertical diagram.
           ```
      +         |
           hello world
             你好世界
      +         |
           ```
      -- (default) the string left edge is the same:
      +- `position: "right"` makes sure that the translated string keeps the same right edge:
           ```
      -    hello world
      -    你好世界
      +    hello world |
      +        你好世界 |
           ```
       
      -After `images.yaml` is ready, to apply translations:
      +After `images.yaml` with translations is ready, it's time to apply translations:
       
      -1. Setup upstream (if you haven't yet) and pull latest changes:
      +1. Setup git upstream (if you haven't yet) and pull latest changes:
           ```bash
           cd /js/zh.javascript.info 
           git remote add upstream https://github.com/javascript-tutorial/en.javascript.info
      @@ -142,7 +146,7 @@ After `images.yaml` is ready, to apply translations:
           NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg
           ```
       
      -The task takes upstream image (English version) and replaces strings to it.
      +The task takes upstream image version (English), replaces strings to it, then writes to same-named image in the tutorial repo.
       
       In order for positioning to work, you need to have ImageMagick installed:  (or use packages for Linux/MacOS). 
           
      
      From 7c8e6c490669aaea5f9f2dec143edf93c3a64a2c Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:44:42 +0300
      Subject: [PATCH 104/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index e1c170b..03e099b 100755
      --- a/README.md
      +++ b/README.md
      @@ -113,7 +113,7 @@ The translated string may become longer or shorter. By default, the translated s
       
           ```
           |hello world (before)
      -    | 你好世界  (after translation)
      +    |你好世界  (after translation)
           ```
       
       Sometimes they need to be repositioned:
      
      From ff21eb4aece8eddd9afdaa8899c41fec71224561 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:45:27 +0300
      Subject: [PATCH 105/218] Update README.md
      
      ---
       README.md | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 03e099b..51a071c 100755
      --- a/README.md
      +++ b/README.md
      @@ -118,7 +118,7 @@ The translated string may become longer or shorter. By default, the translated s
       
       Sometimes they need to be repositioned:
       
      -- `position: "center"` centers the translated string, good if you have a vertical diagram.
      +- `position: "center"` centers the translated string, good if you have a vertical diagram, keeps text centered:
           ```
                |
           hello world
      
      From f1dc9c32bb64edc32977ab67140e8635df24010f Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:46:21 +0300
      Subject: [PATCH 106/218] Update README.md
      
      ---
       README.md | 4 +++-
       1 file changed, 3 insertions(+), 1 deletion(-)
      
      diff --git a/README.md b/README.md
      index 51a071c..8b5bb1f 100755
      --- a/README.md
      +++ b/README.md
      @@ -148,7 +148,9 @@ After `images.yaml` with translations is ready, it's time to apply translations:
       
       The task takes upstream image version (English), replaces strings to it, then writes to same-named image in the tutorial repo.
       
      -In order for positioning to work, you need to have ImageMagick installed:  (or use packages for Linux/MacOS). 
      +You may want to open the resulting SVG file directly in the browser to see it.
      +
      +P.S In order for positioning to work, you need to have ImageMagick installed:  (or use packages for Linux/MacOS). 
           
       ## Extract strings
       
      
      From 75d0d6855136ebf3de4dc224e9390421a6525f33 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:49:08 +0300
      Subject: [PATCH 107/218] Update README.md
      
      ---
       README.md | 17 ++++++++++++-----
       1 file changed, 12 insertions(+), 5 deletions(-)
      
      diff --git a/README.md b/README.md
      index 8b5bb1f..717497c 100755
      --- a/README.md
      +++ b/README.md
      @@ -146,6 +146,13 @@ After `images.yaml` with translations is ready, it's time to apply translations:
           NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg
           ```
       
      +For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
      +
      +```bash
      +cd /js/server
      +cross-env NODE_LANG=zh...
      +```
      +
       The task takes upstream image version (English), replaces strings to it, then writes to same-named image in the tutorial repo.
       
       You may want to open the resulting SVG file directly in the browser to see it.
      @@ -172,12 +179,12 @@ cd /js/server
       NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import
       ```
       
      -For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
      +> For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
       
      -```bash
      -cd /js/server
      -cross-env NODE_LANG=en...
      -```
      +    ```bash
      +    cd /js/server
      +    cross-env NODE_LANG=en...
      +    ```
       
       In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
       
      
      From a5358be003895b9687fe3cfdf4160fba3980d455 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:50:16 +0300
      Subject: [PATCH 108/218] Update README.md
      
      ---
       README.md | 9 ++++-----
       1 file changed, 4 insertions(+), 5 deletions(-)
      
      diff --git a/README.md b/README.md
      index 717497c..4a3ea99 100755
      --- a/README.md
      +++ b/README.md
      @@ -180,11 +180,10 @@ NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutori
       ```
       
       > For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
      -
      -    ```bash
      -    cd /js/server
      -    cross-env NODE_LANG=en...
      -    ```
      +>    ```bash
      +>    cd /js/server
      +>    cross-env NODE_LANG=en...
      +>    ```
       
       In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`.
       
      
      From d619c6253f5293c317250bf8b96d348f20656fa9 Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Mon, 29 Jul 2019 23:50:45 +0300
      Subject: [PATCH 109/218] Update README.md
      
      ---
       README.md | 12 ++++++------
       1 file changed, 6 insertions(+), 6 deletions(-)
      
      diff --git a/README.md b/README.md
      index 4a3ea99..d1b1d02 100755
      --- a/README.md
      +++ b/README.md
      @@ -146,12 +146,12 @@ After `images.yaml` with translations is ready, it's time to apply translations:
           NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg
           ```
       
      -For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
      -
      -```bash
      -cd /js/server
      -cross-env NODE_LANG=zh...
      -```
      +> For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this:
      +>
      +> ```bash
      +> cd /js/server
      +> cross-env NODE_LANG=zh...
      +> ```
       
       The task takes upstream image version (English), replaces strings to it, then writes to same-named image in the tutorial repo.
       
      
      From adc515631aa6cca7ddbe4d49f5a17bc930e0e7ea Mon Sep 17 00:00:00 2001
      From: Ilya Kantor 
      Date: Tue, 30 Jul 2019 13:27:15 +0300
      Subject: [PATCH 110/218] Update README.md
      
      ---
       README.md | 40 +++++++++++++++++++++-------------------
       1 file changed, 21 insertions(+), 19 deletions(-)
      
      diff --git a/README.md b/README.md
      index d1b1d02..f3659a9 100755
      --- a/README.md
      +++ b/README.md
      @@ -98,9 +98,9 @@ Most pictures are in SVG format. Strings inside it are usually just text, they c
       
       That's great, as there are many strings in English in images, like tips, notes, etc. They look nice when translated.
       
      -Image translations reside in `images.yml` file in the repository root, for example: . Please, create it if needed.
      +Image translations reside in `images.yml` in the repository root, for example: . Please, create it if needed.
       
      -The file format is called "YAML", it's quite easy to understand:
      +The file format is "YAML", it's quite easy to understand:
       
       ```yaml
       code-style.svg:  # image file name
      @@ -118,18 +118,19 @@ The translated string may become longer or shorter. By default, the translated s
       
       Sometimes they need to be repositioned:
       
      -- `position: "center"` centers the translated string, good if you have a vertical diagram, keeps text centered:
      -    ```
      -         |
      -    hello world
      -      你好世界
      -         |
      -    ```
      -- `position: "right"` makes sure that the translated string keeps the same right edge:
      -    ```
      -    hello world |
      -        你好世界 |
      -    ```
      +`position: "center"` centers the translated string, good if you have a vertical diagram, keeps text centered:
      +```
      +     |
      +hello world
      +  你好世界
      +     |
      +```
      +
      +`position: "right"` makes sure that the translated string keeps the same right edge:
      +```
      +hello world |
      +    你好世界 |
      +```
       
       After `images.yaml` with translations is ready, it's time to apply translations:
       
      @@ -162,11 +163,12 @@ P.S In order for positioning to work, you need to have ImageMagick installed: 
      Date: Mon, 9 Sep 2019 12:06:25 +0300
      Subject: [PATCH 111/218] tr
      
      ---
       locales/tr.yml                   | 70 ++++++++++++++++++++++++++++++++
       modules/frontpage/locales/tr.yml |  6 +++
       2 files changed, 76 insertions(+)
       create mode 100644 locales/tr.yml
       create mode 100644 modules/frontpage/locales/tr.yml
      
      diff --git a/locales/tr.yml b/locales/tr.yml
      new file mode 100644
      index 0000000..3176cb9
      --- /dev/null
      +++ b/locales/tr.yml
      @@ -0,0 +1,70 @@
      +site:
      +  privacy_policy: Gizlilik Politikası
      +  terms: kullanım şartları
      +
      +  gdpr_dialog:
      +    title: Bu websitesi çerez kullanmaktadır.
      +    text: Çerez, yerel depolama ile sizin tercihlerinizi saklamaktayız. Bunları yapabilmemiz için Gizlilik Politikasını ve kullanım şartlarını onaylamalısınız.
      +    accept: Kabul et
      +    cancel: İptal
      +
      +  toolbar:
      +    lang_switcher:
      +      cta_text: >
      +        

      Bu açık-kaynaklı projenin tüm dünyada kullanılabilir olmasını istiyoruz.

      +

      Kendi dilinizde çeviriye yardım edebilirsiniz!

      + footer_text: İçeriğin yüzde kaçı çevirildi. + old_version: Eski versiyon yayınlandı, eskisine taşıma yapılması lazım. + logo: + normal: + svg: sitetoolbar__logo_en.svg + width: 200 # 171 + normal-white: + svg: sitetoolbar__logo_en-white.svg + small: + svg: sitetoolbar__logo_small_en.svg + width: 70 + small-white: + svg: sitetoolbar__logo_small_en-white.svg + sections: + buy_ebook_extra: 'Satın al' + buy_ebook: 'EPUB/PDF' + search_placeholder: "Javascript.info\'da ara" + search_button: 'Ara' + + public_profile: Genel Profil + account: Hesap + notifications: Bildirimler + admin: Yönetici + logout: Çıkış + + sorry_old_browser: Üzgünüz, IE<10 desteklenmemektedi lütfen daha yeni bir tarayıcı kullanın. + contact_us: iletişime geçin + about_the_project: proje hakkında + ilya_kantor: Ilya Kantor + comments: Yorumlar + loading: Yükleniyor... + search: Ara + share: Paylaş + read_before_commenting: yorum yapmadan önce lütfen okuyun... + last_updated_at: "Son güncelleme #{date}" + meta: + description: 'Modern JavaScript Eğitimi: basit, fakat detaylı açıklamalar ve görevler ile anlatılmıştır. Closures, document, events ve nesne yönelimli programlama üzerine bölümleri bulunmaktadır' + + tablet-menu: + choose_section: Bölüm seçiniz + search_placeholder: Eğitimde ara + search_button: Ara + + comment: + help: + - Eğer geliştirme ile alakalı bir öneriniz var ise yorum yerine github konusu gönderiniz. + - Eğer makalede bir yeri anlamadıysanız lütfen belirtiniz. + - Koda birkaç satır eklemek için <code> kullanınız, birkaç satır eklemek için ise <pre> kullanın. Eğer 10 satırdan fazla kod ekleyecekseniz plnkr kullanabilirsiniz) + + edit_on_github: Github'da düzenle + error: hata + close: kapat + + hide_forever: kalıcı olarak gizle + hidden_forever: Bu bilgi artık görünmeyecektir. diff --git a/modules/frontpage/locales/tr.yml b/modules/frontpage/locales/tr.yml new file mode 100644 index 0000000..5b554af --- /dev/null +++ b/modules/frontpage/locales/tr.yml @@ -0,0 +1,6 @@ +view_github: github +search_placeholder: Eğitimde ara +search_button: Ara +share_text: Paylaş +courses_ongoing: Kaydol +contributors_pluralize: "yazar,yazarlar,yazarlar" From 6be079f2a36aee5f4671e12a749d730de4f22fef Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 9 Sep 2019 12:08:03 +0300 Subject: [PATCH 112/218] tr --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3659a9..097093f 100755 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ cd /js/server ./edit ru ru ``` -Please note, the server must support that language. There must be corresponding locale files for that language in the code of the server, otherwise it exists with an error. As of now, `ru`, `en`, `zh` and `ja` are fully supported. +Please note, the server must support that language. There must be corresponding locale files for that language in the code of the server, otherwise it exists with an error. As of now, `ru`, `en`, `zh`, `tr` and `ja` are fully supported. # Translating images From 59a5b4226aea4d1b24cfa414b6baabfdf83c445c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 22 Oct 2019 10:18:22 +0300 Subject: [PATCH 113/218] rtl --- modules/styles/rtl.styl | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 modules/styles/rtl.styl diff --git a/modules/styles/rtl.styl b/modules/styles/rtl.styl new file mode 100644 index 0000000..c53628c --- /dev/null +++ b/modules/styles/rtl.styl @@ -0,0 +1,3 @@ +if lang=='fa' + body + background green From 25675a27738a2cb2cd3d5c4662080a1e3dceb35a Mon Sep 17 00:00:00 2001 From: Violet-Bora-Lee Date: Sun, 10 Nov 2019 23:04:47 +0900 Subject: [PATCH 114/218] Translate to Korean --- locales/ko.yml | 74 ++++++++++++++++++++++++++++++++ modules/frontpage/locales/ko.yml | 5 +++ 2 files changed, 79 insertions(+) create mode 100644 locales/ko.yml create mode 100644 modules/frontpage/locales/ko.yml diff --git a/locales/ko.yml b/locales/ko.yml new file mode 100644 index 0000000..70345f4 --- /dev/null +++ b/locales/ko.yml @@ -0,0 +1,74 @@ +site: + privacy_policy: 개인정보 취급방침 + + gdpr_dialog: + title: 본 사이트는 쿠키를 사용합니다. + text: 본 사이트는 쿠키, 로컬 스토리지 등의 기술을 사용해 사용자 정보를 수집합니다. 사이트를 이용하려면 개인정보 취급방침회원이용 약관에 동의해야 합니다. + accept: 동의 + cancel: 거부 + + toolbar: + lang_switcher: + cta_text: 본 튜토리얼은 전 세계 사람들이 이용할 수 있는 오픈 소스 프로젝트입니다. 프로젝트 페이지에 방문하셔서 번역을 도와주세요. + footer_text: 얼마나 많은 콘텐츠가 해당 언어로 번역되었는지 + old_version: 튜토리얼 구 버전은 이미 배포되었습니다. 개정이 진행 중입니다. + logo: + normal: + svg: sitetoolbar__logo_en.svg + width: 200 # 171 + normal-white: + svg: sitetoolbar__logo_en-white.svg + small: + svg: sitetoolbar__logo_small_en.svg + width: 70 + small-white: + svg: sitetoolbar__logo_small_en-white.svg + sections: + - slug: '' + url: '/' + title: '튜토리얼' + - slug: '코스' + title: '코스' + buy_ebook_extra: '구매' + buy_ebook: 'EPUB/PDF' + search_placeholder: 'javascript.info에서 검색하기' + search_button: '검색' + + public_profile: 공개 프로필 + account: 계정 + notifications: 알림 + admin: 관리자 + logout: 로그아웃 + + sorry_old_browser: IE10 미만의 브라우저는 지원하지 않습니다. 최신 버전 브라우저를 사용해주세요. + contact_us: 연락처 + about_the_project: 프로젝트 설명 + ilya_kantor: Ilya Kantor + comments: 댓글 + loading: 로딩중... + search: 검색 + share: 공유 + read_before_commenting: 댓글을 달기 전에 아랫글을 읽어주세요. + + meta: + description: '모던 자바스크립트 튜토리얼은 클로저, 문서 객체 모델, 이벤트, 객체 지향 프로그래밍 등의 다양한 주제에 대한 설명과 예시, 과제를 담고 있습니다.' + + tablet-menu: + choose_section: 섹션 선택 + search_placeholder: 튜토리얼 내에서 검색 + search_button: 검색 + + comment: + help: + - 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요. + You're welcome to post additions, questions to the articles and answers to them. + - 댓글에 몇 단어로 구성된 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요. + - 잘 이해되지 않는 부분은 구체적으로 언급해주세요. + + edit_on_github: GitHub에서 수정하기 + error: 에러 + close: 종료 + + hide_forever: 영구적으로 숨기기 + hidden_forever: 이제 이 정보는 더 이상 나타나지 않습니다. + diff --git a/modules/frontpage/locales/ko.yml b/modules/frontpage/locales/ko.yml new file mode 100644 index 0000000..84ea750 --- /dev/null +++ b/modules/frontpage/locales/ko.yml @@ -0,0 +1,5 @@ +modern_javascript_tutorial: "모던 JavaScript 튜토리얼" +view_github: "GitHub에서 보기" +search_placeholder: "튜토리얼 검색하기" +search_button: "검색" +share_text: "공유" From 5826a1862954f95bab75d17139e80b94011f9161 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 13 Nov 2019 22:06:23 +0300 Subject: [PATCH 115/218] minor --- locales/en.yml | 34 +++++++++++++++++++++++++++++--- locales/ko.yml | 7 +++---- modules/frontpage/locales/en.yml | 9 +++++---- modules/frontpage/locales/ko.yml | 1 + 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 0fd63ad..b92d31f 100755 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,5 +1,6 @@ site: privacy_policy: Privacy policy + terms: terms of usage gdpr_dialog: title: This website uses cookies @@ -50,6 +51,7 @@ site: share: Share read_before_commenting: read this before commenting… + last_updated_at: "Last updated at #{date}" meta: description: 'Modern JavaScript Tutorial: simple, but detailed explanations with examples and tasks, including: closures, document and events, object oriented programming and more.' @@ -60,9 +62,9 @@ site: comment: help: - - You're welcome to post additions, questions to the articles and answers to them. - - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…) - - If you can't understand something in the article – please elaborate. + - If you have suggestions what to improve - please submit a GitHub issue or a pull request instead of commenting. + - If you can't understand something in the article – please elaborate. + - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…) edit_on_github: Edit on GitHub error: error @@ -71,3 +73,29 @@ site: hide_forever: hide permanently hidden_forever: This information will not show up any more. + + subscribe: + title: Watch for javascript.info updates + text: 'We do not send advertisements, only relevant stuff. You choose what to receive:' + agreement: 'By signing up to newsletters you agree to the terms of usage.' + button: Subscribe + button_unsubscribe: Unsubscribe from all + common_updates: Common updates + common_updates_text: new courses, master classes, article and screencast releases + your_email: your@email.here + newsletters: 'newsletter,newsletters,newsletters' + no_selected: Nothing selected + + form: + value_must_not_be_empty: Value must not be empty. + value_is_too_long: Value is too long. + value_is_too_short: Value is too short. + invalid_email: Invalid email. + invalid_value: Invalid value. + invalid_autocomplete: Please, choose from the list + invalid_date: 'Invalid date, format: dd.mm.yyyyy.' + invalid_range: This date is invalid here. + save: Save + upload_file: Upload file + cancel: Cancel + server_error: Request error, status code diff --git a/locales/ko.yml b/locales/ko.yml index 70345f4..7200b06 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -60,10 +60,9 @@ site: comment: help: - - 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요. - You're welcome to post additions, questions to the articles and answers to them. - - 댓글에 몇 단어로 구성된 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요. - - 잘 이해되지 않는 부분은 구체적으로 언급해주세요. + - If you have suggestions what to improve - please submit a GitHub issue or a pull request instead of commenting. + - If you can't understand something in the article – please elaborate. + - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…) edit_on_github: GitHub에서 수정하기 error: 에러 diff --git a/modules/frontpage/locales/en.yml b/modules/frontpage/locales/en.yml index d722324..0d69e57 100644 --- a/modules/frontpage/locales/en.yml +++ b/modules/frontpage/locales/en.yml @@ -1,5 +1,6 @@ +view_github: github +search_placeholder: Search in the tutorial +search_button: Search +share_text: Share +contributors_pluralize: "contributor,contributors,contributors" modern_javascript_tutorial: "The Modern JavaScript Tutorial" -view_github: "view on GitHub" -search_placeholder: "Search in the tutorial" -search_button: "Search" -share_text: "Share" diff --git a/modules/frontpage/locales/ko.yml b/modules/frontpage/locales/ko.yml index 84ea750..6a93129 100644 --- a/modules/frontpage/locales/ko.yml +++ b/modules/frontpage/locales/ko.yml @@ -3,3 +3,4 @@ view_github: "GitHub에서 보기" search_placeholder: "튜토리얼 검색하기" search_button: "검색" share_text: "공유" +contributors_pluralize: "contributor,contributors,contributors" From 35b2c9197e2b0f4bcfa3abf3b7a5eede3284074a Mon Sep 17 00:00:00 2001 From: Violet Bora Lee Date: Fri, 6 Dec 2019 15:40:49 +0900 Subject: [PATCH 116/218] Translate to Korean --- locales/ko.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/ko.yml b/locales/ko.yml index 7200b06..2fcecf3 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -48,7 +48,7 @@ site: loading: 로딩중... search: 검색 share: 공유 - read_before_commenting: 댓글을 달기 전에 아랫글을 읽어주세요. + read_before_commenting: 댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요. meta: description: '모던 자바스크립트 튜토리얼은 클로저, 문서 객체 모델, 이벤트, 객체 지향 프로그래밍 등의 다양한 주제에 대한 설명과 예시, 과제를 담고 있습니다.' @@ -60,9 +60,9 @@ site: comment: help: - - If you have suggestions what to improve - please submit a GitHub issue or a pull request instead of commenting. - - If you can't understand something in the article – please elaborate. - - To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…) + - 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요. + - 잘 이해되지 않는 부분은 구체적으로 언급해주세요. + - 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요. edit_on_github: GitHub에서 수정하기 error: 에러 From eef0fd9fa88893abe5d724417c6e7dc83c8fd1d5 Mon Sep 17 00:00:00 2001 From: Violet Bora Lee Date: Fri, 31 Jan 2020 01:48:31 +0900 Subject: [PATCH 117/218] Update contribution repository for Korean --- locales/ko.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ko.yml b/locales/ko.yml index 2fcecf3..e3a9b4b 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -9,7 +9,7 @@ site: toolbar: lang_switcher: - cta_text: 본 튜토리얼은 전 세계 사람들이 이용할 수 있는 오픈 소스 프로젝트입니다. 프로젝트 페이지에 방문하셔서 번역을 도와주세요. + cta_text: 본 튜토리얼은 전 세계 사람들이 이용할 수 있는 오픈 소스 프로젝트입니다. 프로젝트 페이지에 방문하셔서 번역을 도와주세요. footer_text: 얼마나 많은 콘텐츠가 해당 언어로 번역되었는지 old_version: 튜토리얼 구 버전은 이미 배포되었습니다. 개정이 진행 중입니다. logo: From ca38afbccabd01fe46160d1684904e16fe803bb7 Mon Sep 17 00:00:00 2001 From: wangqi Date: Tue, 24 Mar 2020 11:07:47 +0800 Subject: [PATCH 118/218] Add dependencies --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 8e13657..5615626 100755 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "koa-mount": "^3", "koa-router": "^7", "koa-static": "^5", + "limax": "^2.0.0", "lodash": "*", "markdown-it": "*", "markdown-it-container": "*", @@ -48,6 +49,7 @@ "mime": "^2.3", "mini-css-extract-plugin": "^0", "minimatch": "^3.0.4", + "mongoose": "^5.9.5", "multiparty": "*", "mz": "*", "nib": "*", From daf8b1a1dba60741f32e569956ea640bb667a5c8 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 15 Apr 2020 15:05:19 +0300 Subject: [PATCH 119/218] minor fixes --- .gitignore | 1 + mocha.sh | 2 +- package.json | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6e12431..7408f34 100755 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # OS or Editor folders .DS_Store +.vscode .idea .cache .project diff --git a/mocha.sh b/mocha.sh index 966aa80..30968e2 100755 --- a/mocha.sh +++ b/mocha.sh @@ -8,4 +8,4 @@ # tried also gulp-mocha and node `which gulp` test, # but it hangs after tests, not sure why, mocha.sh works fine so leave it as is - NODE_PATH=./modules NODE_ENV=test mocha --reporter spec --colors --timeout 100000 --require should --require co --require co-mocha --recursive --ui bdd -d $* +NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules NODE_ENV=test node node_modules/.bin/mocha --reporter spec --colors --timeout 100000 --require should --recursive --ui bdd $* diff --git a/package.json b/package.json index 8e13657..0a68f61 100755 --- a/package.json +++ b/package.json @@ -82,5 +82,9 @@ "license": "CC BY-NC-SA 3.0", "bugs": { "url": "https://github.com/javascript-tutorial/server/issues" + }, + "devDependencies": { + "mocha": "^7.1.1", + "should": "^13.2.3" } } From c43b873becbc7007dc9ee113ba415819f1074112 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 19 Apr 2020 16:36:00 +0300 Subject: [PATCH 120/218] minor fixes --- mocha.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mocha.sh b/mocha.sh index 30968e2..2a23f46 100755 --- a/mocha.sh +++ b/mocha.sh @@ -8,4 +8,4 @@ # tried also gulp-mocha and node `which gulp` test, # but it hangs after tests, not sure why, mocha.sh works fine so leave it as is -NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules NODE_ENV=test node node_modules/.bin/mocha --reporter spec --colors --timeout 100000 --require should --recursive --ui bdd $* +NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules NODE_ENV=test node node_modules/.bin/mocha --full-trace --allow-uncaught --require should --recursive $* From b9bce7be906466751b3987212179656b6f7dd55e Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 21 May 2020 22:43:00 +0300 Subject: [PATCH 121/218] minor fixes --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0a68f61..8c598b6 100755 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "gm": "*", "gulp": "^4", "gulp-livereload": "^4", + "html-entities": "^1.3.1", "image-size": "*", "js-yaml": "*", "json-loader": "^0.5.7", From 7e535755e5013e6ffd907b40c0e83ca309e7437a Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 21 May 2020 23:12:59 +0300 Subject: [PATCH 122/218] rtl --- modules/config/webpack.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/config/webpack.js b/modules/config/webpack.js index 49c76b8..95dec61 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -199,6 +199,8 @@ module.exports = function () { nib(), function (style) { style.define('lang', config.lang); + style.define('rtl', ['ar','fa'].includes(config.lang)); + style.define('env', config.env); } ] }, From 85e4bad35108238ee6bad03e573da08f86dc6efb Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 22 May 2020 00:29:37 +0300 Subject: [PATCH 123/218] rtl --- edit | 1 + modules/config/webpack.js | 2 +- modules/styles/blocks/body/body.styl | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/edit b/edit index 713fd87..eb54490 100755 --- a/edit +++ b/edit @@ -8,6 +8,7 @@ export TUTORIAL_ROOT="../$1.javascript.info" export NODE_LANG="${2:-en}" +export TUTORIAL_LANG=$1 export NODE_ENV=production export TUTORIAL_EDIT=1 export NODE_PRESERVE_SYMLINKS=1 diff --git a/modules/config/webpack.js b/modules/config/webpack.js index 95dec61..fa7cb5e 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -199,7 +199,7 @@ module.exports = function () { nib(), function (style) { style.define('lang', config.lang); - style.define('rtl', ['ar','fa'].includes(config.lang)); + style.define('rtl', ['ar','fa'].includes(process.env.TUTORIAL_LANG)); style.define('env', config.env); } ] diff --git a/modules/styles/blocks/body/body.styl b/modules/styles/blocks/body/body.styl index 0fd7b3c..d975d43 100755 --- a/modules/styles/blocks/body/body.styl +++ b/modules/styles/blocks/body/body.styl @@ -8,7 +8,10 @@ body color color background background margin 0 + if rtl + background-color green @media print body color black + From 7ddfa7aad860099bb6722ac11bc136781251d315 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 22 May 2020 00:54:34 +0300 Subject: [PATCH 124/218] minor fixes --- dev | 1 + 1 file changed, 1 insertion(+) diff --git a/dev b/dev index 9b3fc16..90ebf2a 100755 --- a/dev +++ b/dev @@ -5,6 +5,7 @@ set -e export NODE_LANG=$1 +export TUTORIAL_LANG=$1 export NODE_ENV=development export ASSET_VERSIONING=query export WATCH=1 From 0bba226eedc02313e7040aafa67233307a08afe8 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 22 May 2020 01:42:13 +0200 Subject: [PATCH 125/218] adding tutorial lang variable and fix rtl reserved word check variable --- dev | 2 +- edit.cmd | 1 + modules/config/webpack.js | 2 +- modules/styles/blocks/body/body.styl | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dev b/dev index 90ebf2a..1428051 100755 --- a/dev +++ b/dev @@ -5,7 +5,7 @@ set -e export NODE_LANG=$1 -export TUTORIAL_LANG=$1 +export TUTORIAL_LANG=$1 export NODE_ENV=development export ASSET_VERSIONING=query export WATCH=1 diff --git a/edit.cmd b/edit.cmd index bd1a5e1..12359c2 100644 --- a/edit.cmd +++ b/edit.cmd @@ -8,6 +8,7 @@ set NODE_LANG=%2 ) +@set TUTORIAL_LANG=%1 @set NODE_ENV=production @set TUTORIAL_EDIT=1 @set ASSET_VERSIONING=query diff --git a/modules/config/webpack.js b/modules/config/webpack.js index fa7cb5e..c3612f3 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -199,7 +199,7 @@ module.exports = function () { nib(), function (style) { style.define('lang', config.lang); - style.define('rtl', ['ar','fa'].includes(process.env.TUTORIAL_LANG)); + style.define('isRtl', ['ar','fa'].includes(process.env.TUTORIAL_LANG)); style.define('env', config.env); } ] diff --git a/modules/styles/blocks/body/body.styl b/modules/styles/blocks/body/body.styl index d975d43..a81bf93 100755 --- a/modules/styles/blocks/body/body.styl +++ b/modules/styles/blocks/body/body.styl @@ -8,8 +8,9 @@ body color color background background margin 0 - if rtl + if isRtl background-color green + direction rtl @media print body From 0366f6f95163b1d3f211305768a5fcf998d334db Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 22 May 2020 13:31:48 +0200 Subject: [PATCH 126/218] adding RTL styles --- modules/config/webpack.js | 251 +++++++++--------- .../blocks/_frontpage-content/index.styl | 5 +- modules/styles/blocks/balance/balance.styl | 5 +- modules/styles/blocks/body/body.styl | 3 +- .../blocks/breadcrumbs/breadcrumbs.styl | 5 +- modules/styles/blocks/extract/extract.styl | 5 +- .../blocks/lessons-list/lessons-list.styl | 15 +- modules/styles/blocks/main/main.styl | 19 +- .../blocks/page-footer/page-footer.styl | 8 +- modules/styles/blocks/page/page.styl | 49 +++- modules/styles/blocks/prism/01-prism.styl | 9 + .../blocks/prism/03-prism-line-numbers.styl | 5 +- .../sitetoolbar-light/sitetoolbar-light.styl | 8 +- .../blocks/sitetoolbar/sitetoolbar.styl | 23 +- modules/styles/blocks/toolbar/toolbar.styl | 5 +- 15 files changed, 254 insertions(+), 161 deletions(-) diff --git a/modules/config/webpack.js b/modules/config/webpack.js index c3612f3..eb0237b 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -6,43 +6,43 @@ let path = require('path'); // no webpack dependencies inside // no es6 (for 6to5 processing) inside // NB: includes angular-* -let noProcessModulesRegExp = new RegExp("node_modules" + (path.sep === '/' ? path.sep : '\\\\') + "(angular|prismjs|sanitize-html|i18n-iso-countries)"); +let noProcessModulesRegExp = new RegExp( + 'node_modules' + (path.sep === '/' ? path.sep : '\\\\') + '(angular|prismjs|sanitize-html|i18n-iso-countries)' +); let devMode = process.env.NODE_ENV == 'development'; - module.exports = function () { - let nib = require('nib'); let rupture = require('rupture'); let chokidar = require('chokidar'); let webpack = require('webpack'); let WriteVersionsPlugin = require('engine/webpack/writeVersionsPlugin'); let CssWatchRebuildPlugin = require('engine/webpack/cssWatchRebuildPlugin'); - const CopyWebpackPlugin = require('copy-webpack-plugin') - const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + const CopyWebpackPlugin = require('copy-webpack-plugin'); + const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); - const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); + const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const fse = require('fs-extra'); - -// tutorial.js?hash -// tutorial.hash.js + // tutorial.js?hash + // tutorial.hash.js function extHash(name, ext, hash) { if (!hash) hash = '[hash]'; - return config.assetVersioning == 'query' ? `${name}.${ext}?${hash}` : - config.assetVersioning == 'file' ? `${name}.${hash}.${ext}` : - `${name}.${ext}`; + return config.assetVersioning == 'query' + ? `${name}.${ext}?${hash}` + : config.assetVersioning == 'file' + ? `${name}.${hash}.${ext}` + : `${name}.${ext}`; } let modulesDirectories = [path.join(process.cwd(), 'node_modules')]; if (process.env.NODE_PATH) { - modulesDirectories = modulesDirectories.concat(process.env.NODE_PATH.split(/[:;]/).map(p => path.resolve(p))); + modulesDirectories = modulesDirectories.concat(process.env.NODE_PATH.split(/[:;]/).map((p) => path.resolve(p))); } //console.log("MODULE DIRS", modulesDirectories); - /** * handler/client/assets/* goes to public/assets/ */ @@ -63,11 +63,11 @@ module.exports = function () { * handler/templates makes handler.css (via CssWatchRebuiltPlugin) */ let entries = { - head: 'client/head', - footer: 'client/footer', - tutorial: 'engine/koa/tutorial/client', - styles: config.tmpRoot + '/styles.styl', - frontpage: config.tmpRoot + '/frontpage.styl' + head: 'client/head', + footer: 'client/footer', + tutorial: 'engine/koa/tutorial/client', + styles: config.tmpRoot + '/styles.styl', + frontpage: config.tmpRoot + '/frontpage.styl', }; /* @@ -88,11 +88,10 @@ module.exports = function () { }*/ //console.log("WEBPACK ENTRIES", entries); - let webpackConfig = { output: { // fs path - path: path.join(config.publicRoot, 'pack'), + path: path.join(config.publicRoot, 'pack'), // path as js sees it // if I use another domain here, need enable Allow-Access-.. header there // and add to scripts, to let error handler track errors @@ -101,27 +100,29 @@ module.exports = function () { // в prod-режиме не можем ?, т.к. CDN его обрезают, поэтому [hash] в имени // (какой-то [hash] здесь необходим, иначе к chunk'ам типа 3.js, которые генерируются require.ensure, // будет обращение без хэша при загрузке внутри сборки. при изменении - барузерный кеш их не подхватит) - filename: extHash("[name]", 'js'), + filename: extHash('[name]', 'js'), - chunkFilename: extHash("[name]-[id]", 'js'), - library: '[name]', - pathinfo: devMode + chunkFilename: extHash('[name]-[id]', 'js'), + library: '[name]', + pathinfo: devMode, }, cache: devMode, - mode: devMode ? 'development' : 'production', // for tests uses prod too watchOptions: { aggregateTimeout: 10, - ignored: /node_modules/ + ignored: /node_modules/, }, watch: devMode, - devtool: devMode ? "cheap-inline-module-source-map" : // try "eval" ? - process.env.NODE_ENV == 'production' ? 'source-map' : false, + devtool: devMode + ? 'cheap-inline-module-source-map' // try "eval" ? + : process.env.NODE_ENV == 'production' + ? 'source-map' + : false, profile: Boolean(process.env.WEBPACK_STATS), @@ -136,81 +137,82 @@ module.exports = function () { },*/ module: { - rules: [ + rules: [ { test: /\.yml$/, - use: ['json-loader', 'yaml-loader'] + use: ['json-loader', 'yaml-loader'], }, { test: /\.pug$/, - use: 'pug-loader?root=' + config.projectRoot + '/templates&globals=__' + use: 'pug-loader?root=' + config.projectRoot + '/templates&globals=__', }, { - test: /\.js$/, + test: /\.js$/, // babel shouldn't process modules which contain ws/browser.js, // which must not be run in strict mode (global becomes undefined) // babel would make all modules strict! exclude: noProcessModulesRegExp, - use: [ + use: [ // babel will work first { - loader: 'babel-loader', + loader: 'babel-loader', options: { presets: [ // use require.resolve here to build files from symlinks - [require.resolve('babel-preset-env'), { - //useBuiltIns: true, - targets: { - browsers: "> 3%" - } - }] - ] - } - } - ] + [ + require.resolve('babel-preset-env'), + { + //useBuiltIns: true, + targets: { + browsers: '> 3%', + }, + }, + ], + ], + }, + }, + ], }, { test: /\.styl$/, // MiniCssExtractPlugin breaks HMR for CSS - use: [ + use: [ MiniCssExtractPlugin.loader, { - loader: 'css-loader', + loader: 'css-loader', options: { - importLoaders: 1 - } + importLoaders: 1, + }, }, { - loader: 'postcss-loader', + loader: 'postcss-loader', options: { - plugins: [ - require('autoprefixer') - ] - } + plugins: [require('autoprefixer')], + }, }, 'engine/webpack/hover-loader', { - loader: 'stylus-loader', + loader: 'stylus-loader', options: { - linenos: true, + linenos: true, 'resolve url': true, - use: [ + use: [ rupture(), nib(), function (style) { style.define('lang', config.lang); - style.define('isRtl', ['ar','fa'].includes(process.env.TUTORIAL_LANG)); + style.define('isRTL', ['ar', 'fa'].includes(process.env.TUTORIAL_LANG)); style.define('env', config.env); - } - ] + }, + ], }, - } - ] + }, + ], }, { test: /\.(png|jpg|gif|woff|eot|otf|ttf|svg)$/, - use: extHash('file-loader?name=[path][name]', '[ext]') - } + use: extHash('file-loader?name=[path][name]', '[ext]'), + }, ], noParse: function (path) { /* @@ -220,52 +222,52 @@ module.exports = function () { */ //console.log(path); return noProcessModulesRegExp.test(path); - } + }, }, - resolve: { // allow require('styles') which looks for styles/index.styl extensions: ['.js', '.styl'], - alias: { + alias: { 'entities/maps/entities.json': 'engine/markit/emptyEntities', - config: 'client/config' + config: 'client/config', }, - modules: modulesDirectories + modules: modulesDirectories, }, - resolveLoader: { - modules: modulesDirectories, - extensions: ['.js'] + modules: modulesDirectories, + extensions: ['.js'], }, node: { - fs: 'empty' + fs: 'empty', }, performance: { maxEntrypointSize: 350000, maxAssetSize: 350000, // warning if asset is bigger than 300k - assetFilter(assetFilename) { // only check js/css + assetFilter(assetFilename) { + // only check js/css // ignore assets copied by CopyWebpackPlugin - if (assetFilename.startsWith('..')) { // they look like ../courses/achievements/course-complete.svg + if (assetFilename.startsWith('..')) { + // they look like ../courses/achievements/course-complete.svg // built assets do not have .. return false; } return assetFilename.endsWith('.js') || assetFilename.endsWith('.css'); - } + }, }, plugins: [ new webpack.DefinePlugin({ - LANG: JSON.stringify(config.lang), - IS_CLIENT: true + LANG: JSON.stringify(config.lang), + IS_CLIENT: true, }), // lodash is loaded when free variable _ occurs in the code new webpack.ProvidePlugin({ - _: 'lodash' + _: 'lodash', }), // ignore all locales except current lang @@ -284,10 +286,9 @@ module.exports = function () { // console.log("ignore moment locale", tmp, arg); return true; } - } + }, }), - // ignore site locale files except the lang new webpack.IgnorePlugin({ checkResource(arg) { @@ -300,93 +301,89 @@ module.exports = function () { let ignore = /\/locales(\/|$)/.test(arg); // console.log("ignore yml", tmp, arg); return ignore; - } + }, }), - new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')), new MiniCssExtractPlugin({ - filename: extHash("[name]", 'css'), - chunkFilename: extHash("[id]", 'css'), + filename: extHash('[name]', 'css'), + chunkFilename: extHash('[id]', 'css'), }), new CssWatchRebuildPlugin(), new CopyWebpackPlugin( - assetPaths.map(path => { + assetPaths.map((path) => { return { from: path, - to: config.publicRoot - } + to: config.publicRoot, + }; }), - {debug: 'warning'} + { debug: 'warning' } ), { apply: function (compiler) { if (process.env.WEBPACK_STATS) { - compiler.plugin("done", function (stats) { + compiler.plugin('done', function (stats) { stats = stats.toJson(); fs.writeFileSync(`${config.tmpRoot}/stats.json`, JSON.stringify(stats)); }); } - } - } + }, + }, ], recordsPath: path.join(config.tmpRoot, 'webpack.json'), - devServer: { - port: 3001, // dev server itself does not use it, but outer tasks do + devServer: { + port: 3001, // dev server itself does not use it, but outer tasks do historyApiFallback: true, - hot: true, - watchDelay: 10, + hot: true, + watchDelay: 10, //noInfo: true, - publicPath: process.env.STATIC_HOST + ':3001/pack/', - contentBase: config.publicRoot + publicPath: process.env.STATIC_HOST + ':3001/pack/', + contentBase: config.publicRoot, }, - optimization: { minimizer: [ new UglifyJsPlugin({ - cache: true, - parallel: 2, + cache: true, + parallel: 2, uglifyOptions: { - ecma: 8, + ecma: 8, warnings: false, compress: { - drop_console: true, - drop_debugger: true + drop_console: true, + drop_debugger: true, }, - output: { - beautify: true, - indent_level: 0 // for error reporting, to see which line actually has the problem + output: { + beautify: true, + indent_level: 0, // for error reporting, to see which line actually has the problem // source maps actually didn't work in Qbaka that's why I put it here - } - } + }, + }, }), - new OptimizeCSSAssetsPlugin({}) - ] - } + new OptimizeCSSAssetsPlugin({}), + ], + }, }; - -//if (process.env.NODE_ENV != 'development') { // production, ebook - if (process.env.NODE_ENV == 'production') { // production, ebook - webpackConfig.plugins.push( - function clearBeforeRun() { - function clear(compiler, callback) { - fse.removeSync(webpackConfig.output.path + '/*'); - callback(); - } - - // in watch mode this will clear between partial rebuilds - // thus removing unchanged files - // => use this plugin only in normal run - this.plugin('run', clear); + //if (process.env.NODE_ENV != 'development') { // production, ebook + if (process.env.NODE_ENV == 'production') { + // production, ebook + webpackConfig.plugins.push(function clearBeforeRun() { + function clear(compiler, callback) { + fse.removeSync(webpackConfig.output.path + '/*'); + callback(); } - ); + + // in watch mode this will clear between partial rebuilds + // thus removing unchanged files + // => use this plugin only in normal run + this.plugin('run', clear); + }); } return webpackConfig; diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.styl b/modules/frontpage/templates/blocks/_frontpage-content/index.styl index 7a9436d..1504d98 100755 --- a/modules/frontpage/templates/blocks/_frontpage-content/index.styl +++ b/modules/frontpage/templates/blocks/_frontpage-content/index.styl @@ -116,7 +116,10 @@ &__title position relative - padding-left 38px + if isRTL + padding-right 38px + else + padding-left 38px margin-bottom 2px &:before diff --git a/modules/styles/blocks/balance/balance.styl b/modules/styles/blocks/balance/balance.styl index 8863e2e..851c4c8 100755 --- a/modules/styles/blocks/balance/balance.styl +++ b/modules/styles/blocks/balance/balance.styl @@ -13,7 +13,10 @@ font-weight bold & &__list - padding-left 19px + if isRTL + padding-right 19px + else + padding-left 19px & li margin 12px 0 diff --git a/modules/styles/blocks/body/body.styl b/modules/styles/blocks/body/body.styl index a81bf93..45e24ff 100755 --- a/modules/styles/blocks/body/body.styl +++ b/modules/styles/blocks/body/body.styl @@ -8,8 +8,7 @@ body color color background background margin 0 - if isRtl - background-color green + if isRTL direction rtl @media print diff --git a/modules/styles/blocks/breadcrumbs/breadcrumbs.styl b/modules/styles/blocks/breadcrumbs/breadcrumbs.styl index 4b59cf5..2863549 100755 --- a/modules/styles/blocks/breadcrumbs/breadcrumbs.styl +++ b/modules/styles/blocks/breadcrumbs/breadcrumbs.styl @@ -10,7 +10,10 @@ margin 0 &::after - content "→" + if isRTL + content "←" + else + content "→" font-family "Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif color #a9a9a9 diff --git a/modules/styles/blocks/extract/extract.styl b/modules/styles/blocks/extract/extract.styl index b7853be..b0fa646 100755 --- a/modules/styles/blocks/extract/extract.styl +++ b/modules/styles/blocks/extract/extract.styl @@ -79,7 +79,10 @@ background #f9edbf &__content - padding-left 20px + if isRTL + padding-right 20px + else + padding-left 20px padding-right 10px &__aside_price diff --git a/modules/styles/blocks/lessons-list/lessons-list.styl b/modules/styles/blocks/lessons-list/lessons-list.styl index b82e0c2..df75b73 100755 --- a/modules/styles/blocks/lessons-list/lessons-list.styl +++ b/modules/styles/blocks/lessons-list/lessons-list.styl @@ -53,7 +53,10 @@ lessons-list-separator = 1px solid #ede8e0 &__lesson_level_1 background #faf8f7 - padding-left 46px + if isRTL + padding-right 46px + else + padding-left 46px &__lesson_level_1 > &__link font-weight 600 @@ -68,7 +71,10 @@ lessons-list-separator = 1px solid #ede8e0 &__lesson_level_2 background #fff - padding-left 71px + if isRTL + padding-right 71px + else + padding-left 71px &__lesson_level_2:first-child border-top lessons-list-separator @@ -176,7 +182,10 @@ lessons-list-separator = 1px solid #ede8e0 width: 21px; height: 23px; line-height: 23px; - padding-left: 3px; + if isRTL + padding-right: 3px; + else + padding-left: 3px; text-align center diff --git a/modules/styles/blocks/main/main.styl b/modules/styles/blocks/main/main.styl index ad3f10d..b306d69 100755 --- a/modules/styles/blocks/main/main.styl +++ b/modules/styles/blocks/main/main.styl @@ -71,7 +71,10 @@ $main-loud border-right 2px solid #f5f2f0 &__lesson-nav-next - padding-left 15px + if isRTL + padding-right 15px + else + padding-left 15px &__lesson-nav-prev:last-child border none @@ -92,7 +95,11 @@ $main-loud ul, ol - padding-left 21px + if isRTL + padding-right 21px + + else + padding-left 21px margin 22px 0 // уравниваем с абзацными отступами > li @@ -105,8 +112,12 @@ $main-loud ul > li::before content "●" - float left // not position: absolute because the latter doesn't show in iBooks (epub) - margin-left -20px + if isRTL + float right + margin-right -20px + else + float left // not position: absolute because the latter doesn't show in iBooks (epub) + margin-left -20px color #000 font-size 8px diff --git a/modules/styles/blocks/page-footer/page-footer.styl b/modules/styles/blocks/page-footer/page-footer.styl index f579702..90e0341 100755 --- a/modules/styles/blocks/page-footer/page-footer.styl +++ b/modules/styles/blocks/page-footer/page-footer.styl @@ -23,8 +23,12 @@ color light_link_color background url(slack.svg) left center no-repeat background-size 16px - padding-left 20px - margin-left -4px + if isRTL + padding-right 20px + margin-right -4px + else + padding-left 20px + margin-left -4px &__left flex-grow 1 diff --git a/modules/styles/blocks/page/page.styl b/modules/styles/blocks/page/page.styl index 924d501..112c193 100755 --- a/modules/styles/blocks/page/page.styl +++ b/modules/styles/blocks/page/page.styl @@ -3,15 +3,26 @@ z-index: 0 padding 0 + .sidebar__toggle + if isRTL + left auto + right 100% &_sidebar_on - padding-left sidebar_width + if isRTL + padding-right sidebar_width + else + padding-left sidebar_width &__sidebar position fixed top 0 bottom 0 - left 0 - transform translateX(-100%) + if isRTL + right 0 + transform translateX(100%) + else + left 0 + transform translateX(-100%) // используется при переключении видимости панели навигации &__inner @@ -52,7 +63,10 @@ text-decoration none &__nav_prev - left 0 + if isRTL + right 0 + else + left 0 transition transform animation_duration, top animation_duration &__nav-text @@ -88,20 +102,29 @@ background rgba(216, 216, 216, .3) &__nav_prev &__nav-text::before - @extend $font-angle-left + if isRTL + @extend $font-angle-right + else + @extend $font-angle-left &__nav-text-alternate display none &_sidebar_on &__nav_prev - transform translateX(sidebar_width) + if isRTL + transform translateX(- sidebar_width) + else + transform translateX(sidebar_width) &__nav_next - right 0 + left 0 transition top animation_duration &__nav_next &__nav-text::before - @extend $font-angle-right + if isRTL + @extend $font-angle-left + else + @extend $font-angle-right &_ebook @@ -114,7 +137,10 @@ display none pre.line-numbers - padding-left 10px + if isRTL + padding-right 10px + else + padding-left 10px // ebook reader has smaller page width, // so I make sure the code fits it @@ -196,7 +222,10 @@ border-color: #7e7e7e &__nav_prev - padding-left 30px + if isRTL + padding-right 30px + else + padding-left 30px border-right-width 0 border-radius: 6px 0 0 6px diff --git a/modules/styles/blocks/prism/01-prism.styl b/modules/styles/blocks/prism/01-prism.styl index cac65c7..8d452ba 100755 --- a/modules/styles/blocks/prism/01-prism.styl +++ b/modules/styles/blocks/prism/01-prism.styl @@ -57,6 +57,15 @@ pre[class*="language-"] .token.punctuation color light_gray_color +.token.punctuation, +.token.function, +.token.comment, +.token.string, +.token.operator, +.token.number + if isRTL + background inherit + .namespace opacity .7 diff --git a/modules/styles/blocks/prism/03-prism-line-numbers.styl b/modules/styles/blocks/prism/03-prism-line-numbers.styl index e8b4828..d036c48 100644 --- a/modules/styles/blocks/prism/03-prism-line-numbers.styl +++ b/modules/styles/blocks/prism/03-prism-line-numbers.styl @@ -1,6 +1,9 @@ pre.line-numbers position relative - padding-left 3.8em + if isRTL + padding-right 3.8em + else + padding-left 3.8em counter-reset linenumber .line-numbers .line-numbers-rows diff --git a/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl b/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl index f68c462..ed27c9b 100755 --- a/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl +++ b/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl @@ -125,8 +125,12 @@ display block &__search .text-input__control - padding-left 37px - padding-right 93px + if isRTL + padding-left 93px + padding-right 37px + else + padding-left 37px + padding-right 93px // we MUST repeat theese rules for each selector to make it work &__search .text-input__control::-webkit-input-placeholder diff --git a/modules/styles/blocks/sitetoolbar/sitetoolbar.styl b/modules/styles/blocks/sitetoolbar/sitetoolbar.styl index 8cf77ce..032af7a 100755 --- a/modules/styles/blocks/sitetoolbar/sitetoolbar.styl +++ b/modules/styles/blocks/sitetoolbar/sitetoolbar.styl @@ -50,7 +50,10 @@ position relative min-width 20px - padding-left 47px + if isRTL + padding-right 47px + else + padding-left 47px transition opacity 0.3s ease-out @@ -247,8 +250,12 @@ display block &__search .text-input__control - padding-left 37px - padding-right 93px + if isRTL + padding-left 93px + padding-right 37px + else + padding-left 37px + padding-right 93px // we MUST repeat theese rules for each selector to make it work &__search .text-input__control::-webkit-input-placeholder @@ -414,13 +421,19 @@ padding-right 5px &__user-wrap - padding-left 40px + if isRTL + padding-right 40px + else + padding-left 40px &__userpic left -38px &__search-wrap - padding-right 10px + if isRTL + padding-left 10px + else + padding-right 10px @media tablet diff --git a/modules/styles/blocks/toolbar/toolbar.styl b/modules/styles/blocks/toolbar/toolbar.styl index e0cc790..b357aed 100755 --- a/modules/styles/blocks/toolbar/toolbar.styl +++ b/modules/styles/blocks/toolbar/toolbar.styl @@ -5,7 +5,10 @@ toolbar_button_background = #c4c2c0 &__tool display table-cell - padding-left 1px + if isRTL + padding-right 1px + else + padding-left 1px & &__button @extend $plain-link From 69f6c3ee803b66f0b2453344615e465e4676e217 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 22 May 2020 13:33:33 +0200 Subject: [PATCH 127/218] reverting webpack format --- modules/config/webpack.js | 251 +++++++++++++++++++------------------- 1 file changed, 127 insertions(+), 124 deletions(-) diff --git a/modules/config/webpack.js b/modules/config/webpack.js index eb0237b..005c11c 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -6,43 +6,43 @@ let path = require('path'); // no webpack dependencies inside // no es6 (for 6to5 processing) inside // NB: includes angular-* -let noProcessModulesRegExp = new RegExp( - 'node_modules' + (path.sep === '/' ? path.sep : '\\\\') + '(angular|prismjs|sanitize-html|i18n-iso-countries)' -); +let noProcessModulesRegExp = new RegExp("node_modules" + (path.sep === '/' ? path.sep : '\\\\') + "(angular|prismjs|sanitize-html|i18n-iso-countries)"); let devMode = process.env.NODE_ENV == 'development'; + module.exports = function () { + let nib = require('nib'); let rupture = require('rupture'); let chokidar = require('chokidar'); let webpack = require('webpack'); let WriteVersionsPlugin = require('engine/webpack/writeVersionsPlugin'); let CssWatchRebuildPlugin = require('engine/webpack/cssWatchRebuildPlugin'); - const CopyWebpackPlugin = require('copy-webpack-plugin'); - const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + const CopyWebpackPlugin = require('copy-webpack-plugin') + const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); - const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); + const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); const fse = require('fs-extra'); - // tutorial.js?hash - // tutorial.hash.js + +// tutorial.js?hash +// tutorial.hash.js function extHash(name, ext, hash) { if (!hash) hash = '[hash]'; - return config.assetVersioning == 'query' - ? `${name}.${ext}?${hash}` - : config.assetVersioning == 'file' - ? `${name}.${hash}.${ext}` - : `${name}.${ext}`; + return config.assetVersioning == 'query' ? `${name}.${ext}?${hash}` : + config.assetVersioning == 'file' ? `${name}.${hash}.${ext}` : + `${name}.${ext}`; } let modulesDirectories = [path.join(process.cwd(), 'node_modules')]; if (process.env.NODE_PATH) { - modulesDirectories = modulesDirectories.concat(process.env.NODE_PATH.split(/[:;]/).map((p) => path.resolve(p))); + modulesDirectories = modulesDirectories.concat(process.env.NODE_PATH.split(/[:;]/).map(p => path.resolve(p))); } //console.log("MODULE DIRS", modulesDirectories); + /** * handler/client/assets/* goes to public/assets/ */ @@ -63,11 +63,11 @@ module.exports = function () { * handler/templates makes handler.css (via CssWatchRebuiltPlugin) */ let entries = { - head: 'client/head', - footer: 'client/footer', - tutorial: 'engine/koa/tutorial/client', - styles: config.tmpRoot + '/styles.styl', - frontpage: config.tmpRoot + '/frontpage.styl', + head: 'client/head', + footer: 'client/footer', + tutorial: 'engine/koa/tutorial/client', + styles: config.tmpRoot + '/styles.styl', + frontpage: config.tmpRoot + '/frontpage.styl' }; /* @@ -88,10 +88,11 @@ module.exports = function () { }*/ //console.log("WEBPACK ENTRIES", entries); + let webpackConfig = { output: { // fs path - path: path.join(config.publicRoot, 'pack'), + path: path.join(config.publicRoot, 'pack'), // path as js sees it // if I use another domain here, need enable Allow-Access-.. header there // and add to scripts, to let error handler track errors @@ -100,29 +101,27 @@ module.exports = function () { // в prod-режиме не можем ?, т.к. CDN его обрезают, поэтому [hash] в имени // (какой-то [hash] здесь необходим, иначе к chunk'ам типа 3.js, которые генерируются require.ensure, // будет обращение без хэша при загрузке внутри сборки. при изменении - барузерный кеш их не подхватит) - filename: extHash('[name]', 'js'), + filename: extHash("[name]", 'js'), - chunkFilename: extHash('[name]-[id]', 'js'), - library: '[name]', - pathinfo: devMode, + chunkFilename: extHash("[name]-[id]", 'js'), + library: '[name]', + pathinfo: devMode }, cache: devMode, + mode: devMode ? 'development' : 'production', // for tests uses prod too watchOptions: { aggregateTimeout: 10, - ignored: /node_modules/, + ignored: /node_modules/ }, watch: devMode, - devtool: devMode - ? 'cheap-inline-module-source-map' // try "eval" ? - : process.env.NODE_ENV == 'production' - ? 'source-map' - : false, + devtool: devMode ? "cheap-inline-module-source-map" : // try "eval" ? + process.env.NODE_ENV == 'production' ? 'source-map' : false, profile: Boolean(process.env.WEBPACK_STATS), @@ -137,82 +136,81 @@ module.exports = function () { },*/ module: { - rules: [ + rules: [ { test: /\.yml$/, - use: ['json-loader', 'yaml-loader'], + use: ['json-loader', 'yaml-loader'] }, { test: /\.pug$/, - use: 'pug-loader?root=' + config.projectRoot + '/templates&globals=__', + use: 'pug-loader?root=' + config.projectRoot + '/templates&globals=__' }, { - test: /\.js$/, + test: /\.js$/, // babel shouldn't process modules which contain ws/browser.js, // which must not be run in strict mode (global becomes undefined) // babel would make all modules strict! exclude: noProcessModulesRegExp, - use: [ + use: [ // babel will work first { - loader: 'babel-loader', + loader: 'babel-loader', options: { presets: [ // use require.resolve here to build files from symlinks - [ - require.resolve('babel-preset-env'), - { - //useBuiltIns: true, - targets: { - browsers: '> 3%', - }, - }, - ], - ], - }, - }, - ], + [require.resolve('babel-preset-env'), { + //useBuiltIns: true, + targets: { + browsers: "> 3%" + } + }] + ] + } + } + ] }, { test: /\.styl$/, // MiniCssExtractPlugin breaks HMR for CSS - use: [ + use: [ MiniCssExtractPlugin.loader, { - loader: 'css-loader', + loader: 'css-loader', options: { - importLoaders: 1, - }, + importLoaders: 1 + } }, { - loader: 'postcss-loader', + loader: 'postcss-loader', options: { - plugins: [require('autoprefixer')], - }, + plugins: [ + require('autoprefixer') + ] + } }, 'engine/webpack/hover-loader', { - loader: 'stylus-loader', + loader: 'stylus-loader', options: { - linenos: true, + linenos: true, 'resolve url': true, - use: [ + use: [ rupture(), nib(), function (style) { style.define('lang', config.lang); - style.define('isRTL', ['ar', 'fa'].includes(process.env.TUTORIAL_LANG)); + style.define('isRTL', ['ar','fa'].includes(process.env.TUTORIAL_LANG)); style.define('env', config.env); - }, - ], + } + ] }, - }, - ], + } + ] }, { test: /\.(png|jpg|gif|woff|eot|otf|ttf|svg)$/, - use: extHash('file-loader?name=[path][name]', '[ext]'), - }, + use: extHash('file-loader?name=[path][name]', '[ext]') + } ], noParse: function (path) { /* @@ -222,52 +220,52 @@ module.exports = function () { */ //console.log(path); return noProcessModulesRegExp.test(path); - }, + } }, + resolve: { // allow require('styles') which looks for styles/index.styl extensions: ['.js', '.styl'], - alias: { + alias: { 'entities/maps/entities.json': 'engine/markit/emptyEntities', - config: 'client/config', + config: 'client/config' }, - modules: modulesDirectories, + modules: modulesDirectories }, + resolveLoader: { - modules: modulesDirectories, - extensions: ['.js'], + modules: modulesDirectories, + extensions: ['.js'] }, node: { - fs: 'empty', + fs: 'empty' }, performance: { maxEntrypointSize: 350000, maxAssetSize: 350000, // warning if asset is bigger than 300k - assetFilter(assetFilename) { - // only check js/css + assetFilter(assetFilename) { // only check js/css // ignore assets copied by CopyWebpackPlugin - if (assetFilename.startsWith('..')) { - // they look like ../courses/achievements/course-complete.svg + if (assetFilename.startsWith('..')) { // they look like ../courses/achievements/course-complete.svg // built assets do not have .. return false; } return assetFilename.endsWith('.js') || assetFilename.endsWith('.css'); - }, + } }, plugins: [ new webpack.DefinePlugin({ - LANG: JSON.stringify(config.lang), - IS_CLIENT: true, + LANG: JSON.stringify(config.lang), + IS_CLIENT: true }), // lodash is loaded when free variable _ occurs in the code new webpack.ProvidePlugin({ - _: 'lodash', + _: 'lodash' }), // ignore all locales except current lang @@ -286,9 +284,10 @@ module.exports = function () { // console.log("ignore moment locale", tmp, arg); return true; } - }, + } }), + // ignore site locale files except the lang new webpack.IgnorePlugin({ checkResource(arg) { @@ -301,89 +300,93 @@ module.exports = function () { let ignore = /\/locales(\/|$)/.test(arg); // console.log("ignore yml", tmp, arg); return ignore; - }, + } }), + new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')), new MiniCssExtractPlugin({ - filename: extHash('[name]', 'css'), - chunkFilename: extHash('[id]', 'css'), + filename: extHash("[name]", 'css'), + chunkFilename: extHash("[id]", 'css'), }), new CssWatchRebuildPlugin(), new CopyWebpackPlugin( - assetPaths.map((path) => { + assetPaths.map(path => { return { from: path, - to: config.publicRoot, - }; + to: config.publicRoot + } }), - { debug: 'warning' } + {debug: 'warning'} ), { apply: function (compiler) { if (process.env.WEBPACK_STATS) { - compiler.plugin('done', function (stats) { + compiler.plugin("done", function (stats) { stats = stats.toJson(); fs.writeFileSync(`${config.tmpRoot}/stats.json`, JSON.stringify(stats)); }); } - }, - }, + } + } ], recordsPath: path.join(config.tmpRoot, 'webpack.json'), - devServer: { - port: 3001, // dev server itself does not use it, but outer tasks do + devServer: { + port: 3001, // dev server itself does not use it, but outer tasks do historyApiFallback: true, - hot: true, - watchDelay: 10, + hot: true, + watchDelay: 10, //noInfo: true, - publicPath: process.env.STATIC_HOST + ':3001/pack/', - contentBase: config.publicRoot, + publicPath: process.env.STATIC_HOST + ':3001/pack/', + contentBase: config.publicRoot }, + optimization: { minimizer: [ new UglifyJsPlugin({ - cache: true, - parallel: 2, + cache: true, + parallel: 2, uglifyOptions: { - ecma: 8, + ecma: 8, warnings: false, compress: { - drop_console: true, - drop_debugger: true, + drop_console: true, + drop_debugger: true }, - output: { - beautify: true, - indent_level: 0, // for error reporting, to see which line actually has the problem + output: { + beautify: true, + indent_level: 0 // for error reporting, to see which line actually has the problem // source maps actually didn't work in Qbaka that's why I put it here - }, - }, + } + } }), - new OptimizeCSSAssetsPlugin({}), - ], - }, + new OptimizeCSSAssetsPlugin({}) + ] + } }; - //if (process.env.NODE_ENV != 'development') { // production, ebook - if (process.env.NODE_ENV == 'production') { - // production, ebook - webpackConfig.plugins.push(function clearBeforeRun() { - function clear(compiler, callback) { - fse.removeSync(webpackConfig.output.path + '/*'); - callback(); - } - // in watch mode this will clear between partial rebuilds - // thus removing unchanged files - // => use this plugin only in normal run - this.plugin('run', clear); - }); +//if (process.env.NODE_ENV != 'development') { // production, ebook + if (process.env.NODE_ENV == 'production') { // production, ebook + webpackConfig.plugins.push( + function clearBeforeRun() { + function clear(compiler, callback) { + fse.removeSync(webpackConfig.output.path + '/*'); + callback(); + } + + // in watch mode this will clear between partial rebuilds + // thus removing unchanged files + // => use this plugin only in normal run + this.plugin('run', clear); + } + ); } return webpackConfig; From e33b874146dfa6ff5b519ff739756e327b105b4a Mon Sep 17 00:00:00 2001 From: mohamedsaad Date: Wed, 27 May 2020 07:44:35 +0200 Subject: [PATCH 128/218] Add ar.yml file to translate into arabic --- locales/ar.yml | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100755 locales/ar.yml diff --git a/locales/ar.yml b/locales/ar.yml new file mode 100755 index 0000000..88a7a28 --- /dev/null +++ b/locales/ar.yml @@ -0,0 +1,104 @@ +site: + privacy_policy: سياسة الخصوصية + terms: شروط الإستخدام + + gdpr_dialog: + title: هذا الموقع يستخدم الكوكيز + text: نحن نستخدم بعض الوسائل فى المتصفح مثل الكوكيز واللوكال ستورج لتخزين تفضيلاتك. يجب أن تقبل سياسة خصوصيتنا و شروط الإستخدام للتمكن من ذلك. + accept: قبول + cancel: إلغاء + + toolbar: + lang_switcher: + cta_text: نريد أن نتيح هذا المشروع المفتوح المصدر إلى كل الناس حول العالم. من فضلك ساعدنا على ترجمة محتوى هذه السلسله للغة التى تعرفها. + footer_text: how much content is translated to the corresponding language + footer_text: كم المحتوى الذى تُرجم إلى لغتك + old_version: تم نشر الإصدار القديم، ويحتاج إلى مراجعه. + logo: + normal: + svg: sitetoolbar__logo_en.svg + width: 200 # 171 + normal-white: + svg: sitetoolbar__logo_en-white.svg + small: + svg: sitetoolbar__logo_small_en.svg + width: 70 + small-white: + svg: sitetoolbar__logo_small_en-white.svg + sections: + - slug: '' + url: '/' + title: 'سلسلة' + - slug: 'كورسات' + title: 'كورسات' + buy_ebook_extra: 'شراء' + buy_ebook: 'EPUB/PDF' + search_placeholder: 'إبحث فى Javascript.info' + search_button: 'بحث' + + public_profile: الصفحه الشخصية + account: الحساب + notifications: الإشعارات + admin: أدمن + logout: تسجيل الخروج + + sorry_old_browser: عفوًا، أى متصفح أقل من IE10 غير مدعوم + contact_us: تواصل معنا + about_the_project: معلومات عن المشروع + ilya_kantor: Ilya Kantor + comments: التعليقات + loading: تحميل... + search: بحث + share: مشاركه + read_before_commenting: إقرأ هذا قبل أن تضع تعليقًا… + + last_updated_at: "آخر تحديث #{date}" + meta: + description: 'سلسلة حديثة لشرح الجافاسكريبت: بسيطه ولكن مفصَّلة مع أمثلة ومهام، وتحتوى على مصطلحات مثل: closures و document و events و البرمجه الكائنيه OOP وغرهم.' + + tablet-menu: + choose_section: اختر جزءًا + search_placeholder: البحث فى السلسلة + search_button: بحث + + comment: + help: + - إذا كان لديك اقتراحات أو تريد تحسينًا - من فضلك من فضلك إفتح موضوعًا فى جيتهاب أو شارك بنفسك بدلًا من التعليقات. + - إذا لم تستطع أن تفهم شيئّا فى المقال - وضّح ماهو. + - إذا كنت تريد عرض كود استخدم عنصر <code> ، وللكثير من السطور استخدم <pre>، ولأكثر من 10 سطور استخدم (plnkr, JSBin, codepen…) + + edit_on_github: عدِّل فى جيتهاب + error: خطأ + close: إغلاق + + hide_forever: إخفاء إلى الأبد + hidden_forever: هذه المعلومه لن تظهر بعد الآن. + + + subscribe: + title: تابع تحديثات javascript.info + text: 'نحن لا نرسل إعلانات ولكن مواضيع متعلقة بالسلسلة فقط. أنت تختار ماتريد إرساله لك:' + agreement: 'By signing up to newsletters you agree to the terms of usage.' + agreement: 'بالمشاركة فى نشرة الأخبار فأنت توافق على شروط الإستخدام.' + button: شارك + button_unsubscribe: إلغاء المشاركة من كل الأخبار + common_updates: التحديثات العامة + common_updates_text: new courses, master classes, article and screencast releases + common_updates_text: كورسات جديدة أو ماستر أو مقال جديد + your_email: your@email.here + newsletters: 'نشرة أخبار,نشرات أخبار,نشرات أخبار' + no_selected: لا شئ تم تحديده + + form: + value_must_not_be_empty: لا يمكن أن تكون القيمه فارغه. + value_is_too_long: القيمة طويلة جدًا. + value_is_too_short: القيمة قصيرة جدًا. + invalid_email: بريد غير صالح. + invalid_value: قيمة غير صالحة. + invalid_autocomplete: من فضلك اختر من القائمة + invalid_date: 'تاريخ غير صالح, الشكل: dd.mm.yyyyy.' + invalid_range: هذا التاريخ ليس صالحًا هنا. + save: حفظ + upload_file: رفع ملف + cancel: إلغاء + server_error: خطأ فى الطلب، كود الرد From 1ed9ca76670fba7c422b9e4fe9f34cdaab110c48 Mon Sep 17 00:00:00 2001 From: Omar Date: Sun, 31 May 2020 12:38:07 +0200 Subject: [PATCH 129/218] fix numbering in index for RTL --- .../frontpage/templates/blocks/_frontpage-content/index.styl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.styl b/modules/frontpage/templates/blocks/_frontpage-content/index.styl index 1504d98..13b7d22 100755 --- a/modules/frontpage/templates/blocks/_frontpage-content/index.styl +++ b/modules/frontpage/templates/blocks/_frontpage-content/index.styl @@ -125,7 +125,10 @@ &:before position absolute top 4px - left 0 + if isRTL + right 0 + else + left 0 font-family fixed_width_font font-size 12px line-height 16px From cda6ff424afb11d6ca7c2ab7cbcd34911f06118f Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:56:09 +0300 Subject: [PATCH 130/218] minor fixes --- README.md | 102 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 097093f..a4fee2f 100755 --- a/README.md +++ b/README.md @@ -94,11 +94,11 @@ Please note, the server must support that language. There must be corresponding # Translating images -Most pictures are in SVG format. Strings inside it are usually just text, they can be replaced. +The text in SVG pictures can be translated as well. -That's great, as there are many strings in English in images, like tips, notes, etc. They look nice when translated. +There's a special script for that. It takes `images.yml` from the repository root, and then replaces strings in all svgs according to its content. -Image translations reside in `images.yml` in the repository root, for example: . Please, create it if needed. +**Step 1.** You should make `images.yml` with translations in the repository root, for example: . The file format is "YAML", it's quite easy to understand: @@ -106,46 +106,35 @@ The file format is "YAML", it's quite easy to understand: code-style.svg: # image file name "No space": # English string text: "Без пробелов" # translation - position: "center" # (optional) "center" or "right" - to position translated string. + position: "center" # (optional) "center" or "right" - to position the translated string, details later ``` -The translated string may become longer or shorter. By default, the translated string starts at the same place: +**Step 2.** Setup git upstream (if you haven't yet) and pull latest changes: - ``` - |hello world (before) - |你好世界 (after translation) - ``` - -Sometimes they need to be repositioned: - -`position: "center"` centers the translated string, good if you have a vertical diagram, keeps text centered: -``` - | -hello world - 你好世界 - | +```bash +cd /js/zh.javascript.info +git remote add upstream https://github.com/javascript-tutorial/en.javascript.info +git fetch upstream master ``` -`position: "right"` makes sure that the translated string keeps the same right edge: -``` -hello world | - 你好世界 | +**Step 3.** Run the translation task: +```bash +cd /js/server +# set NODE_LANG to your language +NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate ``` -After `images.yaml` with translations is ready, it's time to apply translations: +This script checks out all SVG images from `upstream` and replaces the strings according to `images.yml`. + +**Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to check them before doing so. + + +> The `--image` parameter allows to translate a single image: +> ```bash +> # replace strings only in try-catch-flow.svg +> NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg +> ``` -1. Setup git upstream (if you haven't yet) and pull latest changes: - ```bash - cd /js/zh.javascript.info - git remote add upstream https://github.com/javascript-tutorial/en.javascript.info - git fetch upstream master - ``` -2. Run the translation task: - ```bash - cd /js/server - # without --image it applies all translations (to all images) - NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg - ``` > For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this: > @@ -154,13 +143,46 @@ After `images.yaml` with translations is ready, it's time to apply translations: > cross-env NODE_LANG=zh... > ``` -The task takes upstream image version (English), replaces strings to it, then writes to same-named image in the tutorial repo. -You may want to open the resulting SVG file directly in the browser to see it. +## The "overflowing text" problem + +The translated string may become longer than the original. + +The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. + +If you'll notice that, you usually can adjust the translation to make it shorter. Besides, most pictures have some extra space for longer text, so a slight increase wouldn't harm. + +If your translated text absolutely must be longer, let me know, I can adjust the picture. + +## Positioning + + + By default, the translated string replaces the original one, in exactly the same place of the image: + +``` +| hello world (before) +| 你好世界 (after translation) +``` + +Sometimes that's not good, e.g. if the string needs to be centeredm e.g in a vertical diagram. + +The `position: "center"` in `images.yml` centers the translated string: +``` + | +hello world + 你好世界 + | +``` + +The `position: "right"` makes sure that the translated string sticks to the same right edge: +``` +hello world | + 你好世界 | +``` + +P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux/MacOS). -P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux/MacOS). - -## Extract strings +## Helper script: extract strings The task to get all strings from an image as YAML (for translation, to add to `images.yml`): From c83dfb52a11407b3e0f97ac97548e7d2dc481092 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:57:09 +0300 Subject: [PATCH 131/218] minor fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a4fee2f..4ace7bf 100755 --- a/README.md +++ b/README.md @@ -96,9 +96,9 @@ Please note, the server must support that language. There must be corresponding The text in SVG pictures can be translated as well. -There's a special script for that. It takes `images.yml` from the repository root, and then replaces strings in all svgs according to its content. +There's a special script for that. It takes `images.yml` from the repository root, like , and then replaces strings in all svgs according to its content. -**Step 1.** You should make `images.yml` with translations in the repository root, for example: . +**Step 1.** You should make `images.yml` with translations in the repository root. The file format is "YAML", it's quite easy to understand: From abf32c0bfa0ad33c4066da42ba752484dbadfacd Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:57:20 +0300 Subject: [PATCH 132/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ace7bf..d9870ba 100755 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ The text in SVG pictures can be translated as well. There's a special script for that. It takes `images.yml` from the repository root, like , and then replaces strings in all svgs according to its content. -**Step 1.** You should make `images.yml` with translations in the repository root. +**Step 1.** Create `images.yml` with translations in the repository root. The file format is "YAML", it's quite easy to understand: From 696626c56ba52ee50023be56c520c882a3c0c738 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:57:40 +0300 Subject: [PATCH 133/218] minor fixes --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d9870ba..8292993 100755 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ The text in SVG pictures can be translated as well. There's a special script for that. It takes `images.yml` from the repository root, like , and then replaces strings in all svgs according to its content. +Here are the steps to translate images. + **Step 1.** Create `images.yml` with translations in the repository root. The file format is "YAML", it's quite easy to understand: From 9766eda186249cfc14d8198dc9c898860cd63197 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:58:18 +0300 Subject: [PATCH 134/218] minor fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8292993..965e6a8 100755 --- a/README.md +++ b/README.md @@ -114,14 +114,14 @@ code-style.svg: # image file name **Step 2.** Setup git upstream (if you haven't yet) and pull latest changes: ```bash -cd /js/zh.javascript.info +cd /js/zh.javascript.info # in the tutorial folder git remote add upstream https://github.com/javascript-tutorial/en.javascript.info git fetch upstream master ``` **Step 3.** Run the translation task: ```bash -cd /js/server +cd /js/server # in the server folder # set NODE_LANG to your language NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate ``` From aed2ee976ce07a288c718ad53d4ebec1d7bc1363 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:58:33 +0300 Subject: [PATCH 135/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 965e6a8..3532b58 100755 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ git fetch upstream master **Step 3.** Run the translation task: ```bash cd /js/server # in the server folder -# set NODE_LANG to your language +# adjust NODE_LANG to your language NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate ``` From 09de865df2911138bcd9c3a972c4eee6a71b25f4 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:59:02 +0300 Subject: [PATCH 136/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3532b58..1446944 100755 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ The translated string may become longer than the original. The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. -If you'll notice that, you usually can adjust the translation to make it shorter. Besides, most pictures have some extra space for longer text, so a slight increase wouldn't harm. +If you notice that, you usually can adjust the translation to make it shorter. Besides, most pictures have some extra space for longer text, so a slight increase wouldn't harm. If your translated text absolutely must be longer, let me know, I can adjust the picture. From 5dc84c00bbebccbb83c2d7ed4190522f1677ea8f Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:59:28 +0300 Subject: [PATCH 137/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1446944..d687adc 100755 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ The translated string may become longer than the original. The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. -If you notice that, you usually can adjust the translation to make it shorter. Besides, most pictures have some extra space for longer text, so a slight increase wouldn't harm. +If you notice that, you usually can adjust the translation to make it shorter. Besides, most pictures have some extra space for longer text, so a slight increase doesn't harm. If your translated text absolutely must be longer, let me know, I can adjust the picture. From 5f4227e6f115663d17a499bf1dded3b78e1d3327 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 11:59:50 +0300 Subject: [PATCH 138/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d687adc..2e630f5 100755 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ The replacement script only operates on strings, not other graphics, so a long t If you notice that, you usually can adjust the translation to make it shorter. Besides, most pictures have some extra space for longer text, so a slight increase doesn't harm. -If your translated text absolutely must be longer, let me know, I can adjust the picture. +If your translated string absolutely must be longer and doesn't fit, let me know, I can adjust the picture. ## Positioning From e564875f279335ebf7df392f9b9d31a7ff348325 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 12:00:20 +0300 Subject: [PATCH 139/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e630f5..c316b7b 100755 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ If your translated string absolutely must be longer and doesn't fit, let me know | 你好世界 (after translation) ``` -Sometimes that's not good, e.g. if the string needs to be centeredm e.g in a vertical diagram. +Sometimes that's not good, e.g. if the string needs to be centered in a vertical diagram. The `position: "center"` in `images.yml` centers the translated string: ``` From f2b11741d9f9f4b0fd2100fe4617ca199f2d1084 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 11 Jul 2020 12:01:14 +0300 Subject: [PATCH 140/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c316b7b..4739aa8 100755 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ If your translated string absolutely must be longer and doesn't fit, let me know Sometimes that's not good, e.g. if the string needs to be centered in a vertical diagram. -The `position: "center"` in `images.yml` centers the translated string: +The `position: "center"` in `images.yml` centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context: ``` | hello world From 2eb9fe0c357a84e78cd350d7faa1f118e02188a9 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 24 Jul 2020 09:05:54 +0300 Subject: [PATCH 141/218] minor fixes --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4739aa8..2b6fd22 100755 --- a/README.md +++ b/README.md @@ -71,6 +71,12 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call Then access the site at `http://127.0.0.1:3000`. + > To change the port, set the `PORT` environment variable: + > ```bash + > # Runs the server at http://127.0.0.1:8080 + > PORT=8080 ./edit en + > ``` + 7. Edit the tutorial As you edit text files in the tutorial text repository (cloned at step 5), From a817e2accfe339b0190d699b3a42c2d5851183e8 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 24 Jul 2020 09:08:15 +0300 Subject: [PATCH 142/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b6fd22..a2dd9eb 100755 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Please note, the server must support that language. There must be corresponding The text in SVG pictures can be translated as well. -There's a special script for that. It takes `images.yml` from the repository root, like , and then replaces strings in all svgs according to its content. +There's a special script for that. The translated strings should be in the `images.yml` file in the repository root, such as . The script replaces strings in all svgs according to its content. Here are the steps to translate images. From 2818b7d32fea9d875ad6d6a576c8025166603069 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 24 Jul 2020 09:09:02 +0300 Subject: [PATCH 143/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2dd9eb..855cb0c 100755 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ The file format is "YAML", it's quite easy to understand: code-style.svg: # image file name "No space": # English string text: "Без пробелов" # translation - position: "center" # (optional) "center" or "right" - to position the translated string, details later + position: "center" # (optional) "center" or "right" - to position the translated string ``` **Step 2.** Setup git upstream (if you haven't yet) and pull latest changes: From b73e0677d4a0c2e06b80e376070ef3c31fa20f22 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 24 Jul 2020 09:09:58 +0300 Subject: [PATCH 144/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 855cb0c..e4be739 100755 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ This script checks out all SVG images from `upstream` and replaces the strings a **Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to check them before doing so. -> The `--image` parameter allows to translate a single image: +> Use the `--image` parameter of the script to translate a single image: > ```bash > # replace strings only in try-catch-flow.svg > NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg From 9adf145c4ee1f23d0fd2ce84b4085f8bc434d570 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 24 Jul 2020 09:12:29 +0300 Subject: [PATCH 145/218] minor fixes --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4be739..9f7bf99 100755 --- a/README.md +++ b/README.md @@ -158,7 +158,9 @@ The translated string may become longer than the original. The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. -If you notice that, you usually can adjust the translation to make it shorter. Besides, most pictures have some extra space for longer text, so a slight increase doesn't harm. +Most pictures have some extra space for longer text, so a slight increase doesn't harm, but sometimes that happens. + +Usually, you should adjust the translated text, make it shorter to fit. If your translated string absolutely must be longer and doesn't fit, let me know, I can adjust the picture. From 584ded02625801d71b9a9500b09e0048faf89a68 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 24 Jul 2020 09:13:21 +0300 Subject: [PATCH 146/218] minor fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f7bf99..a182aa7 100755 --- a/README.md +++ b/README.md @@ -167,11 +167,11 @@ If your translated string absolutely must be longer and doesn't fit, let me know ## Positioning - By default, the translated string replaces the original one, in exactly the same place of the image: + By default, the translated string replaces the original one, starting in exactly the same place of the image: ``` | hello world (before) -| 你好世界 (after translation) +| 你好世界 (after translation) ``` Sometimes that's not good, e.g. if the string needs to be centered in a vertical diagram. From 44cc0f34be57fa0cb880f2cb2a2a3f693f72113d Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 26 Jul 2020 19:02:37 +0300 Subject: [PATCH 147/218] Update README.md --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a182aa7..64edec3 100755 --- a/README.md +++ b/README.md @@ -83,6 +83,19 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call the webpage will reload automatically. +# Windows: Environment variables + +For Windows, please install `npm i -g cross-env` and prepend calls with `cross-env` to pass environment variables, like this: + +```bash +cd /js/server +cross-env PORT=8080 ./edit en +``` + +In the examples below, the commands are without `cross-env`, prepend it please if you're on Windows. + +Alternatively, you can use other Windows-specific ways to set environment variables, such as `set NODE_LANG=zh` and others. + # Change server language The server uses English by default for navigation and design. @@ -144,14 +157,6 @@ This script checks out all SVG images from `upstream` and replaces the strings a > ``` -> For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this: -> -> ```bash -> cd /js/server -> cross-env NODE_LANG=zh... -> ``` - - ## The "overflowing text" problem The translated string may become longer than the original. From b86beb4120782ebc2f1be91f56be4a7a8248be43 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 7 Aug 2020 17:26:58 +0300 Subject: [PATCH 148/218] minor fixes --- modules/config/index.js | 2 ++ modules/config/webpack.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/config/index.js b/modules/config/index.js index 445d3c2..e226ec2 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -38,6 +38,8 @@ let config = module.exports = { appKeys: [secret.sessionKey], adminKey: secret.adminKey, + certDir: path.join(secret.dir, 'cert'), + lang: lang, plnkrAuthId: secret.plnkrAuthId, diff --git a/modules/config/webpack.js b/modules/config/webpack.js index 005c11c..8c0bc91 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -199,7 +199,7 @@ module.exports = function () { nib(), function (style) { style.define('lang', config.lang); - style.define('isRTL', ['ar','fa'].includes(process.env.TUTORIAL_LANG)); + style.define('isRTL', ['ar','fa','he'].includes(process.env.TUTORIAL_LANG)); style.define('env', config.env); } ] From 084fe7a22d421dce9742d3c75a10afd0efa6fbae Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 14 Aug 2020 17:06:05 +0300 Subject: [PATCH 149/218] minor fixes --- .../blocks/prism/02-prism-line-highlight.styl | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/modules/styles/blocks/prism/02-prism-line-highlight.styl b/modules/styles/blocks/prism/02-prism-line-highlight.styl index 64f0735..7364475 100644 --- a/modules/styles/blocks/prism/02-prism-line-highlight.styl +++ b/modules/styles/blocks/prism/02-prism-line-highlight.styl @@ -1,39 +1,25 @@ .main /* Used this parent class to be able to overwrite some generic rules from main.styl */ .inline-highlight - position absolute + font-family fixed_width_font + display inline-block pointer-events none line-height inherit white-space pre - left 0 - top -2px - z-index -1 - padding 0 - - .mask - padding 0 - background #F5E7C6 - outline 2px solid #F5E7C6 + font-style normal + background #F5E7C6 !important .block-highlight - display block - position absolute - left 0 - right 0 - top -1px - margin-top 1em /* Same as .prism’s padding-top */ - padding 0 + font-family fixed_width_font + display inline-block + width 100% pointer-events none line-height inherit white-space pre - .mask - display block - background #F5E7C6 - outline 1px solid #F5E7C6 - left 0 - right 0 - position absolute - padding 0 + background #F5E7C6 + + font-style normal + From 7f862176ca92c7ad025b51a014a4f8256b17f57c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 02:43:45 +0300 Subject: [PATCH 150/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64edec3..f22cf24 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call Please use Node.js 10+. - (Maybe later, optional) If you're going to change images, please install [GraphicsMagick](http://www.graphicsmagick.org/). + (Maybe later, optional) If you're going to change images, please install [ImageMagick](https://imagemagick.org/script/download.php). 2. Install global Node modules: From 9a3cf7f649fe114e5fae3351b795a07c824df2f3 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 12:41:17 +0300 Subject: [PATCH 151/218] minor fixes --- README.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f22cf24..63e66d4 100755 --- a/README.md +++ b/README.md @@ -171,28 +171,41 @@ If your translated string absolutely must be longer and doesn't fit, let me know ## Positioning +By default, the translated string replaces the original one, starting in exactly the same place of the image. - By default, the translated string replaces the original one, starting in exactly the same place of the image: +Before the translation: ``` -| hello world (before) -| 你好世界 (after translation) +| hello world ``` -Sometimes that's not good, e.g. if the string needs to be centered in a vertical diagram. +After the translation (`你` is at the same place where `h` was, the string is left-aligned): + +``` +| 你好世界 +``` + +Sometimes that's not good, e.g. if the string needs to be centered, e.g. like this: -The `position: "center"` in `images.yml` centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context: ``` | hello world - 你好世界 | ``` +(The "hello world" is in the middle between two `|`). + +The `position: "center"` in `images.yml` centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context: +``` + 🦊 +你好世界 + 🦊 +``` + The `position: "right"` makes sure that the translated string sticks to the same right edge: ``` -hello world | - 你好世界 | +hello world 🦊 + 你好世界 🦊 ``` P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux/MacOS). From d186920cbea980562f9f63d50faa573342599389 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 12:45:35 +0300 Subject: [PATCH 152/218] minor fixes --- README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 63e66d4..8ea1cd9 100755 --- a/README.md +++ b/README.md @@ -157,18 +157,6 @@ This script checks out all SVG images from `upstream` and replaces the strings a > ``` -## The "overflowing text" problem - -The translated string may become longer than the original. - -The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. - -Most pictures have some extra space for longer text, so a slight increase doesn't harm, but sometimes that happens. - -Usually, you should adjust the translated text, make it shorter to fit. - -If your translated string absolutely must be longer and doesn't fit, let me know, I can adjust the picture. - ## Positioning By default, the translated string replaces the original one, starting in exactly the same place of the image. @@ -193,7 +181,7 @@ hello world | ``` -(The "hello world" is in the middle between two `|`). +(The "hello world" is centered between two `|`). The `position: "center"` in `images.yml` centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context: ``` @@ -220,6 +208,17 @@ NODE_LANG=zh npm run gulp engine:koa:tutorial:imageYaml --image hello.svg ``` +## The "overflowing text" problem + +The translated string may become longer than the original. + +The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. Most pictures have some extra space for longer text, so a slight increase doesn't harm, but sometimes that happens. + +Usually, you should adjust the translated text, make it shorter to fit. + +If your translated string absolutely must be longer and doesn't fit, let me know, I can adjust the picture. + + # Dev mode If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to do. From e75d419693d097e25d139e1ac365a9e65c30f9e1 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 12:45:59 +0300 Subject: [PATCH 153/218] minor fixes --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8ea1cd9..63cb323 100755 --- a/README.md +++ b/README.md @@ -185,15 +185,15 @@ hello world The `position: "center"` in `images.yml` centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context: ``` - 🦊 + | 你好世界 - 🦊 + | ``` The `position: "right"` makes sure that the translated string sticks to the same right edge: ``` -hello world 🦊 - 你好世界 🦊 +hello world | + 你好世界 | ``` P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux/MacOS). From 1d8830fc7bd6d1324dcbd88ac4b2a165aba66827 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 12:46:20 +0300 Subject: [PATCH 154/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 63cb323..9adcb98 100755 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ hello world | 你好世界 | ``` -P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux/MacOS). +P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux or homebrew/macports for MacOS). ## Helper script: extract strings From 1e9b510663e1f5410e43bdd07635a130c9b63252 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 12:47:01 +0300 Subject: [PATCH 155/218] minor fixes --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 9adcb98..92a6dc4 100755 --- a/README.md +++ b/README.md @@ -210,8 +210,6 @@ NODE_LANG=zh npm run gulp engine:koa:tutorial:imageYaml --image hello.svg ## The "overflowing text" problem -The translated string may become longer than the original. - The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. Most pictures have some extra space for longer text, so a slight increase doesn't harm, but sometimes that happens. Usually, you should adjust the translated text, make it shorter to fit. From 302d5e77fc90057aaa0b207328233ec17d699c1b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 12:47:50 +0300 Subject: [PATCH 156/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92a6dc4..e103063 100755 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ If it still doesn't work – [file an issue](https://github.com/javascript-tutor Please pull the very latest git code and install latest NPM modules before publishing an issue. --- +--
      Yours, Ilya Kantor iliakan@javascript.info From bad06c59cb3704d6b15ab58f71f0d4b83f000c73 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 15 Aug 2020 12:48:06 +0300 Subject: [PATCH 157/218] minor fixes --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index e103063..014cd72 100755 --- a/README.md +++ b/README.md @@ -257,7 +257,4 @@ If it still doesn't work – [file an issue](https://github.com/javascript-tutor Please pull the very latest git code and install latest NPM modules before publishing an issue. ---
      -Yours, -Ilya Kantor -iliakan@javascript.info +--
      Yours,
      Ilya Kantor
      iliakan@javascript.info From fdcb281481c21662ade35bc3e796690f1ae6415b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 07:58:10 +0300 Subject: [PATCH 158/218] minor fixes --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 014cd72..435e7f4 100755 --- a/README.md +++ b/README.md @@ -52,14 +52,14 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call 6. Run the site - Install local modules: + First, install local modules: ```bash cd /js/server npm install ``` - Run the site with the same language. Above we cloned `en` tutorial, so: + Run the site with the `./edit` command with the language argument. Above we cloned `en` tutorial, so: ```bash ./edit en @@ -76,6 +76,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call > # Runs the server at http://127.0.0.1:8080 > PORT=8080 ./edit en > ``` + > For Windows, read the note about environment variables below. 7. Edit the tutorial @@ -85,16 +86,16 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call # Windows: Environment variables -For Windows, please install `npm i -g cross-env` and prepend calls with `cross-env` to pass environment variables, like this: +For Windows, to pass environment variables, such as `PORT`, you can install `npm i -g cross-env` and prepend calls with `cross-env`, like this: ```bash cd /js/server cross-env PORT=8080 ./edit en ``` -In the examples below, the commands are without `cross-env`, prepend it please if you're on Windows. +In the examples below, the commands are without `cross-env`, prepend it please, if you're on Windows. -Alternatively, you can use other Windows-specific ways to set environment variables, such as `set NODE_LANG=zh` and others. +Alternatively, you can use other Windows-specific ways to set environment variables, such as `set NODE_LANG=zh`. # Change server language @@ -115,7 +116,7 @@ Please note, the server must support that language. There must be corresponding The text in SVG pictures can be translated as well. -There's a special script for that. The translated strings should be in the `images.yml` file in the repository root, such as . The script replaces strings in all svgs according to its content. +There's a special script for that. The translated strings should be in the `images.yml` file in the repository root, such as . The script replaces strings in all svgs according to `images.yml`. Here are the steps to translate images. From dec1d9444cf343d20d6010e9bb0950698f85169b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 07:59:14 +0300 Subject: [PATCH 159/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 435e7f4..4d2f4e6 100755 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ cross-env PORT=8080 ./edit en In the examples below, the commands are without `cross-env`, prepend it please, if you're on Windows. -Alternatively, you can use other Windows-specific ways to set environment variables, such as `set NODE_LANG=zh`. +Alternatively, you can use other Windows-specific ways to set environment variables, such as a separate `set PORT=8080` command. # Change server language From fe86c7bfe64f422c96f5c19c1092a6f7b889f441 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 08:00:51 +0300 Subject: [PATCH 160/218] minor fixes --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4d2f4e6..4c8ce35 100755 --- a/README.md +++ b/README.md @@ -148,6 +148,8 @@ NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate This script checks out all SVG images from `upstream` and replaces the strings according to `images.yml`. +Now images are translated, but not committed yet. + **Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to check them before doing so. From d1235035a35b9bcdfc3048943cb7b131ca285683 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 08:01:24 +0300 Subject: [PATCH 161/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c8ce35..ff7aca9 100755 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate This script checks out all SVG images from `upstream` and replaces the strings according to `images.yml`. -Now images are translated, but not committed yet. +Now images in the tutorial folder are translated, but not committed yet. **Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to check them before doing so. From 7b1997d2162239a18bf3cebfdcaa0300cc12459b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 08:02:13 +0300 Subject: [PATCH 162/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff7aca9..32ab065 100755 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ This script checks out all SVG images from `upstream` and replaces the strings a Now images in the tutorial folder are translated, but not committed yet. -**Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to check them before doing so. +**Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to take a look at them before committing. Just to make sure that the translation looks all right. > Use the `--image` parameter of the script to translate a single image: From 422de8b0389264e7555a04d7cc328ac052fed9ee Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 08:02:51 +0300 Subject: [PATCH 163/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32ab065..e950919 100755 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Now images in the tutorial folder are translated, but not committed yet. **Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to take a look at them before committing. Just to make sure that the translation looks all right. -> Use the `--image` parameter of the script to translate a single image: +> To translate a single image, use the `--image` parameter of the script: > ```bash > # replace strings only in try-catch-flow.svg > NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg From 4cf0b319bb2ff0e8d7add755f4fa1255743c3173 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 08:08:26 +0300 Subject: [PATCH 164/218] minor fixes --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e950919..6edca66 100755 --- a/README.md +++ b/README.md @@ -150,7 +150,9 @@ This script checks out all SVG images from `upstream` and replaces the strings a Now images in the tutorial folder are translated, but not committed yet. -**Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. You may want to open the translated SVGs directly in the browser to take a look at them before committing. Just to make sure that the translation looks all right. +**Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. + +You may want to open the translated SVGs directly in the browser to take a look at them before committing. Just to make sure that the translation looks all right. If an image is untranslated on refresh, use "uncached reload" (hotkeys at https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache). > To translate a single image, use the `--image` parameter of the script: From 74385b90d0500dbcd8fc3d05df0e521d6a25d633 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 16 Aug 2020 08:10:14 +0300 Subject: [PATCH 165/218] minor fixes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6edca66..8c41ab6 100755 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ git fetch upstream master ```bash cd /js/server # in the server folder # adjust NODE_LANG to your language -NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate +NODE_LANG=zh npm run gulp -- engine:koa:tutorial:figuresTranslate ``` This script checks out all SVG images from `upstream` and replaces the strings according to `images.yml`. @@ -152,13 +152,13 @@ Now images in the tutorial folder are translated, but not committed yet. **Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. -You may want to open the translated SVGs directly in the browser to take a look at them before committing. Just to make sure that the translation looks all right. If an image is untranslated on refresh, use "uncached reload" (hotkeys at https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache). +You may want to open the translated SVGs directly in the browser to take a look at them before committing. Just to make sure that the translation looks all right. If an image is untranslated on refresh, force the browser to "reload without cache" ([hotkeys](https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache)). > To translate a single image, use the `--image` parameter of the script: > ```bash > # replace strings only in try-catch-flow.svg -> NODE_LANG=zh glp engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg +> NODE_LANG=zh npm run gulp -- engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg > ``` From 2c852e154a42a26bcd4334beaff95077e0d5a246 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 5 Oct 2020 17:31:17 +0300 Subject: [PATCH 166/218] minor fixes --- modules/client/config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/client/config.js b/modules/client/config.js index dc121c6..a6b5cc2 100755 --- a/modules/client/config.js +++ b/modules/client/config.js @@ -1,3 +1,4 @@ module.exports = { - lang: LANG + lang: LANG, + lookatCodeUrlBase: "https://lookatcode.com" }; From e1804af38bc71f928ee0c6d0870f6e385e292f0b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 10 Oct 2020 09:36:52 +0300 Subject: [PATCH 167/218] minor fixes --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index bb255e4..b0efee4 100755 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "copy-webpack-plugin": "^4.5.2", "cross-env": "^5.2.0", "css-loader": "^0", + "csrf": "^3", "file-loader": "^1.1", "fs-extra": "*", "gm": "*", From 68eb948df7b6f982d5b85fea26e0514103b967f2 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 10 Oct 2020 14:04:14 +0300 Subject: [PATCH 168/218] minor fixes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b0efee4..798d8ff 100755 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "precommit": "NODE_ENV=development node `which gulp` pre-commit", "//": "node-xmpp-client installs for linux only", "dependencies": { - "autoprefixer": "*", + "autoprefixer": "^9", "babel-core": "^6", "babel-loader": "^7", "babel-plugin-transform-flow-strip-types": "*", @@ -61,7 +61,7 @@ "nodemon": "^1.18.4", "optimize-css-assets-webpack-plugin": "^4.0.3", "path-to-regexp": "*", - "postcss-loader": "*", + "postcss-loader": "^3", "prismjs": "^1", "pug": "^2.0.3", "pug-loader": "^2.4.0", From 239bbdba45a39363219376d0e0ac47b0367b6604 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 21 Oct 2020 23:58:41 +0300 Subject: [PATCH 169/218] minor fixes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 798d8ff..43cd894 100755 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "clarify": "^2.1.0", "copy-webpack-plugin": "^4.5.2", "cross-env": "^5.2.0", - "css-loader": "^0", "csrf": "^3", + "css-loader": "^0", "file-loader": "^1.1", "fs-extra": "*", "gm": "*", @@ -74,7 +74,7 @@ "trace": "^3.1.0", "uglify-es": "^3", "uglifyjs-webpack-plugin": "^1", - "webpack": ">=4.17.2", + "webpack": "^4.44.2", "yaml-loader": "^0.5.0" }, "engineStrict": true, From bf1d7ad1caf7bdd57f27e52f8e8f97da0bbffe10 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 15 Dec 2020 11:51:41 +0300 Subject: [PATCH 170/218] minor fixes --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 43cd894..59c363a 100755 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "stylus-loader": "^3", "trace": "^3.1.0", "uglify-es": "^3", + "terser-webpack-plugin": "^4", "uglifyjs-webpack-plugin": "^1", "webpack": "^4.44.2", "yaml-loader": "^0.5.0" From 2b69d89116a36a46ab91b14bd79324a5c7e262ec Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 15 Dec 2020 12:16:39 +0300 Subject: [PATCH 171/218] minor fixes --- edit | 4 +++- gulpfile.js | 9 ++++++--- tasks/livereload.js | 34 +++++++++++++--------------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/edit b/edit index eb54490..081093f 100755 --- a/edit +++ b/edit @@ -6,7 +6,9 @@ : ${1?"Usage: $0 []"} -export TUTORIAL_ROOT="../$1.javascript.info" +if [ -z "$TUTORIAL_ROOT" ]; then + export TUTORIAL_ROOT="../$1.javascript.info" +fi export NODE_LANG="${2:-en}" export TUTORIAL_LANG=$1 export NODE_ENV=production diff --git a/gulpfile.js b/gulpfile.js index d3e7bd7..3053a03 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -30,15 +30,18 @@ task("nodemon", lazyRequireTask('./tasks/nodemon', { watch: ["modules"] })); -task("livereload", lazyRequireTask("./tasks/livereload", { +task('livereload', lazyRequireTask('./tasks/livereload', { // watch files *.*, not directories, no need to reload for new/removed files, // we're only interested in changes + base: `public/${config.lang}`, watch: [ - "public/pack/**/*.*", + `public/${config.lang}/pack/**/*.*`, + // not using this file, using only styles.css (extracttextplugin) + `!public/${config.lang}/pack/styles.js`, // this file changes every time we update styles // don't watch it, so that the page won't reload fully on style change - "!public/pack/head.js" + `!public/${config.lang}/pack/head.js` ] })); diff --git a/tasks/livereload.js b/tasks/livereload.js index 658ad63..89230a1 100644 --- a/tasks/livereload.js +++ b/tasks/livereload.js @@ -1,40 +1,32 @@ -let livereload = require('gulp-livereload'); -let gulp = require('gulp'); -let throttle = require('lodash/throttle'); +let livereloadServer = require('engine/livereloadServer') let chokidar = require('chokidar'); // options.watch must NOT be www/**, because that breaks (why?!?) supervisor reloading // www/**/*.* is fine module.exports = async function(options) { - // listen to changes after the file events finish to arrive - // no one is going to livereload right now anyway - livereload.listen(); + function onChokidarChange(changed) { + changed = changed.slice(options.base.length + 1); + // console.log("CHANGE", changed); - // reload once after all scripts are rebuit - livereload.changedSoon = throttle(livereload.changed, 1000, {leading: false}); - //livereload.changedVerySoon = _.throttle(livereload.changed, 100, {leading: false}); + if (!changed.match(/\.(jpg|css|png|gif|svg)/i)) { + changed = '/fullpage'; // make all requests that cause full-page reload be /fullpage + // otherwise we'll have many reloads (once per diffrent .js url) + } + livereloadServer.queueFlush(changed); + } setTimeout(function() { - console.log("livereload: listen on change " + options.watch); + // console.log("livereload: listen on change " + options.watch); chokidar.watch(options.watch, { awaitWriteFinish: { stabilityThreshold: 300, - pollInterval: 100 + pollInterval: 100 } - }).on('change', function(changed) { - if (changed.match(/\.(js|map)/)) { - // full page reload - livereload.changedSoon(changed); - } else { - livereload.changed(changed); - } - }); + }).on('change', onChokidarChange); }, 1000); await new Promise(resolve => {}); }; - - From 5e1d46976e3f438e3fbbf622ddb06529caaed6df Mon Sep 17 00:00:00 2001 From: liangminhua Date: Sun, 20 Dec 2020 01:27:15 +0800 Subject: [PATCH 172/218] Add DockerfIle #51 --- Dockerfile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2bd5aad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:10 +ARG LANG=en +ENV LANG=${LANG} +ENV HOST=0.0.0.0 +USER root +COPY . /js/server +RUN cd js && \ + npm_config_user=root npm install -g bunyan gulp@4 && \ + git clone https://github.com/javascript-tutorial/engine server/modules/engine --depth=1 && \ + git clone https://github.com/javascript-tutorial/$LANG.javascript.info --depth=1 && \ + cd server && npm install +WORKDIR /js/server +EXPOSE 3000 +CMD ./edit $LANG $LANG From f23dd8b0ce8561b6f32b230329ff0d37d7bf8517 Mon Sep 17 00:00:00 2001 From: joaquinelio Date: Wed, 3 Feb 2021 13:17:35 -0300 Subject: [PATCH 173/218] A nice reminder? : ) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5a80117..fc7cd53 100755 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call ``` Please note, there are two clone commands. That's not a typo: `modules/engine` is cloned from another repository. + + And please don't forget this when updating, `modules/engine` needs to be fetched and merged too. 5. Clone the tutorial text into it. From bd788ff74ea40bca6f25fac365155ddb7baefc50 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 11 Feb 2021 14:34:46 +0300 Subject: [PATCH 174/218] minor fixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 59c363a..0a97fe8 100755 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "gulp-livereload": "^4", "html-entities": "^1.3.1", "image-size": "*", - "js-yaml": "*", + "js-yaml": "^3", "json-loader": "^0.5.7", "koa": "^2", "koa-bodyparser": "^4", From 0226a110955350d39c439ef056cc93a719741058 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 11 Mar 2021 22:01:46 +0300 Subject: [PATCH 175/218] minor fixes --- templates/layouts/base.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/layouts/base.pug b/templates/layouts/base.pug index 86f0ad9..06d7097 100755 --- a/templates/layouts/base.pug +++ b/templates/layouts/base.pug @@ -2,5 +2,5 @@ doctype html include ~bemto.pug/lib/index -html(lang=lang) +html(lang=lang dir=['ar','fa','he'].includes(lang) ? 'rtl' : undefined) block html From 248a4f43f11285951c95fa1058df5977f22aa685 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 12:36:58 +0300 Subject: [PATCH 176/218] Update README.md --- README.md | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fc7cd53..544e766 100755 --- a/README.md +++ b/README.md @@ -116,15 +116,21 @@ Please note, the server must support that language. There must be corresponding # Translating images -The text in SVG pictures can be translated as well. +Please don't translate SVG files manually. -There's a special script for that. The translated strings should be in the `images.yml` file in the repository root, such as . The script replaces strings in all svgs according to `images.yml`. +They are auto-generated from the English variant, with the text phrases substituted from `images.yml` file in the repository root, such as . + +So you need to translate the content of `images.yml` and re-generate the SVGs using a script. Here are the steps to translate images. **Step 1.** Create `images.yml` with translations in the repository root. -The file format is "YAML", it's quite easy to understand: +An example of such file (in Russian): https://github.com/javascript-tutorial/ru.javascript.info/blob/master/images.yml + +The file format is "YAML". + +Here's a quote: ```yaml code-style.svg: # image file name @@ -133,28 +139,42 @@ code-style.svg: # image file name position: "center" # (optional) "center" or "right" - to position the translated string ``` -**Step 2.** Setup git upstream (if you haven't yet) and pull latest changes: +As you can see, for each image file (such as `code-style.svg`), there go English phrases (such as `"No space"`). + +For each phrase, there's the translated `text` and the text `position` (not always needed, details will come soon). + +You can make a small file with only one image for the start. + +**Step 2.** Setup git upstream (if you haven't yet) and pull latest changes from English version: ```bash cd /js/zh.javascript.info # in the tutorial folder + git remote add upstream https://github.com/javascript-tutorial/en.javascript.info + git fetch upstream master ``` **Step 3.** Run the translation task: + ```bash cd /js/server # in the server folder + # adjust NODE_LANG to your language + NODE_LANG=zh npm run gulp -- engine:koa:tutorial:figuresTranslate ``` -This script checks out all SVG images from `upstream` and replaces the strings according to `images.yml`. +This script checks out all SVG images from `upstream` (English version) and replaces the strings inside them according to `images.yml`. So they become translated. + +New SVGs are the tutorial folder now, but not committed yet. + +You may want to open and see them, e.g. in Chrome browser, just to ensure that the translation fits well. -Now images in the tutorial folder are translated, but not committed yet. +P.S. If an image appears untranslated on refresh, force the browser to "reload without cache" ([hotkeys](https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache)). -**Step 4.** Then you'll need `git add/commit/push` the translated SVGs, as a part of the normal translation flow. +**Step 4.** Then you'll need to `git add/commit/push` the translated SVGs, as a part of the normal translation flow. -You may want to open the translated SVGs directly in the browser to take a look at them before committing. Just to make sure that the translation looks all right. If an image is untranslated on refresh, force the browser to "reload without cache" ([hotkeys](https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache)). > To translate a single image, use the `--image` parameter of the script: From 320075a1f185ee25fd83a0ded1f6f0f1143cd657 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 12:37:48 +0300 Subject: [PATCH 177/218] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 544e766..087ff16 100755 --- a/README.md +++ b/README.md @@ -175,8 +175,7 @@ P.S. If an image appears untranslated on refresh, force the browser to "reload w **Step 4.** Then you'll need to `git add/commit/push` the translated SVGs, as a part of the normal translation flow. - - +> Normally, the translation script looks for all images listed in `images.yml`. > To translate a single image, use the `--image` parameter of the script: > ```bash > # replace strings only in try-catch-flow.svg From be2896f63286fff77a11268200e5339838c6b235 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 12:39:43 +0300 Subject: [PATCH 178/218] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 087ff16..fb7c4fc 100755 --- a/README.md +++ b/README.md @@ -175,6 +175,8 @@ P.S. If an image appears untranslated on refresh, force the browser to "reload w **Step 4.** Then you'll need to `git add/commit/push` the translated SVGs, as a part of the normal translation flow. +...And voilà! SVGs are translated! + > Normally, the translation script looks for all images listed in `images.yml`. > To translate a single image, use the `--image` parameter of the script: > ```bash From 63d5a1f815010e1b065c17a3f413a5821286d41c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 12:43:43 +0300 Subject: [PATCH 179/218] Update README.md --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fb7c4fc..282d4a2 100755 --- a/README.md +++ b/README.md @@ -211,7 +211,21 @@ hello world (The "hello world" is centered between two `|`). -The `position: "center"` in `images.yml` centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context: +Then, if we just replace the string, it would become: + +``` + | +你好世界 + | +``` + +As we can see, the new phrase is shorter. We should move it to the right a bit. + +The `position: "center"` in `images.yml` does exactly that. + +It centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context. + +Here's the text with `position: "center"`, centered as it should be: ``` | 你好世界 @@ -224,6 +238,8 @@ hello world | 你好世界 | ``` +That's also useful for images when we expect the text to stick to the right. + P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux or homebrew/macports for MacOS). ## Helper script: extract strings From 5ae4c520ac9507a37b98c2f2c9169431d879f08b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 13:16:50 +0300 Subject: [PATCH 180/218] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 282d4a2..7c3ecb0 100755 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Here are the steps to translate images. An example of such file (in Russian): https://github.com/javascript-tutorial/ru.javascript.info/blob/master/images.yml -The file format is "YAML". +The file format is [YAML](https://learnxinyminutes.com/docs/yaml/). Here's a quote: From 5e78fa6e9629a22776a96bc16b02077cce373e1b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 13:18:38 +0300 Subject: [PATCH 181/218] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c3ecb0..faeadb3 100755 --- a/README.md +++ b/README.md @@ -167,9 +167,11 @@ NODE_LANG=zh npm run gulp -- engine:koa:tutorial:figuresTranslate This script checks out all SVG images from `upstream` (English version) and replaces the strings inside them according to `images.yml`. So they become translated. -New SVGs are the tutorial folder now, but not committed yet. +The new translated SVGs are the tutorial folder now, but not committed yet. -You may want to open and see them, e.g. in Chrome browser, just to ensure that the translation fits well. +You can see them with `git status`. + +Take a moment to open and check them, e.g. in Chrome browser, just to ensure that the translation looks good. P.S. If an image appears untranslated on refresh, force the browser to "reload without cache" ([hotkeys](https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache)). From da9a5d8e34dedfd1a45b837f1e3eb58811ed8ba9 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 13:26:23 +0300 Subject: [PATCH 182/218] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index faeadb3..bcadba8 100755 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ You can make a small file with only one image for the start. **Step 2.** Setup git upstream (if you haven't yet) and pull latest changes from English version: ```bash -cd /js/zh.javascript.info # in the tutorial folder +cd /js/server/repo/zh.javascript.info # in the tutorial folder git remote add upstream https://github.com/javascript-tutorial/en.javascript.info @@ -249,11 +249,11 @@ P.S In order for positioning to work, you need to have ImageMagick installed: Date: Thu, 29 Apr 2021 13:26:55 +0300 Subject: [PATCH 183/218] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bcadba8..62badff 100755 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ The task to get all strings from an image as YAML (for translation, to add to `i ```bash cd /js/server # in the server folder -NODE_LANG=zh npm run gulp engine:koa:tutorial:imageYaml --image hello.svg +NODE_LANG=zh npm run gulp -- engine:koa:tutorial:imageYaml --image hello.svg ``` ## The "overflowing text" problem From 69e75700ea2919750a5ba7a4a071b9d941fd3ad1 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 13:35:03 +0300 Subject: [PATCH 184/218] minor --- README.md | 11 +++++++---- modules/config/index.js | 2 +- package.json | 2 +- repo/.gitkeep | 0 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 repo/.gitkeep diff --git a/README.md b/README.md index 62badff..308de8f 100755 --- a/README.md +++ b/README.md @@ -251,17 +251,20 @@ The task to get all strings from an image as YAML (for translation, to add to `i ```bash cd /js/server # in the server folder -NODE_LANG=zh npm run gulp -- engine:koa:tutorial:imageYaml --image hello.svg +NODE_LANG=zh npm run gulp -- engine:koa:tutorial:imageYaml --image code-style.svg ``` +It extracts all text lines. Useful for debugging, when the translation doesn't "catch up", because the SVG text has an extra space or so. + ## The "overflowing text" problem -The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. Most pictures have some extra space for longer text, so a slight increase doesn't harm, but sometimes that happens. +The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. -Usually, you should adjust the translated text, make it shorter to fit. +Most pictures have some extra space for longer text, so a slight increase doesn't harm. -If your translated string absolutely must be longer and doesn't fit, let me know, I can adjust the picture. +If the translated text is much longer, please try to change it, make it shorter to fit. +If the translated text absolutely must be longer and doesn't fit, let me know, we'll see how to adjust the picture. # Dev mode diff --git a/modules/config/index.js b/modules/config/index.js index e226ec2..2386d90 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -58,7 +58,7 @@ let config = module.exports = { // public files, served by nginx publicRoot: path.join(process.cwd(), 'public', lang), // private files, for expiring links, not directly accessible - tutorialRoot: env.TUTORIAL_ROOT || path.join(process.cwd(), '..', lang + '.javascript.info'), + tutorialRoot: env.TUTORIAL_ROOT || path.join(process.cwd(), 'repo', `${env.TUTORIAL_LANG || lang}.javascript.info`), tmpRoot: path.join(process.cwd(), 'tmp', lang), // js/css build versions cacheRoot: path.join(process.cwd(), 'cache', lang), diff --git a/package.json b/package.json index 0a97fe8..c738ce4 100755 --- a/package.json +++ b/package.json @@ -71,9 +71,9 @@ "rupture": "*", "style-loader": "^0", "stylus-loader": "^3", + "terser-webpack-plugin": "^4", "trace": "^3.1.0", "uglify-es": "^3", - "terser-webpack-plugin": "^4", "uglifyjs-webpack-plugin": "^1", "webpack": "^4.44.2", "yaml-loader": "^0.5.0" diff --git a/repo/.gitkeep b/repo/.gitkeep new file mode 100644 index 0000000..e69de29 From e025d45049aa795b341bc0a88626e5d7936a6ddb Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 29 Apr 2021 13:35:48 +0300 Subject: [PATCH 185/218] minor fixes --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7408f34..db9143e 100755 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ Thumbs.db # database dump for tutorial export /dump +/repo # NPM packages folder. node_modules/ From 14fa198d7ca41e1e22c3589fb3b831e5087aab88 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 30 Apr 2021 00:07:38 +0300 Subject: [PATCH 186/218] Update README.md --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 308de8f..b2e0ce4 100755 --- a/README.md +++ b/README.md @@ -46,15 +46,17 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call The repository starts with the language code, e.g for the French version `fr.javascript.info`, for Russian – `ru.javascript.info` etc. The English version is `en.javascript.info`. + + The tutorial text repository should go into the `repo` subfolder, like this: ```bash - cd /js + cd /js/server/repo git clone https://github.com/javascript-tutorial/en.javascript.info ``` 6. Run the site - First, install local modules: + Install local NPM modules: ```bash cd /js/server @@ -67,7 +69,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call ./edit en ``` - This will import the tutorial from `/js/en.javascript.info` and start the server. + This will import the tutorial from `/js/server/repo/en.javascript.info` and start the server. Wait a bit while it reads the tutorial from the disk and builds static assets. @@ -112,7 +114,7 @@ cd /js/server ./edit ru ru ``` -Please note, the server must support that language. There must be corresponding locale files for that language in the code of the server, otherwise it exists with an error. As of now, `ru`, `en`, `zh`, `tr` and `ja` are fully supported. +Please note, the server must support that language. There must be corresponding locale files for that language in the code of the server, otherwise it exists with an error. As of now, `ru`, `en`, `zh`, `tr`, `ko` and `ja` are fully supported. # Translating images From 5ad2e7f0d2b041540ad06eb79681c32b8f511bc0 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 30 Apr 2021 00:09:13 +0300 Subject: [PATCH 187/218] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2e0ce4..29577b3 100755 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call 3. Create the root folder. - Create a folder `/js` for the project. If you use another directory as the root, adjust the paths below. + Create a folder `/js` for the project. + + You can also use another directory as the root, then adjust the paths below, replace `/js` with it. 4. Clone the tutorial server into it: From fd975b30b8dcfa2d9ad05ef7a4bfe9dbbf6dbf02 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 30 Apr 2021 00:09:35 +0300 Subject: [PATCH 188/218] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 29577b3..98bd45e 100755 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call Create a folder `/js` for the project. - You can also use another directory as the root, then adjust the paths below, replace `/js` with it. + You can also use another directory as the root, then adjust the paths below, replace `/js` with your root. 4. Clone the tutorial server into it: From e1316bc704fef3ff35e7a5e3bf30fb035cc7e033 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 30 Apr 2021 00:10:09 +0300 Subject: [PATCH 189/218] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98bd45e..5211c32 100755 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call Create a folder `/js` for the project. - You can also use another directory as the root, then adjust the paths below, replace `/js` with your root. + You can also use another directory as the root, then change the paths below: replace `/js` with your root. 4. Clone the tutorial server into it: From d53036c5671862e4c4b976217e119aec998546d9 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Fri, 7 May 2021 09:39:03 +0300 Subject: [PATCH 190/218] minor fixes --- README.md | 4 +--- edit | 4 ---- edit.cmd | 2 -- modules/config/index.js | 2 +- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5211c32..0d150cc 100755 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ First, run the command that imports (and caches) the tutorial: ```bash cd /js/server -NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutorial:import +NODE_LANG=en npm run gulp engine:koa:tutorial:import ``` > For Windows: `npm i -g cross-env` and prepend the call with `cross-env` to pass environment variables, like this: @@ -287,8 +287,6 @@ NODE_LANG=en TUTORIAL_ROOT=/js/en.javascript.info npm run gulp engine:koa:tutori > cross-env NODE_LANG=en... > ``` -In the code above, `NODE_LANG` sets server language, while `TUTORIAL_ROOT` is the full path to tutorial repo, by default is `/js/$NODE_LANG.javascript.info`. - Afterwards, call `./dev ` to run the server: ```bash diff --git a/edit b/edit index 081093f..08d5a13 100755 --- a/edit +++ b/edit @@ -5,10 +5,6 @@ : ${1?"Usage: $0 []"} - -if [ -z "$TUTORIAL_ROOT" ]; then - export TUTORIAL_ROOT="../$1.javascript.info" -fi export NODE_LANG="${2:-en}" export TUTORIAL_LANG=$1 export NODE_ENV=production diff --git a/edit.cmd b/edit.cmd index 12359c2..8b693a7 100644 --- a/edit.cmd +++ b/edit.cmd @@ -1,7 +1,5 @@ @if "%~1"=="" goto usage -@set TUTORIAL_ROOT=../%1.javascript.info - @if "%~2"=="" ( set NODE_LANG=en ) else ( diff --git a/modules/config/index.js b/modules/config/index.js index 2386d90..bd2eafb 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -103,4 +103,4 @@ function createRoot(root) { if (!fs.existsSync(root)) { fs.ensureDirSync(root); } -} +} \ No newline at end of file From 3dced741818f3cb680fc6e57b9352166b90b79d4 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 18 May 2021 07:50:52 +0300 Subject: [PATCH 191/218] minor fixes --- modules/config/index.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/config/index.js b/modules/config/index.js index bd2eafb..50feaaf 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -78,7 +78,7 @@ config.tutorialRepo = { require.extensions['.yml'] = function(module, filename) { - module.exports = yaml.safeLoad(fs.readFileSync(filename, 'utf-8')); + module.exports = yaml.load(fs.readFileSync(filename, 'utf-8')); }; diff --git a/package.json b/package.json index c738ce4..121fcc2 100755 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "gulp-livereload": "^4", "html-entities": "^1.3.1", "image-size": "*", - "js-yaml": "^3", + "js-yaml": "^4", "json-loader": "^0.5.7", "koa": "^2", "koa-bodyparser": "^4", From 6bbe73d158eb3e35c0d048b71fbe2ed1d8361cfd Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 1 Sep 2021 16:47:22 +0300 Subject: [PATCH 192/218] lang: id --- locales/id.yml | 97 ++++++++++++++++++++++++++++++++ modules/frontpage/locales/id.yml | 6 ++ 2 files changed, 103 insertions(+) create mode 100644 locales/id.yml create mode 100644 modules/frontpage/locales/id.yml diff --git a/locales/id.yml b/locales/id.yml new file mode 100644 index 0000000..ac29f40 --- /dev/null +++ b/locales/id.yml @@ -0,0 +1,97 @@ +site: + privacy_policy: kebijakan privasi + terms: ketentuan penggunaan + banner_bottom: Tingkatkan keterampilan Anda dengan mengunjungi kursus video tentang JavaScript dan Framework terkait. + action_required: Diperlukan tindakan + gdpr_dialog: + title: Situs web ini menggunakan cookie + text: Kami menggunakan teknologi browser seperti cookie dan penyimpanan lokal untuk menyimpan preferensi Anda. Anda harus menerima Kebijakan Privasi dan Ketentuan Penggunaan kami agar kami dapat melakukannya. + accept: Terima + cancel: Batal + + toolbar: + lang_switcher: + cta_text: > +

      Kami ingin membuat proyek open source ini tersedia untuk orang-orang di seluruh dunia.

      +

      Bantu untuk menerjemahkan konten tutorial ini ke bahasa Anda!

      + footer_text: berapa banyak konten yang diterjemahkan ke bahasa yang sesuai + old_version: Versi lama diterbitkan, perlu dukungan. + logo: + normal: + svg: sitetoolbar__logo_en.svg + width: 200 # 171 + normal-white: + svg: sitetoolbar__logo_en-white.svg + small: + svg: sitetoolbar__logo_small_en.svg + width: 70 + small-white: + svg: sitetoolbar__logo_small_en-white.svg + sections: + buy_ebook_extra: 'Beli' + buy_ebook: 'EPUB/PDF' + search_placeholder: 'Cari pada Javascript.info' + search_button: 'Cari' + + public_profile: Profil publik + account: Akun + notifications: Notifikasi + admin: Admin + logout: Logout + + sorry_old_browser: Maaf, Internet Explorer tidak didukung, harap gunakan browser yang lebih baru. + contact_us: hubungi kami + about_the_project: terkait proyek + ilya_kantor: Ilya Kantor + comments: komentar + loading: Memuat... + search: Cari + share: Bagikan + read_before_commenting: baca ini sebelum berkomentar… + last_updated_at: "Terakhir diperbarui pada #{date}" + meta: + description: 'Tutorial JavaScript Modern: penjelasan sederhana, namun terperinci dengan contoh dan soal, termasuk: closure, document dan events, pemrograman OOP dan banyak lagi.' + + tablet-menu: + choose_section: Pilih bagian + search_placeholder: Cari di tutorial + search_button: Cari + + comment: + help: + - Jika Anda memiliki saran apa yang harus ditingkatkan - silakan kunjungi kirimkan Github issue atau pull request sebagai gantinya berkomentar. + - Jika Anda tidak dapat memahami sesuatu dalam artikel – harap jelaskan. + - Untuk menyisipkan beberapa kata kode, gunakan tag <code>, untuk beberapa baris – bungkus dengan tag <pre>, untuk lebih dari 10 baris – gunakan sandbox (plnkr, jsbin, < a href='http://codepen.io'>codepen…) + + edit_on_github: Sunting di GitHub + error: eror + close: tutup + + hide_forever: sembunyikan secara permanen + hidden_forever: Informasi ini tidak akan muncul lagi. + + subscribe: + title: Perhatikan update pada javascript.info + text: 'Kami tidak mengirim iklan, hanya hal-hal yang relevan. Anda memilih apa yang akan diterima:' + agreement: 'Dengan mendaftar ke buletin, Anda menyetujui persyaratan penggunaan.' + button: Berlangganan + button_unsubscribe: Berhenti berlangganan dari semua + common_updates: Pembaruan umum + common_updates_text: kursus baru, kelas master, artikel, dan rilis screencast + your_email: your@email.here + newsletters: 'buletin, buletin, buletin' + no_selected: Tidak ada yang dipilih + + form: + value_must_not_be_empty: Nilai wajib diisi. + value_is_too_long: Nilai terlalu panjang. + value_is_too_short: Nilai terlalu pendek. + invalid_email: Email tidak valid. + invalid_value: Nilai tidak valid. + invalid_autocomplete: Silakan, pilih dari daftar + invalid_date: 'Tanggal tidak valid, format: dd.mm.yyyy.' + invalid_range: Tanggal ini tidak valid di sini. + save: Simpan + upload_file: Upload file + cancel: Batal + server_error: Kesalahan permintaan, kode status diff --git a/modules/frontpage/locales/id.yml b/modules/frontpage/locales/id.yml new file mode 100644 index 0000000..83f8b85 --- /dev/null +++ b/modules/frontpage/locales/id.yml @@ -0,0 +1,6 @@ +view_github: github +search_placeholder: Cari di tutorial +search_button: Cari +share_text: Bagikan +courses_ongoing: Daftar sekarang +contributors_pluralize: "kontributor, kontributor, kontributor" From 6190c4c7c16703d38217ae654dad4a4d64045c99 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 12 Feb 2022 16:06:38 +0300 Subject: [PATCH 193/218] minor fixes --- modules/styles/blocks/00-variables/icons.styl | 419 +++++++++++------- modules/styles/blocks/font/font-icons.styl | 18 +- modules/styles/blocks/font/icons.woff | Bin 14976 -> 19424 bytes 3 files changed, 264 insertions(+), 173 deletions(-) diff --git a/modules/styles/blocks/00-variables/icons.styl b/modules/styles/blocks/00-variables/icons.styl index c99f6b6..436faf6 100755 --- a/modules/styles/blocks/00-variables/icons.styl +++ b/modules/styles/blocks/00-variables/icons.styl @@ -5,335 +5,416 @@ // символов из незагруженного шрифта $font-icon - & - font-family 'FontIcons' - -webkit-font-smoothing antialiased - -moz-osx-font-smoothing grayscale + & + font-family 'FontIcons' + -webkit-font-smoothing antialiased + -moz-osx-font-smoothing grayscale + font-style normal - .no-icons & - visibility hidden + .no-icons & + visibility hidden $font-close - @extend $font-icon - content '\25b4' + @extend $font-icon + content '\25b4' $font-close-cross - @extend $font-icon - content '\e80d' + @extend $font-icon + content '\e80d' $font-open - @extend $font-icon - content '\25be' + @extend $font-icon + content '\e808' $font-star - @extend $font-icon - content '\2605' + @extend $font-icon + content '\e83f' $font-star-empty - @extend $font-icon - content '\2606' + @extend $font-icon + content '\2606' $font-warning - @extend $font-icon - content '\26a0' + @extend $font-icon + content '\e83a' $font-mail - @extend $font-icon - content '\e810' + @extend $font-icon + content '\e810' $font-mail-inverse - @extend $font-icon - content '\e835' + @extend $font-icon + content '\e835' $font-edit - @extend $font-icon - content '\270d' + @extend $font-icon + content '\e839' $font-ok - @extend $font-icon - content '\2714' + @extend $font-icon + content '\e837' $font-question - @extend $font-icon - content '\e81e' + @extend $font-icon + content '\e81e' $font-qa - @extend $font-icon - content '\2753' + @extend $font-icon + content '\2753' $font-pros - @extend $font-icon - content '\e059' + @extend $font-icon + content '\e840' $font-cons - @extend $font-icon - content '\e075' + @extend $font-icon + content '\e075' $font-help - @extend $font-icon - content '\e81e' + @extend $font-icon + content '\e81e' $font-info - @extend $font-icon - content '\e705' + @extend $font-icon + content '\e838' $font-code - @extend $font-icon - content '\e714' + @extend $font-icon + content '\e714' $font-external - @extend $font-icon - content '\e715' + @extend $font-icon + content '\e83e' $font-download - @extend $font-icon - content '\e805' + @extend $font-icon + content '\e805' $font-print - @extend $font-icon - content '\e716' + @extend $font-icon + content '\e716' $font-chat - @extend $font-icon - content '\e720' + @extend $font-icon + content '\e720' $font-angle-left - @extend $font-icon - content '\e80e' + @extend $font-icon + content '\e80e' $font-angle-right - @extend $font-icon - content '\e807' + @extend $font-icon + content '\e807' $font-run - @extend $font-icon - content '\f00f' + @extend $font-icon + content '\f00f' $font-copy - @extend $font-icon - content '\f0c5' + @extend $font-icon + content '\f0c5' $font-comment - @extend $font-icon - content '\f4ac' + @extend $font-icon + content '\f4ac' $font-search - @extend $font-icon - content '\e81b' + @extend $font-icon + content '\e81b' $font-study - @extend $font-icon - content '\1f393' + @extend $font-icon + content '\1f393' $font-user - @extend $font-icon - content '\1f464' + @extend $font-icon + content '\E83D' $font-job - @extend $font-icon - content '\1f4bc' + @extend $font-icon + content '\1f4bc' $font-blogs - @extend $font-icon - content '\1f4c4' + @extend $font-icon + content '\1f4c4' $font-events - @extend $font-icon - content '\1f4c5' + @extend $font-icon + content '\1f4c5' $font-reference - @extend $font-icon - content '\1f4d5' + @extend $font-icon + content '\1f4d5' $font-time - @extend $font-icon - content '\1f554' + @extend $font-icon + content '\1f554' $font-video - @extend $font-icon - content '\e800' + @extend $font-icon + content '\e800' $font-mobile - @extend $font-icon - content '\e801' + @extend $font-icon + content '\e801' $font-printable - @extend $font-icon - content '\e802' + @extend $font-icon + content '\e802' $font-pencil - @extend $font-icon - content '\e803' + @extend $font-icon + content '\e803' $font-trash - @extend $font-icon - content '\e804' + @extend $font-icon + content '\e804' $font-menu - @extend $font-icon - //- \0021, not just \21 - content '\0021' + @extend $font-icon + //- \0021, not just \21 + content '\0021' $font-link - @extend $font-icon - content '\e80c' + @extend $font-icon + content '\e80c' $font-camera - @extend $font-icon - content '\e812' + @extend $font-icon + content '\e812' $font-home - @extend $font-icon - content '\e813' + @extend $font-icon + content '\e813' $font-sitemap - @extend $font-icon - content '\e814' + @extend $font-icon + content '\e814' $font-close-button - @extend $font-icon - content '\e815' + @extend $font-icon + content '\e815' $font-twitter - @extend $font-icon - content '\e808' + @extend $font-icon + content '\e863' $font-facebook - @extend $font-icon - content '\e809' + @extend $font-icon + content '\e861' $font-google - @extend $font-icon - content '\e80a' + @extend $font-icon + content '\e80a' $font-vkontakte - @extend $font-icon - content '\e80b' + @extend $font-icon + content '\e80b' $font-github - @extend $font-icon - content '\e80f' + @extend $font-icon + content '\e80f' $font-github-large - @extend $font-icon - content '\e853' + @extend $font-icon + content '\e853' $font-yandex - @extend $font-icon - content '\e806' + @extend $font-icon + content '\e806' $font-rur - @extend $font-icon - content '\e811' + @extend $font-icon + content '\e811' $font-single-col - @extend $font-icon - content '\e816' + @extend $font-icon + content '\e816' $font-multi-col - @extend $font-icon - content '\e817' + @extend $font-icon + content '\e817' $font-ldquote - @extend $font-icon - content '\e81a' + @extend $font-icon + content '\e81a' $font-install - @extend $font-icon - content '\e81c' + @extend $font-icon + content '\e81c' $font-basket - @extend $font-icon - content '\e81d' + @extend $font-icon + content '\e81d' $font-like - @extend $font-icon - content '\E81F' + @extend $font-icon + content '\E81F' $font-earth - @extend $font-icon - content '\E823' + @extend $font-icon + content '\E823' $font-chat-bubbles - @extend $font-icon - content '\E820' + @extend $font-icon + content '\E820' $font-college-hat - @extend $font-icon - content '\E822' + @extend $font-icon + content '\E822' $font-star-label - @extend $font-icon - content '\E821' + @extend $font-icon + content '\E821' $font-flag - @extend $font-icon - content '\E824' + @extend $font-icon + content '\E824' $font-back-in-time - @extend $font-icon - content '\E825' + @extend $font-icon + content '\E825' $font-rotate-right - @extend $font-icon - content '\E826' + @extend $font-icon + content '\E826' $font-heart-plus - @extend $font-icon - content '\E827' + @extend $font-icon + content '\E827' $font-private - @extend $font-icon - content '\E828' + @extend $font-icon + content '\E828' $font-bold - @extend $font-icon - content '\E829' + @extend $font-icon + content '\E829' $font-italic - @extend $font-icon - content '\E82A' + @extend $font-icon + content '\E82A' $font-link - @extend $font-icon - content '\E82B' + @extend $font-icon + content '\E82B' $font-redo - @extend $font-icon - content '\E82C' + @extend $font-icon + content '\E82C' $font-cut - @extend $font-icon - content '\E850' + @extend $font-icon + content '\E850' $font-undo - @extend $font-icon - content '\E82D' + @extend $font-icon + content '\E82D' $font-ol - @extend $font-icon - content '\E830' + @extend $font-icon + content '\E830' $font-ul - @extend $font-icon - content '\E82E' + @extend $font-icon + content '\E82E' $font-fenced-code - @extend $font-icon - content '\E82F' + @extend $font-icon + content '\E82F' $font-heading - @extend $font-icon - content '\E831' + @extend $font-icon + content '\E831' $font-image - @extend $font-icon - content '\E832' + @extend $font-icon + content '\E832' $font-code - @extend $font-icon - content '\E833' + @extend $font-icon + content '\E833' $font-editor-quote - @extend $font-icon - content '\E834' + @extend $font-icon + content '\E834' + +$font-lang + @extend $font-icon + content '\E84C' + +$font-vector + @extend $font-icon + content '\E84D' + +$font-facebook-square + @extend $font-icon + content '\F30E' + +$font-calendar + @extend $font-icon + content '\E836' + +$font-globe-blank + @extend $font-icon + content '\e89c' + +$font-mic + @extend $font-icon + content '\e89d' + +$font-branch + @extend $font-icon + content '\e89b' + +$font-tag-squared + @extend $font-icon + content '\e89e' + +$font-info-blank + @extend $font-icon + content '\e842' + +$font-discord + @extend $font-icon + content '\e8fc' + +$font-burger + @extend $font-icon + content '\e854' + +$font-chevron-left + @extend $font-icon + content '\e855' + +$font-chevron-right + @extend $font-icon + content '\e856' + +$font-edit-1 + @extend $font-icon + content '\e857' + +$font-map + @extend $font-icon + content '\e858' + +$font-play-fill + @extend $font-icon + content '\e859' + +$font-moon + @extend $font-icon + content '\e85c' + +$font-sun + @extend $font-icon + content '\e85d' + +$font-buy-ebook + @extend $font-icon + content '\e860' + +$font-qr-code + @extend $font-icon + content '\E865' \ No newline at end of file diff --git a/modules/styles/blocks/font/font-icons.styl b/modules/styles/blocks/font/font-icons.styl index 586a364..fd7b8fe 100755 --- a/modules/styles/blocks/font/font-icons.styl +++ b/modules/styles/blocks/font/font-icons.styl @@ -1,5 +1,15 @@ @font-face - font-family 'FontIcons' - src url('icons.woff') format('woff'), url('icons.otf') format("opentype"); - font-weight normal - font-style normal + font-family 'FontIcons' + font-weight normal + font-style normal + src url('icons.woff?5') format('woff'); + +//- not used now? +@font-face + font-family: 'PT Mono' + font-weight: bold; + font-style: normal; + src: local('PT MonoBold'), + url('/font/PTMonoBold.woff2') format('woff2'), + url('/font/PTMonoBold.woff') format('woff'), + url('/font/PTMonoBold.ttf') format('truetype'); diff --git a/modules/styles/blocks/font/icons.woff b/modules/styles/blocks/font/icons.woff index 2590b1cc04a3f242dc08a2a4874c850da519cc91..68541a795adbba0bdc16073a8eda9e45fad3e1ba 100755 GIT binary patch literal 19424 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 From 3d1212b9bf7ee87223bb11639fe761784b8a5a00 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 29 Mar 2022 15:34:00 +0300 Subject: [PATCH 194/218] Update package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 121fcc2..fac7b41 100755 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "koa-mount": "^3", "koa-router": "^7", "koa-static": "^5", + "@trysound/sax": "^0.2.0", "limax": "^2.0.0", "lodash": "*", "markdown-it": "*", From db9872bb76b88eb7f04617f2065821a4078eb08b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 6 Jul 2022 07:22:11 +0200 Subject: [PATCH 195/218] minor fixes --- README.md | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0d150cc..0a622df 100755 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call 3. Create the root folder. - Create a folder `/js` for the project. - + Create a folder `/js` for the project. + You can also use another directory as the root, then change the paths below: replace `/js` with your root. 4. Clone the tutorial server into it: @@ -40,7 +40,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call ``` Please note, there are two clone commands. That's not a typo: `modules/engine` is cloned from another repository. - + And please don't forget this when updating, `modules/engine` needs to be fetched and merged too. 5. Clone the tutorial text into it. @@ -48,7 +48,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call The repository starts with the language code, e.g for the French version `fr.javascript.info`, for Russian – `ru.javascript.info` etc. The English version is `en.javascript.info`. - + The tutorial text repository should go into the `repo` subfolder, like this: ```bash @@ -122,7 +122,7 @@ Please note, the server must support that language. There must be corresponding Please don't translate SVG files manually. -They are auto-generated from the English variant, with the text phrases substituted from `images.yml` file in the repository root, such as . +They are auto-generated from the English variant, with the text phrases substituted from `images.yml` file in the repository root, such as . So you need to translate the content of `images.yml` and re-generate the SVGs using a script. @@ -147,7 +147,7 @@ As you can see, for each image file (such as `code-style.svg`), there go English For each phrase, there's the translated `text` and the text `position` (not always needed, details will come soon). -You can make a small file with only one image for the start. +You can make a small file with only one image for the start. **Step 2.** Setup git upstream (if you haven't yet) and pull latest changes from English version: @@ -171,7 +171,7 @@ NODE_LANG=zh npm run gulp -- engine:koa:tutorial:figuresTranslate This script checks out all SVG images from `upstream` (English version) and replaces the strings inside them according to `images.yml`. So they become translated. -The new translated SVGs are the tutorial folder now, but not committed yet. +The new translated SVGs are the tutorial folder now, but not committed yet. You can see them with `git status`. @@ -179,7 +179,7 @@ Take a moment to open and check them, e.g. in Chrome browser, just to ensure tha P.S. If an image appears untranslated on refresh, force the browser to "reload without cache" ([hotkeys](https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache)). -**Step 4.** Then you'll need to `git add/commit/push` the translated SVGs, as a part of the normal translation flow. +**Step 4.** Then you'll need to `git add/commit/push` the translated SVGs, as a part of the normal translation flow. ...And voilà! SVGs are translated! @@ -204,7 +204,7 @@ Before the translation: After the translation (`你` is at the same place where `h` was, the string is left-aligned): ``` -| 你好世界 +| 你好世界 ``` Sometimes that's not good, e.g. if the string needs to be centered, e.g. like this: @@ -225,9 +225,9 @@ Then, if we just replace the string, it would become: | ``` -As we can see, the new phrase is shorter. We should move it to the right a bit. +As we can see, the new phrase is shorter. We should move it to the right a bit. -The `position: "center"` in `images.yml` does exactly that. +The `position: "center"` in `images.yml` does exactly that. It centers the translated string, so that it will replace the original one and stay "in the middle" of the surrounding context. @@ -258,18 +258,31 @@ cd /js/server # in the server folder NODE_LANG=zh npm run gulp -- engine:koa:tutorial:imageYaml --image code-style.svg ``` -It extracts all text lines. Useful for debugging, when the translation doesn't "catch up", because the SVG text has an extra space or so. +It extracts all text lines and outputs as a template for the `images.yml`. + +Useful for debugging, when the translation doesn't "catch up", because the SVG text has an extra space or so. ## The "overflowing text" problem -The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. +The replacement script only operates on strings, not other graphics, so a long translated string may not fit the picture. -Most pictures have some extra space for longer text, so a slight increase doesn't harm. +Most pictures have some extra space for longer text, so a slight increase doesn't harm. -If the translated text is much longer, please try to change it, make it shorter to fit. +If the translated text is much longer, please try to change it, make it shorter to fit. If the translated text absolutely must be longer and doesn't fit, let me know, we'll see how to adjust the picture. +## Troubleshooting + +If you add a translation to `images.yml`, but after running the script the SVG remains the same: + +1. Ensure that you have the latest server code and translation repos, fetched the upstream. +2. Check if the English version has the file with the same name. The file could have been renamed. +3. Check that there's only 1 file with the given name in the tutorial. Sometimes there may be duplicates. +4. Check that the translated string in `images.yml` is exactly as in SVG: use the helper script to extract all strings. + +If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). + # Dev mode If you'd like to edit the server code (assuming you're familiar with Node.js), *not* the tutorial text, then there are two steps to do. @@ -332,4 +345,3 @@ $> sudo sysctl -p It is very important that you refer to the documentation for your operating system to change this parameter permanently. --
      Yours,
      Ilya Kantor
      iliakan@javascript.info - From 7872068a4057f3316afb2b59ebfafb990389f6da Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 6 Jul 2022 07:28:18 +0200 Subject: [PATCH 196/218] minor fixes --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 0a622df..6ffb0c3 100755 --- a/README.md +++ b/README.md @@ -281,6 +281,26 @@ If you add a translation to `images.yml`, but after running the script the SVG r 3. Check that there's only 1 file with the given name in the tutorial. Sometimes there may be duplicates. 4. Check that the translated string in `images.yml` is exactly as in SVG: use the helper script to extract all strings. + For example, let's say you added `"White Rabbit": "Coelho Branco"` to the `images.yml`, and it doesn't work. + + Let's get all strings: + + ``` + ❯ NODE_LANG=pt npm run gulp -- engine:koa:tutorial:imageYaml --image proto-constructor-animal-rabbit.svg + proto-constructor-animal-rabbit.svg: + 'eats: true': '' + 'name: "White Rabbit"': '' + animal: '' + Rabbit: '' + rabbit: '' + '[[Prototype]]': '' + prototype: '' + ``` + + Now we can see, that the text is actually longer, it's `name: "White Rabbit"`. + So the line in the `images.yml` should be something like: `"name: \"White Rabbit\"": "text": "name: \"Coelho Branco\""`. + + If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). # Dev mode From 9df741c5c86ab210b6c12873b8837b302fc36347 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 6 Jul 2022 07:28:45 +0200 Subject: [PATCH 197/218] minor fixes --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6ffb0c3..2d08e27 100755 --- a/README.md +++ b/README.md @@ -281,24 +281,24 @@ If you add a translation to `images.yml`, but after running the script the SVG r 3. Check that there's only 1 file with the given name in the tutorial. Sometimes there may be duplicates. 4. Check that the translated string in `images.yml` is exactly as in SVG: use the helper script to extract all strings. - For example, let's say you added `"White Rabbit": "Coelho Branco"` to the `images.yml`, and it doesn't work. - - Let's get all strings: - - ``` - ❯ NODE_LANG=pt npm run gulp -- engine:koa:tutorial:imageYaml --image proto-constructor-animal-rabbit.svg - proto-constructor-animal-rabbit.svg: - 'eats: true': '' - 'name: "White Rabbit"': '' - animal: '' - Rabbit: '' - rabbit: '' - '[[Prototype]]': '' - prototype: '' - ``` - - Now we can see, that the text is actually longer, it's `name: "White Rabbit"`. - So the line in the `images.yml` should be something like: `"name: \"White Rabbit\"": "text": "name: \"Coelho Branco\""`. + For example, let's say you added `"White Rabbit": "Coelho Branco"` to the `images.yml`, and it doesn't work. + + Let's get all strings: + + ``` + ❯ NODE_LANG=pt npm run gulp -- engine:koa:tutorial:imageYaml --image proto-constructor-animal-rabbit.svg + proto-constructor-animal-rabbit.svg: + 'eats: true': '' + 'name: "White Rabbit"': '' + animal: '' + Rabbit: '' + rabbit: '' + '[[Prototype]]': '' + prototype: '' + ``` + + Now we can see, that the text is actually longer, it's `name: "White Rabbit"`. + So the line in the `images.yml` should be something like: `"name: \"White Rabbit\"": "text": "name: \"Coelho Branco\""`. If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). From 6ff0d59aecc841abc30bb0f2c498c62e49545681 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 6 Jul 2022 07:29:19 +0200 Subject: [PATCH 198/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d08e27..ad42758 100755 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ If you add a translation to `images.yml`, but after running the script the SVG r 3. Check that there's only 1 file with the given name in the tutorial. Sometimes there may be duplicates. 4. Check that the translated string in `images.yml` is exactly as in SVG: use the helper script to extract all strings. - For example, let's say you added `"White Rabbit": "Coelho Branco"` to the `images.yml`, and it doesn't work. + For example, let's say you added `"White Rabbit": "Coelho Branco"` to the `images.yml` for `proto-constructor-animal-rabbit.svg` file, and it doesn't work. Let's get all strings: From afcb5fd63a9055d8a48885e2a1911d79d0512ed8 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 6 Jul 2022 07:29:41 +0200 Subject: [PATCH 199/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad42758..0cd9e54 100755 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ If the translated text is much longer, please try to change it, make it shorter If the translated text absolutely must be longer and doesn't fit, let me know, we'll see how to adjust the picture. -## Troubleshooting +## Troubleshooting images translation If you add a translation to `images.yml`, but after running the script the SVG remains the same: From a2c4c6ffd07f554522035514c7ac6dcf27ad1de5 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 6 Jul 2022 07:30:08 +0200 Subject: [PATCH 200/218] minor fixes --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0cd9e54..42ca400 100755 --- a/README.md +++ b/README.md @@ -288,13 +288,13 @@ If you add a translation to `images.yml`, but after running the script the SVG r ``` ❯ NODE_LANG=pt npm run gulp -- engine:koa:tutorial:imageYaml --image proto-constructor-animal-rabbit.svg proto-constructor-animal-rabbit.svg: - 'eats: true': '' - 'name: "White Rabbit"': '' - animal: '' - Rabbit: '' - rabbit: '' - '[[Prototype]]': '' - prototype: '' + 'eats: true': '' + 'name: "White Rabbit"': '' + animal: '' + Rabbit: '' + rabbit: '' + '[[Prototype]]': '' + prototype: '' ``` Now we can see, that the text is actually longer, it's `name: "White Rabbit"`. From 17309e435aed3989177131dc2ed9692da47fe961 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 6 Jul 2022 07:31:41 +0200 Subject: [PATCH 201/218] minor fixes --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42ca400..a1279a6 100755 --- a/README.md +++ b/README.md @@ -297,8 +297,9 @@ If you add a translation to `images.yml`, but after running the script the SVG r prototype: '' ``` - Now we can see, that the text is actually longer, it's `name: "White Rabbit"`. - So the line in the `images.yml` should be something like: `"name: \"White Rabbit\"": "text": "name: \"Coelho Branco\""`. + Now we can see, that the text in the SVG is actually longer, it's `name: "White Rabbit"`. + + So the `images.yml` should translate it as a whole: `"name: \"White Rabbit\"": ...` (note: quotes are escaped). If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). From 049fbac3186be8d65602bd25eb5e4cb8069c423c Mon Sep 17 00:00:00 2001 From: Jonnathan Santos <35238611+jonnathan-ls@users.noreply.github.com> Date: Sat, 9 Jul 2022 20:58:09 -0300 Subject: [PATCH 202/218] docs: adds guidance step for image translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ℹ️ Details guidelines to assist in the image translation procedure using the imageYaml script --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a1279a6..8b5a975 100755 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ So you need to translate the content of `images.yml` and re-generate the SVGs us Here are the steps to translate images. +> Make sure you have installed [ImageMagick](https://imagemagick.org/script/download.php) mentioned in the [installation](#installation) step + **Step 1.** Create `images.yml` with translations in the repository root. An example of such file (in Russian): https://github.com/javascript-tutorial/ru.javascript.info/blob/master/images.yml @@ -159,7 +161,64 @@ git remote add upstream https://github.com/javascript-tutorial/en.javascript.inf git fetch upstream master ``` -**Step 3.** Run the translation task: +**Step 3.** With the help of [script `imageYaml`](https://github.com/javascript-tutorial/server#helper-script-extract-strings), check the existing texts of the image you want to translate, for example to the `code-style.svg` file, when executing the command, you will get the following results: + +```bash +# Adjust NODE_LANG to your language + +❯ NODE_LANG=en npm run gulp -- engine:koa:tutorial:imageYaml --image code-style.svg + +# Remainder of log omitted … + +Processing image code-style.svg +code-style.svg: + '2': '' + No space: '' + between the function name and parentheses: '' + between the parentheses and the parameter: '' + Indentation: '' + 2 spaces: '' + teste a: '' + after for/if/while…: '' + '} else { without a line break': '' + Spaces around a nested call: '' + An empty line: '' + between logical blocks: '' + Lines are not very long: '' + A semicolon ;: '' + is mandatory: '' + Spaces: '' + around operators: '' + Curly brace {: '' + on the same line, after a space: '' + A space: '' + between: '' + arguments: '' + A space between parameters: '' +``` + +It is possible to notice that the script returns a list of a set of words, which separately form the snippet that can be translated. To succeed in the translation, it is necessary to configure the snippet as close as possible to what the script expects. + +Let's take as an example the text `'} else { without a line break'`, in which it is necessary to pay attention to two observations: + +1. The single quotes at the beginning and at the end are not part of the excerpt, as they only surround the set of words. It may be that in some scenarios it appears in the middle of the snippet (for example, `What's`), for such a scenario it is necessary to use it in the configuration, but the example taken is different, and that is why we will not consider the single quote at the beginning and at the end of the text. +2. There is a set of characters for which there is no translation or there is no need (for example, `} else {`), however, it is necessary to register them with part of the text to be translated, otherwise the script will not identify the need for translation. + +Finally, we will have the following configuration to succeed in the translation: + +```yml +code-style.svg: + "} else { without a line break": + text: "} else { xxxxxxx x xxxx xxxxx" +``` + +> **Note** +> +> - You may come across special characters, so the script returns the strings with code formatting, if applicable, configure the representation of the character visually, for example: double quotes `"` (visual) will be returned by script as `"` (code). +> - If you encounter problems related to the size of the translated text being too different from the original text, see the section [The "Text Overflowing"](https://github.com/javascript-tutorial/server#the-overflowing-text-problem) and test without using the [`position`](https://github.com/javascript-tutorial/server#positioning) parameter (maybe trying to center, for example, is interfering). +> - For other translation issues, see the section [Troubleshooting image translation](https://github.com/javascript-tutorial/server#troubleshooting-images-translation). + +**Step 4.** Run the translation task: ```bash cd /js/server # in the server folder @@ -179,7 +238,7 @@ Take a moment to open and check them, e.g. in Chrome browser, just to ensure tha P.S. If an image appears untranslated on refresh, force the browser to "reload without cache" ([hotkeys](https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache#Bypassing_cache)). -**Step 4.** Then you'll need to `git add/commit/push` the translated SVGs, as a part of the normal translation flow. +**Step 5.** Then you'll need to `git add/commit/push` the translated SVGs, as a part of the normal translation flow. ...And voilà! SVGs are translated! @@ -190,7 +249,6 @@ P.S. If an image appears untranslated on refresh, force the browser to "reload w > NODE_LANG=zh npm run gulp -- engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg > ``` - ## Positioning By default, the translated string replaces the original one, starting in exactly the same place of the image. From f58efacbe1e1f6bf33de0c6213393dea48617236 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 Jul 2022 04:56:10 +0200 Subject: [PATCH 203/218] minor fixes --- README.md | 83 +++++++++++++++---------------------------------------- 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 8b5a975..2626c3f 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call Please use Node.js 10+. - (Maybe later, optional) If you're going to change images, please install [ImageMagick](https://imagemagick.org/script/download.php). + (Maybe later, optional) If you're going to change images, please install [ImageMagick](https://imagemagick.org/script/download.php) (use packages for Linux or homebrew/macports for MacOS). 2. Install global Node modules: @@ -128,8 +128,6 @@ So you need to translate the content of `images.yml` and re-generate the SVGs us Here are the steps to translate images. -> Make sure you have installed [ImageMagick](https://imagemagick.org/script/download.php) mentioned in the [installation](#installation) step - **Step 1.** Create `images.yml` with translations in the repository root. An example of such file (in Russian): https://github.com/javascript-tutorial/ru.javascript.info/blob/master/images.yml @@ -165,11 +163,8 @@ git fetch upstream master ```bash # Adjust NODE_LANG to your language - -❯ NODE_LANG=en npm run gulp -- engine:koa:tutorial:imageYaml --image code-style.svg - -# Remainder of log omitted … - +❯ NODE_LANG=zh npm run gulp -- engine:koa:tutorial:imageYaml --image code-style.svg +... Processing image code-style.svg code-style.svg: '2': '' @@ -178,7 +173,7 @@ code-style.svg: between the parentheses and the parameter: '' Indentation: '' 2 spaces: '' - teste a: '' + 'A space ': '' after for/if/while…: '' '} else { without a line break': '' Spaces around a nested call: '' @@ -197,26 +192,24 @@ code-style.svg: A space between parameters: '' ``` -It is possible to notice that the script returns a list of a set of words, which separately form the snippet that can be translated. To succeed in the translation, it is necessary to configure the snippet as close as possible to what the script expects. +As we can see, the script returns a text snippet that can be inserted into `images.yml` fully or partially. -Let's take as an example the text `'} else { without a line break'`, in which it is necessary to pay attention to two observations: +E.g. like this: + +```yml +code-style.svg: + 'A space ': 'Пробел' +``` -1. The single quotes at the beginning and at the end are not part of the excerpt, as they only surround the set of words. It may be that in some scenarios it appears in the middle of the snippet (for example, `What's`), for such a scenario it is necessary to use it in the configuration, but the example taken is different, and that is why we will not consider the single quote at the beginning and at the end of the text. -2. There is a set of characters for which there is no translation or there is no need (for example, `} else {`), however, it is necessary to register them with part of the text to be translated, otherwise the script will not identify the need for translation. - -Finally, we will have the following configuration to succeed in the translation: +Or like this, if we want to position the translation in the center (see below for more examples): ```yml code-style.svg: - "} else { without a line break": - text: "} else { xxxxxxx x xxxx xxxxx" + 'A space ': + position: 'center' + text: 'Пробел' ``` -> **Note** -> -> - You may come across special characters, so the script returns the strings with code formatting, if applicable, configure the representation of the character visually, for example: double quotes `"` (visual) will be returned by script as `"` (code). -> - If you encounter problems related to the size of the translated text being too different from the original text, see the section [The "Text Overflowing"](https://github.com/javascript-tutorial/server#the-overflowing-text-problem) and test without using the [`position`](https://github.com/javascript-tutorial/server#positioning) parameter (maybe trying to center, for example, is interfering). -> - For other translation issues, see the section [Troubleshooting image translation](https://github.com/javascript-tutorial/server#troubleshooting-images-translation). **Step 4.** Run the translation task: @@ -249,8 +242,12 @@ P.S. If an image appears untranslated on refresh, force the browser to "reload w > NODE_LANG=zh npm run gulp -- engine:koa:tutorial:figuresTranslate --image try-catch-flow.svg > ``` +Read on for more advanced options and troubleshooting. + ## Positioning +> For the positioning to work, sure you have installed [ImageMagick](https://imagemagick.org/script/download.php) mentioned in the [installation](#installation) step. + By default, the translated string replaces the original one, starting in exactly the same place of the image. Before the translation: @@ -273,7 +270,7 @@ hello world | ``` -(The "hello world" is centered between two `|`). +Here, the "hello world" is centered between two vertical lines `|`. Then, if we just replace the string, it would become: @@ -304,21 +301,6 @@ hello world | That's also useful for images when we expect the text to stick to the right. -P.S In order for positioning to work, you need to have ImageMagick installed: (or use packages for Linux or homebrew/macports for MacOS). - -## Helper script: extract strings - -The task to get all strings from an image as YAML (for translation, to add to `images.yml`): - -```bash -cd /js/server # in the server folder - -NODE_LANG=zh npm run gulp -- engine:koa:tutorial:imageYaml --image code-style.svg -``` - -It extracts all text lines and outputs as a template for the `images.yml`. - -Useful for debugging, when the translation doesn't "catch up", because the SVG text has an extra space or so. ## The "overflowing text" problem @@ -332,33 +314,12 @@ If the translated text absolutely must be longer and doesn't fit, let me know, w ## Troubleshooting images translation -If you add a translation to `images.yml`, but after running the script the SVG remains the same: +If you add a translation to `images.yml`, but after running the script the SVG remains the same, so that the translation doesn't "catch up": 1. Ensure that you have the latest server code and translation repos, fetched the upstream. 2. Check if the English version has the file with the same name. The file could have been renamed. 3. Check that there's only 1 file with the given name in the tutorial. Sometimes there may be duplicates. -4. Check that the translated string in `images.yml` is exactly as in SVG: use the helper script to extract all strings. - - For example, let's say you added `"White Rabbit": "Coelho Branco"` to the `images.yml` for `proto-constructor-animal-rabbit.svg` file, and it doesn't work. - - Let's get all strings: - - ``` - ❯ NODE_LANG=pt npm run gulp -- engine:koa:tutorial:imageYaml --image proto-constructor-animal-rabbit.svg - proto-constructor-animal-rabbit.svg: - 'eats: true': '' - 'name: "White Rabbit"': '' - animal: '' - Rabbit: '' - rabbit: '' - '[[Prototype]]': '' - prototype: '' - ``` - - Now we can see, that the text in the SVG is actually longer, it's `name: "White Rabbit"`. - - So the `images.yml` should translate it as a whole: `"name: \"White Rabbit\"": ...` (note: quotes are escaped). - +4. Check that the translated string in `images.yml` is exactly as in SVG: use the helper script (Step 3) to extract all strings. If it still doesn't work – [file an issue](https://github.com/javascript-tutorial/server/issues/new). From 9af7d75d717c9402d9132354a7a579c7c5a37b10 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 Jul 2022 04:58:03 +0200 Subject: [PATCH 204/218] minor fixes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2626c3f..95444d6 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call Please use Node.js 10+. - (Maybe later, optional) If you're going to change images, please install [ImageMagick](https://imagemagick.org/script/download.php) (use packages for Linux or homebrew/macports for MacOS). + (Maybe later, optional) If you're going to change images, please install [ImageMagick](https://imagemagick.org/script/download.php), use packages for Linux or homebrew/macports for MacOS. 2. Install global Node modules: @@ -41,11 +41,11 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call Please note, there are two clone commands. That's not a typo: `modules/engine` is cloned from another repository. - And please don't forget this when updating, `modules/engine` needs to be fetched and merged too. + And please don't forget when pulling an updated server code: `modules/engine` needs to be pulled too. 5. Clone the tutorial text into it. - The repository starts with the language code, e.g for the French version `fr.javascript.info`, for Russian – `ru.javascript.info` etc. + The repository starts with the language code, e.g for the French version `fr.javascript.info`, for Russian – `ru.javascript.info`, for Chinese `zh.javascript.info` etc. The English version is `en.javascript.info`. From 74e740cad3e2a1de80c9ed99822a208c643781ac Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 Jul 2022 04:59:37 +0200 Subject: [PATCH 205/218] minor fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95444d6..30a55b7 100755 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ code-style.svg: # image file name position: "center" # (optional) "center" or "right" - to position the translated string ``` -As you can see, for each image file (such as `code-style.svg`), there go English phrases (such as `"No space"`). +As you can see, for each image file there's a name (such as `code-style.svg`), and then goes the list of English phrases (such as `"No space"`). For each phrase, there's the translated `text` and the text `position` (not always needed, details will come soon). From c33c7009c7673cb27d203e58569a6ee20eecce29 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 Jul 2022 05:01:16 +0200 Subject: [PATCH 206/218] minor fixes --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30a55b7..e1e6221 100755 --- a/README.md +++ b/README.md @@ -141,11 +141,15 @@ code-style.svg: # image file name "No space": # English string text: "Без пробелов" # translation position: "center" # (optional) "center" or "right" - to position the translated string + "between the function name and parentheses": + position: "center" + text: "между именем функции и скобками" ``` -As you can see, for each image file there's a name (such as `code-style.svg`), and then goes the list of English phrases (such as `"No space"`). +As you can see, for each image file there's a name (such as `code-style.svg`), and then goes the list of English phrases (such as `"No space"`), accompanied by translations: -For each phrase, there's the translated `text` and the text `position` (not always needed, details will come soon). +- `text` is the translated text +- `position` (not always needed, details will come soon) is the relative position of the text. You can make a small file with only one image for the start. From 8184a8e93cfd1b19990592a53d2839b7a5cf621d Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 Jul 2022 05:05:19 +0200 Subject: [PATCH 207/218] minor fixes --- README.md | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e1e6221..0617d33 100755 --- a/README.md +++ b/README.md @@ -128,7 +128,17 @@ So you need to translate the content of `images.yml` and re-generate the SVGs us Here are the steps to translate images. -**Step 1.** Create `images.yml` with translations in the repository root. +**Step 1.** Setup git upstream (if you haven't yet) and pull latest changes from English version: + +```bash +cd /js/server/repo/zh.javascript.info # in the tutorial folder + +git remote add upstream https://github.com/javascript-tutorial/en.javascript.info + +git fetch upstream master +``` + +**Step 2.** Create `images.yml` with translations in the repository root. An example of such file (in Russian): https://github.com/javascript-tutorial/ru.javascript.info/blob/master/images.yml @@ -146,30 +156,28 @@ code-style.svg: # image file name text: "между именем функции и скобками" ``` -As you can see, for each image file there's a name (such as `code-style.svg`), and then goes the list of English phrases (such as `"No space"`), accompanied by translations: +As you can see, for each image file there's a name (such as `code-style.svg`), and then goes the list of its English phrases (such as `"No space"`), accompanied by translations: - `text` is the translated text - `position` (not always needed, details will come soon) is the relative position of the text. -You can make a small file with only one image for the start. - -**Step 2.** Setup git upstream (if you haven't yet) and pull latest changes from English version: - -```bash -cd /js/server/repo/zh.javascript.info # in the tutorial folder +Initially, the file may be empty, then you can fill it with images one by one. -git remote add upstream https://github.com/javascript-tutorial/en.javascript.info +Only the mentioned images will be translated. -git fetch upstream master -``` +**Step 3.** Use the helper script to get a list of strings to translate: -**Step 3.** With the help of [script `imageYaml`](https://github.com/javascript-tutorial/server#helper-script-extract-strings), check the existing texts of the image you want to translate, for example to the `code-style.svg` file, when executing the command, you will get the following results: +The script is executed from the server root, like this: ```bash # Adjust NODE_LANG to your language + ❯ NODE_LANG=zh npm run gulp -- engine:koa:tutorial:imageYaml --image code-style.svg -... -Processing image code-style.svg +``` + +Here's an example of its output: + +```yml code-style.svg: '2': '' No space: '' @@ -214,7 +222,6 @@ code-style.svg: text: 'Пробел' ``` - **Step 4.** Run the translation task: ```bash From 489bb5df904672b7431b290c1b16af265be47749 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 12 Nov 2022 11:38:27 +0100 Subject: [PATCH 208/218] minor fixes --- README.md | 4 +++- dev | 3 +-- edit | 1 - package.json | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0617d33..7e20d95 100755 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ You can use it to run the tutorial locally and translate it into your language. Windows, Unix systems and macOS are supported. For Windows, you'll need to call scripts with ".cmd" extension, that are present in the code alongside with Unix versions. + # Installation 1. Install [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org). @@ -15,7 +16,8 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call These are required to update and run the project. For Windows just download and install, otherwise use standard OS install tools (packages or whatever convenient). - Please use Node.js 10+. + **Please use Node.js 16.** + You can find it at https://nodejs.org/download/release/v16.18.1/) or use [NVM](https://github.com/nvm-sh/nvm) to install multiple versions of Node.js and switch between them. (Maybe later, optional) If you're going to change images, please install [ImageMagick](https://imagemagick.org/script/download.php), use packages for Linux or homebrew/macports for MacOS. diff --git a/dev b/dev index 1428051..58fc04c 100755 --- a/dev +++ b/dev @@ -5,7 +5,7 @@ set -e export NODE_LANG=$1 -export TUTORIAL_LANG=$1 +export TUTORIAL_LANG=$1 export NODE_ENV=development export ASSET_VERSIONING=query export WATCH=1 @@ -19,4 +19,3 @@ if ! [ -x "$(command -v bunyan)" ]; then fi npm --silent run gulp dev | bunyan -o short -l debug - diff --git a/edit b/edit index 08d5a13..14b1eec 100755 --- a/edit +++ b/edit @@ -17,4 +17,3 @@ if ! [ -x "$(command -v bunyan)" ]; then fi npm --silent run -- gulp edit | bunyan -o short -l debug - diff --git a/package.json b/package.json index fac7b41..3cecfb8 100755 --- a/package.json +++ b/package.json @@ -9,9 +9,13 @@ "build": "cross-env NODE_PATH=./modules ./node_modules/.bin/gulp build", "gulp": "cross-env NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules ./node_modules/.bin/gulp" }, + "engine": { + "node": "^16" + }, "precommit": "NODE_ENV=development node `which gulp` pre-commit", "//": "node-xmpp-client installs for linux only", "dependencies": { + "@trysound/sax": "^0.2.0", "autoprefixer": "^9", "babel-core": "^6", "babel-loader": "^7", @@ -43,7 +47,6 @@ "koa-mount": "^3", "koa-router": "^7", "koa-static": "^5", - "@trysound/sax": "^0.2.0", "limax": "^2.0.0", "lodash": "*", "markdown-it": "*", From 1146c6fcc56834c4a83029412d1b2cf551b70cbd Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 12 Nov 2022 16:17:24 +0100 Subject: [PATCH 209/218] minor fixes --- README.md | 3 --- dev | 1 + edit | 1 + package.json | 3 --- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7e20d95..91b812f 100755 --- a/README.md +++ b/README.md @@ -16,9 +16,6 @@ Windows, Unix systems and macOS are supported. For Windows, you'll need to call These are required to update and run the project. For Windows just download and install, otherwise use standard OS install tools (packages or whatever convenient). - **Please use Node.js 16.** - You can find it at https://nodejs.org/download/release/v16.18.1/) or use [NVM](https://github.com/nvm-sh/nvm) to install multiple versions of Node.js and switch between them. - (Maybe later, optional) If you're going to change images, please install [ImageMagick](https://imagemagick.org/script/download.php), use packages for Linux or homebrew/macports for MacOS. 2. Install global Node modules: diff --git a/dev b/dev index 58fc04c..7983d22 100755 --- a/dev +++ b/dev @@ -12,6 +12,7 @@ export WATCH=1 export SITE_HOST=http://javascript.local export TUTORIAL_EDIT= export NODE_PRESERVE_SYMLINKS=1 +export NODE_OPTIONS=--openssl-legacy-provider # Use a local bunyan if no other is found in the current environment if ! [ -x "$(command -v bunyan)" ]; then diff --git a/edit b/edit index 14b1eec..157863c 100755 --- a/edit +++ b/edit @@ -10,6 +10,7 @@ export TUTORIAL_LANG=$1 export NODE_ENV=production export TUTORIAL_EDIT=1 export NODE_PRESERVE_SYMLINKS=1 +export NODE_OPTIONS=--openssl-legacy-provider # Use a local bunyan if no other is found in the current environment if ! [ -x "$(command -v bunyan)" ]; then diff --git a/package.json b/package.json index 3cecfb8..9f63840 100755 --- a/package.json +++ b/package.json @@ -9,9 +9,6 @@ "build": "cross-env NODE_PATH=./modules ./node_modules/.bin/gulp build", "gulp": "cross-env NODE_PRESERVE_SYMLINKS=1 NODE_PATH=./modules ./node_modules/.bin/gulp" }, - "engine": { - "node": "^16" - }, "precommit": "NODE_ENV=development node `which gulp` pre-commit", "//": "node-xmpp-client installs for linux only", "dependencies": { From 2289de4a1fa14073a759e25fd52ee0629803c8eb Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 13 Nov 2022 17:31:01 +0100 Subject: [PATCH 210/218] minor fixes --- dev.cmd | 1 + edit.cmd | 1 + 2 files changed, 2 insertions(+) diff --git a/dev.cmd b/dev.cmd index 0b142f4..8019819 100644 --- a/dev.cmd +++ b/dev.cmd @@ -5,6 +5,7 @@ set NODE_LANG=%1 @set ASSET_VERSIONING=query @set NODE_PRESERVE_SYMLINKS=1 @set NODE_PATH=./modules +@set NODE_OPTIONS=--openssl-legacy-provider call gulp dev | bunyan diff --git a/edit.cmd b/edit.cmd index 8b693a7..80e8640 100644 --- a/edit.cmd +++ b/edit.cmd @@ -12,6 +12,7 @@ @set ASSET_VERSIONING=query @set NODE_PRESERVE_SYMLINKS=1 @set NODE_PATH=./modules +@set NODE_OPTIONS=--openssl-legacy-provider call gulp edit | bunyan From cfaa16cd35f486a5c8a85fa2daa870c1616a1bef Mon Sep 17 00:00:00 2001 From: I_am_Vietnam <91591390+ImVietnam@users.noreply.github.com> Date: Wed, 7 Jun 2023 14:50:08 +0700 Subject: [PATCH 211/218] Create vi.yml --- locales/vi.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/vi.yml diff --git a/locales/vi.yml b/locales/vi.yml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/locales/vi.yml @@ -0,0 +1 @@ + From de4e185fa2acf4ee56467274c9f1511807ff5f90 Mon Sep 17 00:00:00 2001 From: I_am_Vietnam <91591390+ImVietnam@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:10:20 +0700 Subject: [PATCH 212/218] Update vi.yml --- locales/vi.yml | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/locales/vi.yml b/locales/vi.yml index 8b13789..36a19b1 100644 --- a/locales/vi.yml +++ b/locales/vi.yml @@ -1 +1,101 @@ +site: + privacy_policy: Chính sách bảo mật + terms: điều khoản sử dụng + gdpr_dialog: + title: Trang web này sử dụng cookie + text: Chúng tôi sử dụng các công nghệ trình duyệt như cookie và bộ nhớ cục bộ để lưu trữ các tùy chọn của bạn. Bạn cần chấp nhận Chính sách bảo mậtĐiều khoản sử dụng của chúng tôi để thực hiện điều đó. + accept: Chấp nhận + cancel: Huỷ bỏ + + toolbar: + lang_switcher: + cta_text: Chúng tôi muốn cung cấp dự án mã nguồn mở này cho mọi người trên khắp thế giới. Vui lòng giúp chúng tôi dịch nội dung của hướng dẫn này sang ngôn ngữ bạn biết + footer_text: bao nhiêu nội dung được dịch sang ngôn ngữ tương ứng + old_version: Phiên bản cũ đã được xuất bản, cần nhập lại. + logo: + normal: + svg: sitetoolbar__logo_en.svg + width: 200 # 171 + normal-white: + svg: sitetoolbar__logo_en-white.svg + small: + svg: sitetoolbar__logo_small_en.svg + width: 70 + small-white: + svg: sitetoolbar__logo_small_en-white.svg + sections: + - slug: '' + url: '/' + title: 'Hướng dẫn' + - slug: 'khóa học' + title: 'Khóa học' + buy_ebook_extra: 'Mua' + buy_ebook: 'EPUB/PDF' + search_placeholder: 'Tìm kiếm trên Javascript.info' + search_button: 'Tìm kiếm' + + public_profile: Hồ sơ công khai + account: Tài khoản + notifications: Thông báo + admin: Quản trị viên + logout: Đăng xuất + + sorry_old_browser: Xin lỗi, Internet Explorer <10 không được hỗ trợ, vui lòng sử dụng trình duyệt mới hơn. + contact_us: liên hệ chúng tôi + about_the_project: về dự án + ilya_kantor: Ilya Kantor + comments: Bình luận + loading: Đang tải... + search: Tìm kiếm + share: Chia sẻ + read_before_commenting: đọc cái này trước khi bình luận… + + last_updated_at: "Cập nhật lần cuối vào #{date}" + meta: + description: 'Hướng dẫn JavaScript hiện đại: giải thích đơn giản nhưng chi tiết với các ví dụ và tác vụ, bao gồm: bao đóng, tài liệu và sự kiện, lập trình hướng đối tượng, v.v.' + + tablet-menu: + choose_section: Chọn phần + search_placeholder: Tìm kiếm trong hướng dẫn + search_button: Tìm kiếm + + comment: + help: + - Nếu bạn có đề xuất cần cải thiện - vui lòng gửi vấn đề lên GitHub hoặc pull request thay vì bình luận. + - Nếu bạn không thể hiểu điều gì đó trong bài viết – vui lòng giải thích thêm. + - Để chèn một vài từ mã, hãy sử dụng thẻ <code>, cho nhiều dòng – sử dụng <pre>, cho hơn 10 dòng – sử dụng sandbox ( plnkr, JSBin, codepen…) + + edit_on_github: Chỉnh sửa trên GitHub + error: lỗi + close: đóng + + hide_forever: ẩn vĩnh viễn + hidden_forever: Thông tin này sẽ không hiển thị nữa. + + + subscribe: + title: Theo dõi cập nhật javascript.info + text: 'Chúng tôi không gửi quảng cáo, chỉ những thứ có liên quan. Bạn chọn những gì sẽ nhận được:' + agreement: 'Bằng cách đăng ký nhận bản tin, bạn đồng ý với điều khoản sử dụng.' + button: Đăng ký + button_unsubscribe: Hủy đăng ký khỏi tất cả + common_updates: Cập nhật phổ biến + common_updates_text: các khóa học mới, các lớp học, bài viết và phát hành screencast + your_email: your@email.here + newsletters: 'bản tin, bản tin, bản tin' + no_selected: Nothing selected + + form: + value_must_not_be_empty: Value must not be empty. + value_is_too_long: Value is too long. + value_is_too_short: Value is too short. + invalid_email: Invalid email. + invalid_value: Invalid value. + invalid_autocomplete: Please, choose from the list + invalid_date: 'Invalid date, format: dd.mm.yyyyy.' + invalid_range: This date is invalid here. + save: Save + upload_file: Upload file + cancel: Cancel + server_error: Request error, status code From 1a871c3b4dce1962a9db21c0000b0e50710299e9 Mon Sep 17 00:00:00 2001 From: I_am_Vietnam <91591390+ImVietnam@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:12:54 +0700 Subject: [PATCH 213/218] Update vi.yml --- locales/vi.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/locales/vi.yml b/locales/vi.yml index 36a19b1..7664b2a 100644 --- a/locales/vi.yml +++ b/locales/vi.yml @@ -84,18 +84,18 @@ site: common_updates_text: các khóa học mới, các lớp học, bài viết và phát hành screencast your_email: your@email.here newsletters: 'bản tin, bản tin, bản tin' - no_selected: Nothing selected + no_selected: Không có gì được chọn form: - value_must_not_be_empty: Value must not be empty. - value_is_too_long: Value is too long. - value_is_too_short: Value is too short. - invalid_email: Invalid email. - invalid_value: Invalid value. - invalid_autocomplete: Please, choose from the list - invalid_date: 'Invalid date, format: dd.mm.yyyyy.' - invalid_range: This date is invalid here. - save: Save - upload_file: Upload file - cancel: Cancel - server_error: Request error, status code + value_must_not_be_empty: Giá trị không được để trống. + value_is_too_long: Giá trị quá dài. + value_is_too_short: Giá trị quá ngắn. + invalid_email: Email không hợp lệ. + invalid_value: Giá trị không hợp lệ. + invalid_autocomplete: Vui lòng chọn từ danh sách + invalid_date: 'Định dạng ngày tháng không hợp lệ: dd.mm.yyyyy.' + invalid_range: Ngày này không hợp lệ ở đây. + save: Lưu + upload_file: Tải lên tệp tin + cancel: Hủy bỏ + server_error: Lỗi yêu cầu, mã trạng thái From c4023d795382f61f9695b96186f9bb13d01e7c43 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 17 Jul 2023 23:12:44 +0200 Subject: [PATCH 214/218] minor fixes --- modules/styles/rtl.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/styles/rtl.styl b/modules/styles/rtl.styl index c53628c..6cff2f4 100644 --- a/modules/styles/rtl.styl +++ b/modules/styles/rtl.styl @@ -1,3 +1,3 @@ if lang=='fa' body - background green + background white From 3ce61d214594861d70c169f40902437779caa350 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 5 Oct 2023 19:04:35 +0200 Subject: [PATCH 215/218] minor fixes --- package.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9f63840..a71155b 100755 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "css-loader": "^0", "file-loader": "^1.1", "fs-extra": "*", - "gm": "*", + "gm": "^1", "gulp": "^4", "gulp-livereload": "^4", "html-entities": "^1.3.1", @@ -46,7 +46,7 @@ "koa-static": "^5", "limax": "^2.0.0", "lodash": "*", - "markdown-it": "*", + "markdown-it": "^8", "markdown-it-container": "*", "markdown-it-deflist": "*", "mime": "^2.3", @@ -61,16 +61,17 @@ "node-zip": "*", "nodemon": "^1.18.4", "optimize-css-assets-webpack-plugin": "^4.0.3", - "path-to-regexp": "*", + "path-to-regexp": "^6.2", "postcss-loader": "^3", "prismjs": "^1", "pug": "^2.0.3", "pug-loader": "^2.4.0", "pug-runtime": "^2.0.4", - "request": "*", - "request-promise": "*", - "rupture": "*", + "request": "^2.34", + "request-promise": "^4.2", + "rupture": "^0.7", "style-loader": "^0", + "stylus": "^0.59.0", "stylus-loader": "^3", "terser-webpack-plugin": "^4", "trace": "^3.1.0", From 82741451f3c7ec5e3c0d1b90fd05332b473f51e6 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 18 Nov 2024 07:55:45 +0100 Subject: [PATCH 216/218] update to latest --- .gitignore | 2 +- modules/config/index.js | 5 +- modules/config/webpack.js | 211 ++++++++++-------- modules/styles/blocks/extract/extract.styl | 2 +- modules/styles/blocks/font/PTMonoBold.ttf | Bin 0 -> 143312 bytes modules/styles/blocks/font/PTMonoBold.woff | Bin 0 -> 75756 bytes modules/styles/blocks/font/PTMonoBold.woff2 | Bin 0 -> 52236 bytes .../styles/blocks/font/Vazirmatn-wght.woff2 | Bin 0 -> 111152 bytes modules/styles/blocks/font/font-icons.styl | 16 +- modules/styles/blocks/main/main.styl | 9 +- modules/styles/blocks/page/page.styl | 10 +- .../blocks/prism/03-prism-line-numbers.styl | 8 +- .../sitetoolbar-light/sitetoolbar-light.styl | 12 +- .../blocks/sitetoolbar/sitetoolbar.styl | 10 +- .../styles/blocks/subscribe/subscribe.styl | 2 +- modules/styles/blocks/toolbar/toolbar.styl | 9 +- package.json | 46 ++-- tasks/webpack.js | 2 +- templates/layouts/main.pug | 2 - 19 files changed, 196 insertions(+), 150 deletions(-) create mode 100644 modules/styles/blocks/font/PTMonoBold.ttf create mode 100644 modules/styles/blocks/font/PTMonoBold.woff create mode 100644 modules/styles/blocks/font/PTMonoBold.woff2 create mode 100644 modules/styles/blocks/font/Vazirmatn-wght.woff2 diff --git a/.gitignore b/.gitignore index db9143e..529f3af 100755 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,8 @@ node_modules/ # TMP folder (run-time tmp) /tmp -# Manifest (build-generated content, versions) /cache +/build # contains v8 executable for linux-tick-processor (run from project root) out/* diff --git a/modules/config/index.js b/modules/config/index.js index 50feaaf..08ab50d 100755 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -39,7 +39,7 @@ let config = module.exports = { adminKey: secret.adminKey, certDir: path.join(secret.dir, 'cert'), - + lang: lang, plnkrAuthId: secret.plnkrAuthId, @@ -61,7 +61,8 @@ let config = module.exports = { tutorialRoot: env.TUTORIAL_ROOT || path.join(process.cwd(), 'repo', `${env.TUTORIAL_LANG || lang}.javascript.info`), tmpRoot: path.join(process.cwd(), 'tmp', lang), // js/css build versions - cacheRoot: path.join(process.cwd(), 'cache', lang), + buildRoot: path.join(process.cwd(), 'build', lang), + cacheRoot: path.join(process.cwd(), 'cache', lang), assetsRoot: path.join(process.cwd(), 'assets'), handlers: require('./handlers') diff --git a/modules/config/webpack.js b/modules/config/webpack.js index 8c0bc91..08f1426 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -21,8 +21,10 @@ module.exports = function () { let CssWatchRebuildPlugin = require('engine/webpack/cssWatchRebuildPlugin'); const CopyWebpackPlugin = require('copy-webpack-plugin') const MiniCssExtractPlugin = require("mini-css-extract-plugin"); - const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); - const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); + + const TerserPlugin = require('terser-webpack-plugin'); + const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); + const fse = require('fs-extra'); @@ -91,6 +93,7 @@ module.exports = function () { let webpackConfig = { output: { + devtoolNamespace: 'wp', // fs path path: path.join(config.publicRoot, 'pack'), // path as js sees it @@ -120,8 +123,11 @@ module.exports = function () { watch: devMode, - devtool: devMode ? "cheap-inline-module-source-map" : // try "eval" ? - process.env.NODE_ENV == 'production' ? 'source-map' : false, + devtool: devMode + ? 'inline-cheap-module-source-map' // try "eval" ? + : process.env.NODE_ENV === 'production' + ? 'source-map' + : false, profile: Boolean(process.env.WEBPACK_STATS), @@ -139,10 +145,12 @@ module.exports = function () { rules: [ { test: /\.yml$/, - use: ['json-loader', 'yaml-loader'] + use: 'yaml-loader' }, { - test: /\.pug$/, + // make t global, so that it will not be defined in the compiled template function + // and i18n plugin will substitute + test: /\.pug/, use: 'pug-loader?root=' + config.projectRoot + '/templates&globals=__' }, { @@ -150,18 +158,23 @@ module.exports = function () { // babel shouldn't process modules which contain ws/browser.js, // which must not be run in strict mode (global becomes undefined) // babel would make all modules strict! - exclude: noProcessModulesRegExp, + exclude(path) { + return noProcessModulesRegExp.test(path) || path.includes('node_modules/monaco-editor') || devMode; + }, use: [ // babel will work first { loader: 'babel-loader', options: { + plugins: [ + require.resolve('@babel/plugin-proposal-object-rest-spread') + ], presets: [ - // use require.resolve here to build files from symlinks - [require.resolve('babel-preset-env'), { + [require.resolve('@babel/preset-env'), { //useBuiltIns: true, targets: { - browsers: "> 3%" + // not ie11, don't want regenerator-runtime and @babel/plugin-transform-runtime + browsers: '> 3%' } }] ] @@ -169,6 +182,15 @@ module.exports = function () { } ] }, + { + test: /\.css$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + }, + ], + }, { test: /\.styl$/, // MiniCssExtractPlugin breaks HMR for CSS @@ -183,36 +205,43 @@ module.exports = function () { { loader: 'postcss-loader', options: { - plugins: [ - require('autoprefixer') - ] + postcssOptions: { + plugins: [ + require('autoprefixer') + ] + } } }, 'engine/webpack/hover-loader', { loader: 'stylus-loader', options: { - linenos: true, - 'resolve url': true, - use: [ - rupture(), - nib(), - function (style) { - style.define('lang', config.lang); - style.define('isRTL', ['ar','fa','he'].includes(process.env.TUTORIAL_LANG)); - style.define('env', config.env); - } - ] + stylusOptions: { + linenos: true, + 'resolve url': true, + use: [ + rupture(), + nib(), + function(style) { + style.define('lang', config.lang); + style.define('isRTL', ['ar','fa','he'].includes(process.env.TUTORIAL_LANG)); + style.define('env', config.env); + } + ] + } }, } ] }, { - test: /\.(png|jpg|gif|woff|eot|otf|ttf|svg)$/, - use: extHash('file-loader?name=[path][name]', '[ext]') + test: /\.(png|jpg|gif|woff|woff2|eot|otf|ttf|svg)$/, + type: 'asset/resource', + generator: { + filename: extHash('[name]','[ext]'), // Keeps the original file name and extension + }, } ], - noParse: function (path) { + noParse(path) { /* if (path.indexOf('!') != -1) { path = path.slice(path.lastIndexOf('!') + 1); @@ -227,11 +256,16 @@ module.exports = function () { resolve: { // allow require('styles') which looks for styles/index.styl extensions: ['.js', '.styl'], - alias: { + alias: { 'entities/maps/entities.json': 'engine/markit/emptyEntities', - config: 'client/config' + 'entities/lib/maps/entities.json': 'engine/markit/emptyEntities', + config: 'client/config', + }, + fallback: { + fs: false, // Replace 'empty' with 'false' in Webpack 5 to exclude it }, - modules: modulesDirectories + modules: modulesDirectories, + mainFields: ['browser', 'main', 'module'] // maybe not needed, from eslint webpack }, @@ -240,21 +274,19 @@ module.exports = function () { extensions: ['.js'] }, - node: { - fs: 'empty' - }, - performance: { maxEntrypointSize: 350000, maxAssetSize: 350000, // warning if asset is bigger than 300k - assetFilter(assetFilename) { // only check js/css + assetFilter(assetFilename) { + // only check js/css // ignore assets copied by CopyWebpackPlugin - if (assetFilename.startsWith('..')) { // they look like ../courses/achievements/course-complete.svg + if (assetFilename.startsWith('..')) { + // they look like ../courses/achievements/course-complete.svg // built assets do not have .. return false; } return assetFilename.endsWith('.js') || assetFilename.endsWith('.css'); - } + }, }, plugins: [ @@ -268,43 +300,43 @@ module.exports = function () { _: 'lodash' }), - // ignore all locales except current lang + new webpack.IgnorePlugin({ - checkResource(arg) { + resourceRegExp: /fs-extra$/ + }), + + // ignore all moment.js locales except current lang + new webpack.IgnorePlugin({ + checkResource(resource, context) { // locale requires that file back from it, need to keep it - if (arg === '../moment') return false; // don't ignore this - if (arg === './' + config.lang || arg === './' + config.lang + '.js') return false; // don't ignore current locale - tmp = arg; // for logging only - return true; - }, - // under dirs like: ../locales/.. - checkContext(arg) { - let ignore = arg.endsWith(path.join('node_modules', 'moment', 'locale')); + if (resource === '../moment') return false; // don't ignore this + if (resource.startsWith('./' + config.lang)) return false; // don't ignore current locale ./ru ./ru.js ./zh ./zh-cn.js + + let ignore = context.endsWith(path.join('node_modules', 'moment', 'locale')); + if (ignore) { - // console.log("ignore moment locale", tmp, arg); + // console.log("Ignore", resource, context); return true; } - } + return false; + }, }), - // ignore site locale files except the lang new webpack.IgnorePlugin({ - checkResource(arg) { - let result = arg.endsWith('.yml') && !arg.endsWith('/' + config.lang + '.yml'); - tmp = arg; // for logging - return result; - }, - // under dirs like: ../locales/.. - checkContext(arg) { - let ignore = /\/locales(\/|$)/.test(arg); - // console.log("ignore yml", tmp, arg); - return ignore; + + checkResource(resource, context) { + let isYmlForAnotherLanguage = resource.endsWith('.yml') && !resource.endsWith('/' + config.lang + '.yml'); + let isUnderLocales = /\/locales(\/|$)/.test(context); + if (isYmlForAnotherLanguage && isUnderLocales) return true; + + // console.log("checkResource", resource, context); + return false; } }), - new WriteVersionsPlugin(path.join(config.cacheRoot, 'webpack.versions.json')), + new WriteVersionsPlugin(path.join(config.buildRoot, 'webpack', 'versions', 'all.json')), new MiniCssExtractPlugin({ filename: extHash("[name]", 'css'), @@ -313,26 +345,29 @@ module.exports = function () { new CssWatchRebuildPlugin(), - new CopyWebpackPlugin( - assetPaths.map(path => { + new CopyWebpackPlugin({ + patterns: assetPaths.map((path) => { return { from: path, - to: config.publicRoot - } - }), - {debug: 'warning'} - ), + to: config.publicRoot, + info: (file) => ({ minimized: true }), + noErrorOnMissing: true + }; + }) + }), { - apply: function (compiler) { + apply(compiler) { if (process.env.WEBPACK_STATS) { - compiler.plugin("done", function (stats) { + compiler.hooks.done.tap('MyStats', (stats) => { stats = stats.toJson(); - fs.writeFileSync(`${config.tmpRoot}/stats.json`, JSON.stringify(stats)); + let dir = path.join(config.buildRoot, 'webpack', 'stats'); + fs.ensureDirSync(dir); + fs.writeFileSync(path.join(dir, entryName + '.json'), JSON.stringify(stats)); }); } - } - } + }, + }, ], recordsPath: path.join(config.tmpRoot, 'webpack.json'), @@ -349,30 +384,30 @@ module.exports = function () { optimization: { minimizer: [ - new UglifyJsPlugin({ - cache: true, - parallel: 2, - uglifyOptions: { - ecma: 8, + new TerserPlugin({ + parallel: 2, + terserOptions: { + ecma: 2022, warnings: false, compress: { - drop_console: true, - drop_debugger: true + drop_console: true, + drop_debugger: true, }, - output: { - beautify: true, - indent_level: 0 // for error reporting, to see which line actually has the problem + output: { + beautify: true, + indent_level: 0, // for error reporting, to see which line actually has the problem // source maps actually didn't work in Qbaka that's why I put it here - } - } + }, + }, }), - new OptimizeCSSAssetsPlugin({}) - ] + new CssMinimizerPlugin({}), + ], } }; //if (process.env.NODE_ENV != 'development') { // production, ebook +/* if (process.env.NODE_ENV == 'production') { // production, ebook webpackConfig.plugins.push( function clearBeforeRun() { @@ -388,6 +423,6 @@ module.exports = function () { } ); } - +*/ return webpackConfig; }; diff --git a/modules/styles/blocks/extract/extract.styl b/modules/styles/blocks/extract/extract.styl index b0fa646..2caef39 100755 --- a/modules/styles/blocks/extract/extract.styl +++ b/modules/styles/blocks/extract/extract.styl @@ -83,7 +83,7 @@ padding-right 20px else padding-left 20px - padding-right 10px + width 100% &__aside_price width .1% diff --git a/modules/styles/blocks/font/PTMonoBold.ttf b/modules/styles/blocks/font/PTMonoBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6fcf00c888ca5abc9cd1b8f9cd984fc879d6ed65 GIT binary patch literal 143312 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%0A;E=#i_3CyyhA*D_mMMx!R@B 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 N;%$}W$yCnie*j}%K!N}O literal 0 HcmV?d00001 diff --git a/modules/styles/blocks/font/PTMonoBold.woff2 b/modules/styles/blocks/font/PTMonoBold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c0fb8be6dcc18ec45d0b618e8b84a6b5e255c68e GIT binary patch literal 52236 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 literal 0 HcmV?d00001 diff --git a/modules/styles/blocks/font/Vazirmatn-wght.woff2 b/modules/styles/blocks/font/Vazirmatn-wght.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a501289a85595158570b0c2badcb4608b042e748 GIT binary patch literal 111152 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 Q5x9B`DgL-UO8@`>0Dz@xb^rhX literal 0 HcmV?d00001 diff --git a/modules/styles/blocks/font/font-icons.styl b/modules/styles/blocks/font/font-icons.styl index fd7b8fe..6e2c9af 100755 --- a/modules/styles/blocks/font/font-icons.styl +++ b/modules/styles/blocks/font/font-icons.styl @@ -2,7 +2,15 @@ font-family 'FontIcons' font-weight normal font-style normal - src url('icons.woff?5') format('woff'); + src url('icons.woff?5') format('woff'), url('icons.otf') format('opentype'); + +if isRTL + @font-face + font-family 'Vazirmatn' + src url('Vazirmatn-wght.woff2') format('woff2 supports variations'), url('Vazirmatn-wght.woff2') format('woff2-variations') + font-weight 100 900 + font-style normal + font-display swap //- not used now? @font-face @@ -10,6 +18,6 @@ font-weight: bold; font-style: normal; src: local('PT MonoBold'), - url('/font/PTMonoBold.woff2') format('woff2'), - url('/font/PTMonoBold.woff') format('woff'), - url('/font/PTMonoBold.ttf') format('truetype'); + url('PTMonoBold.woff2') format('woff2'), + url('PTMonoBold.woff') format('woff'), + url('PTMonoBold.ttf') format('truetype'); diff --git a/modules/styles/blocks/main/main.styl b/modules/styles/blocks/main/main.styl index b306d69..210fa12 100755 --- a/modules/styles/blocks/main/main.styl +++ b/modules/styles/blocks/main/main.styl @@ -96,8 +96,7 @@ $main-loud ul, ol if isRTL - padding-right 21px - + padding-right 21px else padding-left 21px margin 22px 0 // уравниваем с абзацными отступами @@ -113,9 +112,9 @@ $main-loud ul > li::before content "●" if isRTL - float right - margin-right -20px - else + float right + margin-right -20px + else float left // not position: absolute because the latter doesn't show in iBooks (epub) margin-left -20px color #000 diff --git a/modules/styles/blocks/page/page.styl b/modules/styles/blocks/page/page.styl index 112c193..a42289d 100755 --- a/modules/styles/blocks/page/page.styl +++ b/modules/styles/blocks/page/page.styl @@ -65,7 +65,7 @@ &__nav_prev if isRTL right 0 - else + else left 0 transition transform animation_duration, top animation_duration @@ -117,7 +117,10 @@ transform translateX(sidebar_width) &__nav_next - left 0 + if isRTL + left 0 + else + right 0 transition top animation_duration &__nav_next &__nav-text::before @@ -224,10 +227,11 @@ &__nav_prev if isRTL padding-right 30px + border-left-width 0 else padding-left 30px + border-right-width 0 - border-right-width 0 border-radius: 6px 0 0 6px &__nav_next diff --git a/modules/styles/blocks/prism/03-prism-line-numbers.styl b/modules/styles/blocks/prism/03-prism-line-numbers.styl index d036c48..f572209 100644 --- a/modules/styles/blocks/prism/03-prism-line-numbers.styl +++ b/modules/styles/blocks/prism/03-prism-line-numbers.styl @@ -1,9 +1,9 @@ pre.line-numbers position relative - if isRTL - padding-right 3.8em - else - padding-left 3.8em + if isRTL + padding-right 3.8em + else + padding-left 3.8em counter-reset linenumber .line-numbers .line-numbers-rows diff --git a/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl b/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl index ed27c9b..966ffb7 100755 --- a/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl +++ b/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl @@ -125,12 +125,12 @@ display block &__search .text-input__control - if isRTL - padding-left 93px - padding-right 37px - else - padding-left 37px - padding-right 93px + if isRTL + padding-left 93px + padding-right 37px + else + padding-left 37px + padding-right 93px // we MUST repeat theese rules for each selector to make it work &__search .text-input__control::-webkit-input-placeholder diff --git a/modules/styles/blocks/sitetoolbar/sitetoolbar.styl b/modules/styles/blocks/sitetoolbar/sitetoolbar.styl index 032af7a..7c89267 100755 --- a/modules/styles/blocks/sitetoolbar/sitetoolbar.styl +++ b/modules/styles/blocks/sitetoolbar/sitetoolbar.styl @@ -50,10 +50,10 @@ position relative min-width 20px - if isRTL - padding-right 47px - else - padding-left 47px + if isRTL + padding-right 47px + else + padding-left 47px transition opacity 0.3s ease-out @@ -251,7 +251,7 @@ &__search .text-input__control if isRTL - padding-left 93px + padding-left 93px padding-right 37px else padding-left 37px diff --git a/modules/styles/blocks/subscribe/subscribe.styl b/modules/styles/blocks/subscribe/subscribe.styl index d00dba8..27f5557 100755 --- a/modules/styles/blocks/subscribe/subscribe.styl +++ b/modules/styles/blocks/subscribe/subscribe.styl @@ -4,7 +4,7 @@ &_fancy padding 25px - background #EBF6FF url('/img/subscribe_bg.svg') 0 80% no-repeat + background #EBF6FF background-size cover border-radius 4px diff --git a/modules/styles/blocks/toolbar/toolbar.styl b/modules/styles/blocks/toolbar/toolbar.styl index b357aed..d01ab49 100755 --- a/modules/styles/blocks/toolbar/toolbar.styl +++ b/modules/styles/blocks/toolbar/toolbar.styl @@ -5,10 +5,10 @@ toolbar_button_background = #c4c2c0 &__tool display table-cell - if isRTL - padding-right 1px - else - padding-left 1px + if isRTL + padding-right 1px + else + padding-left 1px & &__button @extend $plain-link @@ -42,4 +42,3 @@ toolbar_button_background = #c4c2c0 &__button_edit::before @extend $font-edit - diff --git a/package.json b/package.json index a71155b..4fc14b3 100755 --- a/package.json +++ b/package.json @@ -12,23 +12,26 @@ "precommit": "NODE_ENV=development node `which gulp` pre-commit", "//": "node-xmpp-client installs for linux only", "dependencies": { + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/preset-env": "^7.26.0", "@trysound/sax": "^0.2.0", "autoprefixer": "^9", "babel-core": "^6", - "babel-loader": "^7", + "babel-loader": "^9.2.1", "babel-plugin-transform-flow-strip-types": "*", "babel-plugin-transform-runtime": "*", "babel-preset-env": "*", - "babelfish": "^1.1.2", + "babelfish": "^2.0.0", "bemto.pug": "iliakan/bemto", "bunyan": "*", - "chokidar": "^2.0.4", + "chokidar": "^4.0.1", "clarify": "^2.1.0", - "copy-webpack-plugin": "^4.5.2", - "cross-env": "^5.2.0", + "copy-webpack-plugin": "^12.0.2", + "cross-env": "^7.0.3", "csrf": "^3", - "css-loader": "^0", - "file-loader": "^1.1", + "css-loader": "^7.1.2", + "css-minimizer-webpack-plugin": "^7.0.0", + "execa": "^5.1.1", "fs-extra": "*", "gm": "^1", "gulp": "^4", @@ -44,25 +47,25 @@ "koa-mount": "^3", "koa-router": "^7", "koa-static": "^5", - "limax": "^2.0.0", + "limax": "^4.1.0", "lodash": "*", - "markdown-it": "^8", + "lru-cache": "^11.0.2", + "markdown-it": "^13.0.2", "markdown-it-container": "*", "markdown-it-deflist": "*", - "mime": "^2.3", - "mini-css-extract-plugin": "^0", + "mime": "^3.0.0", + "mini-css-extract-plugin": "^2.9.2", "minimatch": "^3.0.4", - "mongoose": "^5.9.5", + "mongoose": "^8.8.1", "multiparty": "*", "mz": "*", "nib": "*", "node-notifier": "*", "node-uuid": "*", "node-zip": "*", - "nodemon": "^1.18.4", - "optimize-css-assets-webpack-plugin": "^4.0.3", + "nodemon": "^3.1.7", "path-to-regexp": "^6.2", - "postcss-loader": "^3", + "postcss-loader": "^8.1.1", "prismjs": "^1", "pug": "^2.0.3", "pug-loader": "^2.4.0", @@ -70,15 +73,14 @@ "request": "^2.34", "request-promise": "^4.2", "rupture": "^0.7", - "style-loader": "^0", - "stylus": "^0.59.0", - "stylus-loader": "^3", - "terser-webpack-plugin": "^4", + "style-loader": "^4.0.0", + "stylus": "^0.64.0", + "stylus-loader": "^8.1.1", + "terser-webpack-plugin": "^5.3.10", "trace": "^3.1.0", "uglify-es": "^3", - "uglifyjs-webpack-plugin": "^1", - "webpack": "^4.44.2", - "yaml-loader": "^0.5.0" + "webpack": "^5.96.1", + "yaml-loader": "^0.8.1" }, "engineStrict": true, "repository": { diff --git a/tasks/webpack.js b/tasks/webpack.js index fd5b95e..a31bafc 100755 --- a/tasks/webpack.js +++ b/tasks/webpack.js @@ -17,7 +17,7 @@ module.exports = async function() { if (err) { notifier.notify({ - message: err + message: err.message }); console.log(err); diff --git a/templates/layouts/main.pug b/templates/layouts/main.pug index de01cdb..1cc6874 100755 --- a/templates/layouts/main.pug +++ b/templates/layouts/main.pug @@ -36,5 +36,3 @@ block main span.page__nav-text span.page__nav-text-shortcut span.page__nav-text-alternate= t('tutorial.article.lesson.next') - - From 12069c6c6b64054b067a97457b4a587dca730f77 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 20 Nov 2024 23:05:39 +0100 Subject: [PATCH 217/218] minor fixes --- dev | 1 - dev.cmd | 1 - edit | 1 - edit.cmd | 1 - 4 files changed, 4 deletions(-) diff --git a/dev b/dev index 7983d22..58fc04c 100755 --- a/dev +++ b/dev @@ -12,7 +12,6 @@ export WATCH=1 export SITE_HOST=http://javascript.local export TUTORIAL_EDIT= export NODE_PRESERVE_SYMLINKS=1 -export NODE_OPTIONS=--openssl-legacy-provider # Use a local bunyan if no other is found in the current environment if ! [ -x "$(command -v bunyan)" ]; then diff --git a/dev.cmd b/dev.cmd index 8019819..0b142f4 100644 --- a/dev.cmd +++ b/dev.cmd @@ -5,7 +5,6 @@ set NODE_LANG=%1 @set ASSET_VERSIONING=query @set NODE_PRESERVE_SYMLINKS=1 @set NODE_PATH=./modules -@set NODE_OPTIONS=--openssl-legacy-provider call gulp dev | bunyan diff --git a/edit b/edit index 157863c..14b1eec 100755 --- a/edit +++ b/edit @@ -10,7 +10,6 @@ export TUTORIAL_LANG=$1 export NODE_ENV=production export TUTORIAL_EDIT=1 export NODE_PRESERVE_SYMLINKS=1 -export NODE_OPTIONS=--openssl-legacy-provider # Use a local bunyan if no other is found in the current environment if ! [ -x "$(command -v bunyan)" ]; then diff --git a/edit.cmd b/edit.cmd index 80e8640..8b693a7 100644 --- a/edit.cmd +++ b/edit.cmd @@ -12,7 +12,6 @@ @set ASSET_VERSIONING=query @set NODE_PRESERVE_SYMLINKS=1 @set NODE_PATH=./modules -@set NODE_OPTIONS=--openssl-legacy-provider call gulp edit | bunyan From 45672ab318a8cb95db99c5b3f48fb98fb0e45605 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 13 Apr 2025 19:59:44 +0200 Subject: [PATCH 218/218] port code changes --- modules/styles/blocks/prism/01-prism.styl | 46 ++++++++++++------- .../blocks/prism/03-prism-line-numbers.styl | 10 +--- modules/styles/blocks/prism/04-my-prism.styl | 37 ++++++--------- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/modules/styles/blocks/prism/01-prism.styl b/modules/styles/blocks/prism/01-prism.styl index 8d452ba..7b22b25 100755 --- a/modules/styles/blocks/prism/01-prism.styl +++ b/modules/styles/blocks/prism/01-prism.styl @@ -1,12 +1,7 @@ -/** - * prism.js default theme for JavaScript, CSS and HTML - * Based on dabblet (http://dabblet.com) - * @author Lea Verou - */ - -code[class*="language-"], -pre[class*="language-"] - color black +pre[class*="language-"], +pre[class*="language-"] > code, + color #d4d4d4 + // text-shadow 0 1px white font-family fixed_width_font direction ltr text-align left @@ -14,7 +9,6 @@ pre[class*="language-"] word-spacing normal tab-size 4 hyphens none - position relative /* In epub all code selected? because Calibre applies this style wrong? @@ -30,23 +24,32 @@ code[class*="language-"]::selection, code[class*="language-"] ::selection */ @media print - code[class*="language-"], - pre[class*="language-"] + pre[class*="language-"], + pre[class*="language-"] > code text-shadow: none + /* Code blocks */ pre[class*="language-"] - padding 1em + padding 0 margin 1.5em 0 overflow auto + background #f7f4f3 -pre[class*="language-"] - background: background_blocks + if isRTL + code + unicode-bidi normal + direction unset /* Inline code */ -:not(pre) > code[class*="language-"] +code[class*="language-"] padding .1em border-radius .3em + color #313130 + background #f7f4f3 + +.namespace + opacity .7 .token.comment, .token.prolog, @@ -103,3 +106,14 @@ pre[class*="language-"] .token.entity cursor help + +/********************************************************* +* Line highlighting +*/ +pre[class*="language-"] > code[class*="language-"] + position relative + z-index 1 + +.line-highlight.line-highlight + background var(--backgroundPrismLineHighlight) + z-index 0 \ No newline at end of file diff --git a/modules/styles/blocks/prism/03-prism-line-numbers.styl b/modules/styles/blocks/prism/03-prism-line-numbers.styl index f572209..6ba260a 100644 --- a/modules/styles/blocks/prism/03-prism-line-numbers.styl +++ b/modules/styles/blocks/prism/03-prism-line-numbers.styl @@ -1,17 +1,11 @@ pre.line-numbers - position relative - if isRTL - padding-right 3.8em - else - padding-left 3.8em counter-reset linenumber + display flex + flex-direction row .line-numbers .line-numbers-rows - position absolute pointer-events none - top 0 font-size 100% - left -3.8em width 3em /* works for line-numbers below 1000 lines */ letter-spacing -1px border-right 1px solid light_gray_color diff --git a/modules/styles/blocks/prism/04-my-prism.styl b/modules/styles/blocks/prism/04-my-prism.styl index 1b9be30..2adb4ed 100644 --- a/modules/styles/blocks/prism/04-my-prism.styl +++ b/modules/styles/blocks/prism/04-my-prism.styl @@ -1,34 +1,35 @@ pre[class*="language-"], code[class*="language-"] - font 14px/17px fixed_width_font + font 15px/20px fixed_width_font z-index 0 text-shadow none margin 0 + //- line height must be the same to avoid jumps of svg arrowed illustrations + @media (min-width: largescreen) + font 16px/20px fixed_width_font + + code.token + background inherit + padding 0 + + pre[class*="language-"] - position relative > code.language-markup color inherit - position relative > code[class*="language-"] background none - padding 0 - -pre.line-numbers - padding-left 3.5em + padding 1em 1em 1em 0.5em + flex-grow 1 -// span with line numbers is moved from to the outer

      ,
      -// because we need to handle many ... inside single 
      -// (this we need for highlighting *!*...* /!* inline
       .line-numbers .line-numbers-rows
      -  left 0
      -  top 0
      -  padding 1em 0
      +  padding 1em 0 .8em
         border 0
         background #e7e5e3
         width auto
       
      +
       .line-numbers .line-numbers-rows:after
         width auto
         display block
      @@ -39,13 +40,5 @@ pre.line-numbers
       .line-numbers-rows > span:before,
       .line-numbers .line-numbers-rows:after
         padding 0 .7em 0 .8em
      -  background #e7e5e3 // #146 https://github.com/iliakan/javascript-nodejs/issues/146#issuecomment-72321753
      +  // background  var(--backgroundAlt) // was s#e7e5e3 // #146 https://github.com/iliakan/javascript-nodejs/issues/146#issuecomment-72321753
         text-shadow none
      -
      -/* not sure if larger code is better
      -@media (min-width: largescreen)
      -  pre[class*="language-"],
      -  code[class*="language-"]
      -    font-size font_size_m
      -    line-height 19px
      -*/
      \ No newline at end of file