diff --git a/.gitignore b/.gitignore index 4daeacd..529f3af 100755 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # OS or Editor folders .DS_Store +.vscode .idea .cache .project @@ -21,16 +22,17 @@ sftp-config.json Thumbs.db # database dump for tutorial export -dump/ +/dump +/repo # NPM packages folder. node_modules/ # TMP folder (run-time tmp) -tmp/ +/tmp -# Manifest (build-generated content, versions) -cache/ +/cache +/build # contains v8 executable for linux-tick-processor (run from project root) out/* @@ -39,5 +41,5 @@ out/* public/* package-lock.json -/modules/jsengine +/modules/engine .gitmodules 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 diff --git a/README.md b/README.md index bd47d44..91b812f 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Tutorial server +# Tutorial Server Hi everyone! @@ -6,124 +6,392 @@ 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 +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. -(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`). +# 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. - - 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/). + + (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: - ``` - npm install -g bunyan gulp + ```bash + npm install -g bunyan gulp@4 ``` 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. + + 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: - ``` + ```bash 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. + + 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 text repository has `"-language"` postfix at the end, e.g for the French version `fr`, for Russian – `ru` etc. - - E.g. for the Russian version: - ``` - cd /js - git clone https://github.com/iliakan/javascript-tutorial-ru + 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`. + + The tutorial text repository should go into the `repo` subfolder, like this: + + ```bash + cd /js/server/repo + git clone https://github.com/javascript-tutorial/en.javascript.info ``` 6. Run the site - Run the site with the same language. Above we cloned `ru` tutorial, so: + Install local NPM modules: + ```bash + cd /js/server + npm install ``` - cd /js/javascript-tutorial-server - ./edit ru + + Run the site with the `./edit` command with the language argument. Above we cloned `en` tutorial, so: + + ```bash + ./edit en ``` - This will import the tutorial from `/js/javascript-tutorial-ru` 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 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`. + > To change the port, set the `PORT` environment variable: + > ```bash + > # 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 - As you edit text files in the tutorial text repository (cloned at step 5), - the webpage gets reloaded automatically. - - + As you edit text files in the tutorial text repository (cloned at step 5), + the webpage will reload automatically. + + +# Windows: Environment variables + +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. + +Alternatively, you can use other Windows-specific ways to set environment variables, such as a separate `set PORT=8080` command. + # 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`. -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 +```bash +cd /js/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`, `tr`, `ko` and `ja` are fully supported. + +# Translating images + +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 . + +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.** 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 + +The file format is [YAML](https://learnxinyminutes.com/docs/yaml/). + +Here's a quote: + +```yaml +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 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. + +Initially, the file may be empty, then you can fill it with images one by one. + +Only the mentioned images will be translated. + +**Step 3.** Use the helper script to get a list of strings to translate: + +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 +``` + +Here's an example of its output: + +```yml +code-style.svg: + '2': '' + No space: '' + between the function name and parentheses: '' + between the parentheses and the parameter: '' + Indentation: '' + 2 spaces: '' + 'A space ': '' + 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: '' +``` + +As we can see, the script returns a text snippet that can be inserted into `images.yml` fully or partially. + +E.g. like this: + +```yml +code-style.svg: + 'A space ': 'Пробел' +``` + +Or like this, if we want to position the translation in the center (see below for more examples): + +```yml +code-style.svg: + 'A space ': + position: 'center' + text: 'Пробел' +``` + +**Step 4.** 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` (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. + +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)). + +**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! + +> 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 +> 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: + +``` +| hello world +``` + +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: + +``` + | +hello world + | +``` + +Here, the "hello world" is centered between two vertical lines `|`. + +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: +``` + | +你好世界 + | +``` + +The `position: "right"` makes sure that the translated string sticks to the same right edge: +``` +hello world | + 你好世界 | +``` + +That's also useful for images when we expect the text to stick to the right. + + +## 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. + +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 images translation + +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 (Step 3) 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, *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 (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: +```bash +cd /js/server +NODE_LANG=en npm run gulp engine:koa:tutorial:import ``` -// 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: +> 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... +> ``` -``` -cd /js/javascript-tutorial-server +Afterwards, call `./dev ` to run the server: + +```bash +cd /js/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. - -# TroubleShooting -If something doesn't work – [file an issue](https://github.com/iliakan/javascript-tutorial-server/issues/new). +# Troubleshooting + +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. + +## Linux: inotify and monitored files -Please mention OS and Node.js version. +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 +$> 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 +``` -Also please pull the very latest git code and install latest Node.js 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, -Ilya Kantor -iliakan@javascript.info +--
Yours,
Ilya Kantor
iliakan@javascript.info 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/dev b/dev index 62a5092..58fc04c 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 @@ -12,5 +13,9 @@ export SITE_HOST=http://javascript.local export TUTORIAL_EDIT= export NODE_PRESERVE_SYMLINKS=1 -npm --silent run gulp dev | bunyan -o short -l debug +# 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 diff --git a/edit b/edit index 9d1fdb7..14b1eec 100755 --- a/edit +++ b/edit @@ -5,12 +5,15 @@ : ${1?"Usage: $0 []"} - -export TUTORIAL_ROOT="../javascript-tutorial-$1" export NODE_LANG="${2:-en}" +export TUTORIAL_LANG=$1 export NODE_ENV=production export TUTORIAL_EDIT=1 export NODE_PRESERVE_SYMLINKS=1 -npm --silent run -- gulp edit | bunyan -o short -l debug +# 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 diff --git a/edit.cmd b/edit.cmd index 7d802c6..8b693a7 100644 --- a/edit.cmd +++ b/edit.cmd @@ -1,13 +1,12 @@ @if "%~1"=="" goto usage -@set TUTORIAL_ROOT=../javascript-tutorial-%1 - @if "%~2"=="" ( set NODE_LANG=en ) else ( set NODE_LANG=%2 ) +@set TUTORIAL_LANG=%1 @set NODE_ENV=production @set TUTORIAL_EDIT=1 @set ASSET_VERSIONING=query diff --git a/gulpfile.js b/gulpfile.js index de28af1..3053a03 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -7,8 +7,8 @@ const gulp = require('gulp'); const glob = require('glob'); const path = require('path'); const fs = require('fs'); -const {lazyRequireTask, requireModuleTasks} = require('jsengine/gulp/requireModuleTasks'); -const runSequence = require('run-sequence'); +const {lazyRequireTask, requireModuleTasks} = require('engine/gulp/requireModuleTasks'); +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", @@ -26,23 +26,26 @@ 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"] })); -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 + 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` ] })); -requireModuleTasks('jsengine/koa/tutorial'); +requireModuleTasks('engine/koa/tutorial'); let testSrcs = ['modules/**/test/**/*.js']; // on Travis, keys are required for E2E Selenium tests @@ -51,55 +54,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('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')); +task('webpack', lazyRequireTask('./tasks/webpack')); // gulp.task('webpack-dev-server', lazyRequireTask('./tasks/webpackDevServer')); -gulp.task('build', function(callback) { - runSequence("sync-resources", 'webpack', callback); -}); +task('build', series('webpack')); -gulp.task('server', lazyRequireTask('./tasks/server')); +task('server', lazyRequireTask('./tasks/server')); -gulp.task('edit', ['webpack', 'jsengine:koa:tutorial:importWatch', "sync-resources", 'livereload', 'server']); +task('edit', parallel('webpack', 'engine:koa:tutorial:importWatch', 'livereload', 'server')); -gulp.task('dev', function(callback) { - runSequence("sync-resources", ['nodemon', 'livereload', 'webpack', 'watch'], callback); -}); +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/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: خطأ فى الطلب، كود الرد diff --git a/locales/en.yml b/locales/en.yml index 6e98fe7..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 @@ -9,7 +10,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: @@ -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,14 +62,40 @@ 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 + edit_on_github: Edit on GitHub error: error close: close 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/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/locales/ja.yml b/locales/ja.yml index da4b6f2..ca2ef32 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: @@ -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 @@ -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/locales/ko.yml b/locales/ko.yml new file mode 100644 index 0000000..e3a9b4b --- /dev/null +++ b/locales/ko.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: '코스' + 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: + - 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요. + - 잘 이해되지 않는 부분은 구체적으로 언급해주세요. + - 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요. + + edit_on_github: GitHub에서 수정하기 + error: 에러 + close: 종료 + + hide_forever: 영구적으로 숨기기 + hidden_forever: 이제 이 정보는 더 이상 나타나지 않습니다. + diff --git a/locales/ru.yml b/locales/ru.yml index 62cb221..e9d499a 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: @@ -66,7 +66,7 @@ site: meta: description: 'Современный учебник JavaScript, начиная с основ, включающий в себя много тонкостей и фишек JavaScript/DOM.' - edit_on_github: Редактировать на Github + edit_on_github: Редактировать на GitHub error: ошибка close: закрыть 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/locales/vi.yml b/locales/vi.yml new file mode 100644 index 0000000..7664b2a --- /dev/null +++ b/locales/vi.yml @@ -0,0 +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: Không có gì được chọn + + form: + 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 diff --git a/locales/zh.yml b/locales/zh.yml new file mode 100644 index 0000000..5daa8bf --- /dev/null +++ b/locales/zh.yml @@ -0,0 +1,73 @@ +site: + privacy_policy: 隐私政策 + + gdpr_dialog: + title: 本网站使用 cookie + text: 我们使用 cookie 和本地存储等浏览器技术来存储你的偏好设置。你需要接受我们的 隐私政策 和本网站的 其他条款。 + 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: 很抱歉,我们不支持 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>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。 + - 如果你无法理解文章中的内容 — 请详细说明。 + + edit_on_github: 在 GitHub 上编辑 + error: 错误 + close: 关闭 + + hide_forever: 永久隐藏 + hidden_forever: 此信息将不再显示。 + diff --git a/mocha.sh b/mocha.sh index 966aa80..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_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 --full-trace --allow-uncaught --require should --recursive $* 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/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" }; 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 31dd552..08ab50d 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; @@ -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, @@ -54,38 +56,52 @@ 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'), + 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'), + buildRoot: path.join(process.cwd(), 'build', lang), + cacheRoot: path.join(process.cwd(), 'cache', lang), + assetsRoot: path.join(process.cwd(), 'assets'), 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', - 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')); + module.exports = yaml.load(fs.readFileSync(filename, 'utf-8')); }; // 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 // 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); + } +} \ No newline at end of file diff --git a/modules/config/webpack.js b/modules/config/webpack.js index 2bdffef..08f1426 100755 --- a/modules/config/webpack.js +++ b/modules/config/webpack.js @@ -17,12 +17,14 @@ 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'); - 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'); @@ -46,7 +48,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`; @@ -65,7 +67,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' }; @@ -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), @@ -131,7 +137,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', },*/ @@ -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,34 +205,43 @@ module.exports = function () { { loader: 'postcss-loader', options: { - plugins: [ - require('autoprefixer') - ] + postcssOptions: { + plugins: [ + require('autoprefixer') + ] + } } }, - 'jsengine/webpack/hover-loader', + 'engine/webpack/hover-loader', { loader: 'stylus-loader', options: { - linenos: true, - 'resolve url': true, - use: [ - rupture(), - nib(), - function (style) { - style.define('lang', config.lang); - } - ] + 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); @@ -225,11 +256,16 @@ module.exports = function () { resolve: { // allow require('styles') which looks for styles/index.styl extensions: ['.js', '.styl'], - alias: { - 'entities/maps/entities.json': 'jsengine/markit/emptyEntities', - config: 'client/config' + alias: { + 'entities/maps/entities.json': 'engine/markit/emptyEntities', + '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 }, @@ -238,8 +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 + // 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: [ @@ -253,43 +300,43 @@ module.exports = function () { _: 'lodash' }), - // ignore all locales except current lang + + new webpack.IgnorePlugin({ + resourceRegExp: /fs-extra$/ + }), + + // ignore all moment.js locales except current lang new webpack.IgnorePlugin({ - checkResource(arg) { + 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'), @@ -298,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'), @@ -334,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() { @@ -373,6 +423,6 @@ module.exports = function () { } ); } - +*/ return webpackConfig; }; 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..0f436bc 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'); @@ -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/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/frontpage/locales/en.yml b/modules/frontpage/locales/en.yml index ce93689..0d69e57 100644 --- a/modules/frontpage/locales/en.yml +++ b/modules/frontpage/locales/en.yml @@ -1,4 +1,6 @@ -view_github: "view on Github" -search_placeholder: "Search in the tutorial" -search_button: "Search" -share_text: "Share" +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" 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" diff --git a/modules/frontpage/locales/ja.yml b/modules/frontpage/locales/ja.yml index ce93689..6b86ada 100644 --- a/modules/frontpage/locales/ja.yml +++ b/modules/frontpage/locales/ja.yml @@ -1,4 +1,5 @@ -view_github: "view on Github" -search_placeholder: "Search in the tutorial" -search_button: "Search" -share_text: "Share" +modern_javascript_tutorial: "現代の JavaScript チュートリアル" +view_github: "GitHubで見る" +search_placeholder: "チュートリアル内を検索" +search_button: "検索" +share_text: "シェア" diff --git a/modules/frontpage/locales/ko.yml b/modules/frontpage/locales/ko.yml new file mode 100644 index 0000000..6a93129 --- /dev/null +++ b/modules/frontpage/locales/ko.yml @@ -0,0 +1,6 @@ +modern_javascript_tutorial: "모던 JavaScript 튜토리얼" +view_github: "GitHub에서 보기" +search_placeholder: "튜토리얼 검색하기" +search_button: "검색" +share_text: "공유" +contributors_pluralize: "contributor,contributors,contributors" diff --git a/modules/frontpage/locales/ru.yml b/modules/frontpage/locales/ru.yml index aae7213..c9f46dd 100644 --- a/modules/frontpage/locales/ru.yml +++ b/modules/frontpage/locales/ru.yml @@ -1,4 +1,5 @@ -view_github: "смотреть на Github" +modern_javascript_tutorial: "Современный учебник JavaScript" +view_github: "смотреть на GitHub" search_placeholder: "Поиск по учебнику" search_button: "Найти" share_text: "Поделиться" 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" diff --git a/modules/frontpage/locales/zh.yml b/modules/frontpage/locales/zh.yml new file mode 100644 index 0000000..a4cdec4 --- /dev/null +++ b/modules/frontpage/locales/zh.yml @@ -0,0 +1,5 @@ +modern_javascript_tutorial: "现代 JavaScript 教程" +view_github: "在 GitHub 上查看" +search_placeholder: "在教程中搜索" +search_button: "搜索" +share_text: "分享" diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.pug b/modules/frontpage/templates/blocks/_frontpage-content/index.pug index 56a5374..a7c43ea 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 @@ -42,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)) diff --git a/modules/frontpage/templates/blocks/_frontpage-content/index.styl b/modules/frontpage/templates/blocks/_frontpage-content/index.styl index c25ef4d..13b7d22 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 @@ -116,13 +116,19 @@ &__title position relative - padding-left 38px + if isRTL + padding-right 38px + else + padding-left 38px margin-bottom 2px &: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 diff --git a/modules/frontpage/templates/frontpage.pug b/modules/frontpage/templates/frontpage.pug index 07a8b28..6bdcb0a 100755 --- a/modules/frontpage/templates/frontpage.pug +++ b/modules/frontpage/templates/frontpage.pug @@ -4,9 +4,10 @@ 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 layout_page_class = 'page_frontpage' - var content_class = 'frontpage' 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/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/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/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/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 0fd7b3c..45e24ff 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 isRTL + direction rtl @media print body color black + 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..2caef39 100755 --- a/modules/styles/blocks/extract/extract.styl +++ b/modules/styles/blocks/extract/extract.styl @@ -79,8 +79,11 @@ background #f9edbf &__content - padding-left 20px - padding-right 10px + if isRTL + padding-right 20px + else + padding-left 20px + 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 0000000..6fcf00c Binary files /dev/null and b/modules/styles/blocks/font/PTMonoBold.ttf differ diff --git a/modules/styles/blocks/font/PTMonoBold.woff b/modules/styles/blocks/font/PTMonoBold.woff new file mode 100644 index 0000000..644fb25 Binary files /dev/null and b/modules/styles/blocks/font/PTMonoBold.woff differ diff --git a/modules/styles/blocks/font/PTMonoBold.woff2 b/modules/styles/blocks/font/PTMonoBold.woff2 new file mode 100644 index 0000000..c0fb8be Binary files /dev/null and b/modules/styles/blocks/font/PTMonoBold.woff2 differ diff --git a/modules/styles/blocks/font/Vazirmatn-wght.woff2 b/modules/styles/blocks/font/Vazirmatn-wght.woff2 new file mode 100644 index 0000000..a501289 Binary files /dev/null and b/modules/styles/blocks/font/Vazirmatn-wght.woff2 differ diff --git a/modules/styles/blocks/font/font-icons.styl b/modules/styles/blocks/font/font-icons.styl index 586a364..6e2c9af 100755 --- a/modules/styles/blocks/font/font-icons.styl +++ b/modules/styles/blocks/font/font-icons.styl @@ -1,5 +1,23 @@ @font-face - font-family 'FontIcons' - src url('icons.woff') format('woff'), url('icons.otf') format("opentype"); - font-weight normal + font-family 'FontIcons' + font-weight normal + font-style normal + 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 + font-family: 'PT Mono' + font-weight: bold; + font-style: normal; + src: local('PT MonoBold'), + url('PTMonoBold.woff2') format('woff2'), + url('PTMonoBold.woff') format('woff'), + url('PTMonoBold.ttf') format('truetype'); diff --git a/modules/styles/blocks/font/icons.woff b/modules/styles/blocks/font/icons.woff index 2590b1c..68541a7 100755 Binary files a/modules/styles/blocks/font/icons.woff and b/modules/styles/blocks/font/icons.woff differ 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..210fa12 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,10 @@ $main-loud ul, ol - padding-left 21px + if isRTL + padding-right 21px + else + padding-left 21px margin 22px 0 // уравниваем с абзацными отступами > li @@ -105,8 +111,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 07a2bd7..a42289d 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 @@ -33,6 +44,10 @@ &_sidebar_on &__sidebar transform translateX(0) + &_frontpage + .main__header + margin 20px 0 -30px 0 + &__nav position fixed top 50% @@ -48,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 @@ -84,20 +102,32 @@ 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 + if isRTL + left 0 + else + right 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 @@ -110,7 +140,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 @@ -192,9 +225,13 @@ border-color: #7e7e7e &__nav_prev - padding-left 30px + 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/01-prism.styl b/modules/styles/blocks/prism/01-prism.styl index 3ad8d35..7b22b25 100755 --- a/modules/styles/blocks/prism/01-prism.styl +++ b/modules/styles/blocks/prism/01-prism.styl @@ -1,13 +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 - text-shadow 0 1px white +pre[class*="language-"], +pre[class*="language-"] > code, + color #d4d4d4 + // text-shadow 0 1px white font-family fixed_width_font direction ltr text-align left @@ -15,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? @@ -31,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, @@ -58,6 +60,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 @@ -95,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/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 + diff --git a/modules/styles/blocks/prism/03-prism-line-numbers.styl b/modules/styles/blocks/prism/03-prism-line-numbers.styl index e8b4828..6ba260a 100644 --- a/modules/styles/blocks/prism/03-prism-line-numbers.styl +++ b/modules/styles/blocks/prism/03-prism-line-numbers.styl @@ -1,14 +1,11 @@ pre.line-numbers - position relative - 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
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/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl b/modules/styles/blocks/sitetoolbar-light/sitetoolbar-light.styl
index f68c462..966ffb7 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..7c89267 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/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('/https/github.com/img/subscribe_bg.svg') 0 80% no-repeat
+    background #EBF6FF
     background-size cover
     border-radius 4px
 
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/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
diff --git a/modules/styles/blocks/toolbar/toolbar.styl b/modules/styles/blocks/toolbar/toolbar.styl
index e0cc790..d01ab49 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
@@ -39,4 +42,3 @@ toolbar_button_background = #c4c2c0
 
     &__button_edit::before
         @extend $font-edit
-
diff --git a/modules/styles/rtl.styl b/modules/styles/rtl.styl
new file mode 100644
index 0000000..6cff2f4
--- /dev/null
+++ b/modules/styles/rtl.styl
@@ -0,0 +1,3 @@
+if lang=='fa'
+  body
+    background white
diff --git a/modules/translate.js b/modules/translate.js
new file mode 100644
index 0000000..a2c335b
--- /dev/null
+++ b/modules/translate.js
@@ -0,0 +1,10 @@
+// stub for translate
+exports.TutorialStats = class Translate {
+  static instance() {
+    return new Translate();
+  }
+
+  isTranslated() {
+    return true;
+  }
+};
diff --git a/package.json b/package.json
index 1a7b898..4fc14b3 100755
--- a/package.json
+++ b/package.json
@@ -1,39 +1,44 @@
 {
-  "name": "javascript-nodejs",
-  "version": "1.0.0",
+  "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_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",
   "dependencies": {
-    "autoprefixer": "*",
+    "@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",
-    "css-loader": "^0",
-    "file-loader": "^1.1",
+    "copy-webpack-plugin": "^12.0.2",
+    "cross-env": "^7.0.3",
+    "csrf": "^3",
+    "css-loader": "^7.1.2",
+    "css-minimizer-webpack-plugin": "^7.0.0",
+    "execa": "^5.1.1",
     "fs-extra": "*",
-    "gm": "*",
-    "gulp": "^3",
-    "gulp-livereload": "*",
-    "gulp-ll": "*",
+    "gm": "^1",
+    "gulp": "^4",
+    "gulp-livereload": "^4",
+    "html-entities": "^1.3.1",
     "image-size": "*",
-    "js-yaml": "*",
+    "js-yaml": "^4",
     "json-loader": "^0.5.7",
     "koa": "^2",
     "koa-bodyparser": "^4",
@@ -42,47 +47,53 @@
     "koa-mount": "^3",
     "koa-router": "^7",
     "koa-static": "^5",
+    "limax": "^4.1.0",
     "lodash": "*",
-    "markdown-it": "*",
+    "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": "^8.8.1",
     "multiparty": "*",
     "mz": "*",
     "nib": "*",
     "node-notifier": "*",
     "node-uuid": "*",
     "node-zip": "*",
-    "nodemon": "^1.18.4",
-    "optimize-css-assets-webpack-plugin": "^4.0.3",
-    "path-to-regexp": "*",
-    "postcss-loader": "*",
+    "nodemon": "^3.1.7",
+    "path-to-regexp": "^6.2",
+    "postcss-loader": "^8.1.1",
     "prismjs": "^1",
     "pug": "^2.0.3",
     "pug-loader": "^2.4.0",
     "pug-runtime": "^2.0.4",
-    "request": "*",
-    "request-promise": "*",
-    "run-sequence": "*",
-    "rupture": "*",
-    "style-loader": "^0",
-    "stylus-loader": "^3",
+    "request": "^2.34",
+    "request-promise": "^4.2",
+    "rupture": "^0.7",
+    "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.17.2",
-    "yaml-loader": "^0.5.0"
+    "webpack": "^5.96.1",
+    "yaml-loader": "^0.8.1"
   },
   "engineStrict": true,
   "repository": {
     "type": "git",
-    "url": "https://github.com/iliakan/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/iliakan/javascript-tutorial-server/issues"
+    "url": "https://github.com/javascript-tutorial/server/issues"
+  },
+  "devDependencies": {
+    "mocha": "^7.1.1",
+    "should": "^13.2.3"
   }
 }
diff --git a/cache/.gitkeep b/repo/.gitkeep
similarity index 100%
rename from cache/.gitkeep
rename to repo/.gitkeep
diff --git a/tasks/livereload.js b/tasks/livereload.js
index ff06dad..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 = 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();
+  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);
+  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);
-  };
-};
+    chokidar.watch(options.watch, {
+      awaitWriteFinish: {
+        stabilityThreshold: 300,
+        pollInterval:       100
+      }
+    }).on('change', onChokidarChange);
 
+  }, 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 184e7d0..bea2aae 100755
--- a/tasks/server.js
+++ b/tasks/server.js
@@ -1,9 +1,7 @@
-let app = require('jsengine/koa/app');
+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..a31bafc 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) {
@@ -18,12 +17,12 @@ module.exports = function() {
       if (err) {
 
         notifier.notify({
-          message: err
+          message: err.message
         });
 
         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();
+      }
     });
+  });
 
-  };
 };
diff --git a/tmp/.gitkeep b/templates/blocks/profile-brief/index.pug
similarity index 100%
rename from tmp/.gitkeep
rename to templates/blocks/profile-brief/index.pug
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/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
diff --git a/templates/layouts/main.pug b/templates/layouts/main.pug
index be704c1..1cc6874 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
@@ -34,5 +36,3 @@ block main
           span.page__nav-text
             span.page__nav-text-shortcut
           span.page__nav-text-alternate= t('tutorial.article.lesson.next')
-
-