diff --git a/.gitignore b/.gitignore index 5756bac73..bc2d203d1 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.* *.diff *.err *.orig @@ -12,21 +11,31 @@ # OS or Editor folders .DS_Store +.idea .cache .project .settings .tmproj -nbproject +.nvmrc Thumbs.db # NPM packages folder. node_modules/ -# Brunch folder for temporary files. +# TMP folder (anything) tmp/ -# Brunch output folder. -www/ +# contains v8 executable for linux-tick-processor (run from project root) +out/* + +# Generated content +www/* +app/stylesheets/sprites/* # Bower stuff. bower_components/ + +# Passwords and other secret stuff +modules/config/secret.js +modules/config/certs/* + diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..5f3a922bb --- /dev/null +++ b/.jshintrc @@ -0,0 +1,15 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "-W004": true, + "-W030": true // for yield* ... +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..0eb41820e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +0.11 \ No newline at end of file diff --git a/README.md b/README.md index 83b578c01..c2d2bc254 100755 --- a/README.md +++ b/README.md @@ -1,46 +1,44 @@ -**Всем желающим предлагается поучаствовать в разработке новой версии сайта http://javascript.ru на Node.JS, Open Source on GitHub.** - -О проекте: - -* Это сайт по JavaScript и смежным технологиям (AJAX, COMET, Browser APIs...) -* Сайт достаточно большой и сложный. В новом проекте предусмотрены разделы: - * учебник (с генерацией PDF) - * вопрос-ответ - * тесты знаний - * онлайн-курсы - * справочник - * события - * работа -* Логин через соц. сети в том числе, личные сообщения и профиль. + +# Новый javascript.ru + +## powered by Node.js + +Всем привет! + +А это исходный код для нового движка сайта http://javascript.ru. + +## Что делаем? + +* Сайт по JavaScript и смежным технологиям (AJAX, COMET, Browser APIs...) * Сайт достаточно посещаемый: порядка 1-1.5 млн просмотров в месяц, и их станет больше при успешной реализации. -* Планируется перевод учебника на английский, после реализации на русском. -* Основная аудитория - разработчики, так что поддержка старых IE не нужна. Совсем. +* Сайт быстрый, при помощи Node.JS и правильного фронтенда он будет "летать". +* Сайт пока на русском, на английском сделаем потом. +* Сайт для разработчиков, да, кстати, они не пользуются старыми и страшными IE. + +С элементами SPA, но не SPA, так как должен хорошо индексироваться поисковиками, и вообще нафига козе баян. + +## Что в опен-сорсе? + +В опен-сорсе весь код, который будет заставлять двигаться эту штуку. +Многие модули из него можно взять и выделить в отдельные проекты, мы об этом подумаем, потом. + +Также в опен-сорсе - учебник JavaScript. Правда, это в другом репозитарии, здесь только код. + +## ♡ + +Пишите в issues, если есть о чём. + + -Так как сайт должен хорошо индексироваться поисковиками, он будет состоять из страниц с переходом между ними, не SPA. Хотя в различных интерфейсах элементы SPA приветствуются. -Мы будем стараться, чтобы сайт работал как можно быстрее. Это означает параллельные запросы к БД и кеширование на сервере и, по возможности, плавную инициализацию на клиенте. -Сейчас есть существенная часть дизайна и его вёрстка в HTML/SASS. -Общий стиль вы можете посмотреть здесь: https://www.dropbox.com/s/mo6yx0ct9rrzic4/Learn_Home.png. -RoadMap: -* Определиться с архитектурой проекта, технологиями. -* Реализовать профиль посетителя, логин через соц. сети, с заглушкой на title-page. -* Реализовать показ учебника и навигацию по нему, древовидные комментарии с оценками, подгрузкой. -* Сделать покупку PDF учебника (оформление, приём оплаты, почтовое уведомление, скачивание). -Это примерно соответствует текущему http://learn.javascript.ru. Когда закончим -- будет первый релиз, вместо старого learn.javascript.ru. -Далее или, если будет возможность, параллельно, реализуем вопрос-ответ, справочник, тесты знаний. -Обсуждение происходит в чате Node.JS (Skype), собрание сегодня 24.06.2014 в 11:00 **GMT+2**. -Если не можете войти - напишите мне в Skype, ник: "ilya.a.kantor". -Code Style: - * https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml - * `use strict` diff --git a/app.js b/app.js deleted file mode 100755 index 0c6865fac..000000000 --- a/app.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; - -const log = require('lib/log')(module); -const config = require('config'); - -const koa = require('koa'); - -require('models'); - -const app = koa(); - -require('setup/static')(app); - -require('setup/errors')(app); - -require('setup/logger')(app); -require('setup/bodyParser')(app); -require('setup/session')(app); -require('setup/render')(app); -require('setup/router')(app); - -require('./routes')(app); - -module.exports = app; \ No newline at end of file diff --git a/app/.jshintrc b/app/.jshintrc new file mode 100644 index 000000000..4b274c1ee --- /dev/null +++ b/app/.jshintrc @@ -0,0 +1,15 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": true, + "node": true, // for browserify require etc + "globals": ["$", "Prism"], + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "multistr": true, + "noyield": true, + "devel": true, + "-W004": true +} diff --git a/app/fonts/icons/icons.eot b/app/fonts/icons/icons.eot new file mode 100644 index 000000000..9a4a5a8c9 Binary files /dev/null and b/app/fonts/icons/icons.eot differ diff --git a/app/fonts/icons/icons.svg b/app/fonts/icons/icons.svg new file mode 100644 index 000000000..5e8c371de --- /dev/null +++ b/app/fonts/icons/icons.svg @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/app/fonts/icons/icons.ttf b/app/fonts/icons/icons.ttf new file mode 100644 index 000000000..5ef3ad0a4 Binary files /dev/null and b/app/fonts/icons/icons.ttf differ diff --git a/app/fonts/icons/icons.woff b/app/fonts/icons/icons.woff new file mode 100644 index 000000000..a4b2f9291 Binary files /dev/null and b/app/fonts/icons/icons.woff differ diff --git a/app/fonts/rur/alsrubl-verdana-regular.eot b/app/fonts/rur/alsrubl-verdana-regular.eot new file mode 100644 index 000000000..bea8a2db9 Binary files /dev/null and b/app/fonts/rur/alsrubl-verdana-regular.eot differ diff --git a/app/fonts/rur/alsrubl-verdana-regular.svg b/app/fonts/rur/alsrubl-verdana-regular.svg new file mode 100644 index 000000000..41756ecff --- /dev/null +++ b/app/fonts/rur/alsrubl-verdana-regular.svg @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/app/fonts/rur/alsrubl-verdana-regular.ttf b/app/fonts/rur/alsrubl-verdana-regular.ttf new file mode 100644 index 000000000..07696f01a Binary files /dev/null and b/app/fonts/rur/alsrubl-verdana-regular.ttf differ diff --git a/app/fonts/rur/alsrubl-verdana-regular.woff b/app/fonts/rur/alsrubl-verdana-regular.woff new file mode 100644 index 000000000..45fbe0d7b Binary files /dev/null and b/app/fonts/rur/alsrubl-verdana-regular.woff differ diff --git a/app/fonts/rur/stylesheet.css b/app/fonts/rur/stylesheet.css new file mode 100644 index 000000000..215f95fcc --- /dev/null +++ b/app/fonts/rur/stylesheet.css @@ -0,0 +1,43 @@ +/* Generated by Font Squirrel, (http://fontsquirrel.com/) */ +/* Compiled by Artem Polikarpov, Artem Gorbunov Design Bureau (http://artgorbunov.ru/) */ + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-regular.eot'); + src: url('alsrubl-verdana-regular.eot?#iefix') format('embedded-opentype'),url('alsrubl-verdana-regular.woff') format('woff'), url('alsrubl-verdana-regular.ttf') format('truetype'), url('alsrubl-verdana-regular.svg#ALSRublRegular') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-italic.eot'); + src: url('alsrubl-verdana-italic.eot?#iefix') format('embedded-opentype'), url('alsrubl-verdana-italic.woff') format('woff'), url('alsrubl-verdana-italic.ttf') format('truetype'), url('alsrubl-verdana-italic.svg#ALSRublItalic') format('svg'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-bold.eot'); + src: url('alsrubl-verdana-bold.eot?#iefix') format('embedded-opentype'), url('alsrubl-verdana-bold.woff') format('woff'), url('alsrubl-verdana-bold.ttf') format('truetype'), url('alsrubl-verdana-bold.svg#ALSRublBold') format('svg'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'ALSRubl-Verdana'; + src: url('alsrubl-verdana-bolditalic.eot'); + src: url('alsrubl-verdana-bolditalic.eot?#iefix') format('embedded-opentype'), url('alsrubl-verdana-bolditalic.woff') format('woff'), url('alsrubl-verdana-bolditalic.ttf') format('truetype'), url('alsrubl-verdana-bolditalic.svg#ALSRublBoldItalic') format('svg'); + font-weight: bold; + font-style: italic; +} + +/* + ХТМЛ: 100 Р + Если вам нужен рубль в нескольких шрифтах, используйте .b-rub_verdana, вместо .b-rub +*/ +.b-rub, .b-rub_verdana { + font-family: 'ALSRubl-Verdana', Verdana, sans-serif; + line-height: normal; +} \ No newline at end of file diff --git a/app/fonts/rur/test.html b/app/fonts/rur/test.html new file mode 100644 index 000000000..6a74b01a4 --- /dev/null +++ b/app/fonts/rur/test.html @@ -0,0 +1,73 @@ + + +
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+ +100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+100 500 Р
+");
+
+ Prism.highlightElement(this.firstChild);
+
+ addLineNumbers(this);
+ addBlockHighlight(this);
+ addInlineHighlight(this);
+ new CodeBox(this);
+ });
+
+
+ });
+
+ $(function() {
+ $('iframe.result__iframe').each(function() {
+ new IframeBox(this);
+ })
+ });
+
+}();
diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl
new file mode 100755
index 000000000..06862ef22
--- /dev/null
+++ b/app/stylesheets/base.styl
@@ -0,0 +1,147 @@
+// import all from nib except
+// -flex
+// -vendor
+// -iconic
+// -reset
+@import 'nib/border'
+@import 'nib/clearfix'
+@import 'nib/color-image'
+@import 'nib/gradients'
+@import 'nib/image'
+@import 'nib/overflow'
+@import 'nib/positions'
+@import 'nib/text'
+@import 'nib/size'
+
+@require "sprites/*"
+@require "blocks/variables/variables"
+@require "blocks/reset/reset"
+@require "blocks/placeholders/*"
+@require "blocks/mixins/*"
+
+@require "blocks/login/login"
+@require "blocks/progress/progress"
+@require "blocks/body/body"
+@require "blocks/top-part/top-part"
+@require "blocks/logo/logo"
+@require "blocks/page/page"
+@require "blocks/links/links"
+@require "blocks/navbar/navbar"
+@require "blocks/nav-dropdown/nav-dropdown"
+@require "blocks/main-content/main-content"
+@require "blocks/main/main"
+@require "blocks/sidebar/sidebar"
+// @require "blocks/sidehelper/sidehelper"
+// @require "blocks/page-contents/page-contents"
+@require "blocks/breadcrumbs/breadcrumbs"
+@require "blocks/important/important"
+
+@require "blocks/summary/summary"
+@require "blocks/shortcut/shortcut"
+@require "blocks/spoiler/spoiler"
+
+@require "blocks/dropdown/dropdown"
+
+@require "blocks/toolbar/toolbar"
+@require "blocks/codebox/codebox"
+@require "blocks/result/result"
+@require "blocks/code-example/code-example"
+// @require "blocks/complex-code/complex-code"
+@require "blocks/balance/balance"
+// @require "blocks/rating/rating"
+@require "blocks/comments/comments"
+// @require "blocks/submit-button/submit-button"
+// @require "blocks/secondary-button/secondary-button"
+@require "blocks/page-footer/page-footer"
+@require "blocks/soc-icon/soc-icon"
+@require "blocks/social/social"
+// @require "blocks/user/user"
+// @require "blocks/user-menu/user-menu"
+// @require "blocks/primary-tabs/primary-tabs"
+// @require "blocks/pager/pager"
+// @require "blocks/disqus/disqus"
+@require "blocks/standard-table/standard-table"
+@require "blocks/prev-next/prev-next"
+@require "blocks/corrector/corrector"
+// @require "blocks/course-type/course-type"
+// @require "blocks/currency/currency"
+// @require "blocks/lessons/lessons"
+// @require "blocks/category-foot/category-foot"
+// @require "blocks/text-input/text-input"
+// @require "blocks/text-marked/text-marked"
+// @require "blocks/input-select/input-select"
+// @require "blocks/textarea-input/textarea-input"
+// @require "blocks/modal-form/modal-form"
+// @require "blocks/social-login/social-login"
+// @require "blocks/switch-input/switch-input"
+// @require "blocks/profile/profile"
+// @require "blocks/profile-insert/profile-insert"
+// @require "blocks/select2/select2"
+// @require "blocks/full-phone/full-phone"
+// @require "blocks/confirm/confirm"
+// @require "blocks/social-link/social-link"
+// @require "blocks/text-compact-label/text-compact-label"
+// @require "blocks/course-search/course-search"
+// @require "blocks/header-note/header-note"
+// @require "blocks/columns/columns"
+// @require "blocks/h2/h2"
+// @require "blocks/nowrap/nowrap"
+// @require "blocks/highlight/highlight"
+// @require "blocks/strong/strong"
+// @require "blocks/bricks/bricks"
+// @require "blocks/nodejs-link/nodejs-link"
+// @require "blocks/highlights/highlights"
+// @require "blocks/presenter-programs/presenter-programs"
+// @require "blocks/presenter/presenter"
+// @require "blocks/large-list/large-list"
+// @require "blocks/slider/slider"
+// @require "blocks/reviews/reviews"
+// @require "blocks/tabs/tabs"
+// @require "blocks/large-tabs/large-tabs"
+// @require "blocks/accordion/accordion"
+// @require "blocks/large-accordion/large-accordion"
+// @require "blocks/sidebyside/sidebyside"
+// @require "blocks/group-list/group-list"
+// @require "blocks/group/group"
+// @require "blocks/special-links-list/special-links-list"
+// @require "blocks/flag/flag"
+// @require "blocks/number-input/number-input"
+// @require "blocks/close-button/close-button"
+// @require "blocks/price/price"
+// @require "blocks/extract/extract"
+// @require "blocks/material/material"
+// @require "blocks/form-insert/form-insert"
+// @require "blocks/order-confirm/order-confirm"
+// @require "blocks/grayed-list/grayed-list"
+// @require "blocks/complex-form/complex-form"
+// @require "blocks/order-form/order-form"
+// @require "blocks/contact-form/contact-form"
+// @require "blocks/pay-form/pay-form"
+// @require "blocks/pay-method/pay-method"
+// @require "blocks/book-form/book-form"
+// @require "blocks/receipts/receipts"
+// @require "blocks/pay-hint/pay-hint"
+// @require "blocks/ol/ol"
+// @require "blocks/ul/ul"
+// @require "blocks/course-start-notify/course-start-notify"
+// @require "blocks/course-start-subscribe/course-start-subscribe"
+@require "blocks/hide/hide"
+// @require "blocks/error/error"
+// @require "blocks/redirect/redirect"
+// @require "blocks/search-query/search-query"
+// @require "blocks/path/path"
+// @require "blocks/search-results/search-results"
+// @require "blocks/search-notfound/search-notfound"
+// @require "blocks/tests-list/tests-list"
+// @require "blocks/task/task"
+
+// @require "blocks/mibbit-irc/mibbit-irc"
+@require "blocks/prism/prism"
+@require "blocks/prism/prism-line-highlight"
+@require "blocks/prism/prism-line-numbers"
+@require "blocks/prism/my-prism"
+// @require "blocks/fancybox/jquery.fancybox"
+
+
+// @require "jquery-modal/jquery.modal"
+
diff --git a/app/stylesheets/blocks/balance/balance.styl b/app/stylesheets/blocks/balance/balance.styl
new file mode 100644
index 000000000..ea9b0cd7a
--- /dev/null
+++ b/app/stylesheets/blocks/balance/balance.styl
@@ -0,0 +1,125 @@
+
+.balance
+ position relative
+ margin 38px -1*content_horizontal_padding
+
+ &__content
+ display: table;
+ width: 100%;
+
+ &__title
+ margin 0 0 20px
+ text-align center
+
+ & &__list
+ padding-left 25px
+
+ // convert to class .balance__list-item
+ & &__list > li
+ margin 1em 0 1em
+
+ & &__list > li::before
+ position absolute
+ margin-left -25px
+
+ &__pluses,
+ &__minuses
+ display table-cell
+ width 50%
+
+ &__pluses
+ padding 20px 35px 20px 40px
+ background: #D7F1B9;
+
+ &__pluses &__title
+ color #004010
+
+ & &__pluses ul&__list > li::before
+ @extend $font-pros
+ color #238C00
+
+ &__minuses
+ padding 20px 40px 20px 35px
+ background #FFCFBF
+
+ &__minuses &__title
+ color #401000
+
+ & &__minuses ul&__list > li::before
+ @extend $font-cons;
+ color #B20000
+
+ &_single
+ margin 20px -1*content_horizontal_padding 30px
+
+ &_single &__content
+ width 100%
+
+ &_single &__title
+ text-align left
+
+ &_single &__pluses,
+ &_single &__minuses
+ padding 20px content_horizontal_padding
+ width auto
+
+ &::before
+ size-of ('scales.png')
+ background url('/img/scales.png')
+ margin-left (image-width('scales.png') / -2)
+ content ""
+ position absolute
+ top -3px
+ left 50%
+
+ &_single::before
+ display none
+
+ @media (min-width: media_step_1)
+ margin: 38px -1*(content_horizontal_padding + 10px)
+
+ &__pluses
+ padding 20px 35px 20px (content_horizontal_padding + 10px)
+
+ &__minuses
+ padding 20px (content_horizontal_padding + 10px) 20px 35px
+
+ &_single
+ margin 20px -1*(content_horizontal_padding + 10px) 30px
+
+ &_single &__pluses,
+ &_single &__minuses
+ padding 20px (content_horizontal_padding + 10px)
+
+ @media (min-width: media_step_2)
+ margin 38px -1*(content_horizontal_padding + 20px)
+
+ &__pluses
+ padding 20px 35px 20px (content_horizontal_padding + 20px)
+
+ &__minuses
+ padding 20px (content_horizontal_padding + 20px) 20px 35px
+
+ &_single
+ margin 20px -1*(content_horizontal_padding + 20px) 30px
+
+ &_single &__pluses,
+ &_single &__minuses
+ padding: 20px (content_horizontal_padding + 20px)
+
+
+ @media (min-width: media_step_3)
+ margin 38px -1*(content_horizontal_padding + 30px)
+
+ &__pluses
+ padding 20px 35px 20px (content_horizontal_padding + 30px)
+
+ &__minuses
+ padding 20px (content_horizontal_padding + 30px) 20px 35px
+
+ &_single
+ margin 20px -1*(content_horizontal_padding + 30px) 30px
+
+ &_single &__pluses,
+ &_single &__minuses
+ padding: 20px (content_horizontal_padding + 30px)
diff --git a/app/stylesheets/blocks/balance/scales.png b/app/stylesheets/blocks/balance/scales.png
new file mode 100644
index 000000000..9e774bf4b
Binary files /dev/null and b/app/stylesheets/blocks/balance/scales.png differ
diff --git a/app/stylesheets/blocks/body/body.styl b/app/stylesheets/blocks/body/body.styl
new file mode 100644
index 000000000..5f248488d
--- /dev/null
+++ b/app/stylesheets/blocks/body/body.styl
@@ -0,0 +1,27 @@
+body
+ @extend $center
+ @extend $min-max-width
+
+ font size/lineheight font
+ -webkit-text-size-adjust none
+ padding 0 60px
+ color color
+ background page_background
+
+.body_redirect
+ background #fff
+
+
+@media (min-width: media_step_1)
+ body
+ padding 0 70px
+
+@media (min-width: media_step_2)
+ body
+ padding 0 80px
+
+
+@media (min-width: media_step_3)
+ body
+ padding 0 90px
+
diff --git a/app/stylesheets/blocks/breadcrumbs/breadcrumbs.styl b/app/stylesheets/blocks/breadcrumbs/breadcrumbs.styl
new file mode 100644
index 000000000..d3720f667
--- /dev/null
+++ b/app/stylesheets/blocks/breadcrumbs/breadcrumbs.styl
@@ -0,0 +1,23 @@
+.breadcrumbs
+ list-style none
+ font-size 13px
+ margin 0
+ padding 0
+ color #777
+
+ & &__item
+ display inline-block
+ margin 0 5px 0 0
+
+ &::after
+ content " →"
+ color #666
+
+ &:last-child::after
+ display none
+
+ &__link
+ color link_color
+
+ &__link:hover
+ color link_hover_color
diff --git a/app/stylesheets/blocks/code-example/code-example.styl b/app/stylesheets/blocks/code-example/code-example.styl
new file mode 100644
index 000000000..6e38bf570
--- /dev/null
+++ b/app/stylesheets/blocks/code-example/code-example.styl
@@ -0,0 +1,10 @@
+.code-example
+ margin 1.5em 0
+
+ &__codebox,
+ &__codebox .codebox,
+ &__codebox pre[class*="language-"]
+ margin-bottom 0
+
+ &__result
+ margin-top 0
diff --git a/app/stylesheets/blocks/codebox/codebox.styl b/app/stylesheets/blocks/codebox/codebox.styl
new file mode 100644
index 000000000..b77dc19c8
--- /dev/null
+++ b/app/stylesheets/blocks/codebox/codebox.styl
@@ -0,0 +1,14 @@
+.codebox
+ position relative
+ margin 1.5em 0
+ overflow auto
+
+.codebox__toolbar
+ position absolute
+ top 0
+ right 0
+ z-index 9
+ opacity 0.8
+
+.codebox__code
+ width 100%
diff --git a/app/stylesheets/blocks/comments/comments.styl b/app/stylesheets/blocks/comments/comments.styl
new file mode 100644
index 000000000..5f8177299
--- /dev/null
+++ b/app/stylesheets/blocks/comments/comments.styl
@@ -0,0 +1,210 @@
+comment-reply-color = #00A3D9
+.comments
+ margin-top 15px
+
+ & &__header-title
+ border-bottom 0
+ display inline
+ margin-right 16px
+ color color
+ font 30px/36px secondary_font
+
+ &__header-title::before
+ @extend $font-comment
+ font-size 80%
+ color #eee
+ margin-right 9px
+
+ &__header-number
+ color #aaa
+
+ &__header-number::before
+ content "("
+
+ &__header-number::after
+ content ")"
+
+ &__header-write:link
+ @extend $pseudo
+ color #666
+ font-size 12px
+ vertical-align .4em
+ position relative
+ display inline-block
+ line-height 1
+
+ &__header-write::after
+ content "↓"
+ position absolute
+ margin-left 3px
+ vertical-align middle
+
+// .comments__items {
+// overflow-x: hidden;
+// margin: 25px -#{$content-horizontal-padding} 0;
+// padding: 0 $content-horizontal-padding;
+// border-bottom: 1px solid $separator-color;
+// }
+
+// .comments__comment {
+// font-size: 92%;
+// line-height: 130%;
+// position: relative;
+// padding-top: 24px;
+// }
+
+// .comments__comment::before {
+// content: "";
+// position: absolute;
+// top: 0;
+// left: -200px;
+// width: 1500px;
+// height: 0;
+// border-bottom: 1px solid $separator-color;
+// }
+
+// .comments__comment .comments__comment {
+// padding-left: 20px;
+// }
+
+// // Don't increase indentation after 4th level
+// .comments__comment
+// .comments__comment
+// .comments__comment
+// .comments__comment
+// .comments__comment {
+// padding-left: 0;
+// }
+
+// .comments__comment-header {
+// @extend %clearfix;
+// border-bottom: 0;
+// padding-bottom: 0;
+// margin-bottom: $lineheight*.65;
+// }
+
+// .comments .comments__username {
+// color: #444;
+// }
+
+// .comments .comments__username:hover,
+// .comments .comments__anchor:hover {
+// color: $link-hover-color;
+// }
+
+// .comments__userpic {
+// float: left;
+// }
+
+// .comments .comments__userpic {
+// margin: 0;
+// }
+
+// .comments__rating {
+// float: right;
+// }
+
+// .comments__info {
+// padding: 0 12px;
+// overflow: auto;
+// }
+
+// .comments__date::before {
+// display: none;
+// }
+
+// .comments__date,
+// .comments .comments__anchor {
+// color: #888;
+// font: 11px Arial, Helvetica, sans-serif;
+// margin-right: 5px;
+// }
+
+// .comments__star::before {
+// @extend %font-star;
+// color: #bbb;
+// }
+
+// .comments__comment pre {
+// font: 12px $fixed-width-font;
+// color: #400000;
+// }
+
+// .comments__footer {
+// margin: 12px 0 22px;
+// }
+
+// .comments .comments__reply {
+// @extend %button-reset;
+
+// color: $comment-reply-color;
+// cursor: pointer;
+// text-decoration: none; // Тут может быть и ссылка, если будет якорь, или кнопка — если инлайн-форма
+// border-bottom: 1px dashed transparent;
+// font-size: 13px;
+// }
+
+// .comments__reply:hover {
+// color: $link-hover-color;
+// border-bottom: 1px dashed transparentize($link-hover-color, .7);
+// }
+
+// .comments__form {
+// margin: 33px 0;
+// }
+
+// .comments__form-header {
+// @extend %clearfix;
+// margin-bottom: 25px;
+// }
+
+// .comments .comments__form-title {
+// color: #888;
+// margin: 0;
+// float: left;
+// }
+// .comments__form-title::before {
+// @extend %font-comment;
+// color: #ccc;
+// margin-right: 15px;
+// }
+// .comments .comments__form-formatting {
+// @extend %pseudo;
+// float: right;
+// font-size: 12px;
+// line-height: 1;
+// margin-top: 8px;
+// color: #656565;
+// }
+// .comments .comments__form-formatting:hover {
+// color: $link-hover-color;
+// }
+// .comment-form__text {
+
+// width: 100%;
+// height: 144px;
+// margin: 16px 0;
+// display: block;
+// clear: both;
+// }
+
+// @media (min-width: $media-step-1) {
+// .comments__items {
+// margin: 23px -#{$content-horizontal-padding + 10px};
+// padding: 0 ($content-horizontal-padding + 10px);
+// }
+// }
+
+// @media (min-width: $media-step-2) {
+// .comments__items {
+// margin: 23px -#{$content-horizontal-padding + 20px};
+// padding: 0 ($content-horizontal-padding + 20px);
+// }
+// }
+
+// @media (min-width: $media-step-3) {
+// .comments__items {
+// margin: 23px -#{$content-horizontal-padding + 30px};
+// padding: 0 ($content-horizontal-padding + 30px);
+// }
+// }
\ No newline at end of file
diff --git a/app/stylesheets/blocks/corrector/corrector.styl b/app/stylesheets/blocks/corrector/corrector.styl
new file mode 100644
index 000000000..38bc3b5a3
--- /dev/null
+++ b/app/stylesheets/blocks/corrector/corrector.styl
@@ -0,0 +1,4 @@
+.corrector
+ padding 18px 0
+ color #999
+ font-style italic
\ No newline at end of file
diff --git a/app/stylesheets/blocks/dropdown/dropdown.styl b/app/stylesheets/blocks/dropdown/dropdown.styl
new file mode 100644
index 000000000..de065a1a9
--- /dev/null
+++ b/app/stylesheets/blocks/dropdown/dropdown.styl
@@ -0,0 +1,2 @@
+.dropdown .dropdown__content
+ display none
diff --git a/app/stylesheets/blocks/hide/hide.styl b/app/stylesheets/blocks/hide/hide.styl
new file mode 100644
index 000000000..40734c21d
--- /dev/null
+++ b/app/stylesheets/blocks/hide/hide.styl
@@ -0,0 +1,29 @@
+.hide-open,
+.hide-closed
+ margin round(lineheight*.65) 0
+
+.hide-closed .hide-content
+ display none
+
+.hide-open .hide-link,
+.hide-closed .hide-link,
+.hide-open .hide-link u,
+.hide-closed .hide-link u
+ text-decoration none
+ color $link-color
+
+.hide-link:hover u
+ border-bottom 1px dashed
+
+.hide-link code
+ font inherit
+ color inherit
+
+.hide-link::after
+ margin-left 8px
+
+.hide-closed .hide-link::after
+ @extend $font-open
+
+.hide-open .hide-link::after
+ @extend $font-close
\ No newline at end of file
diff --git a/app/stylesheets/blocks/important/important.sprite/info.png b/app/stylesheets/blocks/important/important.sprite/info.png
new file mode 100644
index 000000000..17236c791
Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/info.png differ
diff --git a/app/stylesheets/blocks/important/important.sprite/ok.png b/app/stylesheets/blocks/important/important.sprite/ok.png
new file mode 100644
index 000000000..073ddb754
Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/ok.png differ
diff --git a/app/stylesheets/blocks/important/important.sprite/question.png b/app/stylesheets/blocks/important/important.sprite/question.png
new file mode 100644
index 000000000..c53409bd6
Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/question.png differ
diff --git a/app/stylesheets/blocks/important/important.sprite/warning.png b/app/stylesheets/blocks/important/important.sprite/warning.png
new file mode 100644
index 000000000..ce30b1f20
Binary files /dev/null and b/app/stylesheets/blocks/important/important.sprite/warning.png differ
diff --git a/app/stylesheets/blocks/important/important.styl b/app/stylesheets/blocks/important/important.styl
new file mode 100644
index 000000000..62bdd8048
--- /dev/null
+++ b/app/stylesheets/blocks/important/important.styl
@@ -0,0 +1,87 @@
+// $important-icon: sprite-map("important-icons/*.png");
+
+.important
+ margin 18px 0
+ border 3px solid #F5F2F0
+
+ &__header
+ margin 0
+ padding 21px 25px 0 55px
+ border none
+
+ &__type
+ font-weight: 700;
+ font-size: 17px;
+
+ &__type::before
+ content ""
+ spriteImage $important-ok
+ position absolute
+ margin-left -30px
+ margin-top 1px
+
+ &__title
+ display inline
+ margin 0
+
+ a, a u
+ color inherit
+ text-decoration none
+
+ a:hover u
+ text-decoration underline
+
+ & &__task-link
+ @extend $plain-link
+
+ &__task-link
+ margin-right 40px
+
+ &__task-link_empty
+ margin-right 0
+
+ &__task-link::after
+ @extend $font-external
+ position absolute
+ margin-left 5px
+ font-size 14px
+ margin-top 2px
+ color #005DB3
+ font-weight 400
+
+ &__importance
+ font-size 11px
+ color #84836d
+
+ &__task-link_empty + &__importance
+ margin-left 40px
+
+ &__content
+ margin 12px 25px 22px
+
+ & > .spoiler
+ border 0
+ background none
+ margin 15px 0
+ padding 10px 20px 10px 0
+
+ & > .spoiler + .spoiler
+ margin-top -15px
+
+ & .spoiler__button
+ color #747361
+
+ &_warn &__type::before
+ spriteElem $important-warning
+
+ &_smart &__type::before
+ spriteElem $important-info
+
+ &_ponder &__type::before
+ spriteElem $important-question
+
+ &_ok &__type::before
+ spriteElem $important-ok
+
+ @media print
+ page-break-inside avoid
diff --git a/app/stylesheets/blocks/links/link-types/doc.png b/app/stylesheets/blocks/links/link-types/doc.png
new file mode 100644
index 000000000..ad02369dc
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/doc.png differ
diff --git a/app/stylesheets/blocks/links/link-types/ecma.png b/app/stylesheets/blocks/links/link-types/ecma.png
new file mode 100644
index 000000000..319fa6a12
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/ecma.png differ
diff --git a/app/stylesheets/blocks/links/link-types/mailto.png b/app/stylesheets/blocks/links/link-types/mailto.png
new file mode 100644
index 000000000..9cddd1d13
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/mailto.png differ
diff --git a/app/stylesheets/blocks/links/link-types/mdn.png b/app/stylesheets/blocks/links/link-types/mdn.png
new file mode 100644
index 000000000..e4d6c9f2c
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/mdn.png differ
diff --git a/app/stylesheets/blocks/links/link-types/msdn.png b/app/stylesheets/blocks/links/link-types/msdn.png
new file mode 100644
index 000000000..27b86de7d
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/msdn.png differ
diff --git a/app/stylesheets/blocks/links/link-types/newwindow.png b/app/stylesheets/blocks/links/link-types/newwindow.png
new file mode 100644
index 000000000..38521c4d9
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/newwindow.png differ
diff --git a/app/stylesheets/blocks/links/link-types/pdf.png b/app/stylesheets/blocks/links/link-types/pdf.png
new file mode 100644
index 000000000..f5be21a11
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/pdf.png differ
diff --git a/app/stylesheets/blocks/links/link-types/sandbox.png b/app/stylesheets/blocks/links/link-types/sandbox.png
new file mode 100644
index 000000000..095527198
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/sandbox.png differ
diff --git a/app/stylesheets/blocks/links/link-types/w3c.png b/app/stylesheets/blocks/links/link-types/w3c.png
new file mode 100644
index 000000000..dfcd3d10f
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/w3c.png differ
diff --git a/app/stylesheets/blocks/links/link-types/wiki.png b/app/stylesheets/blocks/links/link-types/wiki.png
new file mode 100644
index 000000000..b9857a9cc
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/wiki.png differ
diff --git a/app/stylesheets/blocks/links/link-types/xls.png b/app/stylesheets/blocks/links/link-types/xls.png
new file mode 100644
index 000000000..ef3c95a3c
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/xls.png differ
diff --git a/app/stylesheets/blocks/links/link-types/zip.png b/app/stylesheets/blocks/links/link-types/zip.png
new file mode 100644
index 000000000..a53b7d7f0
Binary files /dev/null and b/app/stylesheets/blocks/links/link-types/zip.png differ
diff --git a/app/stylesheets/blocks/links/links.styl b/app/stylesheets/blocks/links/links.styl
new file mode 100644
index 000000000..d191a1e73
--- /dev/null
+++ b/app/stylesheets/blocks/links/links.styl
@@ -0,0 +1,143 @@
+link-type(type)
+ padding-right image-width('link-types/' + type + '.png') + 3
+ background-image url('/img/link-types/' + type + '.png')
+
+$pseudo
+ text-decoration none
+ border-bottom 1px dashed transparent
+ &:hover
+ border-bottom 1px dashed
+ text-decoration none
+
+$link-type
+ background-position 100% 60%
+ background-repeat no-repeat
+
+// .pseudo is for any tag but link that should look like pseudo-link
+.pseudo
+ @extend $pseudo
+ cursor pointer
+ color link_color
+
+.pseudo:hover
+ color link_hover_color
+
+:link
+ color link_color
+
+:visited
+ color link_visited_color
+
+@media (print)
+ a:visited
+ color link_color
+
+:link,
+:visited
+ text-decoration none
+
+a:hover,
+a:active
+ color link_hover_color
+ text-decoration underline
+
+.anchor,
+.pseudo
+ @extend $pseudo
+
+.link-ref
+ vertical-align super
+ font-size 90%
+
+// --
+.main [href^="mailto:"],
+a.mailto
+ @extend $link-type
+ link-type 'mailto'
+
+// --
+.main [target="_blank"],
+a.external
+ @extend $link-type
+ link-type 'newwindow'
+
+// --
+.main [href^='/play/'],
+.main [href^='http://plnkr.co/'],
+a.sandbox
+ @extend $link-type
+ link-type 'sandbox'
+
+// --
+.main [href$=".doc"],
+.main [href$=".docx"],
+a.doc
+ @extend $link-type
+ link-type 'doc'
+
+// --
+.main [href$=".zip"],
+a.zip
+ @extend $link-type
+ link-type 'zip'
+
+// --
+.main [href$=".xls"],
+.main [href$=".xlsx"],
+a.xls
+ @extend $link-type
+ link-type 'xls'
+
+// --
+.main [href$=".pdf"],
+a.pdf
+ @extend $link-type
+ link-type 'pdf'
+
+// --
+.main [href^='http://developer.mozilla.org'],
+.main [href^='https://developer.mozilla.org'],
+a.mdn
+ @extend $link-type
+ link-type 'mdn'
+
+// --
+.main [href^='http://msdn.microsoft.com'],
+.main [href^='https://msdn.microsoft.com'],
+a.msdn
+ @extend $link-type
+ link-type 'msdn'
+
+// --
+.main [href^='http://wikipedia.org'],
+.main [href*='wikipedia.org'],
+a.wiki
+ @extend $link-type
+ link-type 'wiki'
+
+// --
+.main [href^='http://w3.org'],
+.main [href^='http://dev.w3.org'],
+.main [href^='http://www.w3.org'],
+.main [href^='https://www.w3.org'],
+.main [href^='https://w3.org'],
+a.w3c
+ @extend $link-type
+ link-type 'w3c'
+
+// --
+.main [href^='http://es5.github.com'],
+a.ecma
+ @extend $link-type
+ link-type 'ecma'
+// --
+
+// placeholder selector, can't be moved to the top
+// because order matters
+// decared as placeholder to be extended easier
+$plain-link
+ padding 0
+ background-image none
+
+.main .plain
+ @extend $plain-link
diff --git a/app/stylesheets/blocks/login/login.styl b/app/stylesheets/blocks/login/login.styl
new file mode 100644
index 000000000..f964e3c67
--- /dev/null
+++ b/app/stylesheets/blocks/login/login.styl
@@ -0,0 +1,15 @@
+
+.auth-form
+ fixed top 50% left 50%
+ width 400px
+ height 400px
+ margin-left -(@width / 2)
+ margin-top -(@height / 2)
+ background white
+ border 1px solid black
+ z-index 10000
+
+.auth-form .progress
+ margin: 100px auto 0 auto
+
+
diff --git a/app/stylesheets/blocks/logo/logo-light.png b/app/stylesheets/blocks/logo/logo-light.png
new file mode 100644
index 000000000..58cb6048a
Binary files /dev/null and b/app/stylesheets/blocks/logo/logo-light.png differ
diff --git a/app/stylesheets/blocks/logo/logo.png b/app/stylesheets/blocks/logo/logo.png
new file mode 100644
index 000000000..eae73179d
Binary files /dev/null and b/app/stylesheets/blocks/logo/logo.png differ
diff --git a/app/stylesheets/blocks/logo/logo.styl b/app/stylesheets/blocks/logo/logo.styl
new file mode 100644
index 000000000..d76222fcc
--- /dev/null
+++ b/app/stylesheets/blocks/logo/logo.styl
@@ -0,0 +1,18 @@
+////////// Блок из костылей и подпорок, все равно его не обновить //////////
+////////// простой заменой картинки, при редизайне - переделывать //////////
+.logo
+ width 500px
+ height image-height('logo-light.png') - 2px
+ background url('/img/logo-light.png') 25px 50% no-repeat
+ position relative
+ z-index 99
+ padding 1px
+
+ a, img
+ image-size('logo.png')
+ @extend $center
+ display block
+ margin 19px 0 0 100px
+
+ a img
+ margin 0
diff --git a/app/stylesheets/blocks/main-content/main-content.styl b/app/stylesheets/blocks/main-content/main-content.styl
new file mode 100644
index 000000000..c330275db
--- /dev/null
+++ b/app/stylesheets/blocks/main-content/main-content.styl
@@ -0,0 +1,4 @@
+.main-content
+ display table
+ width 100%
+ table-layout fixed
diff --git a/app/stylesheets/blocks/main/main.styl b/app/stylesheets/blocks/main/main.styl
new file mode 100644
index 000000000..6b9ccab8b
--- /dev/null
+++ b/app/stylesheets/blocks/main/main.styl
@@ -0,0 +1,287 @@
+// we use placeholder selector to make it easier to rename the block
+// we can't extend selectors with &, so we don't use them for extends
+$main-loud
+ font-size 15px
+ font-weight 700
+ line-height 21px
+ margin 1em 0
+
+.main
+ display table-cell
+ // position relative
+ padding 0 content_horizontal_padding 35px
+
+ .breadcrumbs
+ margin 34px 0 15px
+ padding 0
+
+ &__header
+ padding 0 0 12px
+ border-bottom 2px solid #f5f2f0
+
+ &__header_center
+ border 0
+
+ &__header-title,
+ .breadcrumbs
+ text-align center
+
+ &__header_search
+ border-bottom 0
+ margin-top 33px
+
+ & &__header-title
+ border-bottom 2px solid #f5f2f0
+ font 700 40px/40px secondary_font
+ margin 18px 0 12px
+ padding 0 0 14px
+
+ &__header-title_noseparator
+ border-bottom 0
+
+ &__header_search &__header-title
+ margin-bottom 0
+ padding-bottom 10px
+
+ &__username
+ font-style inherit
+ font-weight inherit
+ color secondary_color
+
+ &__header-date,
+ &__header-comments a
+ color #666
+
+ &__header-comments a
+ text-decoration underline
+
+ &__header-comments:hover
+ color link_hover_color
+
+ &__header-date
+ margin-right 23px
+
+ &__header-date::before,
+ &__header-comments::before
+ color secondary_color
+
+ &__header-date::before
+ @extend $font-time
+ margin-right 3px
+
+ &__header-comments::before
+ @extend $font-comment
+ margin-right 3px
+
+ &__header-nav
+ float right
+
+ &__lesson-nav
+ display table
+ float right
+
+ &__lesson-nav-prev,
+ &__lesson-nav-next
+ display table-cell
+
+ &__lesson-nav-prev
+ padding-right 15px
+ border-right 2px solid #f5f2f0
+
+ &__lesson-nav-next
+ padding-left 15px
+
+ &__lesson-nav-prev:last-child
+ border none
+ padding-right 0
+
+ &__lesson-nav kbd
+ font-size 11px
+ color #999
+ font: inherit
+
+ &__lesson-nav-arr
+ position relative
+ top -.1em
+
+ &__lesson-nav-link
+ color #656565
+ text-decoration underline
+
+ ul,
+ ol
+ padding-left 25px
+ margin 1em 0
+
+ > li
+ margin: .5em 0
+
+ ul > li::before
+ @extend $font-bullet
+ position absolute
+ margin-left -20px
+ color #B2C1C1
+
+ // TODO: h1 вне блока .main__header ??! Проверить, встречается ли
+ h1
+ margin-bottom .5em
+ /* FIXME пока некрасиво с этим: page-break-before: always; */
+
+ h2
+ padding-bottom 11px
+ margin 36px 0 6px
+ font-size 34px
+ line-height 40px
+ font-family secondary_font
+
+ h3,
+ h4
+ margin 24px 0 8px
+
+ h3
+ font-size 128%
+ line-height 22px
+
+ h4
+ font-size 114%
+ line-height 20px
+
+ h1 a,
+ h1 code,
+ h2 a,
+ h2 code,
+ h3 a,
+ h3 code,
+ h4 a,
+ h4 code
+ font inherit
+ color inherit
+ text-decoration none
+
+ // .format_error - old code support?
+ .format_error
+ color red
+
+ div.format_error
+ border 1px solid red
+ padding 5px
+
+ .admin_link
+ float right
+ color gray
+
+ h2 a[href^='#']:focus,
+ h3 a[href^='#']:focus,
+ h4 a[href^='#']:focus
+ outline none
+
+ code
+ color code_color
+ font-family fixed_width_font
+
+ /* for regexps */
+ code.pattern
+ border-bottom 1px solid red
+
+ code.subject
+ border-bottom 1px solid blue
+
+ code.match
+ border-bottom 1px solid green
+
+ span.shortcut
+ white-space nowrap
+
+ span.shortcut code
+ border 1px solid rgb(51,51,51)
+ padding 0 1px
+ display inline-block
+ margin 1px 0
+
+ p
+ margin round(lineheight*.65) 0
+
+ // Is it really used?
+ dl
+ margin 1em 0
+
+ dt
+ font-weight 700
+
+ dd
+ margin 0 0 round(lineheight*.8) 40px
+
+ figure,
+ img
+ margin 30px 0
+
+ figure img
+ margin 0
+
+ &__loud
+ @extend $main-loud
+
+ &__strong
+ @extend $main-loud
+ font-style italic
+ color #8C0000
+
+ &__footer
+ @extend $clearfix
+ background #F5F5F2
+ margin 30px -40px 27px
+ padding 16px 40px 9px
+
+ .rating
+ margin-right 22px
+
+ .social
+ float right
+
+ &__footer-date,
+ &__footer-author,
+ &__footer-author a
+ color #444
+ font 13px Arial, Helvetica, sans-serif
+
+ &__footer-date
+ margin-right 15px
+
+ &__footer-author
+ margin-right 10px
+
+ &__footer-star
+ position absolute
+ font-size 18px
+ color #BBB
+ margin 0 10px
+
+ &__footer-star::before
+ @extend $font-star
+
+ @media (min-width: media_step_1)
+ padding 0 (content_horizontal_padding + 10px) 35px
+ line-height 19px
+
+ &__footer
+ margin 30px -1*(content_horizontal_padding + 10px) 27px
+ padding 16px (content_horizontal_padding + 10px) 9px
+
+ @media (min-width: media_step_2)
+ padding 0 (content_horizontal_padding + 20px) 35px
+ line-height 20px
+
+ &__footer
+ margin 30px -1*(content_horizontal_padding + 20px) 27px
+ padding 16px (content_horizontal_padding + 20px) 9px
+
+ @media (min-width: media_step_3)
+ padding 0 (content_horizontal_padding + 30px) 35px
+ line-height 22px
+
+ &__footer
+ margin 30px -1*(content_horizontal_padding + 30px) 27px
+ padding 16px (content_horizontal_padding + 30px) 9px
+
+ @media (min-width: largescreen)
+ font-size 16px
diff --git a/app/stylesheets/blocks/mixins/imagesize.styl b/app/stylesheets/blocks/mixins/imagesize.styl
new file mode 100644
index 000000000..5a9b77d90
--- /dev/null
+++ b/app/stylesheets/blocks/mixins/imagesize.styl
@@ -0,0 +1,9 @@
+image-width(img)
+ return image-size(img)[0]
+
+image-height(img)
+ return image-size(img)[1]
+
+size-of(img)
+ width image-width(img)
+ height image-height(img)
\ No newline at end of file
diff --git a/app/stylesheets/blocks/mixins/sprite.styl b/app/stylesheets/blocks/mixins/sprite.styl
new file mode 100644
index 000000000..13ead9f1a
--- /dev/null
+++ b/app/stylesheets/blocks/mixins/sprite.styl
@@ -0,0 +1,27 @@
+spriteWidth($sprite)
+ width $sprite[4]
+
+spriteHeight($sprite)
+ height $sprite[5]
+
+spritePosition($sprite)
+ background-position $sprite[2] $sprite[3]
+
+spriteImage($sprite)
+ if length($sprite) == 3
+ background-image url($sprite[2])
+ else
+ background-image url($sprite[8])
+
+sprite($sprite)
+ if !match('hover', selector()) && !match('active', selector())
+ spriteImage($sprite)
+ spritePosition($sprite)
+ spriteWidth($sprite)
+ spriteHeight($sprite)
+
+
+spriteElem($sprite)
+ spritePosition($sprite)
+ spriteWidth($sprite)
+ spriteHeight($sprite)
\ No newline at end of file
diff --git a/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl b/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl
new file mode 100644
index 000000000..6a97ae5bf
--- /dev/null
+++ b/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl
@@ -0,0 +1,27 @@
+.dropdown_nav
+ position relative
+
+.dropdown__toggle_nav::after
+ @extend $font-open
+ margin-left 5px
+ color #b0b2b3
+ line-height 0
+
+.navbar__sections-item.open .dropdown__toggle_nav::after,
+.navbar__sections-item_active .dropdown__toggle_nav::after
+ color secondary_color
+
+.dropdown__content_nav
+ background #e9e9dd
+ padding 4px 0 12px
+
+ & > li
+ margin 0 15px 4px 40px
+ font-size 12px
+
+ a
+ color #0063b9
+
+ a:hover
+ color link_hover_color
+ text-decoration none
diff --git a/app/stylesheets/blocks/navbar/navbar.styl b/app/stylesheets/blocks/navbar/navbar.styl
new file mode 100644
index 000000000..e28a1f7b1
--- /dev/null
+++ b/app/stylesheets/blocks/navbar/navbar.styl
@@ -0,0 +1,121 @@
+.navbar
+ background #e9e9dd
+ border-top-left-radius 4px
+ border-top-right-radius 4px
+ font 700 15px secondary_font
+
+.navbar__sections
+ line-height 30px
+ display table
+ width 100%
+
+.navbar__sections-item
+ display table-cell
+ vertical-align top
+ text-align center
+ border-right 1px solid #384144
+ text-transform uppercase
+ white-space nowrap
+ color secondary_color
+
+.navbar__sections-item_active,
+.navbar__sections-link
+ padding 4px 15px 7px 10px
+
+.navbar__sections-item_active .navbar__sections-link
+ margin -4px -15px -7px -10px
+
+.navbar__sections-item_active .navbar__sections-link:link,
+.navbar__sections-item_active .navbar__sections-link:visited,
+.navbar__sections-item_active .navbar__sections-link:hover,
+.navbar__sections-item_active .navbar__sections-link:active,
+.navbar__sections-item.open .navbar__sections-link
+ background none
+ color secondary_color
+
+.navbar__sections-link
+ display block
+ background #22292b
+
+.navbar__sections-link
+ &:hover
+ background #373E3F
+ &:link
+ &:visited
+ &:hover
+ &:active
+ color #fff
+ text-decoration none
+
+.navbar__icon
+ font-weight 400
+ padding-right 10px
+ line-height 0
+ color #caf3ff
+
+.navbar__sections-item_active .navbar__icon,
+.navbar__sections-item.open .navbar__icon
+ color secondary_color
+
+.navbar__sections-link::before
+ color #caf3ff
+
+.navbar__icon_study::before
+ @extend $font-study
+
+.navbar__icon_blogs::before
+ @extend $font-blogs
+
+.navbar__icon_qa::before
+ @extend $font-qa
+
+.navbar__icon_reference::before
+ @extend $font-reference
+
+.navbar__icon_events::before
+ @extend $font-events
+
+.navbar__icon_job::before
+ @extend $font-job
+
+.navbar__icon_sandbox::before
+ @extend $font-code
+
+.navbar__icon_chat::before
+ @extend $font-chat
+
+.navbar__icon_search
+ padding-right 0
+ color #fff
+
+.navbar__icon_search::before
+ @extend $font-search
+
+.navbar__sections-item_search
+ padding 0
+ width 40px
+ text-align center
+ background #22292b
+
+ .dropdown__toggle
+ @extend $button-reset
+ font-size 16px
+ width 40px
+ height 40px
+ display block
+
+
+ &.open
+ background #B81800
+
+.navbar__sections-item:first-child,
+.navbar__sections-item:first-child .navbar__sections-link
+ border-top-left-radius 4px
+
+
+.navbar__sections-item:nth-last-child(2).navbar__sections-item_active
+ border-right 1px solid transparent
+
+.navbar__sections-item:last-child
+ border-right 0
+ border-top-right-radius 4px
diff --git a/app/stylesheets/blocks/page-footer/page-footer.png b/app/stylesheets/blocks/page-footer/page-footer.png
new file mode 100644
index 000000000..4c165cc2d
Binary files /dev/null and b/app/stylesheets/blocks/page-footer/page-footer.png differ
diff --git a/app/stylesheets/blocks/page-footer/page-footer.styl b/app/stylesheets/blocks/page-footer/page-footer.styl
new file mode 100644
index 000000000..59c893dba
--- /dev/null
+++ b/app/stylesheets/blocks/page-footer/page-footer.styl
@@ -0,0 +1,69 @@
+.page-footer
+ display table
+ width 100%
+ background #666
+
+ &__contents,
+ &__copy
+ display table-cell
+ vertical-align top
+
+ &__title,
+ a
+ color #fff
+ font-size 15px
+
+ a
+ text-decoration underline
+
+ &__contents
+ padding 35px 40px 60px
+ background url('/img/page-footer.png') 97.5% 35px no-repeat
+
+ &__list
+ font-weight 700
+
+ &__list-item
+ margin-bottom 6px
+
+ &__title
+ margin-bottom 8px
+
+ &__list.small &__list-item
+ margin-bottom 3px
+ a
+ font-weight 400
+ font-size 13px
+
+ &__col
+ float left
+ width 20.6%
+ margin 0 3% 0 0
+
+ &__copy
+ width sidebar_width
+ padding 35px sidebar_padding_right 60px sidebar_padding_left
+ color #fff
+ font-style normal
+ a
+ font-size 13px
+
+ &__line
+ display block
+ margin-bottom 3px
+
+ &__line.author
+ margin-bottom 6px
+
+ @media (min-width: media_step_1)
+ &__contents
+ padding-left content_horizontal_padding + 10px
+
+
+ @media (min-width: media_step_2)
+ &__contents
+ padding-left content_horizontal_padding + 20px
+
+ @media (min-width: media_step_3)
+ &__contents
+ padding-left content_horizontal_padding + 30px
diff --git a/app/stylesheets/blocks/page/page-central-cloud.png b/app/stylesheets/blocks/page/page-central-cloud.png
new file mode 100644
index 000000000..05d434a68
Binary files /dev/null and b/app/stylesheets/blocks/page/page-central-cloud.png differ
diff --git a/app/stylesheets/blocks/page/page-left-cloud.png b/app/stylesheets/blocks/page/page-left-cloud.png
new file mode 100644
index 000000000..dbf0bc14c
Binary files /dev/null and b/app/stylesheets/blocks/page/page-left-cloud.png differ
diff --git a/app/stylesheets/blocks/page/page-right-cloud.png b/app/stylesheets/blocks/page/page-right-cloud.png
new file mode 100644
index 000000000..c1c2d2586
Binary files /dev/null and b/app/stylesheets/blocks/page/page-right-cloud.png differ
diff --git a/app/stylesheets/blocks/page/page.styl b/app/stylesheets/blocks/page/page.styl
new file mode 100644
index 000000000..5fab892fb
--- /dev/null
+++ b/app/stylesheets/blocks/page/page.styl
@@ -0,0 +1,22 @@
+.page
+ @extend $min-max-width
+ @extend $center
+
+ box-shadow: 0 0 3px 3px rgba(128, 128, 128, 0.3)
+ position: relative
+ z-index: 0
+ &::before
+ content: ""
+ display: block
+ position: absolute
+ top: -120px
+ left: -20px
+ right: -8px
+ height: 182px
+ z-index: -1
+ background: url('/img/page-left-cloud.png') 0 35px no-repeat,
+ url('/img/page-central-cloud.png') 50% 35px no-repeat,
+ url('/img/page-right-cloud.png') 100% 0 no-repeat
+
+ .page__inner
+ background: background
diff --git a/app/stylesheets/blocks/placeholders/button-reset.styl b/app/stylesheets/blocks/placeholders/button-reset.styl
new file mode 100644
index 000000000..36187b5c5
--- /dev/null
+++ b/app/stylesheets/blocks/placeholders/button-reset.styl
@@ -0,0 +1,14 @@
+$button-reset
+ border 0
+ background none
+ display inline
+ margin 0
+ padding 0
+ cursor pointer
+
+ &::-moz-focus-inner
+ border none
+ padding 0
+
+ &:active
+ position relative
diff --git a/app/stylesheets/blocks/placeholders/center.styl b/app/stylesheets/blocks/placeholders/center.styl
new file mode 100644
index 000000000..a35116746
--- /dev/null
+++ b/app/stylesheets/blocks/placeholders/center.styl
@@ -0,0 +1,2 @@
+$center
+ margin auto
\ No newline at end of file
diff --git a/app/stylesheets/blocks/placeholders/clearfix.styl b/app/stylesheets/blocks/placeholders/clearfix.styl
new file mode 100644
index 000000000..4f20bdede
--- /dev/null
+++ b/app/stylesheets/blocks/placeholders/clearfix.styl
@@ -0,0 +1,6 @@
+$clearfix
+ &::after
+ content ""
+ display block
+ overflow hidden
+ clear both
\ No newline at end of file
diff --git a/app/stylesheets/blocks/placeholders/font-icons.styl b/app/stylesheets/blocks/placeholders/font-icons.styl
new file mode 100644
index 000000000..ebb7fd724
--- /dev/null
+++ b/app/stylesheets/blocks/placeholders/font-icons.styl
@@ -0,0 +1,153 @@
+@font-face
+ font-family 'FontIcons'
+ src url('/fonts/icons/icons.eot')
+ src local('☺'), url('/fonts/icons/icons.woff') format('woff'), url('/fonts/icons/icons.ttf') format('truetype'), url('/fonts/icons/icons.svg') format('svg')
+ font-weight normal
+ font-style normal
+
+$font-icon
+ font-family 'FontIcons'
+
+$font-close
+ @extend $font-icon
+ content '\25b4'
+
+$font-open
+ @extend $font-icon
+ content '\25be'
+
+$font-star
+ @extend $font-icon
+ content '\2605'
+
+$font-star-empty
+ @extend $font-icon
+ content '\2606'
+
+$font-warning
+ @extend $font-icon
+ content '\26a0'
+
+$font-mail
+ @extend $font-icon
+ content '\2709'
+
+$font-edit
+ @extend $font-icon
+ content '\270d'
+
+$font-ok
+ @extend $font-icon
+ content '\2714'
+
+$font-qa
+ @extend $font-icon
+ content '\2753'
+
+$font-pros
+ @extend $font-icon
+ content '\e059'
+
+$font-cons
+ @extend $font-icon
+ content '\e075'
+
+$font-help
+ @extend $font-icon
+ content '\e704'
+
+$font-info
+ @extend $font-icon
+ content '\e705'
+
+$font-code
+ @extend $font-icon
+ content '\e714'
+
+$font-external
+ @extend $font-icon
+ content '\e715'
+
+$font-download
+ @extend $font-icon
+ content '\e805'
+
+$font-print
+ @extend $font-icon
+ content '\e716'
+
+$font-chat
+ @extend $font-icon
+ content '\e720'
+
+$font-bullet
+ @extend $font-icon
+ content '\e75e'
+
+$font-run
+ @extend $font-icon
+ content '\f00f'
+
+$font-copy
+ @extend $font-icon
+ content '\f0c5'
+
+$font-comment
+ @extend $font-icon
+ content '\f4ac'
+
+$font-search
+ @extend $font-icon
+ content '\f50d'
+
+$font-study
+ @extend $font-icon
+ content '\1f393'
+
+$font-user
+ @extend $font-icon
+ content '\1f464'
+
+$font-job
+ @extend $font-icon
+ content '\1f4bc'
+
+$font-blogs
+ @extend $font-icon
+ content '\1f4c4'
+
+$font-events
+ @extend $font-icon
+ content '\1f4c5'
+
+$font-reference
+ @extend $font-icon
+ content '\1f4d5'
+
+$font-time
+ @extend $font-icon
+ content '\1f554'
+
+$font-video
+ @extend $font-icon
+ content '\e800'
+
+$font-mobile
+ @extend $font-icon
+ content '\e801'
+
+$font-printable
+ @extend $font-icon
+ content '\e802'
+
+$font-pencil
+ @extend $font-icon
+ content '\e803'
+
+$font-trash
+ @extend $font-icon
+ content '\e804'
+
+$font-sitemap
+ @extend $font-icon
+ content '\e806'
diff --git a/app/stylesheets/blocks/placeholders/min-max-width.styl b/app/stylesheets/blocks/placeholders/min-max-width.styl
new file mode 100644
index 000000000..7e515bc85
--- /dev/null
+++ b/app/stylesheets/blocks/placeholders/min-max-width.styl
@@ -0,0 +1,4 @@
+$min-max-width
+ min-width min_width
+ max-width max_width
+
\ No newline at end of file
diff --git a/app/stylesheets/blocks/prev-next/prev-next.styl b/app/stylesheets/blocks/prev-next/prev-next.styl
new file mode 100644
index 000000000..434938e1e
--- /dev/null
+++ b/app/stylesheets/blocks/prev-next/prev-next.styl
@@ -0,0 +1,53 @@
+.prev-next
+ display table
+ table-layout fixed
+
+ &__prev,
+ &__next
+ display table-cell
+
+ &__prev
+ text-align left
+ padding-right 10px
+
+ &__next
+ text-align right
+ padding-left 10px
+
+ &__shortcut
+ color #999
+ font inherit
+
+ &__arr
+ position relative
+ top -.1em
+
+ &_top &__link
+ color #656565
+ text-decoration underline
+ margin 0 .5ex
+
+ &_top &__prev
+ border-right 2px solid #f5f2f0
+
+ &_top &__prev:last-child
+ text-align right
+ border 0
+ padding-right 0
+
+ &_top &__next:first-child
+ padding-left 0
+
+ &_top &__shortcut
+ font-size 11px
+
+ &_bottom
+ width 100%
+ padding 12px 0
+ border-bottom 1px solid #ddd
+
+ &_bottom &__shortcut
+ margin-left .5ex
+
+ &_bottom &__shortcut-wrap
+ margin-bottom 5px
diff --git a/app/stylesheets/blocks/prism/my-prism.styl b/app/stylesheets/blocks/prism/my-prism.styl
new file mode 100644
index 000000000..c47905c06
--- /dev/null
+++ b/app/stylesheets/blocks/prism/my-prism.styl
@@ -0,0 +1,36 @@
+pre[class*="language-"],
+code[class*="language-"]
+ font 14px/17px fixed_width_font
+ z-index 0
+ text-shadow none
+ margin 0
+
+pre[class*="language-"]
+ position relative
+ code
+ color inherit
+ position relative
+
+pre.line-numbers
+ padding-left 3.2em
+
+// span with line numbers is moved from to the outer ,
+// because we need to handle many ... inside single
+// (this we need for highlighting *!*...* /!* inline
+.line-numbers .line-numbers-rows
+ left 0
+ top 0
+ padding 1em 0
+ border 0
+ background #e7e5e3
+ width auto
+
+.line-numbers-rows > span:before
+ padding 0 .7em 0 .8em
+ text-shadow none
+
+@media (min-width: largescreen)
+ pre[class*="language-"],
+ code[class*="language-"]
+ font-size 16px
+ line-height 19px
diff --git a/app/stylesheets/blocks/prism/prism-line-highlight.styl b/app/stylesheets/blocks/prism/prism-line-highlight.styl
new file mode 100644
index 000000000..5e053e0d9
--- /dev/null
+++ b/app/stylesheets/blocks/prism/prism-line-highlight.styl
@@ -0,0 +1,31 @@
+.inline-highlight
+ position absolute
+ pointer-events none
+ line-height inherit
+ white-space pre
+ left 0
+ top -1px
+ z-index -1
+
+ .mask
+ background #F5E7C6
+ outline 2px solid #F5E7C6
+
+.block-highlight
+ position absolute
+ left 0
+ right 0
+ top -1px
+ padding inherit 0
+ margin-top 1em /* Same as .prism’s padding-top */
+
+ pointer-events none
+
+ line-height inherit
+ white-space pre
+ .mask
+ background #F5E7C6
+ outline 2px solid #F5E7C6
+ left 0
+ right 0
+ position absolute
\ No newline at end of file
diff --git a/app/stylesheets/blocks/prism/prism-line-numbers.styl b/app/stylesheets/blocks/prism/prism-line-numbers.styl
new file mode 100644
index 000000000..7f4a87511
--- /dev/null
+++ b/app/stylesheets/blocks/prism/prism-line-numbers.styl
@@ -0,0 +1,31 @@
+pre.line-numbers
+ position relative
+ padding-left 3.8em
+ counter-reset linenumber
+
+.line-numbers .line-numbers-rows
+ position absolute
+ pointer-events none
+ top 0
+ font-size 100%
+ left -3.8em
+ width 3em /* works for line-numbers below 1000 lines */
+ letter-spacing -1px
+ border-right 1px solid #999
+
+ -webkit-user-select none
+ -moz-user-select none
+ -ms-user-select none
+ user-select none
+
+.line-numbers-rows > span
+ pointer-events none
+ display block
+ counter-increment linenumber
+
+.line-numbers-rows > span:before
+ content counter(linenumber)
+ color #999
+ display block
+ padding-right 0.8em
+ text-align right
\ No newline at end of file
diff --git a/app/stylesheets/blocks/prism/prism.styl b/app/stylesheets/blocks/prism/prism.styl
new file mode 100644
index 000000000..3a42ad81d
--- /dev/null
+++ b/app/stylesheets/blocks/prism/prism.styl
@@ -0,0 +1,101 @@
+/**
+ * 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
+ font-family Consolas, Monaco, 'Andale Mono', monospace
+ direction ltr
+ text-align left
+ white-space pre
+ word-spacing normal
+
+ -moz-tab-size 4
+ -o-tab-size 4
+ tab-size 4
+
+ -webkit-hyphens none
+ -moz-hyphens none
+ -ms-hyphens none
+ hyphens none
+
+pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection
+ text-shadow none
+ background #b3d4fc
+
+pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
+code[class*="language-"]::selection, code[class*="language-"] ::selection
+ text-shadow none
+ background #b3d4fc
+
+@media print
+ code[class*="language-"],
+ pre[class*="language-"]
+ text-shadow: none
+
+/* Code blocks */
+pre[class*="language-"]
+ padding 1em
+ margin 1.5em 0
+ overflow auto
+
+:not(pre) > code[class*="language-"],
+pre[class*="language-"]
+ background: background_blocks
+
+/* Inline code */
+:not(pre) > code[class*="language-"]
+ padding .1em
+ border-radius .3em
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata
+ color slategray
+
+.token.punctuation
+ color #999
+
+.namespace
+ opacity .7
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number
+ color #905
+
+.token.selector,
+.token.attr-name,
+.token.string
+ color #690
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string
+ color #a67f59
+ /* background: hsla(0,0%,100%,.5); */
+
+.token.atrule,
+.token.attr-value,
+.token.keyword
+ color #07a
+
+
+.token.regex,
+.token.important
+ color #e90
+
+.token.important
+ font-weight bold
+
+.token.entity
+ cursor help
diff --git a/app/stylesheets/blocks/progress/progress.styl b/app/stylesheets/blocks/progress/progress.styl
new file mode 100644
index 000000000..1ce520f07
--- /dev/null
+++ b/app/stylesheets/blocks/progress/progress.styl
@@ -0,0 +1,69 @@
+/**
+ * (C)Leanest CSS spinner ever
+ * From http://lea.verou.me/2013/11/cleanest-css-spinner-ever/
+ *
+ * Usage: Loading…
+
+Loading…
+
+Loading…
+ */
+
+@keyframes spin {
+ to { transform: rotate(1turn); }
+}
+
+.progress {
+ position: relative;
+// display: inline-block;
+ width: 5em;
+ height: 5em;
+ margin: 0 .5em;
+ font-size: 12px;
+ text-indent: 999em;
+ overflow: hidden;
+ animation: spin 1s infinite steps(8);
+}
+
+.small.progress {
+ font-size: 6px;
+}
+
+.large.progress {
+ font-size: 24px;
+}
+
+.progress:before,
+.progress:after,
+.progress > div:before,
+.progress > div:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 2.25em; /* (container width - part width)/2 */
+ width: .5em;
+ height: 1.5em;
+ border-radius: .2em;
+ background: #eee;
+ box-shadow: 0 3.5em #eee; /* container height - part height */
+ transform-origin: 50% 2.5em; /* container height / 2 */
+}
+
+.progress:before {
+ background: #555;
+}
+
+.progress:after {
+ transform: rotate(-45deg);
+ background: #777;
+}
+
+.progress > div:before {
+ transform: rotate(-90deg);
+ background: #999;
+}
+
+.progress > div:after {
+ transform: rotate(-135deg);
+ background: #bbb;
+}
diff --git a/app/stylesheets/blocks/reset/reset.styl b/app/stylesheets/blocks/reset/reset.styl
new file mode 100644
index 000000000..b101ae61d
--- /dev/null
+++ b/app/stylesheets/blocks/reset/reset.styl
@@ -0,0 +1,28 @@
+body,
+h1, h2, h3, h4, h5, h6,
+p, blockquote, pre, address,
+dl, dd, ul, ol,
+fieldset, form, legend,
+th, td,
+figure, figcaption
+ margin 0
+ padding 0
+
+section, article, aside,
+figure, figcaption,
+header, hgroup, footer,
+nav
+ display block
+
+a img
+ border 0
+
+ul
+ list-style none
+
+p
+ margin $lineheight 0
+
+.invisible
+ visibility hidden
+
\ No newline at end of file
diff --git a/app/stylesheets/blocks/result/result.styl b/app/stylesheets/blocks/result/result.styl
new file mode 100644
index 000000000..55cf73c5f
--- /dev/null
+++ b/app/stylesheets/blocks/result/result.styl
@@ -0,0 +1,15 @@
+.result
+ position relative
+ border 1px solid #e7e5e3
+ margin 1.5em 0
+
+ &__toolbar
+ position absolute
+ top 0
+ right 0
+
+ &__iframe
+ display block
+ border 0
+ width 100%
+ height 100px
diff --git a/app/stylesheets/blocks/shortcut/shortcut.styl b/app/stylesheets/blocks/shortcut/shortcut.styl
new file mode 100644
index 000000000..c8f62e089
--- /dev/null
+++ b/app/stylesheets/blocks/shortcut/shortcut.styl
@@ -0,0 +1,11 @@
+.shortcut
+ background #f6f4f2
+ border 1px solid #e8e6e5
+ border-radius 3px
+ padding 1px 2px 0
+ font-family fixed_width_font
+ line-height inherit
+ word-spacing -.4ex
+
+ &__plus
+ color #b8b7b7
diff --git a/app/stylesheets/blocks/sidebar/sidebar-bg.png b/app/stylesheets/blocks/sidebar/sidebar-bg.png
new file mode 100644
index 000000000..121144413
Binary files /dev/null and b/app/stylesheets/blocks/sidebar/sidebar-bg.png differ
diff --git a/app/stylesheets/blocks/sidebar/sidebar.styl b/app/stylesheets/blocks/sidebar/sidebar.styl
new file mode 100644
index 000000000..634a1cb60
--- /dev/null
+++ b/app/stylesheets/blocks/sidebar/sidebar.styl
@@ -0,0 +1,87 @@
+.sidebar
+ box-shadow inset 10px 0 10px -10px #ddd
+ display table-cell
+ vertical-align top
+ width sidebar_width
+ min-width sidebar_width
+ padding: 0 sidebar_padding_right 35px sidebar_padding_left;
+ background url('/img/sidebar-bg.png') #F1F1F1
+
+// .sidebar__section {
+// margin: 25px 0 35px;
+// }
+
+// .sidebar__list ul {
+// margin: 8px 0 0 10px;
+// }
+
+// .sidebar__list-item {
+// margin: 0 0 6px;
+// list-style: none;
+// text-overflow: ellipsis;
+// overflow: hidden;
+// line-height: normal;
+
+// > .menu {
+// margin: 10px 0 0 10px;
+// }
+// }
+
+// .sidebar__title {
+// display: block;
+// text-decoration: none;
+// margin: 25px 0 12px;
+// padding: 0 0 12px;
+// font-size: 16px;
+// font-weight: 700;
+// color: #666;
+// border-bottom: 1px solid #ccc;
+// }
+
+// .sidebar__title_small {
+// font-size: 12px;
+// padding: 0 0 8px;
+// }
+
+// .sidebar__complex-title {
+// margin: 35px 0 10px;
+// }
+
+// .sidebar__subtitle {
+// font-size: 12px;
+// font-weight: 400;
+// }
+
+// .sidebar__subtitle + .sidebar__title {
+// margin: -3px 0 10px;
+// }
+
+// a {
+// font-size: 13px;
+// color: $link-color;
+// }
+
+// a:hover {
+// color: $link-hover-color;
+// }
+
+// .sidebar__list-item.active > a {
+// color: #B20000;
+// font-weight: 700;
+// }
+
+// .keep-visible.cloned {
+// width: $sidebar-width;
+// }
+
+// .fixed {
+// position: fixed;
+// top: 10px;
+// margin: 0;
+// width: $sidebar-width;
+// .sidebar__title,
+// .sidebar__complex-title {
+// margin-top: 0;
+// }
+// }
+// }
\ No newline at end of file
diff --git a/app/stylesheets/blocks/soc-icon/soc-icon.styl b/app/stylesheets/blocks/soc-icon/soc-icon.styl
new file mode 100644
index 000000000..b8bb3fc51
--- /dev/null
+++ b/app/stylesheets/blocks/soc-icon/soc-icon.styl
@@ -0,0 +1,14 @@
+.soc-icon
+ spriteImage $soc_icon
+
+ &.google
+ spriteElem $soc_icon-google
+
+ &.facebook
+ spriteElem $soc_icon-facebook
+
+ &.twitter
+ spriteElem $soc_icon-twitter
+
+ &.vk
+ spriteElem $soc_icon-vk
diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png
new file mode 100644
index 000000000..dfb94bc83
Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png differ
diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png
new file mode 100644
index 000000000..d4d94951d
Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png differ
diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/twitter.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/twitter.png
new file mode 100644
index 000000000..bd610c8a3
Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/twitter.png differ
diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/vk.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/vk.png
new file mode 100644
index 000000000..e8b827e34
Binary files /dev/null and b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/vk.png differ
diff --git a/app/stylesheets/blocks/social/social.styl b/app/stylesheets/blocks/social/social.styl
new file mode 100644
index 000000000..830b325b6
--- /dev/null
+++ b/app/stylesheets/blocks/social/social.styl
@@ -0,0 +1,74 @@
+.social
+ & &__anchor-up
+ @extend $pseudo;
+ font-size 11px
+ color #868686
+ border-bottom 1px dashed
+ line-height 1
+
+ & &__anchor-up:hover
+ color link_hover_color
+
+ & &__soc
+ line-height 0
+ img
+ margin 0
+ opacity .5
+
+ body & &__soc
+ @extend $plain-link
+
+ &__soc:hover
+ img
+ opacity 1
+
+ &.aside
+ box-shadow 0 0 1px 1px rgba(128, 128, 128, 0.3)
+ float left
+ margin-left -57px
+ max-width 45px
+ padding 5px 7px 14px
+ background #ECF3F3
+ border-radius 3px
+
+ &.aside &__anchor-up
+ display inline-block
+ margin-bottom 8px
+
+ &.aside &__soc
+ @extend $center
+ display block
+ width 24px
+ margin-top 4px
+ img
+ display block
+
+ &.aside.fixed
+ position fixed
+ top 20px
+
+ &.inline
+ font-size 0
+ line-height 0
+ display inline-block
+ & &__soc
+ font-size: size
+ display inline-block
+ width 24px
+ margin-left 5px
+
+ @media (min-width: media_step_1)
+ &.aside
+ margin-left -62px
+
+ @media (min-width: media_step_2)
+ &.aside
+ margin-left -66px
+
+ @media (min-width: media_step_3)
+ &.aside
+ margin-left -68px
+
+ @media screen and (max-device-width: 760px)
+ &.aside
+ display none
\ No newline at end of file
diff --git a/app/stylesheets/blocks/spoiler/spoiler.styl b/app/stylesheets/blocks/spoiler/spoiler.styl
new file mode 100644
index 000000000..9564b427e
--- /dev/null
+++ b/app/stylesheets/blocks/spoiler/spoiler.styl
@@ -0,0 +1,38 @@
+.spoiler
+ background background_blocks
+ border-top 1px solid #D9D7D6
+ margin 20px 0
+ padding 15px 20px 20px
+
+ &__button
+ @extend $button-reset
+ color link_color
+ position relative
+ font size/lineheight font
+ padding 1px 1px 1px 0
+ text-decoration none
+ outline none
+ u
+ text-decoration none
+
+
+ &__button:hover
+ color link_hover_color
+ u
+ @extend $pseudo
+
+ &__button::after
+ margin-left 1ex
+ @extend $font-close
+
+ &.closed &__button::after
+ @extend $font-open
+
+ &__content
+ margin-top 20px
+
+ & + &
+ margin-top -20px
+
+ &.closed &__content
+ display none
diff --git a/app/stylesheets/blocks/standard-table/standard-table.styl b/app/stylesheets/blocks/standard-table/standard-table.styl
new file mode 100644
index 000000000..a15827e78
--- /dev/null
+++ b/app/stylesheets/blocks/standard-table/standard-table.styl
@@ -0,0 +1,41 @@
+.main table
+ width 100%
+ border-collapse collapse
+ font-size 13px
+ margin 30px 0
+
+ tbody
+ border 0
+
+ tr
+ border-bottom 1px solid #ccc
+
+ tr:first-child th
+ border-bottom 3px solid #CCC
+ vertical-align bottom
+
+ th
+ text-align left
+
+ th,
+ td
+ padding 2px 1em 2px 5px
+
+ tr:nth-child(even)
+ background #f9f9f9
+
+ code
+ font-weight inherit
+
+.main table.bordered
+ td,
+ th
+ border solid #ccc
+ border-width 0 1px
+
+ th
+ border-width 0 0 1px
+
+@media (min-width: largescreen)
+ .main table
+ font-size: 15px;
diff --git a/app/stylesheets/blocks/summary/summary.styl b/app/stylesheets/blocks/summary/summary.styl
new file mode 100644
index 000000000..35ea3c092
--- /dev/null
+++ b/app/stylesheets/blocks/summary/summary.styl
@@ -0,0 +1,15 @@
+.summary
+ background background_blocks
+ margin round(lineheight*.65) 0
+ border 1px solid separator_color
+
+ &__content
+ margin 22px
+
+ &_noborder
+ border 0
+ padding 1px 0
+ border-radius 4px
+
+ @media print
+ page-break-inside: avoid;
diff --git a/app/stylesheets/blocks/toolbar/toolbar.styl b/app/stylesheets/blocks/toolbar/toolbar.styl
new file mode 100644
index 000000000..deea71802
--- /dev/null
+++ b/app/stylesheets/blocks/toolbar/toolbar.styl
@@ -0,0 +1,40 @@
+.toolbar
+ display table-row
+
+.toolbar__tool
+ display table-cell
+ padding-left 1px
+
+.toolbar__button
+ display inline-block
+ vertical-align bottom
+ width 30px
+ height 30px
+ background #c4c2c0
+ text-align center
+ line-height 30px
+ font-size 16px
+ @extend $plain-link
+ &:link,
+ &:visited,
+ &:hover,
+ &:active
+ color #fff
+ text-decoration none
+ &:hover
+ background darken(@background, 5%)
+
+.toolbar__button_run::before
+ @extend $font-run
+
+.toolbar__button_external::before
+ @extend $font-external
+
+.toolbar__button_download::before
+ @extend $font-download
+
+.toolbar__button_edit::before
+ @extend $font-edit
+
+.toolbar__button-text
+ display none
diff --git a/app/stylesheets/blocks/top-part/top-part.png b/app/stylesheets/blocks/top-part/top-part.png
new file mode 100644
index 000000000..b036c50fb
Binary files /dev/null and b/app/stylesheets/blocks/top-part/top-part.png differ
diff --git a/app/stylesheets/blocks/top-part/top-part.styl b/app/stylesheets/blocks/top-part/top-part.styl
new file mode 100644
index 000000000..02d173d22
--- /dev/null
+++ b/app/stylesheets/blocks/top-part/top-part.styl
@@ -0,0 +1,13 @@
+.top-part
+ @extend $center
+ @extend $min-max-width
+ position relative
+ background url('/img/top-part.png') 50% 0 no-repeat
+ .user
+ position absolute
+ z-index 999
+ bottom 36px
+ right 46px
+
+ .logo
+ @extend $center
diff --git a/app/stylesheets/blocks/variables/variables.styl b/app/stylesheets/blocks/variables/variables.styl
new file mode 100644
index 000000000..fbfdf1348
--- /dev/null
+++ b/app/stylesheets/blocks/variables/variables.styl
@@ -0,0 +1,33 @@
+link_color = #0059B2;
+alternate_link_color = #2974BB; // there are some a bit lighter links that need separate color
+link_hover_color = #BA1000;
+link_visited_color = #990099;
+
+background = #fff;
+color = #333;
+page_background = #D1E9EC;
+separator_color = #DFDFD0;
+secondary_color = #B20600;
+code_color = #8C6900;
+background_blocks = #F5F2F0;
+
+font = 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
+size = 14px;
+lineheight = 18px;
+
+secondary_font = 'Open Sans Condensed', Arial, Helvetica, sans-serif;
+fixed_width_font = 'Consolas', 'Monaco', 'Menlo', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace;
+
+content_horizontal_padding = 40px;
+
+sidebar_width = 205px;
+sidebar_padding_left = 23px;
+sidebar_padding_right = 30px;
+
+min_width = 960px;
+max_width = 1200px;
+
+media_step_1 = 1120px;
+media_step_2 = 1170px;
+media_step_3 = 1220px;
+largescreen = 1420px;
\ No newline at end of file
diff --git a/app/stylesheets/main.styl b/app/stylesheets/main.styl
deleted file mode 100755
index 6eec76731..000000000
--- a/app/stylesheets/main.styl
+++ /dev/null
@@ -1,4 +0,0 @@
-/* :-) */
-h1
- font-style italic
-
diff --git a/app/stylesheets/sprites/.gitkeep b/app/stylesheets/sprites/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/bin/www b/bin/www
index 103dff3c5..f9616793f 100755
--- a/bin/www
+++ b/bin/www
@@ -1,10 +1,15 @@
#!/usr/bin/env node
'use strict';
-var app = require('app');
-var log = require('lib/log')(module);
-var config = require('config');
+const log = require('js-log')();
+const config = require('config');
+const co = require('co');
+const app = require('app');
-app.listen(config.port, config.host, function() {
- log.info('App listen %s:%d', config.host, config.port);
-});
\ No newline at end of file
+co(function*() {
+
+ yield* app.run();
+
+})(function(err) {
+ if (err) throw err;
+});
diff --git a/brunch-config.coffee b/brunch-config.coffee
deleted file mode 100755
index 34f086d1f..000000000
--- a/brunch-config.coffee
+++ /dev/null
@@ -1,32 +0,0 @@
-exports.config =
-# See https://github.com/brunch/brunch/blob/master/docs/config.md for documentation.
- paths:
- public: 'www'
- files:
- javascripts:
- joinTo:
- 'javascripts/app.js': /^app/
- 'javascripts/vendor.js': /^(vendor|bower_components)/
- order:
- before: []
-
- stylesheets:
- joinTo:
- 'stylesheets/app.css': /^app/
- order:
- before: []
- after: []
-
- templates:
- joinTo: 'javascripts/app.js'
-
-
- plugins:
- jade:
- pretty: yes # Adds pretty-indentation whitespaces to output (false by default)
- stylus:
- linenos: true
- includeCss: true
-
- conventions:
- assets: /^assets/
\ No newline at end of file
diff --git a/config/base.js b/config/base.js
deleted file mode 100755
index 5e59fe976..000000000
--- a/config/base.js
+++ /dev/null
@@ -1,27 +0,0 @@
-module.exports = function() {
- return {
- "port": process.env.PORT || 3000,
- "host": process.env.HOST || '0.0.0.0',
- "mongoose": {
- "uri": "mongodb://localhost/javascript",
- "options": {
- "server": {
- "socketOptions": {
- "keepAlive": 1
- },
- "poolSize": 5
- }
- }
- },
- "session": {
- "keys": ["KillerIsJim"]
- },
- template: {
- path: process.cwd() + '/views',
- options: {
- 'default': 'jade',
- 'cache': true
- }
- }
- }
-};
\ No newline at end of file
diff --git a/config/env/development.js b/config/env/development.js
deleted file mode 100755
index 188b92380..000000000
--- a/config/env/development.js
+++ /dev/null
@@ -1,5 +0,0 @@
-var _ = require('lodash');
-
-module.exports = function(config) {
- return config;
-};
\ No newline at end of file
diff --git a/config/env/production.js b/config/env/production.js
deleted file mode 100755
index 188b92380..000000000
--- a/config/env/production.js
+++ /dev/null
@@ -1,5 +0,0 @@
-var _ = require('lodash');
-
-module.exports = function(config) {
- return config;
-};
\ No newline at end of file
diff --git a/config/env/test.js b/config/env/test.js
deleted file mode 100755
index 10dfe4268..000000000
--- a/config/env/test.js
+++ /dev/null
@@ -1,9 +0,0 @@
-var _ = require('lodash');
-
-module.exports = function(config) {
- return _.merge(config, {
- "mongoose": {
- "uri": "mongodb://localhost/javascript_test"
- }
- });
-};
diff --git a/config/index.js b/config/index.js
deleted file mode 100755
index defd7abe4..000000000
--- a/config/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-
-if (!process.env.NODE_ENV) {
- throw new Error("NODE_ENV environment variable is required");
-}
-
-
-var base = require('./base')();
-var env = require('./env/' + process.env.NODE_ENV)(base);
-
-module.exports = env;
diff --git a/db b/db
new file mode 100755
index 000000000..b514d4487
--- /dev/null
+++ b/db
@@ -0,0 +1,3 @@
+#!/bin/bash
+echo Loading the "default" site fixture.
+./gulp loaddb --db fixture/db.js
diff --git a/docs/payment.sketch/Data b/docs/payment.sketch/Data
new file mode 100644
index 000000000..3ec90aed0
Binary files /dev/null and b/docs/payment.sketch/Data differ
diff --git a/docs/payment.sketch/metadata b/docs/payment.sketch/metadata
new file mode 100644
index 000000000..e0754f9f3
--- /dev/null
+++ b/docs/payment.sketch/metadata
@@ -0,0 +1,21 @@
+
+
+
+
+ app
+ com.bohemiancoding.sketch3
+ build
+ 7891
+ commit
+ debc570766a4cc5a2e31258967910f7e5776f485
+ fonts
+
+ Helvetica
+ Helvetica-Bold
+
+ length
+ 240489
+ version
+ 37
+
+
diff --git a/docs/payment.sketch/version b/docs/payment.sketch/version
new file mode 100644
index 000000000..7c091989d
--- /dev/null
+++ b/docs/payment.sketch/version
@@ -0,0 +1 @@
+37
\ No newline at end of file
diff --git a/error/httpError.js b/error/httpError.js
deleted file mode 100755
index cd5a3dd78..000000000
--- a/error/httpError.js
+++ /dev/null
@@ -1,19 +0,0 @@
-var util = require('util');
-var http = require('http');
-
-// ошибки для выдачи посетителю
-function HttpError(status, message) {
- Error.apply(this, arguments);
- Error.captureStackTrace(this, HttpError);
-
- this.status = status;
- this.message = message || http.STATUS_CODES[status] || "Error";
-}
-
-util.inherits(HttpError, Error);
-module.exports = HttpError;
-
-HttpError.prototype.name = 'HttpError';
-
-
-
diff --git a/error/index.js b/error/index.js
deleted file mode 100755
index 3522cd29c..000000000
--- a/error/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-var fs = require('fs');
-var path = require('path');
-
-var files = fs.readdirSync(__dirname);
-files.forEach(function(file) {
- if (file == 'index.js') return;
- var errorClass = require(path.join(__dirname, file));
- module.exports[errorClass.prototype.name] = errorClass;
-});
diff --git a/files/Readme.md b/files/Readme.md
new file mode 100644
index 000000000..484c968f8
--- /dev/null
+++ b/files/Readme.md
@@ -0,0 +1,3 @@
+Restricted files, not directly accessible from outside.
+
+A user can download these using ExpiringDownloadLink or by other non-direct means.
diff --git a/fixture/db.js b/fixture/db.js
new file mode 100644
index 000000000..5d00fe295
--- /dev/null
+++ b/fixture/db.js
@@ -0,0 +1,31 @@
+const mongoose = require('mongoose');
+
+var OrderTemplate = require('payments').OrderTemplate;
+var User = require('auth/models/user');
+
+exports.OrderTemplate = [
+ {
+ title: "Основы JavaScript",
+ description: "500 стр, 10мб",
+ slug: "jsbasics",
+ amount: 1
+ },
+ {
+ title: "JS-DOM",
+ description: "400 стр, 8мб",
+ slug: "dom",
+ amount: 1
+ },
+ {
+ title: "Две книги сразу",
+ description: "500 стр, 8мб",
+ slug: "api",
+ amount: 1
+ }
+];
+
+exports.User = [{
+ email: "iliakan@gmail.com",
+ username: "Ilya Kantor",
+ password: "123456"
+}];
diff --git a/gulp b/gulp
new file mode 100755
index 000000000..cae819387
--- /dev/null
+++ b/gulp
@@ -0,0 +1,2 @@
+#!/bin/bash
+NODE_ENV=development node --harmony `which gulp` $*
diff --git a/gulp-debug b/gulp-debug
new file mode 100644
index 000000000..51118e6c0
--- /dev/null
+++ b/gulp-debug
@@ -0,0 +1,2 @@
+#!/bin/bash
+NODE_ENV=development node --debug-brk --harmony `which gulp` $*
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 000000000..e43b33a12
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,122 @@
+/**
+ * NB: All tasks are initialized lazily, even plugins are required lazily,
+ * running 1 task does not require all tasks' files
+ */
+
+const gulp = require('gulp');
+const path = require('path');
+const fs = require('fs');
+const assert = require('assert');
+
+const development = (process.env.NODE_ENV == 'development');
+
+const serverSources = [
+ 'hmvc/**/*.js', 'modules/**/*.js', 'tasks/**/*.js', '*.js'
+];
+
+function lazyRequireTask(name) {
+ var args = [].slice.call(arguments, 1);
+
+ return function(callback) {
+ var task = require('./tasks/' + name).apply(this, args);
+
+ return task(callback);
+ };
+}
+
+function wrapWatch(watch, task) {
+ return function(callback) {
+ if (process.env.NODE_ENV == 'development') {
+ gulp.watch(watch, [task]);
+ } else {
+ callback();
+ }
+ };
+}
+
+gulp.task('lint-once', lazyRequireTask('lintOnce', { src: serverSources }));
+
+gulp.task('lint-or-die', lazyRequireTask('lintOnce', { src: serverSources, dieOnError: true }));
+
+gulp.task('lint', ['lint-once'], lazyRequireTask('lint', {src: serverSources}));
+
+// usage: gulp loaddb --db fixture/db
+gulp.task('loaddb', lazyRequireTask('loadDb'));
+
+gulp.task("supervisor", ['link-modules'], lazyRequireTask('supervisor', { cmd: "./bin/www", watch: ["hmvc", "modules"] }));
+
+gulp.task("app:livereload", lazyRequireTask("livereload", { watch: "www/**/*.*" }));
+
+gulp.task('link-modules', lazyRequireTask('linkModules', { src: ['modules/*', 'hmvc/*'] }));
+
+
+gulp.task("app:sync-resources", lazyRequireTask('syncResources', {
+ 'app/fonts' : 'www/fonts',
+ 'app/img': 'www/img'
+}));
+
+gulp.task("app:sync-css-images-once", lazyRequireTask('syncCssImages', {
+ src: 'app/stylesheets/**/*.{png,svg,gif,jpg}',
+ dst: 'www/i'
+}));
+
+gulp.task('app:sync-css-images', ['app:sync-css-images-once'],
+ wrapWatch('app/stylesheets/**/*.{png,svg,gif,jpg}', 'app:sync-css-images-once')
+);
+
+
+gulp.task('app:sprite-once', lazyRequireTask('sprite', {
+ spritesSearchFsRoot: 'app',
+ spritesWebRoot: '/i',
+ spritesFsDir: 'www/i',
+ styleFsDir: 'app/stylesheets/sprites'
+}));
+
+gulp.task('app:sprite', ['app:sprite-once'], wrapWatch("app/**/*.sprite/**", 'sprite'));
+
+gulp.task('app:clean-compiled-css', function(callback) {
+ fs.unlink('./www/stylesheets/base.css', function(err) {
+ if (err && err.code == 'ENOENT') err = null;
+ callback(err);
+ });
+});
+
+// Show errors if encountered
+gulp.task('app:compile-css-once',
+ ['app:clean-compiled-css'],
+ lazyRequireTask('compileCss', {
+ src: './app/stylesheets/base.styl',
+ dst: './www/stylesheets'
+ })
+);
+
+
+
+gulp.task('app:compile-css', ['app:compile-css-once'], wrapWatch("app/**/*.styl", "app:compile-css-once"));
+
+
+gulp.task("app:browserify:clean", lazyRequireTask('browserifyClean', { dst: './www/js'} ));
+
+
+gulp.task("app:browserify", ['app:browserify:clean'], lazyRequireTask('browserify'));
+
+
+// compile-css and sprites are independant tasks
+// run both or run *-once separately
+gulp.task('run', ['supervisor', 'app:livereload', "app:sync-resources", 'app:compile-css', 'app:sprite', 'app:browserify', 'app:sync-css-images']);
+
+
+// TODO: refactor me out!
+gulp.task('import', function(callback) {
+ const mongoose = require('config/mongoose');
+ const taskImport = require('tutorial/tasks/import');
+
+ taskImport({
+ root: path.join(path.dirname(__dirname), 'javascript-tutorial'),
+ updateFiles: true // skip same size files
+ //minify: true // takes time(!)
+ })(function() {
+ mongoose.disconnect();
+ callback.apply(null, arguments);
+ });
+});
diff --git a/hmvc/activities/Readme.md b/hmvc/activities/Readme.md
new file mode 100644
index 000000000..d1330f8ce
--- /dev/null
+++ b/hmvc/activities/Readme.md
@@ -0,0 +1 @@
+Курсы. TODO.
diff --git a/hmvc/auth/controller/form.js b/hmvc/auth/controller/form.js
new file mode 100644
index 000000000..1f93c3baf
--- /dev/null
+++ b/hmvc/auth/controller/form.js
@@ -0,0 +1,6 @@
+
+exports.get = function *get (next) {
+ this.render(__dirname, 'form');
+};
+
+
diff --git a/hmvc/auth/controller/login/local.js b/hmvc/auth/controller/login/local.js
new file mode 100644
index 000000000..afb018313
--- /dev/null
+++ b/hmvc/auth/controller/login/local.js
@@ -0,0 +1,18 @@
+var passport = require('koa-passport');
+
+exports.post = function*(next) {
+ var ctx = this;
+ yield passport.authenticate('local', function*(err, user, info) {
+ // missing credentials ?!?
+// console.log("HERE 2", err, user, info);
+ if (err) throw err;
+ if (user === false) {
+ ctx.status = 401;
+ ctx.body = { success: false };
+ } else {
+ yield ctx.login(user);
+ ctx.body = { success: true };
+ }
+ }).call(this, next);
+};
+
diff --git a/hmvc/auth/controller/logout.js b/hmvc/auth/controller/logout.js
new file mode 100644
index 000000000..4032eb244
--- /dev/null
+++ b/hmvc/auth/controller/logout.js
@@ -0,0 +1,6 @@
+
+exports.post = function*(next) {
+ this.logout();
+ this.redirect('/');
+};
+
diff --git a/hmvc/auth/controller/user.js b/hmvc/auth/controller/user.js
new file mode 100644
index 000000000..8b8b2acdb
--- /dev/null
+++ b/hmvc/auth/controller/user.js
@@ -0,0 +1,13 @@
+
+exports.get = function *get (next) {
+
+ this.body = {
+ username: this.req.user.username,
+ email: this.req.user.email,
+ created: this.req.user.created
+ };
+
+
+};
+
+
diff --git a/hmvc/auth/index.js b/hmvc/auth/index.js
new file mode 100644
index 000000000..5a299106e
--- /dev/null
+++ b/hmvc/auth/index.js
@@ -0,0 +1,5 @@
+const router = require('./router');
+
+require('./lib/passport');
+
+exports.middleware = router.middleware();
diff --git a/hmvc/auth/lib/hash.js b/hmvc/auth/lib/hash.js
new file mode 100644
index 000000000..ab2803a03
--- /dev/null
+++ b/hmvc/auth/lib/hash.js
@@ -0,0 +1,11 @@
+const crypto = require('crypto');
+const config = require('config');
+
+// warning, takes time, about ~70ms for length=128, iterations=12000
+exports.createHashSlow = function(password, salt) {
+ return crypto.pbkdf2Sync(password, salt, config.crypto.hash.iterations, config.crypto.hash.length);
+};
+
+exports.createSalt = function() {
+ return crypto.randomBytes(config.crypto.hash.length).toString('base64');
+};
diff --git a/hmvc/auth/lib/passport.js b/hmvc/auth/lib/passport.js
new file mode 100644
index 000000000..02bf0b384
--- /dev/null
+++ b/hmvc/auth/lib/passport.js
@@ -0,0 +1,32 @@
+const passport = require('koa-passport');
+const LocalStrategy = require('passport-local').Strategy;
+const User = require('../models/user');
+
+// setup auth strategy
+passport.serializeUser(function(user, done) {
+ done(null, user.id);
+});
+
+passport.deserializeUser(function(id, done) {
+ User.findById(id, function (err, user) {
+ done(err, user);
+ });
+});
+
+passport.use(new LocalStrategy({
+ usernameField: 'email',
+ passwordField: 'password'
+}, function(email, password, done) {
+
+ if (!email) return done(null, false, { message: 'Please provide email.' });
+ if (!password) return done(null, false, { message: 'Please provide password.' });
+ User.findOne({email: email}, function(err, user) {
+ console.log(email, password, err, user);
+
+ if (err) return done(err);
+ if (!user) return done(null, false, { message: 'Non-registered email.' });
+ return user.checkPassword(password)
+ ? done(null, user)
+ : done(null, false, { message: 'Incorrect password.' });
+ });
+}));
diff --git a/hmvc/auth/models/user.js b/hmvc/auth/models/user.js
new file mode 100644
index 000000000..65c1d9a71
--- /dev/null
+++ b/hmvc/auth/models/user.js
@@ -0,0 +1,57 @@
+var mongoose = require('mongoose');
+var hash = require('../lib/hash');
+
+var Schema = mongoose.Schema;
+
+var schema = new Schema({
+ username: {
+ type: String,
+ required: true
+ },
+ email: {
+ type: String,
+ unique: true,
+ required: true,
+ index: true
+ },
+ salt: {
+ type: String,
+ required: true
+ },
+ passwordHash: {
+ type: String,
+ required: true
+ },
+ created: {
+ type: Date,
+ default: Date.now
+ },
+ avatar: {
+ type: String
+ }
+});
+
+schema.virtual('password')
+ .set(function(password) {
+ this._plainPassword = password;
+ this.salt = hash.createSalt();
+ this.passwordHash = hash.createHashSlow(password, this.salt);
+ })
+ .get(function() {
+ return this._plainPassword;
+ });
+
+schema.methods.checkPassword = function(password) {
+ return hash.createHashSlow(password, this.salt) == this.passwordHash;
+};
+
+schema.path('email').validate(function(value) {
+ // wrap in new RegExp instead of /.../, to evade WebStorm validation errors (buggy webstorm)
+ return new RegExp('^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,12}$').test(value);
+}, 'Укажите, пожалуйста, корретный email.');
+
+// all references using mongoose.model for safe recreation
+// when I recreate model (for tests) => I can reload it from mongoose.model (single source of truth)
+// exports are less convenient to update
+module.exports = mongoose.model('User', schema);
+
diff --git a/hmvc/auth/router.js b/hmvc/auth/router.js
new file mode 100644
index 000000000..dc44f0a68
--- /dev/null
+++ b/hmvc/auth/router.js
@@ -0,0 +1,14 @@
+var Router = require('koa-router');
+var form = require('./controller/form');
+var user = require('./controller/user');
+var local = require('./controller/login/local');
+var logout = require('./controller/logout');
+
+var router = module.exports = new Router();
+
+router.get('/form', form.get);
+router.get('/user', user.get);
+
+router.post('/login/local', local.post);
+
+router.post('/logout', logout.post);
diff --git a/hmvc/auth/templates/form.jade b/hmvc/auth/templates/form.jade
new file mode 100644
index 000000000..a2d69fc73
--- /dev/null
+++ b/hmvc/auth/templates/form.jade
@@ -0,0 +1,20 @@
+form.login-form
+ h2 Вход в систему
+
+ div
+ label Email
+ input(name="email" type="email")
+
+ div
+ label Пароль
+ input(name="password" type="password")
+
+ div
+ input(type="submit" value="Войти")
+
+
+form.register-form(style="display:none")
+ h2 Регистрация
+
+ p ...
+
diff --git a/hmvc/auth/test/.jshintrc b/hmvc/auth/test/.jshintrc
new file mode 100644
index 000000000..077663629
--- /dev/null
+++ b/hmvc/auth/test/.jshintrc
@@ -0,0 +1,23 @@
+{
+ "maxerr": 25,
+ "latedef": "nofunc",
+ "browser": false,
+ "node": true,
+ "indent": 2,
+ "camelcase": true,
+ "newcap": true,
+ "undef": true,
+ "esnext": true,
+ "multistr": true,
+ "noyield": true,
+ "expr": true,
+ "-W004": true,
+ "globals" : {
+ "describe" : false,
+ "it" : false,
+ "before" : false,
+ "beforeEach" : false,
+ "after" : false,
+ "afterEach" : false
+ }
+}
diff --git a/hmvc/auth/test/auth.js b/hmvc/auth/test/auth.js
new file mode 100644
index 000000000..66610fe41
--- /dev/null
+++ b/hmvc/auth/test/auth.js
@@ -0,0 +1,71 @@
+/* globals describe, it, before */
+
+const db = require('lib/dataUtil');
+const mongoose = require('mongoose');
+const path = require('path');
+const request = require('supertest');
+const fixtures = require(path.join(__dirname, './fixtures/db'));
+const app = require('app');
+const assert = require('better-assert');
+
+describe('Authorization', function() {
+
+ var agent;
+ before(function * () {
+ yield db.loadDb(path.join(__dirname, './fixtures/db'));
+ yield app.run();
+
+ agent = request.agent(app);
+
+ });
+
+ describe('login', function() {
+
+ it('should log me in', function(done) {
+ agent
+ .post('/auth/login/local')
+ .send({
+ email: fixtures.User[0].email,
+ password: fixtures.User[0].password
+ })
+ .expect(200)
+ .end(function(err, res) {
+// console.log(res.headers);
+ // sessionId = res.headers['set-cookie'][0];
+ done(err);
+ });
+ });
+
+ it('should return current user info', function(done) {
+ agent
+ .get('/auth/user')
+ .expect(200)
+ .end(function(err, res) {
+ res.body.email.should.be.eql(fixtures.User[0].email);
+ done(err);
+ });
+ });
+
+ });
+
+ 0 && describe('logout', function() {
+ it('should log me out', function(done) {
+ request(app)
+ .get('/api/logout')
+ .set('Cookie', sessionId)
+ .expect(200)
+ .end(function(err, res) {
+ done(err);
+ });
+ });
+ it('should return error because session is incorrected', function(done) {
+ request(app)
+ .get('/api/auth/user')
+ .set('Cookie', sessionId)
+ .expect(401)
+ .end(function(err, res) {
+ done(err);
+ });
+ });
+ });
+});
diff --git a/hmvc/auth/test/fixtures/db.js b/hmvc/auth/test/fixtures/db.js
new file mode 100644
index 000000000..fe8fba922
--- /dev/null
+++ b/hmvc/auth/test/fixtures/db.js
@@ -0,0 +1,20 @@
+exports.User = [
+ { "_id": "000000000000000000000001",
+ "created": new Date(2014,0,1),
+ "username": "ilya kantor",
+ "email": "iliakan@gmail.com",
+ "password": "123"
+ },
+ { "_id": "000000000000000000000002",
+ "created": new Date(2014,0,1),
+ "username": "tester",
+ "email": "tester@mail.com",
+ "password": "123"
+ },
+ { "_id": "000000000000000000000003",
+ "created": new Date(2014,0,1),
+ "username": "vasya",
+ "email": "vasya@mail.com",
+ "password": "123"
+ }
+];
\ No newline at end of file
diff --git a/hmvc/auth/test/unit/model/user.js b/hmvc/auth/test/unit/model/user.js
new file mode 100644
index 000000000..4de1a3391
--- /dev/null
+++ b/hmvc/auth/test/unit/model/user.js
@@ -0,0 +1,82 @@
+var app = require('app');
+var mongoose = require('config/mongoose');
+
+var dataUtil = require('lib/dataUtil');
+
+describe('User', function() {
+
+ var User = require('../../../models/user');
+
+ before(function* () {
+ yield dataUtil.createEmptyDb;
+ });
+
+ it('given bad email errors on save', function*() {
+ var user = new User({
+ email: "BAD",
+ username: "John",
+ password: "123"
+ });
+
+ user.persist()(function(err) {
+ err.name.should.equal('ValidationError');
+ err.errors.email.value.should.equal(user.get('email'));
+ });
+
+ });
+
+ it('requires password & email & username', function*() {
+ [
+ {
+ email: "my@gmail.com",
+ username: "John"
+ },
+ {
+ email: "my@gmail.com",
+ password: "John"
+ },
+ {
+ username: "John",
+ password: "****"
+ }
+ ].map(function(data) {
+ var user = new User(data);
+ user.persist()(function(err) {
+ err.name.should.equal('ValidationError');
+ });
+ });
+
+ });
+
+ it('autogenerates salt and hash', function* () {
+
+ var user = new User({
+ email: "a@b.ru",
+ username: "John",
+ password: "pass"
+ });
+
+ user.get('salt').should.not.be.empty;
+ user.get('passwordHash').should.not.be.empty;
+ user.checkPassword("pass").should.be.true;
+
+ });
+
+ it('requires unique email', function* () {
+
+ var data = {
+ username: "nonunique",
+ email: "nonunique@b.ru",
+ password: "pass"
+ };
+
+ yield new User(data).persist();
+ try {
+ yield new User(data).persist();
+ throw new Error("Same email is saved twice!");
+ } catch(err) {
+ err.code.should.equal(11000); // unique index is checked by mongo
+ }
+
+ });
+});
diff --git a/controllers/frontpage.js b/hmvc/frontpage/controller/frontpage.js
similarity index 67%
rename from controllers/frontpage.js
rename to hmvc/frontpage/controller/frontpage.js
index 8782b0332..f6f0296b6 100644
--- a/controllers/frontpage.js
+++ b/hmvc/frontpage/controller/frontpage.js
@@ -1,6 +1,6 @@
exports.get = function *get (next) {
- yield this.render('index', {
+ this.render(__dirname, 'index', {
title: 'Hello, world'
});
};
diff --git a/hmvc/frontpage/index.js b/hmvc/frontpage/index.js
new file mode 100644
index 000000000..cdfa18f19
--- /dev/null
+++ b/hmvc/frontpage/index.js
@@ -0,0 +1,4 @@
+
+var router = require('./router');
+
+exports.middleware = router.middleware();
diff --git a/hmvc/frontpage/router.js b/hmvc/frontpage/router.js
new file mode 100644
index 000000000..a5799db4d
--- /dev/null
+++ b/hmvc/frontpage/router.js
@@ -0,0 +1,8 @@
+var Router = require('koa-router');
+
+var frontpage = require('./controller/frontpage');
+
+var router = module.exports = new Router();
+
+router.get('/', frontpage.get);
+
diff --git a/views/index.jade b/hmvc/frontpage/templates/index.jade
old mode 100755
new mode 100644
similarity index 70%
rename from views/index.jade
rename to hmvc/frontpage/templates/index.jade
index 128334f63..382d24d71
--- a/views/index.jade
+++ b/hmvc/frontpage/templates/index.jade
@@ -1,9 +1,5 @@
-extends layout
+extends layouts/base
block content
h1= title
p Welcome to #{title}
-
-
-
-
diff --git a/hmvc/getpdf/controller/checkout.js b/hmvc/getpdf/controller/checkout.js
new file mode 100644
index 000000000..5d91e5223
--- /dev/null
+++ b/hmvc/getpdf/controller/checkout.js
@@ -0,0 +1,63 @@
+var mongoose = require('mongoose');
+var log = require('js-log')();
+var payments = require('payments');
+var Order = payments.Order;
+var OrderTemplate = payments.OrderTemplate;
+var methods = require('../paymentMethods').methods;
+
+log.debugOn();
+
+exports.post = function*(next) {
+
+ yield* this.loadOrder();
+ var method = methods[this.request.body.paymentMethod];
+ if (!method) {
+ this.throw(403, "Unsupported payment method");
+ }
+
+ if (this.order) {
+ log.debug("order exists", this.order.number);
+ yield* updateOrderFromBody(this.request.body, this.order);
+ } else {
+ // if we don't have the order in our database, then make a new one
+ // (use the incoming order post for that, but don't trust it)
+
+ console.log(this.request.body.orderTemplate);
+
+ var orderTemplate = yield OrderTemplate.findOne({
+ slug: this.request.body.orderTemplate
+ }).exec();
+
+ if (!orderTemplate) {
+ this.throw(404);
+ }
+
+ console.log("GOT TEMPLATE");
+ // create order from template, don't trust the incoming post
+ this.order = Order.createFromTemplate(orderTemplate, {
+ module: 'getpdf',
+ email: this.request.body.email
+ });
+
+ yield* updateOrderFromBody(this.request.body, this.order);
+
+ log.debug("order created", this.order.number);
+
+ if (!this.session.orders) {
+ this.session.orders = [];
+ }
+ this.session.orders.push(this.order.number);
+ }
+
+ var form = yield* payments.createTransactionForm(this.order, method.name);
+
+ this.body = form;
+
+};
+
+function* updateOrderFromBody(body, order) {
+ order.email = body.email;
+ order.markModified('data');
+
+ yield order.persist();
+}
diff --git a/hmvc/getpdf/controller/orders.js b/hmvc/getpdf/controller/orders.js
new file mode 100644
index 000000000..e545bd855
--- /dev/null
+++ b/hmvc/getpdf/controller/orders.js
@@ -0,0 +1,38 @@
+const payments = require('payments');
+var Order = payments.Order;
+var OrderTemplate = payments.OrderTemplate;
+var Transaction = payments.Transaction;
+
+exports.get = function*(next) {
+ this.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+
+ if (this.params.orderNumber) {
+ yield* this.loadOrder();
+ } else {
+
+ var orderTemplate = yield OrderTemplate.findOne({
+ slug: this.params.orderTemplate
+ }).exec();
+
+ if (!orderTemplate) {
+ this.throw(404);
+ }
+
+ this.locals.orderTemplate = this.params.orderTemplate;
+
+ // this order is not saved anywhere,
+ // it's only used to initially fill the form
+ // order.isNew = true!
+ this.order = Order.createFromTemplate(orderTemplate, {
+ module: 'getpdf',
+ email: Math.round(Math.random()*1e6).toString(36) + '@gmail.com'
+ });
+
+ }
+
+ this.locals.order = this.order;
+
+ this.locals.paymentMethods = require('../paymentMethods').methods;
+
+ this.render(__dirname, 'main');
+};
diff --git a/hmvc/getpdf/controller/payResult.js b/hmvc/getpdf/controller/payResult.js
new file mode 100644
index 000000000..dc0b8d50a
--- /dev/null
+++ b/hmvc/getpdf/controller/payResult.js
@@ -0,0 +1,79 @@
+const payments = require('payments');
+var Order = payments.Order;
+var OrderTemplate = payments.OrderTemplate;
+var Transaction = payments.Transaction;
+const escapeHtml = require('escape-html');
+
+/**
+ * 3 kinds of response
+ * 1) { status, successHtml } - if order success
+ * 2) { status, statusMessage(optional) } - if another status
+ * 3) "" - empty string if no information
+ * @param next
+ */
+exports.get = function*(next) {
+ this.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+
+ yield* this.loadOrder();
+
+
+ if (this.order.status == Transaction.STATUS_SUCCESS) {
+ this.body = {
+ status: Transaction.STATUS_SUCCESS,
+ html: 'Спасибо за покупку! Вот ваши ништяки.'
+ };
+ return;
+ }
+
+
+ var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec();
+
+ // no payment at all?!? strange
+ if (!lastTransaction) {
+ this.body = {
+ status: Transaction.STATUS_FAIL,
+ html: 'Оплаты не было.'
+ };
+ return;
+ }
+
+ // the order is not yet successful, but the last transaction is not,
+ // that's possible if order.onSuccess hook has not yet finished
+ // let's wait a little bit
+ if (lastTransaction.status == Transaction.STATUS_SUCCESS) {
+ this.body = '';
+ return;
+ }
+
+ // transaction status unknown
+ // -> it means we're awaiting a response from the payment system
+ if (!lastTransaction.status) {
+ this.body = '';
+ return;
+ }
+
+ if (lastTransaction.status == Transaction.STATUS_FAIL) {
+ this.body = {
+ status: lastTransaction.status,
+ html: 'Оплата не прошла.'
+ };
+
+ if (lastTransaction.statusMessage) {
+ this.body.html += '' + escapeHtml(lastTransaction.statusMessage) + '';
+ }
+ return;
+ }
+
+
+ if (lastTransaction.status == Transaction.STATUS_PENDING) {
+ this.body = {
+ status: lastTransaction.status,
+ html: 'Оплата ожидается.'
+ };
+
+ if (lastTransaction.statusMessage) {
+ this.body.html += '' + escapeHtml(lastTransaction.statusMessage) + '';
+ }
+ }
+
+};
diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js
new file mode 100644
index 000000000..362d503c3
--- /dev/null
+++ b/hmvc/getpdf/index.js
@@ -0,0 +1,6 @@
+
+var router = require('./router');
+
+exports.middleware = router.middleware();
+
+exports.onSuccess = require('./onSuccess');
diff --git a/hmvc/getpdf/onSuccess.js b/hmvc/getpdf/onSuccess.js
new file mode 100644
index 000000000..3eb9a7db3
--- /dev/null
+++ b/hmvc/getpdf/onSuccess.js
@@ -0,0 +1,33 @@
+const Transaction = require('payments').Transaction;
+const expiringDownload = require('expiring-download');
+
+const ExpiringDownloadLink = expiringDownload.ExpiringDownloadLink;
+const nodemailer = require('nodemailer');
+const ses = require('nodemailer-ses-transport');
+
+module.exports = function* (order) {
+
+ yield order.persist({
+ status: Transaction.STATUS_SUCCESS
+ });
+
+ // CREATE DOWNLOAD LINK
+
+ // EMAIL IT TO USER (move nodemailer to a separate site-wide "mail" module)
+ /*
+ var transporter = nodemailer.createTransport(ses({
+ accessKeyId: 'AWSACCESSKEY',
+ secretAccessKey: 'AWS/Secret/key'
+ }));
+ transporter.sendMail({
+ from: 'sender@address',
+ to: 'receiver@address',
+ subject: 'hello',
+ text: 'hello world!'
+ });
+*/
+
+ // ...
+
+ console.log("Order success: " + order.number);
+};
diff --git a/hmvc/getpdf/paymentMethods.js b/hmvc/getpdf/paymentMethods.js
new file mode 100644
index 000000000..27067fc7b
--- /dev/null
+++ b/hmvc/getpdf/paymentMethods.js
@@ -0,0 +1,6 @@
+exports.methods = {
+ 'yandexmoney': {name: "yandexmoney", title: "Яндекс.Деньги"},
+ 'webmoney': {name: "webmoney", title: "Webmoney"},
+ 'payanyway': {name: "payanyway", title: "PayAnyWay"},
+ 'paypal': {name: "paypal", title: "Paypal"}
+};
diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js
new file mode 100644
index 000000000..c956568c7
--- /dev/null
+++ b/hmvc/getpdf/router.js
@@ -0,0 +1,14 @@
+var Router = require('koa-router');
+
+var router = module.exports = new Router();
+
+var orders = require('./controller/orders');
+var payResult = require('./controller/payResult');
+var checkout = require('./controller/checkout');
+
+router.get('/:orderTemplate', orders.get);
+router.get('/orders/:orderNumber(\\d+)', orders.get);
+
+router.get('/pay-result/:orderNumber(\\d+)', payResult.get);
+
+router.post('/checkout', checkout.post);
diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade
new file mode 100644
index 000000000..bc9e33ea0
--- /dev/null
+++ b/hmvc/getpdf/templates/main.jade
@@ -0,0 +1,225 @@
+p.
+ TODO: доделать форму оплаты, как частный случай более сложных курсов
+ TODO: при выборе способа оплаты "позже" не надо перезагружать страницу, нужно менять состояние формы на ней
+
+
+style.
+
+ /* выбор оплаты - только для новых заказов или для неудавшейся оплаты */
+ .pay-chooser {
+ display: none;
+ }
+ form.order-form[data-new="1"] .pay-chooser {
+ display: block;
+ }
+ form.order-form[data-payment-state="fail"] .pay-chooser {
+ display: block;
+ }
+
+ /* индикатор загрузки - только для существующих заказов без статуса оплаты */
+ .pay-loading {
+ display: none;
+ }
+
+ form.order-form[data-new="0"]:not([data-payment-state]) .pay-loading {
+ display: block;
+ }
+
+ /* информация о проблеме с оплатой оплаты (над .pay-chooser) - только когда она есть */
+ .pay-fail {
+ display: none;
+ }
+
+ form.order-form[data-payment-state="fail"] .pay-fail {
+ display: block;
+ }
+
+ /* результат заказа оплаченного */
+ .order-result {
+ display: none;
+ }
+ form.order-form[data-payment-state="success"] .order-result,
+ form.order-form[data-payment-state="pending"] .order-result {
+ display: block;
+ }
+
+script var csrf = "#{csrf}";
+script var orderNumber = #{order.isNew ? order.number : 'null'};
+script var orderTemplate = "#{orderTemplate}";
+ // TODO
+
+
+div
+
+ form.order-form(data-new=(order.isNew ? "1" : "0"))
+
+ fieldset
+ legend Описание книги и стоимость
+ h2= order.title
+ div= order.description
+ div #{order.amount}р.
+
+ fieldset
+ legend Укажите свой email (если не авторизован)
+
+ input(name="email" value=order.email placeholder="E-mail" disabled=!order.isNew)
+
+ fieldset
+ legend Оплата или результат оплаты
+
+ .pay-loading Загружаем информацию об оплате...
+
+ .pay-fail
+
+ .pay-chooser
+ p Выберите способ оплаты:
+ select(name="paymentMethod")
+ each paymentMethod in paymentMethods
+ option(value=paymentMethod.name) #{paymentMethod.title}
+ input(type="submit" value="Оплатить")
+
+
+ .pay-result
+ legend Результат заказа при успешной оплате
+
+ .content
+
+
+script(src="http://code.jquery.com/jquery-2.1.1.js")
+
+script.
+ var orderForm = $('.order-form');
+ var payResult = $('.pay-result');
+
+ if (orderForm.data('new') == 0) {
+ var requestPayResultStart = new Date();
+ requestPayResult();
+ }
+
+ orderForm.on('submit', onPayFormSubmit);
+
+ function requestPayResult() {
+ if (new Date() - requestPayResultStart > 120e3) { // 2 mins
+ orderForm.attr('data-payment-state', 'timeout');
+ payResult.html("Таймаут ответа от платёжной системы. Попробуйте обновить эту страницу позже или обратиться в поддержку.");
+ return;
+ }
+
+ $.ajax({
+ method: 'GET',
+ url: '/getpdf/pay-result/' + orderForm[0].elements.orderNumber.value,
+ data: {
+ orderNumber: orderForm[0].elements.orderNumber.value
+ }
+ })
+ .done(function(result) {
+ if (!result) {
+ setTimeout(requestPayResult, 1000);
+ return;
+ }
+ showPayResult(result);
+ })
+ .fail(function(err) {
+ setTimeout(requestPayResult, 1000);
+ });
+ }
+
+ function showPayResult(result) {
+ orderForm.attr('data-payment-state', result.status);
+ switch(result.status) {
+ case 'success':
+ orderForm.find('.order-success').html(result.successHtml);
+ break;
+ case 'pending':
+ payResult.html('Оплата ожидается, о результате мы напишем по email');
+ break;
+ case 'fail':
+ $('.pay-fail').html(result.html);
+ break;
+ default:
+ throw new Error("Unknown payment status: " + result.status);
+ }
+ }
+
+ function onPayFormSubmit(e) {
+ e.preventDefault();
+ $.ajax({
+ method: 'POST',
+ url: '/getpdf/checkout',
+ data: {
+ _csrf: csrf,
+ orderNumber: this.elements.orderNumber.value,
+ orderTemplate: this.elements.orderTemplate.value,
+ email: this.elements.email.value,
+ paymentMethod: this.elements.paymentMethod.value
+ }
+ })
+ .fail(function(err) {
+ alert("Ошибка на сервере");
+ })
+ .done(function(htmlForm) {
+ $(htmlForm).submit();
+ });
+ }
+ "mailto:help@javascript.ru\">поддержку.");
+ return;
+ }
+
+ $.ajax({
+ method: 'GET',
+ url: '/getpdf/pay-result/' + payForm[0].elements.orderNumber.value,
+ data: {
+ orderNumber: payForm[0].elements.orderNumber.value
+ }
+ })
+ .done(function(result) {
+ if (!result) {
+ setTimeout(requestPayResult, 1000);
+ return;
+ }
+ showPayResult(result);
+ })
+ .fail(function(err) {
+ setTimeout(requestPayResult, 1000);
+ });
+ }
+
+ function showPayResult(result) {
+ payForm.attr('data-payment-state', result.status);
+ switch(result.status) {
+ case 'success':
+ payForm.find('.order-success').html(result.successHtml);
+ break;
+ case 'pending':
+ payResult.html('Оплата ожидается, о результате мы напишем по email');
+ break;
+ case 'fail':
+ var message = 'Оплата не прошла';
+ if (result.statusMessage) message += 'Ошибка: ' + result.statusMessage + '';
+ payResult.html(message);
+ break;
+ default:
+ throw new Error("Unknown payment status: " + result.status);
+ }
+ }
+
+ function onPayFormSubmit(e) {
+ e.preventDefault();
+ $.ajax({
+ method: 'POST',
+ url: '/getpdf/checkout',
+ data: {
+ _csrf: csrf,
+ orderNumber: this.elements.orderNumber.value,
+ orderTemplate: this.elements.orderTemplate.value,
+ email: this.elements.email.value,
+ paymentMethod: this.elements.paymentMethod.value
+ }
+ })
+ .fail(function(err) {
+ alert("Ошибка на сервере");
+ })
+ .done(function(htmlForm) {
+ $(htmlForm).submit();
+ });
+ }
diff --git a/hmvc/markup/controller/markup.js b/hmvc/markup/controller/markup.js
new file mode 100644
index 000000000..b63a5c833
--- /dev/null
+++ b/hmvc/markup/controller/markup.js
@@ -0,0 +1,8 @@
+var join = require('path').join;
+
+exports.get = function *get(next) {
+
+ var path = this.params[0];
+ this.render(__dirname, path);
+};
+
diff --git a/hmvc/markup/index.js b/hmvc/markup/index.js
new file mode 100644
index 000000000..cdfa18f19
--- /dev/null
+++ b/hmvc/markup/index.js
@@ -0,0 +1,4 @@
+
+var router = require('./router');
+
+exports.middleware = router.middleware();
diff --git a/hmvc/markup/router.js b/hmvc/markup/router.js
new file mode 100644
index 000000000..98798a4d0
--- /dev/null
+++ b/hmvc/markup/router.js
@@ -0,0 +1,8 @@
+var Router = require('koa-router');
+
+var markup = require('./controller/markup');
+
+var router = module.exports = new Router();
+
+router.get(/^\/([^.]*)$/, markup.get);
+
diff --git a/hmvc/markup/templates/blocks/article-foot.jade b/hmvc/markup/templates/blocks/article-foot.jade
new file mode 100644
index 000000000..6726fec7c
--- /dev/null
+++ b/hmvc/markup/templates/blocks/article-foot.jade
@@ -0,0 +1,7 @@
+footer.main__footer
+ time.main__footer-date(datetime="2010-12-03") 12.03.2010
+ span.main__footer-author
+ a(href="http://ikantor.moikrug.ru") Илья Кантор
+ span.main__footer-star
+
+ include ../blocks/social-inline
\ No newline at end of file
diff --git a/hmvc/markup/templates/blocks/breadcrumbs.jade b/hmvc/markup/templates/blocks/breadcrumbs.jade
new file mode 100644
index 000000000..a94baeaf3
--- /dev/null
+++ b/hmvc/markup/templates/blocks/breadcrumbs.jade
@@ -0,0 +1,9 @@
+ol.breadcrumbs
+ li.breadcrumbs__item
+ a.breadcrumbs__link(href='/') Главная
+ li.breadcrumbs__item
+ a.breadcrumbs__link(href='/tutorial') Учебник
+ li.breadcrumbs__item
+ a.breadcrumbs__link(href='/getting-started') Общая информация
+ li.breadcrumbs__item
+ | Введение в JavaScript
\ No newline at end of file
diff --git a/hmvc/markup/templates/blocks/comments.jade b/hmvc/markup/templates/blocks/comments.jade
new file mode 100644
index 000000000..613799d41
--- /dev/null
+++ b/hmvc/markup/templates/blocks/comments.jade
@@ -0,0 +1,30 @@
+.comments#comments
+ .comments__header
+ h2.comments__header-title
+ | Комментарии
+ span.comments__header-number 5
+ a.comments__header-write(href="#write-comment") Написать
+ ul
+ li Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
+ li Если ваш комментарий касается задачи — откройте её в отдельном окне и напишите там.
+ li
+ | Для кода внутри строки используйте тег
+ <code>
+ | , для блока кода — тег
+ <pre>
+ | , если больше 10 строк — ссылку на
+ песочницу
+ | .
+ li Если что-то непонятно — пишите, что именно и с какого места.
+//- Content
Content
"
+-var solution = [{title:"",content:"Текст
"}]
+
+section.important.important_ok
+ header.important__header
+ span.important__type Задание:
+ h3.important__title
+ a.important__task-link(href=url)
+ u!=title
+ span.important__importance(title="Насколько эта задача важна для освоения материала, от 1 до 5")
+ | Важность:
+ = ' ' + importance
+
+ .important__content
+ != content
+ each part in solution
+ .spoiler.closed
+ button.spoiler__button
+ u!= part.title || "Решение"
+ .spoiler__content
+ u!= part.content
+
diff --git a/hmvc/markup/templates/blocks/top-parts.jade b/hmvc/markup/templates/blocks/top-parts.jade
new file mode 100644
index 000000000..299c88205
--- /dev/null
+++ b/hmvc/markup/templates/blocks/top-parts.jade
@@ -0,0 +1,6 @@
+.top-part
+ .logo
+ a(href='http://javascript.ru/')
+ img(src='/img/logo.png', alt='Javascript.ru')
+ .user.dropdown.down-left.inherit-min-width
+ a.user__entrance(href='/user') Вход на сайт
diff --git a/hmvc/markup/templates/example/user.jade b/hmvc/markup/templates/example/user.jade
new file mode 100644
index 000000000..3970dae38
--- /dev/null
+++ b/hmvc/markup/templates/example/user.jade
@@ -0,0 +1,11 @@
+//- Статическое объявление переменных шаблона
+//- с описаниями, что есть что и зачем :)
+-var name = "Vasya"
+
+doctype html
+
+html
+ head
+ title my jade template
+ body
+ h1 Hello #{name}
diff --git a/hmvc/markup/templates/layouts/base.jade b/hmvc/markup/templates/layouts/base.jade
new file mode 100644
index 000000000..846013168
--- /dev/null
+++ b/hmvc/markup/templates/layouts/base.jade
@@ -0,0 +1,24 @@
+block variables
+include ../blocks/head
+body(id='#{self.bodyId}')
+ include ../blocks/top-parts
+ .page
+ .page__inner
+ include ../blocks/navbar
+ include ../blocks/social-aside
+ .main-content
+ article.main
+ header.main__header
+ include ../blocks/breadcrumbs
+ h1.main__header-title= self.title
+ span.main__header-comments
+ a(href='http://learn.javascript.ru/intro#disqus_thread') Комментариев #{self.comments.length}
+ include ../blocks/prev-next-top
+ block content
+ include ../blocks/article-foot
+ include ../blocks/prev-next-bottom
+ include ../blocks/corrector
+ include ../blocks/comments
+ include ../blocks/sidebar
+ include ../blocks/page-footer
+ include ../blocks/scripts
\ No newline at end of file
diff --git a/hmvc/markup/templates/pages/article.html b/hmvc/markup/templates/pages/article.html
new file mode 100644
index 000000000..17c245627
--- /dev/null
+++ b/hmvc/markup/templates/pages/article.html
@@ -0,0 +1,476 @@
+Давайте посмотрим, что такого особенного в JavaScript, почему именно он, и какие еще технологии существуют, кроме JavaScript.
+
+Что такое JavaScript?
+
+JavaScript изначально создавался для того, чтобы сделать web-странички «живыми».
+Программы на этом языке называются скриптами. Они подключаются напрямую к HTML и, как только загружается страничка — тут же выполняются.
+ECMAScript
+намылить новая вкладка песочница песочница plnkr.co документ архив таблица pdf-документ справка в MDN справка microsoft документация w3c спецификация ECMA
+ECMAScript
+намылить новая вкладка песочница песочница plnkr.co документ архив таблица pdf-документ справка в MDN справка microsoft документация w3c спецификация ECMA
+ECMAScript
+намылить новая вкладка песочница песочница plnkr.co документ архив таблица pdf-документ справка в MDN справка microsoft документация w3c спецификация ECMA
+Программы на JavaScript — обычный текст. Они не требуют какой-то специальной подготовки.
+А здесь мы приведем пример сочетания клавиш Cmd + R прямо в тексте урока.
+Примеры кода и результата, которые должны создаваться динамически:
+
+<div class="code-example">
+ <div class="codebox code-example__codebox">
+ <div class="toolbar codebox__toolbar">
+ <div class="toolbar__tool">
+ <a href="/play/abcdef" class="toolbar__button toolbar__button_run" title="выполнить">
+ <span class="toolbar__button-text">выполнить</span>
+ </a>
+ </div>
+ <div class="toolbar__tool">
+ <a href="/files/file.doc" class="toolbar__button toolbar__button_edit" title="редактировать">
+ <span class="toolbar__button-text">редактировать</span>
+ </a>
+ </div>
+ </div>
+<pre class="language-javascript line-numbers">function sayHi(name) {
+ var phrase = "Привет, " + name;
+ alert(phrase);
+}
+
+sayHi('Вася');</pre>
+ </div>
+
+ <div class="result code-example__result">
+ <div class="toolbar result__toolbar">
+ <div class="toolbar__tool">
+ <a href="/files/file.doc" class="toolbar__button toolbar__button_edit" title="редактировать">
+ <span class="toolbar__button-text">редактировать</span>
+ </a>
+ </div>
+ </div>
+ <iframe class="result__iframe" src="http://sass-lang.com/documentation/Sass/Script/Functions"></iframe>
+ </div>
+</div>
+
+Пример кода
+
+function sayHi(name) {
+ var phrase = "Привет, " + name;
+ alert(phrase);
+}
+
+function HelloWorld(world) {
+ alert('Hello, ' + world);
+}
+
+HelloWorld('World');
+
+sayHi('Вася');
+
+Пример результата без кода, такой код должен создаваться динамически в js
+<div class="code-example">
+ <div class="result code-example__result">
+ <div class="toolbar result__toolbar">
+ <div class="toolbar__tool">
+ <a href="/files/file.doc" class="toolbar__button toolbar__button_edit" title="редактировать">
+ <span class="toolbar__button-text">редактировать</span>
+ </a>
+ </div>
+ </div>
+ <iframe class="result__iframe" src="http://sass-lang.com/documentation/Sass/Script/Functions"></iframe>
+ </div>
+</div>
+
+В этом плане JavaScript сильно отличается от другого языка, который называется Java.
+
+Выделенный блок с информацией.
+
+
+
+
+ Почему JavaScript?
+
+
+ Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.
+ Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.
+ У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся.
+
+
+
+
+
+
+
+ XMLHttpRequest
+ IFRAME
+ SCRIPT
+ EventSource
+ WebSocket
+
+
+ Кросс-доменность
+ да, кроме IE<10x1
+ да, сложности в IE<8i1
+ да
+ да
+ да
+
+
+ Методы
+ Любые
+ GET / POST
+ GET
+ GET
+ Свой протокол
+
+
+ COMET
+ Длинные опросыx2
+ Непрерывное соединение
+ Длинные опросы
+ Непрерывное соединение
+ Непрерывное соединение в обе стороны
+
+
+ Поддержка
+ Все браузеры, ограничения в IE<10x3
+ Все браузеры
+ Все браузеры
+ Кроме IE
+ IE 10, FF11, Chrome 16, Safari 6, Opera 12.5w1
+
+
+
+
+Выделенный блок c предупреждением
+
+
+
+ Важно:
+ Почему JavaScript?
+
+
+ Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.
+ Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.
+ У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся.
+
+
+
+Выделенный блок c вопросом
+
+
+
+ Вопрос:
+ Почему JavaScript?
+
+
+ Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.
+ Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.
+ У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся.
+
+
+
+Выделенный блок c ответом
+
+
+
+
+ Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.
+ Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.
+ У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся в песочнице.
+
+
+
+Чтобы читать и выполнять текст на JavaScript, нужна специальная программа — интерпретатор. Процесс выполнения скрипта называют «интерпретацией».
+
+
+
+ На заметку:
+ Компиляция и интерпретация, для программистов
+
+
+ Строго говоря, для выполнения программ существуют «компиляторы» и «интерпретаторы».
+ Когда-то между ними была большая разница. Компиляторы преобразовывали программу в машинный код, который потом можно выполнять. А интерпретаторы — просто выполняли.
+ Сейчас разница гораздо меньше. Современные интерпретаторы перед выполнением преобразуют JavaScript в машинный код (или близко к нему), чтобы выполнялся он быстрее. То есть, по сути, компилируют, а затем запускают.
+
+
+
+Во все основные браузеры встроен интерпретатор JavaScript, именно поэтому они могут выполнять скрипты на странице.
+
+Но, разумеется, этим возможности JavaScript не ограничены. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор.
+
+Что умеет JavaScript?
+
+Современный JavaScript — это «безопасный» язык программирования общего назначения. Он не предоставляет низкоуровневых средств работы с памятью, процессором, так как изначально был ориентирован на браузеры, в которых это не требуется.
+В браузере JavaScript умеет делать все, что относится к манипуляции со страницей, взаимодействию с посетителем и, в какой-то мере, с сервером:
+
+ - Создавать новые HTML-теги, удалять существующие, менять стили элементов, прятать, показывать элементы и т.п.
+ - Реагировать на действия посетителя, обрабатывать клики мыши, перемещение курсора, нажатие на клавиатуру и т.п.
+ - Посылать запросы на сервер и загружать данные без перезагрузки страницы(эта технология называется "AJAX").
+ - Получать и устанавливать cookie, запрашивать данные, выводить сообщения…
+ - …и многое, многое другое!
+
+
+Что НЕ умеет JavaScript?
+JavaScript — быстрый и мощный язык, но браузер накладывает на его исполнение некоторые ограничения.
+Это сделано для безопасности пользователей, чтобы злоумышленник не мог с помощью JavaScript получить личные данные или как-то навредить компьютеру пользователя. В браузере Firefox существует способ «подписи» скриптов с целью обхода части ограничений, но он нестандартный и не кросс-браузерный.
+
+
+
+ Задание:
+ Вызов «на месте»
+ Важность: 4
+
+
+ Каков будет результат выполнения кода? Почему?
+
+ var a = 5
+
+(function() {
+alert(a)
+})()
+
+
+ P.S. Подумайте хорошо! Здесь все ошибаются!
+ P.P.S. Внимание, здесь подводный камень! Ок, вы предупреждены.
+
+
+
+
+ Результат - ошибка. Попробуйте:
+
+ var a = 5
+
+(function() {
+alert(a)
+})()
+
+ Дело в том, что после var a = 5 нет точки с запятой.
+ JavaScript воспринимает этот код как если бы перевода строки не было:
+
+
+ var a = 5(function() {
+alert(a)
+})()
+
+ То есть, он пытается вызвать функцию 5, что и приводит к ошибке.
+ Если точку с запятой поставить, все будет хорошо:
+
+ var a = 5;
+
+(function() {
+alert(a)
+})()
+
+ Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто не ставит точки с запятой.
+
+
+
+
+
+ К одной задаче могут быть добавлены одно или несколько решений. Решения, идущие подряд, «стыкуются» без промежутков
+
+
+
+
+
+Спойлеры вне блока .important
+
+
+
+
+ Результат - ошибка. Попробуйте:
+
+ var a = 5
+
+(function() {
+alert(a)
+})()
+
+ Дело в том, что после var a = 5 нет точки с запятой.
+ JavaScript воспринимает этот код как если бы перевода строки не было:
+
+
+ var a = 5(function() {
+alert(a)
+})()
+
+ То есть, он пытается вызвать функцию 5, что и приводит к ошибке.
+ Если точку с запятой поставить, все будет хорошо:
+
+ var a = 5;
+
+(function() {
+alert(a)
+})()
+
+ Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто не ставит точки с запятой.
+
+
+
+
+
+ К одной задаче могут быть добавлены одно или несколько решений. Решения, идущие подряд, «стыкуются» без промежутков
+
+
+
+Этих ограничений нет там, где JavaScript используется вне браузера, например на сервере.
+Большинство возможностей JavaScript в браузере ограничено текущим окном и страницей.
+
+
+ -
+
JavaScript не может читать/записывать произвольные файлы на жесткий диск, копировать их или вызывать программы. Он не имеет прямого доступа к операционной системе.
+ Современные браузеры могут работать с файлами, но эта возможность ограничена специально выделенной директорией — песочницей. Возможности по доступу к устройствам также прорабатываются в современных стандартах и, частично, доступны в некоторых браузерах.
+ -
+
JavaScript, работающий в одной вкладке, почти не может общаться с другими вкладками и окнами. За исключением случая, когда он сам открыл это окно или несколько вкладок из одного источника (одинаковый домен, порт, протокол).
+ Есть способы это обойти, и они раскрыты в учебнике, но для этого требуется как минимум явное согласие обеих сторон. Просто так взять и залезть в произвольную вкладку с другого домена нельзя.
+
+ -
+
Из JavaScript можно легко посылать запросы на сервер, с которого пришла страничка. Запрос на другой домен тоже возможен, но менее удобен, т.к. и здесь есть ограничения безопасности.
+
+
+
+В чем уникальность JavaScript?
+Есть как минимум три замечательных особенности JavaScript:
+
+
+
+
+
+ Полная интеграция с HTML/CSS. function f (x, y, z) { return x + y + z; }
+ Простые вещи делаются просто.
+
function sayHi(name) {
+ var phrase = "Привет, " + name;
+ alert(phrase);
+}
+
+sayHi('Вася');
+ Поддерживается всеми распространенными браузерами и включен по умолчанию.
+
+
+
+
+
+Вариант блока с достоинствами и недостатками
+
+
+
+
+ Достоинства
+
+ Полная интеграция с HTML/CSS.
+ Простые вещи делаются просто.
+ Поддерживается всеми распространенными браузерами и включен по умолчанию.
+
+
+
+ Недостатки
+
+ Полная интеграция с HTML/CSS.
+ Простые вещи делаются просто.
+ Поддерживается всеми распространенными браузерами и включен по умолчанию.
+
+
+
+
+
+Этих трех вещей одновременно нет больше ни в одной браузерной технологии. Поэтому JavaScript и является самым распространенным средством создания браузерных интерфейсов.
+Пример блока "balance" с узким контентом, который может вызвать ошибку
+
+
+
+
+ Достоинства
+
+ -
+
Простота реализации.
+
+
+
+
+ Недостатки
+
+ -
+
Задержки между событием и уведомлением.
+
+ -
+
Лишний трафик и запросы на сервер.
+
+
+
+
+
+
+
+
+ Тенденции развития.
+ Перед тем, как вы планируете изучить новую технологию, полезно ознакомиться с ее развитием и перспективами. Здесь в JavaScript все более чем хорошо.
+
+ HTML 5
+ HTML 5 — эволюция стандарта HTML, добавляющая новые теги и, что более важно, ряд новых возможностей браузерам.
+
+
+
+
+ Здесь может быть что угодно
+
+
+Вот несколько примеров:
+
+ - Чтение/запись файлов на диск (в специальной «песочнице», то есть не любые).
+ - Встроенная в браузер база данных, которая позволяет хранить данные на компьютере пользователя.
+ - Многозадачность с одновременным использованием нескольких ядер процессора.
+ - Проигрывание видео/аудио, без Flash.
+ - 2d и 3d-рисование с аппаратной поддержкой, как в современных играх.
+
+Многие возможности HTML5 все еще в разработке, но браузеры постепенно начинают их поддерживать.
+
+ Тенденция: JavaScript становится все более и более мощным и возможности браузера растут в сторону десктопных приложений.
+
+
+EcmaScript
+Сам язык JavaScript улучшается. Современный стандарт EcmaScript 5 включает в себя новые возможности для разработки.
+Современные браузеры улучшают свои движки, чтобы увеличить скорость исполнения JavaScript, исправляют баги и стараются следовать стандартам.
+
+
+ Тенденция: JavaScript становится все быстрее и стабильнее.
+
+
+Очень важно то, что новые стандарты HTML5 и ECMAScript сохраняют максимальную совместимость с предыдущими версиями. Это позволяет избежать неприятностей с уже существующими приложениями.
+Впрочем, небольшая проблема с HTML5 все же есть. Иногда браузеры стараются включить новые возможности, которые еще не полностью описаны в стандарте, но настолько интересны, что разработчики просто не могут ждать.
+…Однако, со временем стандарт меняется и браузерам приходится подстраиваться к нему, что может привести к ошибкам в уже написанном (старом) коде. Поэтому следует дважды подумать перед тем, как применять на практике такие «супер-новые» решения.
+При этом все браузеры сходятся к стандарту, и различий между ними уже гораздо меньше, чем всего лишь несколько лет назад.
+
+
+ Тенденция: все идет к полной совместимости со стандартом.
+
+
+
+Недостатки JavaScript
+Зачастую, недостатки подходов и технологий — это обратная сторона их полезности. Стоит ли упрекать молоток в том, что он — тяжелый? Да, неудобно, зато гвозди забиваются лучше.
+
+Заголовок третьего уровня
+В JavaScript, однако, есть вполне объективные недоработки, связанные с тем, что язык, по выражению его автора (Brendan Eich) делался «за 10 бессонных дней и ночей». Поэтому некоторые моменты продуманы плохо, есть и откровенные ошибки (которые признает тот же Brendan).
+Конкретные примеры мы увидим в дальнейшем, т.к. их удобнее обсуждать в процессе освоения языка.
+
+Заголовок четвертого уровня
+Пока что нам важно знать, что некоторые «странности» языка не являются чем-то очень умным, а просто не были достаточно хорошо продуманы в своё время. В этом учебнике мы будем обращать особое внимание на основные недоработки и «грабли». Ничего критичного в них нет, если знаешь — не наступишь.
+В новых версиях JavaScript (ECMAScript) эти недостатки постепенно убирают. Процесс внедрения небыстрый, в первую очередь из-за старых версий IE, но они постепенно вымирают. Современный IE в этом отношении несравнимо лучше.
diff --git a/hmvc/markup/templates/pages/article.jade b/hmvc/markup/templates/pages/article.jade
new file mode 100644
index 000000000..2ce0bd653
--- /dev/null
+++ b/hmvc/markup/templates/pages/article.jade
@@ -0,0 +1,12 @@
+extends ../layouts/base
+
+// - http://stackoverflow.com/questions/12646451/how-to-pass-variables-between-jade-templates
+block variables
+ - var self = {};
+ - self.bodyId = 'page';
+ - self.title = 'Учебник — Javascript.ru';
+ - self.comments = {} // хм?
+ - self.comments.lenght = 5;
+
+block content
+ include ./article.html
diff --git a/hmvc/markup/templates/pages/form.jade b/hmvc/markup/templates/pages/form.jade
new file mode 100644
index 000000000..588d99efd
--- /dev/null
+++ b/hmvc/markup/templates/pages/form.jade
@@ -0,0 +1,4 @@
+form(method="POST",action="/test")
+ input(type="hidden",name="name",value="value")
+ input(type="submit",value="submit")
+
diff --git a/hmvc/markup/templates/pages/my.html b/hmvc/markup/templates/pages/my.html
new file mode 100644
index 000000000..9db1c64de
--- /dev/null
+++ b/hmvc/markup/templates/pages/my.html
@@ -0,0 +1 @@
+Hello
diff --git a/hmvc/payments/bank-simple/index.js b/hmvc/payments/bank-simple/index.js
new file mode 100644
index 000000000..ad9f729eb
--- /dev/null
+++ b/hmvc/payments/bank-simple/index.js
@@ -0,0 +1,4 @@
+
+// TODO
+
+exports.renderForm = require('./renderForm');
diff --git a/hmvc/payments/bank-simple/renderForm.js b/hmvc/payments/bank-simple/renderForm.js
new file mode 100644
index 000000000..0a6f6d526
--- /dev/null
+++ b/hmvc/payments/bank-simple/renderForm.js
@@ -0,0 +1,15 @@
+const jade = require('jade');
+const config = require('config');
+const path = require('path');
+
+module.exports = function* (transaction) {
+
+ return jade.renderFile(path.join(__dirname, 'templates/form.jade'), {
+ amount: transaction.amount,
+ number: transaction.number,
+ webmoney: config.payments.modules.webmoney
+ });
+
+};
+
+
diff --git a/hmvc/payments/bank-simple/templates/form.jade b/hmvc/payments/bank-simple/templates/form.jade
new file mode 100644
index 000000000..8ad6d67ce
--- /dev/null
+++ b/hmvc/payments/bank-simple/templates/form.jade
@@ -0,0 +1,7 @@
+form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp")
+ input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount)
+ input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64'))
+ input(type="hidden",name="LMI_PAYMENT_NO",value=number)
+ input(type="hidden",name="LMI_PAYEE_PURSE",value=webmoney.purse)
+ input(type="hidden",name="LMI_SIM_MODE",value=(isTest ? 1 : 0))
+ input(type="submit",value="Оплатить")
diff --git a/hmvc/payments/bank-simple/test/.jshintrc b/hmvc/payments/bank-simple/test/.jshintrc
new file mode 100644
index 000000000..077663629
--- /dev/null
+++ b/hmvc/payments/bank-simple/test/.jshintrc
@@ -0,0 +1,23 @@
+{
+ "maxerr": 25,
+ "latedef": "nofunc",
+ "browser": false,
+ "node": true,
+ "indent": 2,
+ "camelcase": true,
+ "newcap": true,
+ "undef": true,
+ "esnext": true,
+ "multistr": true,
+ "noyield": true,
+ "expr": true,
+ "-W004": true,
+ "globals" : {
+ "describe" : false,
+ "it" : false,
+ "before" : false,
+ "beforeEach" : false,
+ "after" : false,
+ "afterEach" : false
+ }
+}
diff --git a/hmvc/payments/index.js b/hmvc/payments/index.js
new file mode 100644
index 000000000..8907bb8c4
--- /dev/null
+++ b/hmvc/payments/index.js
@@ -0,0 +1,64 @@
+var mount = require('koa-mount');
+var config = require('config');
+var compose = require('koa-compose');
+
+// Interaction with payment systems only.
+
+exports.loadOrder = require('./lib/loadOrder');
+exports.loadTransaction = require('./lib/loadTransaction');
+
+var Order = exports.Order = require('./models/order');
+var OrderTemplate = exports.OrderTemplate = require('./models/orderTemplate');
+var Transaction = exports.Transaction = require('./models/transaction');
+var TransactionLog = exports.TransactionLog = require('./models/transactionLog');
+
+// all submodules
+var paymentModules = {};
+for(var name in config.payments.modules) {
+ paymentModules[name] = require('./' + name);
+}
+
+// mount('/webmoney', webmoney.middleware())
+var paymentMounts = [];
+for(var name in paymentModules) {
+ paymentMounts.push(mount('/' + name, paymentModules[name].middleware));
+}
+
+// delegate all HTTP calls to payment modules
+exports.middleware = compose(paymentMounts);
+
+
+exports.populateContextMiddleware = function*(next) {
+ this.redirectToOrder = function(order) {
+ order = order || this.order;
+ this.redirect('/' + order.module + '/orders/' + order.number);
+ };
+ this.loadOrder = exports.loadOrder;
+ this.loadTransaction = exports.loadTransaction;
+ yield* next;
+};
+
+// creates transaction and returns the form to submit for its payment
+// delegates form to the method
+exports.createTransactionForm = function* (order, method) {
+
+ var transaction = new Transaction({
+ order: order._id,
+ amount: order.amount,
+ module: method
+ });
+
+ yield transaction.persist();
+
+ console.log(transaction);
+
+ var form = yield* paymentModules[method].renderForm(transaction);
+
+ yield transaction.log('form', form);
+
+ return form;
+
+};
+
+
+
diff --git a/hmvc/payments/lib/loadOrder.js b/hmvc/payments/lib/loadOrder.js
new file mode 100644
index 000000000..f71e937ca
--- /dev/null
+++ b/hmvc/payments/lib/loadOrder.js
@@ -0,0 +1,32 @@
+var mongoose = require('mongoose');
+var Order = require('../models/order');
+var assert = require('assert');
+
+// Populates this.order with the order by "orderNumber" parameter
+module.exports = function* (field) {
+
+ if (!field) field = 'orderNumber';
+
+ var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field];
+
+ if (!orderNumber) {
+ return;
+ }
+
+ var order = yield Order.findOne({number: orderNumber}).populate('order').exec();
+
+ if (!order) {
+ this.throw(404, 'Нет такого заказа');
+ }
+
+ // todo: add belongs to check (with auth)
+ if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) {
+ this.throw(403, 'Заказ в сессии не найден');
+ }
+
+
+ assert(!this.order, "this.order is already set (by loadTransaction?)");
+
+ this.order = order;
+
+};
diff --git a/hmvc/payments/lib/loadTransaction.js b/hmvc/payments/lib/loadTransaction.js
new file mode 100644
index 000000000..2603a7c34
--- /dev/null
+++ b/hmvc/payments/lib/loadTransaction.js
@@ -0,0 +1,38 @@
+var mongoose = require('mongoose');
+var Transaction = require('../models/transaction');
+var log = require('js-log')();
+var assert = require('assert');
+
+// Populates this.transaction with the transaction by "transactionNumber" parameter
+// options.skipOwnerCheck is for signed submissions, set to true allows anyone to load transaction
+module.exports = function* (field, options) {
+ options = options || {};
+ if (!field) field = 'transactionNumber';
+
+ var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field];
+
+ log.debug('tx number: ' + transactionNumber);
+ if (!transactionNumber) {
+ return;
+ }
+
+ var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec();
+
+ if (!transaction) {
+ this.throw(404, 'Нет такой транзакции');
+ }
+
+ if (!options.skipOwnerCheck) {
+ // todo: add belongs to check (with auth)
+ if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) {
+ this.throw(403, 'Не найден заказ в сессии для этой транзакции');
+ }
+ }
+
+ assert(!this.transaction, "this.transaction is already set");
+ assert(!this.order, "this.order is already set");
+
+ this.transaction = transaction;
+ this.order = transaction.order;
+
+};
diff --git a/hmvc/payments/models/order.js b/hmvc/payments/models/order.js
new file mode 100644
index 000000000..0618939c2
--- /dev/null
+++ b/hmvc/payments/models/order.js
@@ -0,0 +1,60 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+var autoIncrement = require('mongoose-auto-increment');
+var OrderTemplate = require('./orderTemplate');
+var _ = require('lodash');
+
+var schema = new Schema({
+ amount: {
+ type: Number,
+ required: true
+ },
+ module: { // module so that transaction handler knows where to go back e.g. 'getpdf'
+ type: String,
+ required: true
+ },
+ title: {
+ type: String,
+ required: true
+ },
+ description: {
+ type: String
+ },
+ status: {
+ type: String
+ },
+
+ // order can be bound to either an email or a user
+ email: {
+ type: String
+ },
+ user: {
+ type: Schema.Types.ObjectId,
+ ref: 'User'
+ },
+
+ data: Schema.Types.Mixed,
+ created: {
+ type: Date,
+ default: Date.now
+ }
+});
+
+schema.statics.createFromTemplate = function(orderTemplate, body) {
+ var Order = this;
+
+ var data = _.assign({
+ title: orderTemplate.title,
+ description: orderTemplate.description,
+ amount: orderTemplate.amount,
+ data: {}
+ }, body || {});
+
+ return new Order(data);
+
+};
+
+schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number', startAt: 1});
+
+module.exports = mongoose.model('Order', schema);
+
diff --git a/hmvc/payments/models/orderTemplate.js b/hmvc/payments/models/orderTemplate.js
new file mode 100644
index 000000000..04626e66a
--- /dev/null
+++ b/hmvc/payments/models/orderTemplate.js
@@ -0,0 +1,37 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+/**
+ * In other words, "Store items"
+ * New orders do *not* reference the items, because store items may change
+ * Instead new orders contain full information about themselves.
+ *
+ * OrderTemplate can be deleted, but the order is self-contained.
+ * @type {Schema}
+ */
+var schema = new Schema({
+ title: {
+ type: String,
+ required: true
+ },
+ description: {
+ type: String
+ },
+ // when a user visits /order/slug, the new order is created from this template
+ slug: {
+ type: String,
+ required: true,
+ index: true
+ },
+ amount: {
+ type: Number,
+ required: true
+ },
+ created: {
+ type: Date,
+ default: Date.now
+ }
+});
+
+module.exports = mongoose.model('OrderTemplate', schema);
+
diff --git a/hmvc/payments/models/transaction.js b/hmvc/payments/models/transaction.js
new file mode 100644
index 000000000..98ae34f11
--- /dev/null
+++ b/hmvc/payments/models/transaction.js
@@ -0,0 +1,132 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+var autoIncrement = require('mongoose-auto-increment');
+var Order = require('./order');
+var TransactionLog = require('./transactionLog');
+
+/**
+ * Transaction is an actual payment attempt (successful or not) for something
+ * - Order may exist without any transactions (pay later)
+ * - Transaction has it's own separate number (payment attempt)
+ * - Transaction amount can be different from order amount (partial payment)
+ * - Every transaction save generates a log record
+ * @type {Schema}
+ */
+var schema = new Schema({
+ order: {
+ type: Schema.Types.ObjectId,
+ ref: 'Order'
+ },
+ amount: {
+ type: Number,
+ required: true
+ },
+ module: {
+ type: String,
+ required: true
+ },
+ created: {
+ type: Date,
+ default: Date.now
+ },
+ status: {
+ type: String
+ },
+ statusMessage: {
+ type: String
+ }
+});
+
+schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number', startAt: 1});
+
+schema.statics.STATUS_SUCCESS = 'success';
+schema.statics.STATUS_PENDING = 'pending';
+schema.statics.STATUS_FAIL = 'fail';
+
+/*
+// DEPRECATED: orderModule.onSuccess updates its status
+// autoupdate order to SUCCESS when succeeded
+schema.pre('save', function(next) {
+ if (this.status == Transaction.STATUS_SUCCESS) {
+ var orderId = this.order._id || this.order;
+ Order.findByIdAndUpdate(orderId, {status: Transaction.STATUS_SUCCESS}, next);
+ } else {
+ next();
+ }
+});
+*/
+
+// autolog all changes
+schema.pre('save', function(next) {
+
+ var log = new TransactionLog({
+ transaction: this._id,
+ event: 'save',
+ data: {
+ status: this.status,
+ statusMessage: this.statusMessage,
+ amount: this.amount
+ }
+ });
+
+ log.save(function(err, doc) {
+ next(err);
+ });
+});
+/*
+schema.methods.getStatusDescription = function() {
+ if (this.status == Transaction.STATUS_SUCCESS) {
+ return 'Оплата прошла успешно.';
+ }
+ if (this.status == Transaction.STATUS_PENDING) {
+ return 'Оплата ожидается, о её успешномо окончании вы будете извещены по e-mail.';
+ }
+
+ if (this.status == Transaction.STATUS_FAIL) {
+ var result = 'Оплата не прошла';
+ if (this.statusMessage) result += ': ' + this.statusMessage;
+ return result + '.';
+ }
+
+ if (!this.status) {
+ return 'нет информации об оплате';
+ }
+
+ throw new Error("неподдерживаемый статус транзакции");
+};
+*/
+
+schema.methods.logRequest = function*(event, request) {
+ yield this.log(event, {url: request.originalUrl, body: request.body});
+};
+
+// log anything related to the transaction
+schema.methods.log = function*(event, data) {
+
+ if (typeof event != "string") {
+ throw new Error("event name must be a string");
+ }
+
+ var options = {
+ transaction: this._id,
+ event: event,
+ data: data
+ };
+
+ // for complex objects -> prior to logging make them simple (must be jsonable)
+ // e.g for HTTP response (HTTP.IncomingMessage)
+ if (options.data && typeof options.data == 'object') {
+ // object keys may not contain "." in mongodb, so I may not store arbitrary objects
+ // only json can help
+ options.data = JSON.stringify(options.data);
+ }
+
+// console.log(options);
+
+ var log = new TransactionLog(options);
+ yield log.persist();
+};
+
+/* jshint -W003 */
+var Transaction = module.exports = mongoose.model('Transaction', schema);
+
diff --git a/hmvc/payments/models/transactionLog.js b/hmvc/payments/models/transactionLog.js
new file mode 100644
index 000000000..0ef24a5a8
--- /dev/null
+++ b/hmvc/payments/models/transactionLog.js
@@ -0,0 +1,24 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var schema = new Schema({
+
+ transaction: {
+ type: Schema.Types.ObjectId,
+ ref: 'Transaction',
+ index: true
+ },
+ event: {
+ type: String,
+ index: true
+ },
+ data: Schema.Types.Mixed,
+
+ created: {
+ type: Date,
+ default: Date.now
+ }
+});
+
+module.exports = mongoose.model('TransactionLog', schema);
+
diff --git a/hmvc/payments/payanyway/controller/callback.js b/hmvc/payments/payanyway/controller/callback.js
new file mode 100644
index 000000000..c0d80df54
--- /dev/null
+++ b/hmvc/payments/payanyway/controller/callback.js
@@ -0,0 +1,75 @@
+const config = require('config');
+//require('config/mongoose');
+const payanywayConfig = config.payments.modules.payanyway;
+const Order = require('../../models/order');
+const Transaction = require('../../models/transaction');
+const log = require('js-log')();
+const md5 = require('MD5');
+
+log.debugOn();
+
+exports.post = function* (next) {
+
+ checkSignature(this.request.body);
+ this.body = 'SUCCESS';
+ return;
+
+ yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true});
+
+
+ yield this.transaction.logRequest('callback unverified', this.request);
+
+ if (!checkSignature(this.request.body)) {
+ log.debug("wrong signature");
+ this.throw(403, "wrong signature");
+ }
+
+ yield this.transaction.logRequest('callback', this.request);
+
+ // signature is valid, so everything MUST be fine
+ if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) ||
+ this.request.body.MNT_ID != payanywayConfig.id) {
+ yield this.transaction.persist({
+ status: Transaction.STATUS_FAIL,
+ statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой"
+ });
+ this.throw(404, "transaction data doesn't match the POST body");
+ }
+
+ yield this.transaction.persist({
+ status: Transaction.STATUS_SUCCESS
+ });
+
+ var order = this.order;
+ log.debug("will call order onSuccess module=" + order.module);
+ yield* require(order.module).onSuccess(order);
+
+ this.body = 'SUCCESS';
+};
+
+function checkSignature(body) {
+
+ var signature = body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_AMOUNT +
+ body.MNT_CURRENCY_CODE + (body.MNT_SUBSCRIBER_ID || '') + (+body.MNT_TEST_MODE ? '1' : '0') + payanywayConfig.secret;
+
+ console.log(signature);
+ signature = md5(signature);
+
+ console.log(signature);
+ return signature == body.MNT_SIGNATURE;
+}
+
+/*
+var body ={ MNT_ID: '31873866',
+ MNT_TRANSACTION_ID: '12',
+ MNT_OPERATION_ID: '55923826',
+ MNT_AMOUNT: '1.00',
+ MNT_CURRENCY_CODE: 'RUB',
+ MNT_TEST_MODE: '0',
+ MNT_SIGNATURE: 'ebf8d4b9fa10301b858cc314b356cc41',
+ 'paymentSystem.unitId': '822360',
+ MNT_CORRACCOUNT: '15598507',
+ qiwiphone: '9035419441' }
+
+console.log(+checkSignature(body));
+*/
diff --git a/hmvc/payments/payanyway/controller/cancel.js b/hmvc/payments/payanyway/controller/cancel.js
new file mode 100644
index 000000000..9c366a4fa
--- /dev/null
+++ b/hmvc/payments/payanyway/controller/cancel.js
@@ -0,0 +1,17 @@
+const Transaction = require('../../models/transaction');
+const log = require('js-log')();
+
+
+exports.get = function* (next) {
+
+ yield* this.loadTransaction('MNT_TRANSACTION_ID');
+
+ this.transaction.persist({
+ status: Transaction.STATUS_FAIL,
+ statusMessage: 'отказ от оплаты'
+ });
+
+ this.redirectToOrder();
+};
+
+
diff --git a/hmvc/payments/payanyway/controller/fail.js b/hmvc/payments/payanyway/controller/fail.js
new file mode 100644
index 000000000..af3f6b83f
--- /dev/null
+++ b/hmvc/payments/payanyway/controller/fail.js
@@ -0,0 +1,14 @@
+const Transaction = require('../../models/transaction');
+
+exports.get = function* (next) {
+
+ yield* this.loadTransaction('MNT_TRANSACTION_ID');
+
+ this.transaction.persist({
+ status: Transaction.STATUS_FAIL
+ });
+
+ this.redirectToOrder();
+};
+
+
diff --git a/hmvc/payments/payanyway/controller/inprogress.js b/hmvc/payments/payanyway/controller/inprogress.js
new file mode 100644
index 000000000..f5d43d90f
--- /dev/null
+++ b/hmvc/payments/payanyway/controller/inprogress.js
@@ -0,0 +1,11 @@
+const Transaction = require('../../models/transaction');
+
+
+exports.get = function* (next) {
+
+ yield* this.loadTransaction('MNT_TRANSACTION_ID');
+
+ this.redirectToOrder();
+};
+
+
diff --git a/hmvc/payments/payanyway/controller/success.js b/hmvc/payments/payanyway/controller/success.js
new file mode 100644
index 000000000..b80caa6f7
--- /dev/null
+++ b/hmvc/payments/payanyway/controller/success.js
@@ -0,0 +1,7 @@
+const Transaction = require('../../models/transaction');
+
+exports.get = function* (next) {
+ yield* this.loadTransaction('MNT_TRANSACTION_ID');
+
+ this.redirectToOrder();
+};
diff --git a/hmvc/payments/payanyway/index.js b/hmvc/payments/payanyway/index.js
new file mode 100644
index 000000000..6a6df2fb3
--- /dev/null
+++ b/hmvc/payments/payanyway/index.js
@@ -0,0 +1,7 @@
+
+var router = require('./router');
+
+exports.middleware = router.middleware();
+
+exports.renderForm = require('./renderForm');
+
diff --git a/hmvc/payments/payanyway/renderForm.js b/hmvc/payments/payanyway/renderForm.js
new file mode 100644
index 000000000..922e3a11c
--- /dev/null
+++ b/hmvc/payments/payanyway/renderForm.js
@@ -0,0 +1,17 @@
+const jade = require('jade');
+const config = require('config');
+const path = require('path');
+
+module.exports = function* (transaction) {
+
+ return jade.renderFile(path.join(__dirname, 'templates/form.jade'), {
+ amount: transaction.amount,
+ number: transaction.number,
+ currency: config.payments.currency,
+ id: config.payments.modules.payanyway.id,
+ limitIds: process.env.NODE_ENV == 'development' ? '' : '843858,248362,822360,545234,1028,499669'
+ });
+
+};
+
+
diff --git a/hmvc/payments/payanyway/router.js b/hmvc/payments/payanyway/router.js
new file mode 100644
index 000000000..aaaf0ed6b
--- /dev/null
+++ b/hmvc/payments/payanyway/router.js
@@ -0,0 +1,18 @@
+var Router = require('koa-router');
+
+var router = module.exports = new Router();
+
+var callback = require('./controller/callback');
+
+var success = require('./controller/success');
+var inprogress = require('./controller/inprogress');
+var cancel = require('./controller/cancel');
+
+router.post('/callback', callback.post);
+
+router.get('/success', success.get);
+router.get('/inprogress', inprogress.get);
+
+router.get('/cancel', cancel.get);
+
+
diff --git a/hmvc/payments/payanyway/templates/form.jade b/hmvc/payments/payanyway/templates/form.jade
new file mode 100644
index 000000000..afe7fdb6c
--- /dev/null
+++ b/hmvc/payments/payanyway/templates/form.jade
@@ -0,0 +1,10 @@
+
+form(method="POST" action="https://www.moneta.ru/assistant.htm" accept-charset="UTF-8")
+ input(type="hidden",name="MNT_ID",value=id)
+ input(type="hidden",name="MNT_TRANSACTION_ID",value=number)
+ input(type="hidden",name="MNT_CURRENCY_CODE",value=currency)
+ input(type="hidden",name="MNT_AMOUNT",value=amount)
+ if limitIds
+ input(type="hidden",name="paymentSystem.limitIds",value=limitIds)
+ input(type="submit",value="Оплатить")
+
diff --git a/hmvc/payments/paypal/controller/callback.js b/hmvc/payments/paypal/controller/callback.js
new file mode 100644
index 000000000..8cc387785
--- /dev/null
+++ b/hmvc/payments/paypal/controller/callback.js
@@ -0,0 +1,127 @@
+const config = require('config')
+const paypalConfig = config.payments.modules.paypal;
+const Order = require('../../models/order');
+const Transaction = require('../../models/transaction');
+const TransactionLog = require('../../models/transactionLog');
+const log = require('js-log')();
+const request = require('koa-request');
+
+// docs:
+//
+// https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNIntro/
+
+log.debugOn();
+
+/* jshint -W106 */
+exports.post = function* (next) {
+
+ yield* this.loadTransaction('invoice', {skipOwnerCheck: true});
+
+ yield this.transaction.logRequest('ipn-request unverified', this.request);
+
+ var qs = {
+ 'cmd': '_notify-validate'
+ };
+
+ for (var field in this.request.body) {
+ qs[field] = this.request.body[field];
+ }
+
+ // request oauth token
+ var options = {
+ method: 'GET',
+ qs: qs,
+ url: 'https://www.paypal.com/cgi-bin/webscr'
+ };
+
+
+ yield this.transaction.log('request ipn verify', options);
+
+ var response;
+ try {
+ response = yield request(options);
+ } catch(e) {
+ yield this.transaction.log('request ipn verify failed', e.message);
+ this.throw(403, "Couldn't verify ipn");
+ }
+
+ if (response.body != "VERIFIED") {
+ this.throw(403, "Invalid IPN");
+ }
+
+ // ipn is verified now! But we check if it's data matches the transaction (as recommended in docs)
+ if (this.transaction.amount != parseFloat(this.request.body.mc_gross) ||
+ this.request.body.receiver_email != paypalConfig.email ||
+ this.request.body.mc_currency != config.payments.currency) {
+
+ yield this.transaction.persist({
+ status: Transaction.STATUS_FAIL,
+ statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой"
+ });
+ this.throw(404, "transaction data doesn't match the POST body");
+ }
+
+ // match agains latest ipn in logs as recommended:
+ // if there was an IPN about the same transaction, and it's state is the same
+ // => then the current one is a duplicate
+
+ var previousIpn = yield TransactionLog.findOne({
+ event: "ipn",
+ transaction: this.transaction._id
+ }).sort({created: -1}).exec();
+
+ if (previousIpn && previousIpn.data.payment_status == this.request.body.payment_status) {
+ yield this.transaction.log("ipn duplicate", this.request.body);
+ // ignore duplicate
+ this.body = '';
+ return;
+ }
+
+ // now we have a valid non-duplicate IPN, let's update the transaction
+
+ // log it right now to evade conflicts with duplicates
+ yield this.transaction.log("ipn", this.request.body);
+
+ // Do not perform any processing on WPS transactions here that do not have
+ // transaction IDs, indicating they are non-payment IPNs such as those used
+ // for subscription signup requests.
+ if (!this.request.body.txn_id) {
+ yield this.transaction.log("ipn without txn_id", this.request.body);
+ this.body = '';
+ return;
+ }
+
+ switch(this.request.body.payment_status) {
+ case 'Failed':
+ case 'Voided':
+ yield this.transaction.persist({
+ status: Transaction.STATUS_FAIL
+ });
+ this.body = '';
+ return;
+ case 'Pending':
+ yield this.transaction.persist({
+ status: Transaction.STATUS_PENDING,
+ statusMessage: this.request.body.pending_reason
+ });
+ this.body = '';
+ return;
+ case 'Completed':
+ yield this.transaction.persist({
+ status: Transaction.STATUS_SUCCESS
+ });
+
+ yield* require(this.order.module).onSuccess(this.order);
+ this.body = '';
+ return;
+ default:
+ // Refunded ...
+ yield this.transaction.log("ipn payment_status unknown", this.request.body);
+
+ this.body = '';
+ return;
+ }
+
+
+
+};
diff --git a/hmvc/payments/paypal/controller/cancel.js b/hmvc/payments/paypal/controller/cancel.js
new file mode 100644
index 000000000..fcef4fd87
--- /dev/null
+++ b/hmvc/payments/paypal/controller/cancel.js
@@ -0,0 +1,16 @@
+const Transaction = require('../../models/transaction');
+const log = require('js-log')();
+
+
+exports.get = function* (next) {
+
+ yield* this.loadTransaction();
+
+ yield this.transaction.persist({
+ status: Transaction.STATUS_FAIL,
+ statusMessage: 'отказ от оплаты'
+ });
+
+ this.redirectToOrder();
+};
+
diff --git a/hmvc/payments/paypal/controller/success.js b/hmvc/payments/paypal/controller/success.js
new file mode 100644
index 000000000..fecd136ef
--- /dev/null
+++ b/hmvc/payments/paypal/controller/success.js
@@ -0,0 +1,6 @@
+
+exports.post = function* (next) {
+ yield* this.loadTransaction();
+
+ this.redirectToOrder();
+};
diff --git a/hmvc/payments/paypal/index.js b/hmvc/payments/paypal/index.js
new file mode 100644
index 000000000..fb208bb72
--- /dev/null
+++ b/hmvc/payments/paypal/index.js
@@ -0,0 +1,7 @@
+var router = require('./router');
+
+exports.middleware = router.middleware();
+
+exports.renderForm = require('./renderForm');
+
+
diff --git a/hmvc/payments/paypal/renderForm.js b/hmvc/payments/paypal/renderForm.js
new file mode 100644
index 000000000..43c3c29ab
--- /dev/null
+++ b/hmvc/payments/paypal/renderForm.js
@@ -0,0 +1,46 @@
+const jade = require('jade');
+const config = require('config');
+const paypalConfig = config.payments.modules.paypal;
+const path = require('path');
+const signCart = require('./signCart')(paypalConfig.myCertPath, paypalConfig.myKeyPath, paypalConfig.paypalCertPath);
+
+module.exports = function* (transaction) {
+
+ /* jshint -W106 */
+ var cart = {
+ cert_id: paypalConfig.certId,
+ business: paypalConfig.email,
+ invoice: transaction.number,
+ amount: transaction.amount,
+ item_name: "Оплата по счёту " + transaction.number,
+ cmd: "_xclick", // Buy now button (buying a single item, works with Encrypted Website Payments, not sure if _cart works too)
+ charset: "utf-8",
+ no_note: 1,
+ no_shipping: 1,
+ rm: 2,
+ currency_code: config.payments.currency,
+ lc: "RU"
+ };
+
+ yield transaction.log("cart", cart);
+
+ var cartFormatted = [];
+ for(var key in cart) {
+ cartFormatted.push(key + '=' + cart[key]);
+ }
+ cartFormatted = cartFormatted.join("\n");
+
+ var cartEncrypted = yield signCart(cartFormatted);
+
+ var form = jade.renderFile(path.join(__dirname, 'templates/form.jade'), {
+ encrypted: cartEncrypted,
+ notifyUrl: paypalConfig.callbackUrl + '?transactionNumber=' + transaction.number,
+ cancelUrl: paypalConfig.cancelUrl + '?transactionNumber=' + transaction.number,
+ returnUrl: paypalConfig.successUrl + '?transactionNumber=' + transaction.number
+ });
+
+ return form;
+
+};
+
+
diff --git a/hmvc/payments/paypal/router.js b/hmvc/payments/paypal/router.js
new file mode 100644
index 000000000..ec7df27a6
--- /dev/null
+++ b/hmvc/payments/paypal/router.js
@@ -0,0 +1,17 @@
+var Router = require('koa-router');
+
+var router = module.exports = new Router();
+
+var callback = require('./controller/callback');
+var success = require('./controller/success');
+var cancel = require('./controller/cancel');
+
+// webmoney server posts here (in background)
+router.post('/callback', callback.post);
+
+// webmoney server redirects here if payment successful
+router.post('/success', success.post);
+
+router.get('/cancel', cancel.get);
+
+
diff --git a/hmvc/payments/paypal/signCart.js b/hmvc/payments/paypal/signCart.js
new file mode 100644
index 000000000..a5b8e0542
--- /dev/null
+++ b/hmvc/payments/paypal/signCart.js
@@ -0,0 +1,76 @@
+/*
+ # Signing the cart for paypal WPS
+
+ ## Why OpenSSL Cli ?
+ Node crypto can't sign pkcs7
+ node-forge (not sure) couldn't find the way
+ so I'm using openssl CLI
+
+ ## OpenSSL params origins
+ Thanks to from http://www.stellarwebsolutions.com/en/articles/paypal_button_encryption_php.php
+
+ ## Preparation
+ First, make certs:
+ openssl genrsa -out app_key.pem 1024
+ openssl req -new -key app_key.pem -x509 -days 365 -out app_cert.pem
+
+ (for java optional) openssl pkcs12 -export -inkey app_key.pem -in app_cert.pem -out app_cert.p12
+ (for java optional) p12 is a format which groups key and cert and signs, so password is required
+
+ ## Alternative way to sign:
+ EWP Soft:
+ https://www.paypal.com/us/cgi-bin/webscr?cmd=p/xcl/rec/ewp-code
+ to compile java, get http://www.bouncycastle.org/archive/124/crypto-124.tar.gz
+ put to parent dir and run the compiler like this (modified build_app.sh):
+ ===
+ export CRYPTO_HOME="/js/javascript-nodejs/tmp/crypto"
+
+ CLASSPATH="."
+ CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcmail-jdk15-124.jar"
+ CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcpg-jdk15-124.jar"
+ CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcprov-jdk15-124.jar"
+ CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bctest-jdk15-124.jar"
+ CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/jce-jdk13-124.jar"
+ export CLASSPATH
+
+ javac -g -classpath "$CLASSPATH" ButtonEncryption.java com/paypal/crypto/sample/*.java
+
+ Then:
+ (where Kah1voo8 is p12 password, 7BXVJJ3YFS3HQ is cert id from paypal (upload app_cert.pem to paypal to see it)
+ java ButtonEncryption app_cert.pem app_cert.p12 paypal_cert.pem Kah1voo8 "cert_id=7BXVJJ3YFS3HQ,business=iliakan@gmail.com,notify_url=http://stage.javascript.ru/payments/paypal/callback?transactionNumber=123,cancel_return=http://stage.javascript.ru/payments/paypal/cancel?transactionNumber=123,notify_url=http://stage.javascript.ru/payments/paypal/callback?transactionNumber=123,return=http://stage.javascript.ru/payments/paypal/success?transactionNumber=123,invoice=123,amount=2,item_name=Оплата по счету 123,cmd=_xclick,charset=utf-8,no_note=1,no_shipping=1,rm=2,currency_code=RUB,lc=RU,secret=blabla" out.html
+ =====
+*/
+
+/* jshint -W021 */
+
+var exec = require('child_process').exec;
+var fs = require('fs');
+var assert = require('assert');
+var thunkify = require('thunkify');
+
+function signCart(myCertPath, myKeyPath, paypalCertPath, message, callback) {
+
+ var cmd = 'openssl smime -sign -signer ' + myCertPath + ' -inkey ' + myKeyPath + ' -outform der -nodetach -binary | openssl smime -encrypt -des3 -binary -outform pem ' + paypalCertPath;
+
+ var child = exec(
+ cmd,
+ function(err, stdout, stderr) {
+ if (err) return callback(err);
+ return callback(null, stdout);
+ }
+ );
+
+ child.stdin.end(message);
+}
+
+module.exports = function(myCertPath, myKeyPath, paypalCertPath) {
+ assert(fs.existsSync(myCertPath));
+ assert(fs.existsSync(myKeyPath));
+ assert(fs.existsSync(paypalCertPath));
+
+ return thunkify(signCart.bind(null, myCertPath, myKeyPath, paypalCertPath));
+};
+
+
+//openssl smime -sign -signer $MY_CERT_FILE -inkey $MY_KEY_FILE -in content -outform der -nodetach -binary
+// | openssl smime -encrypt -des3 -binary -outform pem $PAYPAL_CERT_FILE
diff --git a/hmvc/payments/paypal/templates/form.jade b/hmvc/payments/paypal/templates/form.jade
new file mode 100644
index 000000000..a195ad254
--- /dev/null
+++ b/hmvc/payments/paypal/templates/form.jade
@@ -0,0 +1,8 @@
+
+form(method="post" action="https://www.paypal.com/cgi-bin/webscr")
+ input(type="hidden",name="cmd",value="_s-xclick")
+ input(type="submit" value="submit")
+ input(type="hidden",name="encrypted",value=encrypted)
+ input(type="hidden",name="notify_url",value=notifyUrl)
+ input(type="hidden",name="cancel_return",value=cancelUrl)
+ input(type="hidden",name="return",value=returnUrl)
diff --git a/hmvc/payments/paypal/test/.jshintrc b/hmvc/payments/paypal/test/.jshintrc
new file mode 100644
index 000000000..077663629
--- /dev/null
+++ b/hmvc/payments/paypal/test/.jshintrc
@@ -0,0 +1,23 @@
+{
+ "maxerr": 25,
+ "latedef": "nofunc",
+ "browser": false,
+ "node": true,
+ "indent": 2,
+ "camelcase": true,
+ "newcap": true,
+ "undef": true,
+ "esnext": true,
+ "multistr": true,
+ "noyield": true,
+ "expr": true,
+ "-W004": true,
+ "globals" : {
+ "describe" : false,
+ "it" : false,
+ "before" : false,
+ "beforeEach" : false,
+ "after" : false,
+ "afterEach" : false
+ }
+}
diff --git a/hmvc/payments/paypal/test/unit/signCart.js b/hmvc/payments/paypal/test/unit/signCart.js
new file mode 100644
index 000000000..784f51f66
--- /dev/null
+++ b/hmvc/payments/paypal/test/unit/signCart.js
@@ -0,0 +1,17 @@
+var signCart = require('../../signCart');
+var paypalConfig = require('config').payments.modules.paypal;
+
+describe("signCart", function() {
+
+ it("signs a message", function*() {
+
+ var signed = yield signCart("cart content",
+ paypalConfig.myCertPath, paypalConfig.myKeyPath, paypalConfig.paypalCertPath);
+
+ var header = '-----BEGIN PKCS7-----';
+
+ signed.slice(0, header.length).should.eql(header);
+
+ });
+
+});
diff --git a/hmvc/payments/webmoney/controller/callback.js b/hmvc/payments/webmoney/controller/callback.js
new file mode 100644
index 000000000..84d4545fb
--- /dev/null
+++ b/hmvc/payments/webmoney/controller/callback.js
@@ -0,0 +1,71 @@
+const webmoneyConfig = require('config').payments.modules.webmoney;
+const mongoose = require('mongoose');
+const Order = require('../../models/order');
+const Transaction = require('../../models/transaction');
+const log = require('js-log')();
+const md5 = require('MD5');
+
+log.debugOn();
+
+// ONLY ACCESSED from WEBMONEY SERVER
+exports.prerequest = function* (next) {
+ yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true});
+
+ log.debug("prerequest");
+
+ yield this.transaction.logRequest('prerequest', this.request);
+
+ if (this.transaction.status == Transaction.STATUS_SUCCESS ||
+ this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) ||
+ this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse
+ ) {
+ log.debug("no pending transaction " + this.request.body.LMI_PAYMENT_NO);
+ this.throw(404, 'unfinished transaction with given params not found');
+ }
+
+ this.body = 'YES';
+
+};
+
+exports.post = function* (next) {
+
+ yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true});
+
+ if (!checkSignature(this.request.body)) {
+ log.debug("wrong signature");
+ this.throw(403, "wrong signature");
+ }
+
+ yield this.transaction.logRequest('callback', this.request);
+
+ if (this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) ||
+ this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse) {
+ // STRANGE, signature is correct
+ yield this.transaction.persist({
+ status: Transaction.STATUS_FAIL,
+ statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой"
+ });
+ this.throw(404, "transaction data doesn't match the POST body");
+ }
+
+ if (!this.request.body.LMI_SIM_MODE || this.request.body.LMI_SIM_MODE == '0') {
+ this.transaction.status = Transaction.STATUS_SUCCESS;
+ yield this.transaction.persist();
+ }
+
+ var order = this.transaction.order;
+ log.debug("will call order onSuccess module=" + order.module);
+ yield* require(order.module).onSuccess(order);
+
+ this.body = 'OK';
+
+};
+
+function checkSignature(body) {
+
+ var signature = md5(body.LMI_PAYEE_PURSE + body.LMI_PAYMENT_AMOUNT + body.LMI_PAYMENT_NO +
+ body.LMI_MODE + body.LMI_SYS_INVS_NO + body.LMI_SYS_TRANS_NO + body.LMI_SYS_TRANS_DATE +
+ webmoneyConfig.secretKey + body.LMI_PAYER_PURSE + body.LMI_PAYER_WM).toUpperCase();
+
+ return signature == body.LMI_HASH;
+}
diff --git a/hmvc/payments/webmoney/controller/fail.js b/hmvc/payments/webmoney/controller/fail.js
new file mode 100644
index 000000000..0a50f9bb6
--- /dev/null
+++ b/hmvc/payments/webmoney/controller/fail.js
@@ -0,0 +1,15 @@
+const mongoose = require('mongoose');
+const Transaction = require('../../models/transaction');
+const log = require('js-log')();
+
+exports.post = function* (next) {
+
+ yield* this.loadTransaction('LMI_PAYMENT_NO');
+
+ yield this.transaction.persist({
+ status: Transaction.STATUS_FAIL
+ });
+
+ this.redirectToOrder();
+
+};
diff --git a/hmvc/payments/webmoney/controller/success.js b/hmvc/payments/webmoney/controller/success.js
new file mode 100644
index 000000000..6b4d94e5f
--- /dev/null
+++ b/hmvc/payments/webmoney/controller/success.js
@@ -0,0 +1,9 @@
+const config = require('config');
+const mongoose = require('mongoose');
+const log = require('js-log')();
+
+exports.post = function* (next) {
+ yield* this.loadTransaction('LMI_PAYMENT_NO');
+
+ this.redirectToOrder();
+};
diff --git a/hmvc/payments/webmoney/index.js b/hmvc/payments/webmoney/index.js
new file mode 100644
index 000000000..1a9c21a42
--- /dev/null
+++ b/hmvc/payments/webmoney/index.js
@@ -0,0 +1,5 @@
+var router = require('./router');
+
+exports.middleware = router.middleware();
+
+exports.renderForm = require('./renderForm');
diff --git a/hmvc/payments/webmoney/renderForm.js b/hmvc/payments/webmoney/renderForm.js
new file mode 100644
index 000000000..0a6f6d526
--- /dev/null
+++ b/hmvc/payments/webmoney/renderForm.js
@@ -0,0 +1,15 @@
+const jade = require('jade');
+const config = require('config');
+const path = require('path');
+
+module.exports = function* (transaction) {
+
+ return jade.renderFile(path.join(__dirname, 'templates/form.jade'), {
+ amount: transaction.amount,
+ number: transaction.number,
+ webmoney: config.payments.modules.webmoney
+ });
+
+};
+
+
diff --git a/hmvc/payments/webmoney/router.js b/hmvc/payments/webmoney/router.js
new file mode 100644
index 000000000..adcb5361e
--- /dev/null
+++ b/hmvc/payments/webmoney/router.js
@@ -0,0 +1,25 @@
+var Router = require('koa-router');
+
+var router = module.exports = new Router();
+
+var callback = require('./controller/callback');
+var success = require('./controller/success');
+var fail = require('./controller/fail');
+
+
+// webmoney server posts here (in background)
+router.post('/callback', function* (next) {
+ if (this.request.body.LMI_PREREQUEST == '1') {
+ yield* callback.prerequest.call(this, next);
+ } else {
+ yield* callback.post.call(this, next);
+ }
+});
+
+// webmoney server redirects here if payment successful
+router.post('/success', success.post);
+
+// webmoney server redirects here if payment failed
+router.post('/fail', fail.post);
+
+
diff --git a/hmvc/payments/webmoney/templates/form.jade b/hmvc/payments/webmoney/templates/form.jade
new file mode 100644
index 000000000..8ad6d67ce
--- /dev/null
+++ b/hmvc/payments/webmoney/templates/form.jade
@@ -0,0 +1,7 @@
+form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp")
+ input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount)
+ input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64'))
+ input(type="hidden",name="LMI_PAYMENT_NO",value=number)
+ input(type="hidden",name="LMI_PAYEE_PURSE",value=webmoney.purse)
+ input(type="hidden",name="LMI_SIM_MODE",value=(isTest ? 1 : 0))
+ input(type="submit",value="Оплатить")
diff --git a/hmvc/payments/webmoney/test/.jshintrc b/hmvc/payments/webmoney/test/.jshintrc
new file mode 100644
index 000000000..077663629
--- /dev/null
+++ b/hmvc/payments/webmoney/test/.jshintrc
@@ -0,0 +1,23 @@
+{
+ "maxerr": 25,
+ "latedef": "nofunc",
+ "browser": false,
+ "node": true,
+ "indent": 2,
+ "camelcase": true,
+ "newcap": true,
+ "undef": true,
+ "esnext": true,
+ "multistr": true,
+ "noyield": true,
+ "expr": true,
+ "-W004": true,
+ "globals" : {
+ "describe" : false,
+ "it" : false,
+ "before" : false,
+ "beforeEach" : false,
+ "after" : false,
+ "afterEach" : false
+ }
+}
diff --git a/hmvc/payments/yandexmoney/controller/back.js b/hmvc/payments/yandexmoney/controller/back.js
new file mode 100644
index 000000000..794f8b121
--- /dev/null
+++ b/hmvc/payments/yandexmoney/controller/back.js
@@ -0,0 +1,198 @@
+const config = require('config');
+const Order = require('../../models/order');
+const Transaction = require('../../models/transaction');
+const log = require('js-log')();
+const request = require('koa-request');
+
+log.debugOn();
+
+/* jshint -W106 */
+exports.get = function* (next) {
+
+ var self = this;
+
+ yield* this.loadTransaction();
+
+ yield this.transaction.logRequest('back', this.request);
+
+ if (!this.query.code) {
+ yield* fail(this.query.error_description || this.query.error);
+ return;
+ }
+
+
+ try {
+ var oauthTokenResponse = yield* requestOauthToken(this.query.code);
+
+ var oauthToken = oauthTokenResponse.access_token;
+ if (!oauthToken) {
+ throwResponseError(oauthTokenResponse);
+ }
+
+ var requestPaymentResponse = yield* requestPayment(oauthToken);
+
+ if (requestPaymentResponse.status != "success") {
+
+ if (requestPaymentResponse.error == 'ext_action_required') {
+ self.redirect(requestPaymentResponse.ext_action_uri);
+ return;
+ }
+
+ throwResponseError(requestPaymentResponse);
+ }
+
+ var requestId = requestPaymentResponse.request_id;
+
+ var startTime = new Date();
+
+ while_in_progress:
+ while (true) {
+ if (new Date() - startTime > 5 * 60 * 1e3) { // 5 minutes wait max
+ yield* fail("timeout");
+ return;
+ }
+ var processPaymentResponse = yield* processPayment(oauthToken, requestId);
+
+ switch (processPaymentResponse.status) {
+ case 'success':
+ break while_in_progress;
+ case 'refused':
+ yield* fail(processPaymentResponse.error);
+ return;
+ case 'ext_auth_required':
+ yield* fail("необходимо подтвердить авторизацию по технологии 3D-Secure");
+ return;
+ case 'in_progress':
+ yield delay(processPaymentResponse.next_retry);
+ break;
+ default:
+ yield delay(1000);
+ }
+
+ }
+
+
+ // success!
+ var orderModule = require(this.order.module);
+ yield* orderModule.onSuccess(this.order);
+
+
+ self.redirect(self.getOrderSuccessUrl());
+
+ } catch (e) {
+ if (e instanceof URIError) {
+ yield* fail(e.message);
+ return;
+ } else if (e instanceof SyntaxError) {
+ yield* fail("некорректный ответ платёжной системы");
+ return;
+ } else {
+ throw e;
+ }
+ }
+
+ /* jshint -W106 */
+ function* fail(reason) {
+ self.transaction.status = Transaction.STATUS_FAIL;
+ self.transaction.statusMessage = reason;
+
+ yield self.transaction.persist();
+
+ self.redirectToOrder();
+ }
+
+
+ function* requestOauthToken(code) {
+
+ // request oauth token
+ var options = {
+ method: 'POST',
+ form: {
+ code: code,
+ client_id: config.payments.modules.yandexmoney.clientId,
+ grant_type: 'authorization_code',
+ redirect_uri: config.payments.modules.yandexmoney.redirectUri + '?transactionNumber=' + self.transaction.number,
+ client_secret: config.payments.modules.yandexmoney.clientSecret
+ },
+ url: 'https://sp-money.yandex.ru/oauth/token'
+ };
+
+
+ yield self.transaction.log('request oauth/token', options);
+
+ var response = yield request(options);
+
+ yield self.transaction.log('response oauth/token', response.body);
+
+ return JSON.parse(response.body);
+ }
+
+ // request payment
+ // return return request_id
+ function* requestPayment(oauthToken) {
+ var options = {
+ method: 'POST',
+ form: {
+ pattern_id: 'p2p',
+ to: config.payments.modules.yandexmoney.purse,
+ amount: self.transaction.amount,
+ comment: 'оплата по счету ' + self.transaction.number,
+ message: 'оплата по счету ' + self.transaction.number,
+ identifier_type: 'account'
+ },
+ headers: {
+ 'Authorization': 'Bearer ' + oauthToken
+ },
+ url: 'https://money.yandex.ru/api/request-payment'
+ };
+
+ yield self.transaction.log('request api/request-payment', options);
+
+ var response = yield request(options);
+ yield self.transaction.log('response api/request-payment', response.body);
+
+ return JSON.parse(response.body);
+ }
+
+ function* processPayment(oauthToken, requestId) {
+ var options = {
+ method: 'POST',
+ form: {
+ request_id: requestId
+ },
+ headers: {
+ 'Authorization': 'Bearer ' + oauthToken
+ },
+ url: 'https://money.yandex.ru/api/process-payment'
+ };
+
+ yield self.transaction.log('request api/process-payment', options);
+
+ var response = yield request(options);
+ yield self.transaction.log('response api/process-payment', response.body);
+
+ return JSON.parse(response.body);
+ }
+
+
+};
+
+function throwResponseError(response) {
+ var message;
+
+ if (response.error && response.error_description) {
+ message = '[' + response.error + '] ' + response.error_description;
+ } else if (response.error) {
+ message = response.error;
+ } else {
+ message = "детали ошибки не указаны";
+ }
+
+ throw new URIError(message);
+}
+
+function delay(ms) {
+ return function(callback) {
+ setTimeout(callback, ms);
+ };
+}
diff --git a/hmvc/payments/yandexmoney/index.js b/hmvc/payments/yandexmoney/index.js
new file mode 100644
index 000000000..741de07fe
--- /dev/null
+++ b/hmvc/payments/yandexmoney/index.js
@@ -0,0 +1,9 @@
+const config = require('config');
+const jade = require('jade');
+const path = require('path');
+
+var router = require('./router');
+
+exports.middleware = router.middleware();
+
+exports.renderForm = require('./renderForm');
diff --git a/hmvc/payments/yandexmoney/renderForm.js b/hmvc/payments/yandexmoney/renderForm.js
new file mode 100644
index 000000000..868baa511
--- /dev/null
+++ b/hmvc/payments/yandexmoney/renderForm.js
@@ -0,0 +1,18 @@
+const jade = require('jade');
+const config = require('config');
+const path = require('path');
+
+module.exports = function* (transaction) {
+
+ return jade.renderFile(path.join(__dirname, 'templates/form.jade'), {
+ clientId: config.payments.modules.yandexmoney.clientId,
+ redirectUri: config.payments.modules.yandexmoney.redirectUri,
+ purse: config.payments.modules.yandexmoney.purse,
+ transactionNumber: transaction.number,
+ amount: transaction.amount
+ });
+
+};
+
+
+
diff --git a/hmvc/payments/yandexmoney/router.js b/hmvc/payments/yandexmoney/router.js
new file mode 100644
index 000000000..bb51a3220
--- /dev/null
+++ b/hmvc/payments/yandexmoney/router.js
@@ -0,0 +1,9 @@
+var Router = require('koa-router');
+
+var router = module.exports = new Router();
+
+var back = require('./controller/back');
+
+router.get('/back', back.get);
+
+
diff --git a/hmvc/payments/yandexmoney/templates/form.jade b/hmvc/payments/yandexmoney/templates/form.jade
new file mode 100644
index 000000000..5612a281c
--- /dev/null
+++ b/hmvc/payments/yandexmoney/templates/form.jade
@@ -0,0 +1,7 @@
+
+form(method="POST",action="https://sp-money.yandex.ru/oauth/authorize")
+ input(type="hidden",name="client_id",value=clientId)
+ input(type="hidden",name="response_type",value="code")
+ input(type="hidden",name="redirect_uri",value=(redirectUri + '?transactionNumber=' + transactionNumber))
+ input(type="hidden",name="scope",value=('payment.to-account("' + purse + '").limit(,' + amount + ')'))
+ input(type="submit",value="Оплатить")
diff --git a/hmvc/tutorial/controller/article.js b/hmvc/tutorial/controller/article.js
new file mode 100644
index 000000000..1578b6a99
--- /dev/null
+++ b/hmvc/tutorial/controller/article.js
@@ -0,0 +1,48 @@
+const mongoose = require('mongoose');
+const Article = require('../models/article');
+const ArticleRenderer = require('../renderer/articleRenderer').ArticleRenderer;
+const treeUtil = require('lib/treeUtil');
+const jade = require('jade');
+const _ = require('lodash');
+
+exports.get = function *get(next) {
+
+ const article = yield Article.findOne({ slug: this.params.slug }).exec();
+ if (!article) {
+ yield next;
+ return;
+ }
+
+ const renderer = new ArticleRenderer();
+ const articleBody = yield renderer.render(article);
+
+ const tree = yield Article.findTree();
+
+ var prevNext = treeUtil.findPrevNextById(tree, article._id);
+
+ var prev = prevNext.prev;
+ if (prev) {
+ prev.url = Article.getUrlBySlug(prev.slug);
+ }
+
+ var next = prevNext.next;
+ if (next) {
+ next.url = Article.getUrlBySlug(next.slug);
+ }
+
+ var locals = {
+ title: article.title,
+ content: articleBody,
+ modified: article.modified,
+ prev: prev,
+ next: next
+ };
+ _.assign(this.locals, locals);
+
+ this.render(__dirname, "article", locals);
+
+ //yield this.render("/hmvc/tutorial/template/article", );
+
+
+};
+
diff --git a/hmvc/tutorial/controller/task.js b/hmvc/tutorial/controller/task.js
new file mode 100644
index 000000000..182a261da
--- /dev/null
+++ b/hmvc/tutorial/controller/task.js
@@ -0,0 +1,20 @@
+const mongoose = require('mongoose');
+const Task = require('../models/task');
+const TaskRenderer = require('../renderer/taskRenderer').TaskRenderer;
+
+exports.get = function *get(next) {
+ const task = yield Task.findOne({
+ slug: this.params.slug
+ }).exec();
+
+ if (!task) {
+ yield next;
+ return;
+ }
+
+ const renderer = new TaskRenderer();
+ // todo: implement this
+ this.body = yield renderer.render(task);
+
+};
+
diff --git a/hmvc/tutorial/index.js b/hmvc/tutorial/index.js
new file mode 100644
index 000000000..05c3ee659
--- /dev/null
+++ b/hmvc/tutorial/index.js
@@ -0,0 +1,7 @@
+//var requireTree = require('require-tree');
+//
+//requireTree('./model');
+
+var router = require('./router');
+
+exports.middleware = router.middleware();
diff --git a/hmvc/tutorial/models/article.js b/hmvc/tutorial/models/article.js
new file mode 100644
index 000000000..b8b9cdced
--- /dev/null
+++ b/hmvc/tutorial/models/article.js
@@ -0,0 +1,139 @@
+const mongoose = require('mongoose');
+const troop = require('mongoose-troop');
+const ObjectId = mongoose.Schema.Types.ObjectId;
+const Schema = mongoose.Schema;
+const config = require('config');
+const path = require('path');
+const Reference = require('./reference');
+
+const schema = new Schema({
+ title: {
+ type: String,
+ required: true
+ },
+
+ slug: {
+ type: String,
+ unique: true,
+ required: true,
+ index: true
+ },
+
+ content: {
+ type: String,
+ required: true
+ },
+
+ parent: {
+ type: ObjectId,
+ ref: 'Article',
+ index: true
+ },
+
+ weight: {
+ type: Number,
+ required: true
+ },
+
+ isFolder: {
+ type: Boolean,
+ required: true
+ }
+
+});
+
+// all resources are here
+schema.statics.resourceFsRoot = path.join(config.publicPath, 'article');
+
+schema.statics.getResourceFsRootBySlug = function(slug) {
+ return path.join(schema.statics.resourceFsRoot, slug);
+};
+
+schema.statics.getResourceWebRootBySlug = function(slug) {
+ return '/article/' + slug;
+};
+
+schema.statics.getUrlBySlug = function(slug) {
+ return '/' + slug;
+};
+
+schema.methods.getResourceFsRoot = function() {
+ return schema.statics.getResourceFsRootBySlug(this.get('slug'));
+};
+
+schema.methods.getResourceWebRoot = function() {
+ return schema.statics.getResourceWebRootBySlug(this.get('slug'));
+};
+
+schema.methods.getUrl = function() {
+ return schema.statics.getUrlBySlug(this.get('slug'));
+};
+
+/**
+ * Returns {children: [whole article tree]} with nested children
+ * @returns {{children: Array}}
+ */
+schema.statics.findTree = function* () {
+ const Article = this;
+ var articles = yield Article.find({}).sort({weight: 1}).select('parent slug title weight isFolder').lean().exec();
+
+ // arrange by ids
+ var articlesById = {};
+ for (var i=0; i anchor
+var schema = new Schema({
+
+ article: {
+ type: Schema.Types.ObjectId,
+ ref: 'Article'
+ },
+
+ anchor: {
+ type: String,
+ index: true,
+ unique: true,
+ required: true
+ }
+
+});
+
+schema.methods.getUrl = function() {
+ return this.article.getUrl() + '#' + this.anchor;
+};
+
+module.exports = mongoose.model('Reference', schema);
diff --git a/hmvc/tutorial/models/task.js b/hmvc/tutorial/models/task.js
new file mode 100644
index 000000000..3e9471e3b
--- /dev/null
+++ b/hmvc/tutorial/models/task.js
@@ -0,0 +1,75 @@
+var mongoose = require('mongoose');
+var troop = require('mongoose-troop');
+var ObjectId = mongoose.Schema.Types.ObjectId;
+var Schema = mongoose.Schema;
+const config = require('config');
+const path = require('path');
+
+var schema = new Schema({
+ title: {
+ type: String,
+ required: true
+ },
+
+ importance: {
+ type: Number
+ },
+
+ slug: {
+ type: String,
+ unique: true,
+ required: true,
+ index: true
+ },
+
+ content: {
+ type: String,
+ required: true
+ },
+
+ solution: {
+ type: String,
+ required: true
+ },
+
+ parent: {
+ type: ObjectId,
+ ref: 'Article',
+ required: true,
+ index: true
+ }
+});
+
+// all resources are here
+schema.statics.resourceFsRoot = path.join(config.publicPath, 'task');
+
+schema.statics.getResourceFsRootBySlug = function(slug) {
+ return path.join(schema.statics.resourceFsRoot, slug);
+};
+
+schema.statics.getResourceWebRootBySlug = function(slug) {
+ return '/task/' + slug;
+};
+
+schema.statics.getUrlBySlug = function(slug) {
+ return '/' + slug;
+};
+
+schema.methods.getResourceFsRoot = function() {
+ return schema.statics.getResourceFsRootBySlug(this.get('slug'));
+};
+
+schema.methods.getResourceWebRoot = function() {
+ return schema.statics.getResourceWebRootBySlug(this.get('slug'));
+};
+
+schema.methods.getUrl = function() {
+ return schema.statics.getUrlBySlug(this.get('slug'));
+};
+
+
+schema.plugin(troop.timestamp);
+
+module.exports = mongoose.model('Task', schema);
+
+
diff --git a/hmvc/tutorial/renderer/articleRenderer.js b/hmvc/tutorial/renderer/articleRenderer.js
new file mode 100644
index 000000000..7165c7bd5
--- /dev/null
+++ b/hmvc/tutorial/renderer/articleRenderer.js
@@ -0,0 +1,39 @@
+const HtmlTransformer = require('javascript-parser').HtmlTransformer;
+const ReferenceResolver = require('./referenceResolver').ReferenceResolver;
+const TaskResolver = require('./taskResolver').TaskResolver;
+const BodyParser = require('javascript-parser').BodyParser;
+
+/**
+ * Can render many articles, keeping metadata
+ * @constructor
+ */
+function ArticleRenderer() {
+ this.metadata = {};
+}
+
+ArticleRenderer.prototype.render = function* (article) {
+
+ const options = {
+ resourceFsRoot: article.getResourceFsRoot(),
+ resourceWebRoot: article.getResourceWebRoot(),
+ metadata: this.metadata,
+ trusted: true
+ };
+
+ // shift off the title header
+ const articleNode = yield new BodyParser(article.get('content'), options).parseAndWrap();
+ articleNode.removeChild(articleNode.getChild(0));
+
+ const referenceResolver = new ReferenceResolver(articleNode);
+ yield referenceResolver.run();
+
+ const taskResolver = new TaskResolver(articleNode);
+ yield taskResolver.run();
+
+ const transformer = new HtmlTransformer(articleNode, options);
+ const content = yield transformer.run();
+ return content;
+};
+
+
+exports.ArticleRenderer = ArticleRenderer;
\ No newline at end of file
diff --git a/hmvc/tutorial/renderer/referenceResolver.js b/hmvc/tutorial/renderer/referenceResolver.js
new file mode 100644
index 000000000..17481966f
--- /dev/null
+++ b/hmvc/tutorial/renderer/referenceResolver.js
@@ -0,0 +1,62 @@
+const util = require('util');
+const TreeWalker = require('javascript-parser').TreeWalker;
+const ReferenceNode = require('javascript-parser').ReferenceNode;
+const CompositeTag = require('javascript-parser').CompositeTag;
+const TextNode = require('javascript-parser').TextNode;
+const ErrorTag = require('javascript-parser').ErrorTag;
+const mongoose = require('mongoose');
+const Reference = require('../models/reference');
+const Article = require('../models/article');
+const Task = require('../models/task');
+
+function ReferenceResolver(root) {
+ this.root = root;
+}
+
+
+ReferenceResolver.prototype.run = function* () {
+
+ var treeWalker = new TreeWalker(this.root);
+
+ var self = this;
+ yield treeWalker.walk(function*(node) {
+ if (!(node instanceof ReferenceNode)) return;
+
+ // need
+ const referenceObj = yield self.resolve(node.ref);
+
+ if (!referenceObj) {
+ return new ErrorTag('span', 'No such reference: ' + node.ref);
+ }
+
+ var newNode = new CompositeTag('a', node.getChildren(), {href: referenceObj.url});
+
+ if (newNode.getChildren().length === 0) {
+ if (node.ref[0] == '#') {
+ newNode.appendChild(new TextNode(node.ref.slice(1)));
+ } else {
+ newNode.appendChild(new TextNode(referenceObj.title));
+ }
+ }
+
+ return newNode;
+
+ });
+};
+
+ReferenceResolver.prototype.resolve = function* (value) {
+ if (value[0] == '#') {
+ var ref = yield Reference.findOne({anchor: value.slice(1)}).populate('article', 'slug title').exec();
+ return ref && { title: ref.article.title, url: ref.getUrl() };
+ }
+
+ if (value.indexOf('/task/') === 0) {
+ var task = yield Task.findOne({slug: value.slice('/task/'.length)}, 'slug title').exec();
+ return task && {title: task.title, url: task.getUrl()};
+ }
+
+ var article = yield Article.findOne({slug: value.slice(1)}, 'slug title').exec();
+ return article && {title: article.title, url: article.getUrl()};
+};
+
+exports.ReferenceResolver = ReferenceResolver;
diff --git a/hmvc/tutorial/renderer/taskRenderer.js b/hmvc/tutorial/renderer/taskRenderer.js
new file mode 100644
index 000000000..ab40cec33
--- /dev/null
+++ b/hmvc/tutorial/renderer/taskRenderer.js
@@ -0,0 +1,86 @@
+const TreeWalker = require('javascript-parser').TreeWalker;
+const HtmlTransformer = require('javascript-parser').HtmlTransformer;
+const ReferenceResolver = require('./referenceResolver').ReferenceResolver;
+const BodyParser = require('javascript-parser').BodyParser;
+const TaskNode = require('javascript-parser').TaskNode;
+const HeaderTag = require('javascript-parser').HeaderTag;
+const TextNode = require('javascript-parser').TextNode;
+const CompositeTag = require('javascript-parser').CompositeTag;
+
+/**
+ * Can render many articles, keeping metadata
+ * @constructor
+ */
+function TaskRenderer() {
+ this.metadata = {};
+}
+
+TaskRenderer.prototype.renderContent = function* (task) {
+
+ const options = {
+ resourceFsRoot: task.getResourceFsRoot(),
+ resourceWebRoot: task.getResourceWebRoot(),
+ metadata: this.metadata,
+ trusted: true
+ };
+
+ // shift off the title header
+ const taskNode = yield new BodyParser(task.content, options).parseAndWrap();
+ taskNode.removeChild(taskNode.getChild(0));
+
+ const referenceResolver = new ReferenceResolver(taskNode);
+ yield referenceResolver.run();
+
+ const transformer = new HtmlTransformer(taskNode, options);
+ const content = yield transformer.run();
+ return content;
+};
+
+
+TaskRenderer.prototype.renderSolution = function* (task) {
+
+ const options = {
+ resourceFsRoot: task.getResourceFsRoot(),
+ resourceWebRoot: task.getResourceWebRoot(),
+ metadata: this.metadata,
+ trusted: true
+ };
+
+ // shift off the title header
+ const solutionNode = yield new BodyParser(task.solution, options).parseAndWrap();
+
+ const referenceResolver = new ReferenceResolver(solutionNode);
+ yield referenceResolver.run();
+
+ const newChildren = new CompositeTag();
+
+ var children = solutionNode.getChildren();
+
+ const solutionParts = [];
+ if (children[0] instanceof HeaderTag) {
+ // split into parts
+ var currentPart;
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ if (child instanceof HeaderTag) {
+ currentPart = { title: yield new HtmlTransformer(child, options).run(), content: [] };
+ solutionParts.push(currentPart);
+ continue;
+ }
+
+ currentPart.content.push(child);
+ }
+ } else {
+ solutionParts.push({title: "", content: children});
+ }
+
+ for (var i = 0; i < solutionParts.length; i++) {
+ var part = solutionParts[i];
+ part.content = yield new HtmlTransformer(part.content, options).run();
+ }
+
+ return solutionParts;
+};
+
+
+exports.TaskRenderer = TaskRenderer;
diff --git a/hmvc/tutorial/renderer/taskResolver.js b/hmvc/tutorial/renderer/taskResolver.js
new file mode 100644
index 000000000..35fb78d94
--- /dev/null
+++ b/hmvc/tutorial/renderer/taskResolver.js
@@ -0,0 +1,32 @@
+const util = require('util');
+const TreeWalker = require('javascript-parser').TreeWalker;
+const TaskNode = require('javascript-parser').TaskNode;
+const CompositeTag = require('javascript-parser').CompositeTag;
+const TextNode = require('javascript-parser').TextNode;
+const ErrorTag = require('javascript-parser').ErrorTag;
+const mongoose = require('mongoose');
+const Reference = require('../models/reference');
+const Article = require('../models/article');
+const Task = require('../models/task');
+
+
+function TaskResolver(root) {
+ this.root = root;
+}
+
+TaskResolver.prototype.run = function* () {
+
+ var treeWalker = new TreeWalker(this.root);
+
+ var self = this;
+ yield treeWalker.walk(function*(node) {
+ if (!(node instanceof TaskNode)) return;
+
+
+
+ return new TextNode("[task " + node.slug + "]");
+
+ });
+};
+
+exports.TaskResolver = TaskResolver;
diff --git a/hmvc/tutorial/router.js b/hmvc/tutorial/router.js
new file mode 100644
index 000000000..d3f854a22
--- /dev/null
+++ b/hmvc/tutorial/router.js
@@ -0,0 +1,10 @@
+var Router = require('koa-router');
+
+var task = require('./controller/task');
+var article = require('./controller/article');
+
+var router = module.exports = new Router();
+
+router.get('/task/:slug', task.get);
+router.get('/:slug', article.get);
+
diff --git a/hmvc/tutorial/tasks/import.js b/hmvc/tutorial/tasks/import.js
new file mode 100644
index 000000000..7a4d441a7
--- /dev/null
+++ b/hmvc/tutorial/tasks/import.js
@@ -0,0 +1,401 @@
+const co = require('co');
+const fs = require('fs');
+const fse = require('fs-extra');
+const path = require('path');
+const config = require('config');
+const log = require('js-log')();
+const mongoose = require('mongoose');
+const Article = require('../models/article');
+const Reference = require('../models/reference');
+const Task = require('../models/task');
+const BodyParser = require('javascript-parser').BodyParser;
+const HtmlTransformer = require('javascript-parser').HtmlTransformer;
+const TreeWalker = require('javascript-parser').TreeWalker;
+const HeaderTag = require('javascript-parser').HeaderTag;
+const Imagemin = require('imagemin');
+const pngcrush = require('imagemin-pngcrush');
+const gm = require('gm');
+const svgo = require('imagemin-svgo');
+
+// TODO: use htmlhint/jslint for html/js examples
+
+
+/**
+ *
+ * @param options
+ * options.minify => true to enable minification
+ * options.root => the root to import from
+ * @returns {Function}
+ */
+module.exports = function(options) {
+ const root = options.root;
+
+ return function(callback) {
+
+ co(function* () {
+
+ yield Article.destroy({});
+ yield Task.destroy({});
+ yield Reference.destroy({});
+
+ if (!options.updateFiles) {
+ fse.removeSync(Article.resourceFsRoot);
+ fse.removeSync(Task.resourceFsRoot);
+
+ fs.mkdirSync(Article.resourceFsRoot);
+ fs.mkdirSync(Task.resourceFsRoot);
+ }
+
+
+ const subPaths = fs.readdirSync(root);
+
+ for (var i = 0; i < subPaths.length; i++) {
+ var subPath = path.join(root, subPaths[i]);
+ if (fs.existsSync(path.join(subPath, 'index.md'))) {
+ yield importFolder(subPath, null);
+ }
+ }
+
+ mongoose.disconnect();
+
+ })(callback);
+ };
+
+ function stripTags(str) {
+ return str.replace(/<\/?[a-z].*?>/gim, '');
+ }
+
+ function htmlTransform(roots, options) {
+ return new HtmlTransformer(roots, options).run();
+ }
+
+ function* importFolder(sourceFolderPath, parent) {
+ log.info("importFolder", sourceFolderPath);
+
+ const contentPath = path.join(sourceFolderPath, 'index.md');
+ const content = fs.readFileSync(contentPath, 'utf-8').trim();
+
+ const folderFileName = path.basename(sourceFolderPath);
+
+ const data = {
+ isFolder: true,
+ content: content
+ };
+
+ if (parent) {
+ data.parent = parent._id;
+ }
+
+ data.weight = parseInt(folderFileName);
+ data.slug = folderFileName.slice(3);
+
+ const options = {
+ resourceFsRoot: sourceFolderPath,
+ resourceWebRoot: Article.getResourceWebRootBySlug(data.slug),
+ metadata: {},
+ trusted: true
+ };
+
+ const parsed = yield new BodyParser(content, options).parse();
+
+ checkIfErrorsInParsed(parsed);
+
+ const titleHeader = parsed[0];
+ if (parsed[0].getType() != 'HeaderTag') {
+ throw new Error(contentPath + ": must start with a #Header");
+ }
+
+ data.title = stripTags(yield htmlTransform(titleHeader, options));
+
+
+ const folder = new Article(data);
+ yield folder.persist();
+
+ const subPaths = fs.readdirSync(sourceFolderPath);
+
+ for (var i = 0; i < subPaths.length; i++) {
+ if (subPaths[i] == 'index.md') continue;
+
+ var subPath = path.join(sourceFolderPath, subPaths[i]);
+
+ if (fs.existsSync(path.join(subPath, 'index.md'))) {
+ yield importFolder(subPath, folder);
+ } else if (fs.existsSync(path.join(subPath, 'article.md'))) {
+ yield importArticle(subPath, folder);
+ } else {
+ yield importResource(subPath, folder.getResourceFsRoot());
+ }
+ }
+
+ }
+
+ // todo with incremental import: move to separate task?
+ function checkIfErrorsInParsed(parsed) {
+ const walker = new TreeWalker(parsed);
+ const errors = [];
+
+ walker.walk(function*(node) {
+ if (node.getType() == 'ErrorTag') {
+ errors.push(node.text);
+ }
+ });
+ if (errors.length) {
+ throw new Error("Errors: " + errors.join());
+ }
+ }
+
+ function* importArticle(articlePath, parent) {
+ log.info("importArticle", articlePath);
+
+ const contentPath = path.join(articlePath, 'article.md');
+ const content = fs.readFileSync(contentPath, 'utf-8').trim();
+
+ const articlePathName = path.basename(articlePath);
+
+ const data = {
+ content: content,
+ isFolder: false
+ };
+
+ if (parent) {
+ data.parent = parent._id;
+ }
+
+ data.weight = parseInt(articlePathName);
+ data.slug = articlePathName.slice(3);
+
+ const options = {
+ resourceFsRoot: articlePath,
+ resourceWebRoot: Article.getResourceWebRootBySlug(data.slug),
+ metadata: { },
+ trusted: true
+ };
+
+ const parsed = yield new BodyParser(content, options).parse();
+
+ checkIfErrorsInParsed(parsed);
+
+ const titleHeader = parsed[0];
+ if (parsed[0].getType() != 'HeaderTag') {
+ throw new Error(contentPath + ": must start with a #Header");
+ }
+
+ data.title = stripTags(yield htmlTransform(titleHeader, options));
+
+ // todo: updating:
+ // first check if references are unique,
+ // -> fail w/o deleting old article if not unique,
+ // delete old article & insert the new one & insert refs
+ const article = new Article(data);
+ yield article.persist();
+
+
+ const refs = options.metadata.refs.toArray();
+ const refThunks = refs.map(function(anchor) {
+ return new Reference({anchor: anchor, article: article._id}).persist();
+ });
+
+ try {
+ // save all references in parallel
+ yield refThunks;
+ } catch (e) {
+ // something went wrong => we don't need an unfinished article
+ yield article.destroy(); // will kill it's refs too
+ throw e;
+ }
+
+
+ const subPaths = fs.readdirSync(articlePath);
+
+ for (var i = 0; i < subPaths.length; i++) {
+ if (subPaths[i] == 'article.md') continue;
+
+ var subPath = path.join(articlePath, subPaths[i]);
+ if (fs.existsSync(path.join(subPath, 'task.md'))) {
+ yield importTask(subPath, article);
+ } else {
+ // resources
+ yield importResource(subPath, article.getResourceFsRoot());
+ }
+ }
+
+
+ }
+
+ function* importResource(sourcePath, destDir) {
+ fse.ensureDirSync(destDir);
+
+ log.info("importResource", sourcePath, destDir);
+
+ const stat = fs.statSync(sourcePath);
+ const ext = getFileExt(sourcePath);
+ const destPath = path.join(destDir, path.basename(sourcePath));
+
+ if (stat.isFile()) {
+ if (ext == 'png' || ext == 'jpg' || ext == 'gif' || ext == 'svg') {
+ yield importImage(sourcePath, destDir);
+ return;
+ }
+ copySync(sourcePath, destPath);
+ } else if (stat.isDirectory()) {
+ fse.ensureDirSync(destPath);
+ const subPathNames = fs.readdirSync(sourcePath);
+ for (var i = 0; i < subPathNames.length; i++) {
+ var subPath = path.join(sourcePath, subPathNames[i]);
+ yield importResource(subPath, destPath);
+ }
+
+ } else {
+ throw new Error("Unsupported file type at " + sourcePath);
+ }
+
+ }
+
+ function getFileExt(filePath) {
+ var ext = filePath.match(/\.([^.]+)$/);
+ return ext && ext[1];
+ }
+
+ function* importImage(srcPath, dstDir) {
+ log.info("importImage", srcPath, "to", dstDir);
+ const filename = path.basename(srcPath);
+ const dstPath = path.join(dstDir, filename);
+
+ if (checkSameSizeFiles(srcPath, dstPath)) {
+ return;
+ }
+
+ copySync(srcPath, dstPath);
+ yield minifyImage(dstPath);
+
+
+ var isRetina = /@2x\.[^.]+$/.test(srcPath);
+
+
+ if (isRetina) {
+ var normalResolutionFilename = filename.replace(/@2x(?=\.[^.]+$)/, '');
+ var normalResolutionPath = path.join(dstDir, normalResolutionFilename);
+
+ yield function(callback) {
+ gm(srcPath).resize("50%").write(normalResolutionPath, callback);
+ };
+ yield minifyImage(normalResolutionPath);
+ }
+
+ }
+
+ function copySync(srcPath, dstPath) {
+ if (options.updateFiles && checkSameSizeFiles(srcPath, dstPath)) {
+ return;
+ }
+
+ fse.copySync(srcPath, dstPath);
+
+ }
+
+ function* minifyImage(imagePath) {
+ if (!options.minify) return;
+ log.info("minifyImage", imagePath);
+
+ var plugin;
+ switch (getFileExt(imagePath)) {
+ case 'jpg':
+ plugin = Imagemin.jpegtran({ progressive: true });
+ break;
+ case 'gif':
+ plugin = Imagemin.gifsicle({ interlaced: true });
+ break;
+ case 'png':
+ plugin = pngcrush({ reduce: true });
+ break;
+ case 'svg':
+ plugin = svgo({});
+ break;
+ }
+
+ var imagemin = new Imagemin()
+ .src(imagePath)
+ .dest(imagePath)
+ .use(plugin);
+
+ yield function(callback) {
+ imagemin.optimize(callback);
+ };
+
+ }
+
+ function* importTask(taskPath, parent) {
+ log.info("importTask", taskPath);
+
+ const contentPath = path.join(taskPath, 'task.md');
+ const content = fs.readFileSync(contentPath, 'utf-8').trim();
+
+ const taskPathName = path.basename(taskPath);
+
+ const data = {
+ content: content,
+ parent: parent._id,
+ slug: taskPathName
+ };
+
+ const options = {
+ resourceFsRoot: taskPath,
+ resourceWebRoot: Task.getResourceWebRootBySlug(data.slug),
+ metadata: {},
+ trusted: true
+ };
+
+ const parsed = yield new BodyParser(content, options).parse();
+
+ checkIfErrorsInParsed(parsed);
+
+ const titleHeader = parsed[0];
+ if (parsed[0].getType() != 'HeaderTag') {
+ throw new Error(contentPath + ": must start with a #Header");
+ }
+
+ data.title = stripTags(yield htmlTransform(titleHeader, options));
+
+ data.importance = options.metadata.importance;
+
+ const solutionPath = path.join(taskPath, 'solution.md');
+ const solution = fs.readFileSync(solutionPath, 'utf-8').trim();
+ data.solution = solution;
+
+
+ const parsedSolution = yield new BodyParser(solution, options).parse();
+ checkIfErrorsInParsed(parsedSolution);
+
+ const task = new Task(data);
+ yield task.persist();
+
+ const subPaths = fs.readdirSync(taskPath);
+
+ for (var i = 0; i < subPaths.length; i++) {
+ if (subPaths[i] == 'task.md' || subPaths[i] == 'solution.md') continue;
+
+ var subPath = path.join(taskPath, subPaths[i]);
+ yield importResource(subPath, task.getResourceFsRoot());
+ }
+
+ }
+
+};
+
+
+function checkSameSizeFiles(filePath1, filePath2) {
+ if (!fs.existsSync(filePath2)) return false;
+
+ const stat1 = fs.statSync(filePath1);
+ if (!stat1.isFile()) {
+ throw new Error("not a file: " + filePath1);
+ }
+
+ const stat2 = fs.statSync(filePath2);
+ if (!stat2.isFile()) {
+ throw new Error("not a file: " + filePath2);
+ }
+
+ return stat1.size == stat2.size;
+
+}
diff --git a/hmvc/tutorial/templates/article.jade b/hmvc/tutorial/templates/article.jade
new file mode 100644
index 000000000..d41002e3c
--- /dev/null
+++ b/hmvc/tutorial/templates/article.jade
@@ -0,0 +1,16 @@
+extends layouts/base
+
+block content
+ article.main
+ header.main__header.main__header_center
+ include blocks/breadcrumbs
+ h1.main__header-title!= title
+ span.main__header-comments
+ a(href='http://learn.javascript.ru/intro#disqus_thread')
+ include blocks/tutorial/top-navigation
+ != content
+ include blocks/tutorial/footer
+ include blocks/tutorial/bottom-navigation
+ .corrector Нашли опечатку на сайте? Что-то кажется странным? Выделите соответствующий текст и нажмите Ctrl+Enter
+ .sidebar
+ | TODO: SIDEBAR
diff --git a/hmvc/tutorial/test/.jshintrc b/hmvc/tutorial/test/.jshintrc
new file mode 100644
index 000000000..077663629
--- /dev/null
+++ b/hmvc/tutorial/test/.jshintrc
@@ -0,0 +1,23 @@
+{
+ "maxerr": 25,
+ "latedef": "nofunc",
+ "browser": false,
+ "node": true,
+ "indent": 2,
+ "camelcase": true,
+ "newcap": true,
+ "undef": true,
+ "esnext": true,
+ "multistr": true,
+ "noyield": true,
+ "expr": true,
+ "-W004": true,
+ "globals" : {
+ "describe" : false,
+ "it" : false,
+ "before" : false,
+ "beforeEach" : false,
+ "after" : false,
+ "afterEach" : false
+ }
+}
diff --git a/hmvc/tutorial/test/data/article.js b/hmvc/tutorial/test/data/article.js
new file mode 100644
index 000000000..2d1f9b25d
--- /dev/null
+++ b/hmvc/tutorial/test/data/article.js
@@ -0,0 +1,65 @@
+const mongoose = require('mongoose');
+
+var ids = [];
+for(var i=0; i<10; i++) ids[i] = new mongoose.Types.ObjectId();
+
+exports.Article = [
+ {
+ _id: ids[0],
+ title: "Article 1",
+ slug: "article-1",
+ content: "Content 1",
+ modified: new Date(2014, 0, 1),
+ isFolder: true,
+ weight: 0
+ },
+ {
+ _id: ids[1],
+ title: "Article 2",
+ slug: "article-2",
+ content: "Content 2",
+ modified: new Date(2014, 0, 2),
+ isFolder: true,
+ weight: 1
+ },
+ {
+ _id: ids[2],
+ title: "Article 1.1",
+ slug: "article-1.1",
+ content: "Content 1.1",
+ modified: new Date(2014, 0, 2),
+ isFolder: false,
+ weight: 0,
+ parent: ids[0]
+ },
+ {
+ _id: ids[3],
+ title: "Article 1.2",
+ slug: "article-1.2",
+ content: "Content 1.2",
+ modified: new Date(2014, 0, 3),
+ isFolder: false,
+ weight: 1,
+ parent: ids[0]
+ },
+ {
+ _id: ids[4],
+ title: "Article 2.1",
+ slug: "article-2.1",
+ content: "Content 2.1",
+ modified: new Date(2014, 0, 4),
+ isFolder: false,
+ weight: 0,
+ parent: ids[1]
+ },
+ {
+ _id: ids[5],
+ title: "Article 2.2",
+ slug: "article-2.2",
+ content: "Content 2.2",
+ modified: new Date(2014, 0, 5),
+ isFolder: false,
+ weight: 1,
+ parent: ids[1]
+ }
+];
diff --git a/hmvc/tutorial/test/model/article.js b/hmvc/tutorial/test/model/article.js
new file mode 100644
index 000000000..0f296ebe4
--- /dev/null
+++ b/hmvc/tutorial/test/model/article.js
@@ -0,0 +1,46 @@
+require('app');
+
+var dataUtil = require('lib/dataUtil');
+var mongoose = require('mongoose');
+var assert = require('assert');
+var path = require('path');
+var treeUtil = require('lib/treeUtil');
+var Article = require('../../models/article');
+
+describe('Article', function() {
+
+ before(function* () {
+ yield dataUtil.loadDb(path.join(__dirname, '../data/article'));
+
+ });
+
+
+ it('sets created & modified', function*() {
+ var date = new Date();
+
+ var article = new Article({
+ title: "Title",
+ slug: 'slug',
+ content: "Content",
+ isFolder: false,
+ weight: 0
+ });
+
+ yield article.persist();
+
+ assert(article.modified >= date);
+
+ yield article.destroy();
+ });
+
+ describe('findTree', function() {
+
+ it("works", function* () {
+ var tree = yield Article.findTree();
+ console.log(treeUtil.flattenArray(tree));
+// console.log(util.inspect(tree, {depth:100}));
+ });
+
+ });
+
+});
diff --git a/hmvc/tutorial/test/renderer/articleRenderer.js b/hmvc/tutorial/test/renderer/articleRenderer.js
new file mode 100644
index 000000000..d0cd9b835
--- /dev/null
+++ b/hmvc/tutorial/test/renderer/articleRenderer.js
@@ -0,0 +1,55 @@
+const app = require('app');
+
+const ArticleRenderer = require('../../renderer/articleRenderer').ArticleRenderer;
+const mongoose = require('config/mongoose');
+const Article = require('../../models/article');
+
+describe("ArticleRenderer", function() {
+
+ beforeEach(function* () {
+ yield Article.destroy();
+ });
+
+ it("appens -2, -3... to header with same title", function* () {
+
+ const article = new Article({
+ title: "Title",
+ slug: "test",
+ content: "# Title\n\n## Title\n\nMy html *string*."
+ });
+ const renderer = new ArticleRenderer();
+
+ const result = yield renderer.render(article);
+ result.replace(/\n/g, '').should.be.eql(
+ 'Title
My html string.
'
+ );
+
+ });
+
+ it("resolves references to article", function* () {
+
+ yield new Article({
+ title: "Title",
+ slug: "test",
+ weight: 0,
+ isFolder: false,
+ content: "# Title\n\n## Title\n\nMy html *string*."
+ }).persist();
+
+ const article = new Article({
+ title: "Title",
+ slug: "test",
+ weight: 0,
+ isFolder: false,
+ content: "# Title\n\n[](/test)"
+ });
+
+ const renderer = new ArticleRenderer();
+
+ const result = yield renderer.render(article);
+ result.replace(/\n/g, '').should.be.eql(
+ ''
+ );
+
+ });
+});
diff --git a/hmvc/tutorial/test/renderer/taskRenderer.js b/hmvc/tutorial/test/renderer/taskRenderer.js
new file mode 100644
index 000000000..867110cb3
--- /dev/null
+++ b/hmvc/tutorial/test/renderer/taskRenderer.js
@@ -0,0 +1,57 @@
+const app = require('app');
+
+const TaskRenderer = require('../../renderer/taskRenderer').TaskRenderer;
+const mongoose = require('config/mongoose');
+const Task = require('../../models/task');
+
+describe("TaskRenderer", function() {
+
+ beforeEach(function* () {
+ yield Task.destroy();
+ });
+
+ it("renderContent", function* () {
+
+ const task = new Task({
+ "content": "# Title\n\nContent",
+ "slug": "margin-between-pairs",
+ "title": "Title",
+ "importance": 4,
+ "solution": "..."
+ });
+ const renderer = new TaskRenderer();
+
+ const result = yield renderer.renderContent(task);
+
+ result.replace(/\n/g, '').should.be.eql('Content
');
+ });
+
+
+ it("renderSolution", function* () {
+
+ const task = new Task({
+ "content": "# Title\n\nContent",
+ "slug": "margin-between-pairs",
+ "title": "Title",
+ "importance": 4,
+ "solution": "# Part 1\n\nContent 1\n\n# Part 2\n\nContent 2"
+ });
+ const renderer = new TaskRenderer();
+
+ const result = yield renderer.renderSolution(task);
+/*
+ result.replace(/\n/g, '').should.be.eql('
+
+ Content 1
+
+
+
+
+ Content 2
+ ')
+ */
+ console.log(result);
+
+
+ });
+});
diff --git a/lib/error/httpError.js b/lib/error/httpError.js
deleted file mode 100755
index cd5a3dd78..000000000
--- a/lib/error/httpError.js
+++ /dev/null
@@ -1,19 +0,0 @@
-var util = require('util');
-var http = require('http');
-
-// ошибки для выдачи посетителю
-function HttpError(status, message) {
- Error.apply(this, arguments);
- Error.captureStackTrace(this, HttpError);
-
- this.status = status;
- this.message = message || http.STATUS_CODES[status] || "Error";
-}
-
-util.inherits(HttpError, Error);
-module.exports = HttpError;
-
-HttpError.prototype.name = 'HttpError';
-
-
-
diff --git a/lib/error/index.js b/lib/error/index.js
deleted file mode 100755
index 3522cd29c..000000000
--- a/lib/error/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-var fs = require('fs');
-var path = require('path');
-
-var files = fs.readdirSync(__dirname);
-files.forEach(function(file) {
- if (file == 'index.js') return;
- var errorClass = require(path.join(__dirname, file));
- module.exports[errorClass.prototype.name] = errorClass;
-});
diff --git a/lib/hash.js b/lib/hash.js
deleted file mode 100755
index 70dfb8177..000000000
--- a/lib/hash.js
+++ /dev/null
@@ -1,16 +0,0 @@
-var crypto = require('crypto');
-
-var LEN = 128;
-
-/**
- * Iterations. ~300ms
- */
-var ITERATIONS = 12000;
-
-exports.createHash = function(password, salt) {
- return crypto.pbkdf2Sync(password, salt, ITERATIONS, LEN);
-};
-
-exports.createSalt = function() {
- return crypto.randomBytes(LEN).toString('base64');
-};
diff --git a/lib/log.js b/lib/log.js
deleted file mode 100755
index 9ab59ca4a..000000000
--- a/lib/log.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Flexible logging wrapper around windston
- *
- * usage:
- * var log = require('lib/log')(module)
- * log.debug("Winston %s", "syntax");
- *
- * enabling debug directly in the code:
- * log.debugOn();
- * enabling debug for paths from CLI:
- * DEBUG=path node app
- * where path is calculated from the project root (where package.json resides)
- * example:
- * DEBUG=models/* node app
- * DEBUG=models/*,lib/* node app
- * exclusion:
- * DEBUG=-models/user,models/* node app (to log all models except user)
- */
-
-var winston = require('winston');
-var path = require('path');
-var fs = require('fs');
-
-var names = [], skips = [];
-
-(process.env.DEBUG || '')
- .split(/[\s,]+/)
- .forEach(function(name) {
- name = name.replace('*', '.*?');
- if (name[0] === '-') {
- skips.push(new RegExp('^' + name.substr(1) + '$'));
- } else {
- names.push(new RegExp('^' + name + '$'));
- }
- });
-
-function findProjectRoot() {
-
- var dir = __dirname;
- while (!fs.existsSync(path.join(dir, 'package.json'))) {
- dir = path.dirname(dir);
- }
-
- return path.normalize(dir);
-}
-
-var projectRoot = findProjectRoot();
-
-function getLogLevel(module) {
-
- var modulePath = module.filename.slice(projectRoot.length + 1); // models/user.js
- modulePath = modulePath.replace(/\.js$/, ''); // models.user
-
- var logLevel = 'info';
-
- var isSkipped = skips.some(function(re) {
- return re.test(modulePath);
- });
-
- if (!isSkipped) {
- var isIncluded = names.some(function(re) {
- return re.test(modulePath);
- });
-
- if (isIncluded) logLevel = 'debug';
- }
-
- return logLevel;
-}
-
-function getLogger(module) {
-
- var showPath = module.filename.split('/').slice(-2).join('/');
-
- var logLevel = getLogLevel(module);
- var logger = new winston.Logger({
- transports: [
- new winston.transports.Console({
- colorize: true,
- level: logLevel,
- label: showPath
- })
- ]
- });
-
- logger.debugOn = function() {
- Object.keys(this.transports).forEach(function(key) {
- logger.transports[key].level = 'debug';
- }, this);
- };
-
- return logger;
-}
-
-module.exports = getLogger;
\ No newline at end of file
diff --git a/lib/middleware/auth.js b/lib/middleware/auth.js
deleted file mode 100755
index 44d2a9225..000000000
--- a/lib/middleware/auth.js
+++ /dev/null
@@ -1,26 +0,0 @@
-var HttpError = require('lib/error').HttpError;
-
-exports.mustBeAuthenticated = function(req, res, next) {
- if (req.isAuthenticated()) {
- next();
- } else {
- return next(403);
- }
-};
-
-exports.userIdMustBeCurrentUser = function(req, res, next) {
- if (req.params.userId != req.user.id) {
- next(403);
- } else {
- next();
- }
-};
-
-exports.mustBeAnonymous = function(req, res, next) {
- if (req.isAuthenticated()) {
- res.redirect('/');
- } else {
- next();
- }
-
-};
\ No newline at end of file
diff --git a/lib/middleware/sendHttpError.js b/lib/middleware/sendHttpError.js
deleted file mode 100755
index ee8c6ebec..000000000
--- a/lib/middleware/sendHttpError.js
+++ /dev/null
@@ -1,17 +0,0 @@
-module.exports = function(req, res, next) {
-
- res.sendHttpError = function(error) {
-
- res.status(error.status);
-
- var preferredType = res.req.accepts('html, json');
- if (preferredType == 'json') {
- res.json(error);
- } else {
- res.render("error", {error: error});
- }
- };
-
- next();
-
-};
\ No newline at end of file
diff --git a/lib/mongoose.js b/lib/mongoose.js
deleted file mode 100755
index 03b6b9f6b..000000000
--- a/lib/mongoose.js
+++ /dev/null
@@ -1,9 +0,0 @@
-var mongoose = require('mongoose');
-var config = require('config');
-
-mongoose.connect(config.mongoose.uri, config.mongoose.options);
-
-module.exports = mongoose;
-
-// require('models');
-
diff --git a/lib/passport.js b/lib/passport.js
deleted file mode 100755
index 912f974cf..000000000
--- a/lib/passport.js
+++ /dev/null
@@ -1,42 +0,0 @@
-var passport = require("passport");
-var mongoose = require('mongoose');
-var LocalStrategy = require('passport-local').Strategy;
-
-var User = mongoose.models.User;
-
-passport.serializeUser(function(user, done) {
- done(null, user.id);
-});
-
-passport.deserializeUser(function(id, done) {
- User.findOne({ _id: id }, function(err, user) {
- done(err, user);
- });
-});
-
-passport.use(new LocalStrategy({
- usernameField: 'email',
- passwordField: 'password'
- },
- function(email, password, done) {
- if (!email) return done(null, false, { message: 'Please provide email.' });
- if (!password) return done(null, false, { message: 'Please provide password.' });
-
- User.findOne({email: email}, function(err, user) {
- if (err) return done(err);
- if (!user) return done(null, false, { message: 'Non-registered email.' });
-
- if (user.checkPassword(password)) {
- return done(null, user);
- } else {
- done(null, false, {
- message: 'Incorrect password.'
- });
- }
- });
- }
-));
-
-
-
-module.exports = passport;
diff --git a/lib/sessionStore.js b/lib/sessionStore.js
deleted file mode 100755
index 011e6a1c9..000000000
--- a/lib/sessionStore.js
+++ /dev/null
@@ -1,7 +0,0 @@
-var mongoose = require('mongoose');
-var session = require('express-session');
-var MongoStore = require('connect-mongo')(session);
-
-var sessionStore = new MongoStore({mongoose_connection: mongoose.connection});
-
-module.exports = sessionStore;
\ No newline at end of file
diff --git a/middleware/auth.js b/middleware/auth.js
deleted file mode 100755
index 44d2a9225..000000000
--- a/middleware/auth.js
+++ /dev/null
@@ -1,26 +0,0 @@
-var HttpError = require('lib/error').HttpError;
-
-exports.mustBeAuthenticated = function(req, res, next) {
- if (req.isAuthenticated()) {
- next();
- } else {
- return next(403);
- }
-};
-
-exports.userIdMustBeCurrentUser = function(req, res, next) {
- if (req.params.userId != req.user.id) {
- next(403);
- } else {
- next();
- }
-};
-
-exports.mustBeAnonymous = function(req, res, next) {
- if (req.isAuthenticated()) {
- res.redirect('/');
- } else {
- next();
- }
-
-};
\ No newline at end of file
diff --git a/middleware/sendHttpError.js b/middleware/sendHttpError.js
deleted file mode 100755
index ee8c6ebec..000000000
--- a/middleware/sendHttpError.js
+++ /dev/null
@@ -1,17 +0,0 @@
-module.exports = function(req, res, next) {
-
- res.sendHttpError = function(error) {
-
- res.status(error.status);
-
- var preferredType = res.req.accepts('html, json');
- if (preferredType == 'json') {
- res.json(error);
- } else {
- res.render("error", {error: error});
- }
- };
-
- next();
-
-};
\ No newline at end of file
diff --git a/middleware/templateHelpers.js b/middleware/templateHelpers.js
deleted file mode 100755
index cee101805..000000000
--- a/middleware/templateHelpers.js
+++ /dev/null
@@ -1,53 +0,0 @@
-var util = require('util');
-
-function Block(name) {
- this.members = [];
-}
-
-Block.prototype.unshift = function(name) {
- if (name instanceof Array) {
- [].unshift.apply(this.members, name);
- } else {
- this.members.unshift(name);
- }
-};
-
-Block.prototype.push = function(name) {
- if (name instanceof Array) {
- [].push.apply(this.members, name);
- } else {
- this.members.push(name);
- }
-};
-
-function Script() {
- Block.apply(this, arguments);
-}
-util.inherits(Script, Block);
-Script.prototype.toString = function() {
- var result = '';
- this.members.forEach(function(script) {
- result += '\n';
- });
- return result;
-};
-
-function Style() {
- Block.apply(this, arguments);
-}
-util.inherits(Style, Block);
-
-Style.prototype.toString = function() {
- var result = '';
- this.members.forEach(function(style) {
- result += '\n';
- });
- return result;
-};
-
-
-module.exports = function(req, res, next) {
- res.locals.script = new Script;
- res.locals.style = new Style;
- next();
-};
\ No newline at end of file
diff --git a/mocha b/mocha
new file mode 100644
index 000000000..7b01318e1
--- /dev/null
+++ b/mocha
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# this script is used to run all or standalone mocha scripts
+# like this:
+# ./mocha.sh
+# OR
+# ./mocha.sh test/unit/model/user.js
+
+# tried also gulp-mocha and node --harmony `which gulp` test,
+# but it hangs after tests, not sure why, mocha.sh works fine so leave it as is
+NODE_ENV=development ./gulp link-modules
+NODE_ENV=test mocha --harmony $*
+
diff --git a/mocha.sh b/mocha.sh
deleted file mode 100755
index 1f9e10e2b..000000000
--- a/mocha.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-NODE_ENV=test NODE_PATH=. mocha --timeout 10000 --require should --require test/env $*
-
diff --git a/models/index.js b/models/index.js
deleted file mode 100644
index c484a6f15..000000000
--- a/models/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-var fs = require("fs");
-
-fs.readdirSync(__dirname).forEach(function(file) {
- if (file == 'index.js') return;
- require("./" + file);
-});
diff --git a/models/user.js b/models/user.js
deleted file mode 100755
index 20f9db57c..000000000
--- a/models/user.js
+++ /dev/null
@@ -1,78 +0,0 @@
-var mongoose = require('lib/mongoose');
-var hash = require('lib/hash');
-var Schema = mongoose.Schema;
-
-var schema = Schema({
- username: {
- type: String,
- required: true,
- unique: true
- },
- email: {
- type: String,
- unique: true,
- required: true,
- index: true
- },
- salt: String,
- hash: String,
- created: {
- type: Date,
- default: Date.now
- },
- avatar: {
- type: String,
- default: '1.jpg'
- }
-});
-
-schema.methods.toPublicObject = function() {
- return {
- username: this.get('username'),
- email: this.get('email'),
- avatar: this.get('avatar'),
- following: this.get('following'),
- created: this.get('created'),
- messagesCount: this.get('messagesCount'),
- id: this.id
- };
-};
-
-schema.virtual('password')
- .set(function(password) {
- this._plainPassword = password;
- this.salt = hash.createSalt();
- this.hash = hash.createHash(password, this.salt);
- })
- .get(function() {
- return this._plainPassword;
- });
-
-
-schema.statics.signup = function(username, email, password, done) {
- var User = this;
-
- User.create({
- username: username,
- email: email,
- password: password,
- avatar: (Math.random()*42^0) + '.jpg'
- }, done);
-
-};
-
-schema.methods.checkPassword = function(password) {
- return hash.createHash(password, this.salt) == this.hash;
-};
-
-schema.path('email').validate(function(value) {
- // wrap in new RegExp instead of /.../, to evade WebStorm validation errors (buggy webstorm)
- return new RegExp('^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,5}$').test(value);
-}, 'Please provide the correct e-mail.');
-
-// all references using mongoose.model for safe recreation
-// when I recreate model (for tests) => I can reload it from mongoose.model (single source of truth)
-// exports are less convenient to update
-mongoose.model('User', schema);
-
-
diff --git a/modules/app.js b/modules/app.js
new file mode 100644
index 000000000..74aead9c7
--- /dev/null
+++ b/modules/app.js
@@ -0,0 +1,96 @@
+"use strict";
+
+require('lib/debug');
+const koa = require('koa');
+const log = require('js-log')();
+const config = require('config');
+const mongoose = require('config/mongoose');
+const app = koa();
+
+// trust all headers from proxy
+// X-Forwarded-Host
+// X-Forwarded-Proto
+// X-Forwarded-For -> ip
+app.proxy = true;
+
+function requireSetup(path) {
+ // if debug is on => will log the middleware travel chain
+ if (process.env.NODE_ENV == 'development') {
+ app.use(function *(next) {
+ log.debug("-> setup " + path);
+ yield next;
+ log.debug("<- setup " + path);
+ });
+ }
+ require(path)(app);
+}
+
+// usually nginx will handle this before node
+// that's why we put it at the top
+requireSetup('setup/static');
+
+// this middleware adds this.render method
+// it is *before errorHandler*, because errors need this.render
+requireSetup('setup/render');
+
+// errors wrap everything
+requireSetup('setup/errorHandler');
+
+// this logger only logs HTTP status and URL
+// before everything to make sure it log all
+requireSetup('setup/accessLogger');
+
+// before anything that may deal with body
+requireSetup('setup/httpPostParser');
+
+// right after parsing body, make sure we logged for development
+requireSetup('setup/verboseLogger');
+
+if (process.env.NODE_ENV == 'development') {
+// app.verboseLogger.addPath('/:any*');
+}
+
+requireSetup('setup/session');
+
+requireSetup('setup/passport');
+
+requireSetup('setup/csrf');
+
+requireSetup('setup/payments');
+
+requireSetup('setup/router');
+
+// wait for full app load and all associated warm-ups to finish
+app.waitBoot = function* () {
+ yield function(callback) {
+ mongoose.waitConnect(callback);
+ };
+};
+
+
+// adding middlewares only possible before app.run
+app.run = function*() {
+ yield* app.waitBoot();
+
+ // every test may use app.run()
+ // app will only start the 1st time
+ if (!app.isListening) {
+ yield function(callback) {
+ app.listen(config.port, config.host, function() {
+ log.info('App listen %s:%d', config.host, config.port);
+ callback();
+ });
+ };
+ app.isListening = true;
+ }
+};
+
+// for supertest(app), it wants app.address().port
+app.address = function() {
+ return {
+ port: config.port
+ };
+};
+
+module.exports = app;
+
diff --git a/modules/config/index.js b/modules/config/index.js
new file mode 100644
index 000000000..b13637cab
--- /dev/null
+++ b/modules/config/index.js
@@ -0,0 +1,50 @@
+if (!process.env.NODE_ENV) {
+ process.env.NODE_ENV = 'development';
+}
+
+if (process.env.NODE_ENV == 'development' && process.env.DEV_TRACE) {
+ // @see https://github.com/AndreasMadsen/trace
+ require('trace'); // active long stack trace
+ require('clarify'); // Exclude node internal calls from the stack
+}
+
+
+var path = require('path');
+var fs = require('fs');
+
+var secretPath = fs.existsSync(path.join(__dirname, 'secret.js')) ? './secret' : './secret.template';
+var secret = require(secretPath);
+
+module.exports = {
+ "port": process.env.PORT || 3000,
+ "host": process.env.HOST || '0.0.0.0',
+ "domain": "stage.javascript.ru",
+ "mongoose": {
+ "uri": "mongodb://localhost/" + (process.env.NODE_ENV == 'test' ? "js_test" : "js"),
+ "options": {
+ "server": {
+ "socketOptions": {
+ "keepAlive": 1
+ },
+ "poolSize": 5
+ }
+ }
+ },
+ session: {
+ keys: [secret.sessionKey]
+ },
+ payments: secret.payments,
+ template: {
+ options: {
+ 'cache': process.env.NODE_ENV != 'development'
+ }
+ },
+ crypto: {
+ hash: {
+ length: 128,
+ // may be slow(!): iterations = 12000 take ~60ms to generate strong password
+ iterations: process.env.NODE_ENV == 'prod' ? 12000 : 1
+ }
+ },
+ publicPath: path.join(process.cwd(), 'www')
+};
diff --git a/modules/config/mongoose.js b/modules/config/mongoose.js
new file mode 100644
index 000000000..567a3aa14
--- /dev/null
+++ b/modules/config/mongoose.js
@@ -0,0 +1,106 @@
+/**
+ * This file must be required at least ONCE.
+ * After it's done, one can use require('mongoose')
+ *
+ * In web-app: this is done at init phase
+ * In tests: in mocha.opts
+ * In gulpfile: in beginning
+ */
+
+var mongoose = require('mongoose');
+var path = require('path');
+var fs = require('fs');
+var log = require('js-log')();
+var autoIncrement = require('mongoose-auto-increment');
+
+//mongoose.set('debug', true);
+
+var config = require('config');
+var _ = require('lodash');
+
+mongoose.connect(config.mongoose.uri, config.mongoose.options);
+
+autoIncrement.initialize(mongoose.connection);
+
+// bind context now for thunkify without bind
+_.bindAll(mongoose.connection);
+_.bindAll(mongoose.connection.db);
+
+// plugin from https://github.com/LearnBoost/mongoose/issues/1859
+// yield.. .persist() or .destroy() for generators instead of save/remove
+// mongoose 3.10 will not need that (!)
+mongoose.plugin(function(schema) {
+ schema.methods.persist = function(body) {
+ var model = this;
+
+ return function(callback) {
+ if (body) model.set(body);
+ model.save(callback);
+ };
+ };
+ schema.methods.destroy = function() {
+ var model = this;
+
+ return function(callback) {
+ model.remove(callback);
+ };
+ };
+
+ schema.statics.destroy = function(query) {
+ return function(callback) {
+ this.remove(query, callback);
+ }.bind(this);
+ };
+});
+
+mongoose.waitConnect = function(callback) {
+ if (mongoose.connection.readyState == 1) {
+ setImmediate(callback);
+ } else {
+ // we wait either for an error
+ // OR
+ // for a successful connection
+ mongoose.connection.on("connected", onConnected);
+ mongoose.connection.on("error", onError);
+ }
+
+ function onConnected() {
+ log.debug("Mongoose has just connected");
+ cleanUp();
+ callback();
+ }
+
+ function onError(err) {
+ log.debug('Failed to connect to DB', err);
+ cleanUp();
+ callback(err);
+ }
+
+ function cleanUp() {
+ mongoose.connection.removeListener("connected", onConnected);
+ mongoose.connection.removeListener("error", onError);
+ }
+};
+
+module.exports = mongoose;
+
+/*
+// models may want lib/mongoose that's why we require them AFTER module.exports = mongoose
+
+
+// read ALL models NOW, so that any hmvc app may require a model of another app
+requireModels();
+
+function requireModels() {
+ var root = process.cwd();
+
+ requireTree(path.join(root, 'model'));
+
+ var hmvcApps = fs.readdirSync(path.join(root, 'hmvc'));
+ hmvcApps.forEach(function(hmvcDir) {
+ var modelPath = path.join(path.join(root), 'hmvc', hmvcDir, 'model');
+ if (fs.existsSync(modelPath)) requireTree(modelPath);
+ });
+}
+
+*/
diff --git a/modules/config/secret.template.js b/modules/config/secret.template.js
new file mode 100644
index 000000000..9674cb1db
--- /dev/null
+++ b/modules/config/secret.template.js
@@ -0,0 +1,8 @@
+// this file contains all passwords etc,
+// should not be in repo
+
+exports.SESSION_KEY = "KillerIsJim";
+
+exports.payments = {
+ modules: {}
+};
\ No newline at end of file
diff --git a/modules/expiring-download/index.js b/modules/expiring-download/index.js
new file mode 100644
index 000000000..418e8b804
--- /dev/null
+++ b/modules/expiring-download/index.js
@@ -0,0 +1 @@
+exports.ExpiringDownloadLink = require('./models/expiringDownloadLink');
diff --git a/modules/expiring-download/models/expiringDownloadLink.js b/modules/expiring-download/models/expiringDownloadLink.js
new file mode 100644
index 000000000..8047f0c61
--- /dev/null
+++ b/modules/expiring-download/models/expiringDownloadLink.js
@@ -0,0 +1,21 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+// files use /files/ dir
+var schema = new Schema({
+ // use _id as unique token
+
+ // path from /files/
+ relativePath: {
+ type: String,
+ required: true
+ },
+ created: {
+ type: Date,
+ default: Date.now,
+ expires: '3d' // link must die in 3 days
+ }
+});
+
+module.exports = mongoose.model('ExpiringDownloadLink', schema);
+
diff --git a/modules/httpPostParser/index.js b/modules/httpPostParser/index.js
new file mode 100644
index 000000000..dfb61dc3e
--- /dev/null
+++ b/modules/httpPostParser/index.js
@@ -0,0 +1,75 @@
+'use strict';
+
+const koaFormidable = require('koa-formidable');
+const _ = require('lodash');
+const pathToRegexp = require('path-to-regexp');
+const log = require('js-log')();
+
+/**
+ * Wrapper around koa-bodyparser
+ * allows to set per-path options which are used in middleware
+ * usage:
+ *
+ * app.httpPostParser = new HttpPostParser
+ * app.use(app.bodyParser.middleware())
+ * ...
+ * app.httpPostParser.addPathOptions('/upload/path', {bytesExpected: 1e10});
+ * @constructor
+ */
+function HttpPostParser() {
+ this.pathOptions = [];
+}
+
+// options should be an object { path: string|regexp, options }
+HttpPostParser.prototype.addPathOptions = function(path, options) {
+ if (path instanceof RegExp) {
+ this.pathOptions.push({path: path, options: options});
+ } else if (typeof path == 'string') {
+ this.pathOptions.push({path: pathToRegexp(path), options: options});
+ } else {
+ throw new Error("unsupported path type: " + path);
+ }
+};
+
+HttpPostParser.prototype.middleware = function() {
+
+ var self = this;
+ var optionsDefault = { bytesExpected: 1e7 };
+
+ return function* (next) {
+ var options = Object.create(optionsDefault);
+
+ for (var i = 0; i < self.pathOptions.length; i++) {
+ var path = self.pathOptions[i].path;
+ log.debug("test " + this.req.url + " against " + path);
+ if (path.test(this.req.url)) {
+ log.debug("found options", self.pathOptions[i].options);
+ _.assign(options, self.pathOptions[i].options);
+ break;
+ }
+ }
+
+ // if request file too big, don't start accepting it
+ // in normal situation, large uploads have the header and get stopped here
+ if (this.get('content-length')) {
+ var bytesExpected = parseInt(this.get('content-length'), 10);
+
+ if (bytesExpected > options.bytesExpected) {
+ this.status = 413;
+ this.body = 'Request entity too large: ' + bytesExpected + ' > ' + options.bytesExpected;
+ return;
+ }
+ }
+
+ // safety:
+ // even if a bad person did not supply content-length,
+ // formidable will not read more than options.bytesExpected
+
+
+ yield* koaFormidable(options).call(this, next);
+
+ };
+};
+
+
+module.exports = HttpPostParser;
diff --git a/modules/httpPostParser/test/post.js b/modules/httpPostParser/test/post.js
new file mode 100644
index 000000000..b58572fcb
--- /dev/null
+++ b/modules/httpPostParser/test/post.js
@@ -0,0 +1,45 @@
+const app = require('app');
+const supertest = require('supertest');
+const should = require('should');
+
+describe("HttpPostParser", function() {
+
+ before(function* () {
+
+ // if app.isListening, then we can't add our middleware
+ should.not.exist(app.isListening);
+
+ app.use(function* echoBody(next) {
+ if ('/test/http-post-parser' != this.path) return yield next;
+ this.body = this.request.body;
+ });
+
+ yield app.run();
+
+ });
+
+ it("parses body", function(done) {
+
+ var message = { name: 'Manny', species: 'cat' };
+ supertest(app)
+ .post('/test/http-post-parser')
+ .send(message)
+ .end(function(error, res) {
+ res.body.should.be.eql(message);
+ done(error);
+ });
+
+ });
+
+ it("dies when the file is too big", function(done) {
+
+ // fixme: superagent console.warns: double callback!
+ // seems like a bug in superagent: https://github.com/visionmedia/superagent/issues/351
+ supertest(app)
+ .post('/test/http-post-parser')
+ .send({big: new Array(1e7).join(' ')})
+ .expect(413, done);
+
+ });
+
+});
diff --git a/modules/lib/csrf.js b/modules/lib/csrf.js
new file mode 100644
index 000000000..dfc7c0447
--- /dev/null
+++ b/modules/lib/csrf.js
@@ -0,0 +1,55 @@
+const pathToRegexp = require('path-to-regexp');
+const log = require('js-log')();
+
+function Csrf() {
+ this.ignorePaths = [];
+}
+
+
+// csrf.addIgnore adds a path into "disabled csrf" list
+Csrf.prototype.addIgnorePath = function(path) {
+ if (path instanceof RegExp) {
+ this.ignorePaths.push(path);
+ } else if (typeof path == 'string') {
+ this.ignorePaths.push(pathToRegexp(path));
+ } else {
+ throw new Error("unsupported path type: " + path);
+ }
+};
+
+Csrf.prototype.middleware = function() {
+ var self = this;
+
+ return function*(next) {
+ // skip these methods
+ if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'OPTIONS') {
+ return yield* next;
+ }
+
+ var checkCsrf = true;
+ for (var i = 0; i < self.ignorePaths.length; i++) {
+ var path = self.ignorePaths[i];
+ log.debug("test " + this.req.url + " against " + path);
+ if (path.test(this.req.url)) {
+ log.debug("match found, disable csrf check");
+ checkCsrf = false;
+ break;
+ }
+ }
+
+ // If test check CSRF only when "X-Test-Csrf" header is set
+ if (process.env.NODE_ENV == 'test') {
+ if (!this.get('X-Test-Csrf')) {
+ checkCsrf = false;
+ }
+ }
+
+ if (checkCsrf) {
+ this.assertCSRF(this.request.body);
+ }
+
+ yield* next;
+ };
+};
+
+exports.Csrf = Csrf;
diff --git a/modules/lib/dataUtil.js b/modules/lib/dataUtil.js
new file mode 100644
index 000000000..3af7ba54f
--- /dev/null
+++ b/modules/lib/dataUtil.js
@@ -0,0 +1,120 @@
+"use strict";
+
+var mongoose = require('mongoose');
+var log = require('js-log')();
+var co = require('co');
+var thunk = require('thunkify');
+var glob = require('glob');
+
+var db;
+//log.debugOn();
+
+function *createEmptyDb() {
+
+ function *open() {
+
+ if (mongoose.connection.readyState == 1) { // connected
+ return mongoose.connection.db;
+ }
+
+ yield thunk(mongoose.connection.on)('open');
+
+ return mongoose.connection.db;
+ }
+
+ function *clearDatabase() {
+
+ var collections = yield thunk(db.collectionNames)();
+
+ var collectionNames = collections
+ .map(function(collection) {
+ var collectionName = collection.name.slice(db.databaseName.length + 1);
+ if (collectionName.indexOf('system.') === 0) {
+ return null;
+ }
+ return collectionName;
+ })
+ .filter(function(name) {
+ return name;
+ });
+
+ yield collectionNames.map(function(name) {
+ log.debug("drop ", name);
+ return thunk(db.dropCollection)(name);
+ });
+
+ }
+
+ // wait till indexes are complete, especially unique
+ // required to throw errors
+ function *ensureIndexes() {
+
+ yield mongoose.modelNames().map(function(modelName) {
+ var model = mongoose.models[modelName];
+ return thunk(model.ensureIndexes.bind(model))();
+ });
+
+ }
+
+ // ensure that capped collections are actually capped
+ function *ensureCapped() {
+
+ yield mongoose.modelNames().map(function(modelName) {
+ var model = mongoose.models[modelName];
+ var schema = model.schema;
+ if (!schema.options.capped) return;
+
+ return thunk(db.command)({convertToCapped: model.collection.name, size: schema.options.capped});
+ });
+ }
+
+ log.debug("co");
+
+ db = yield open();
+ log.debug("open");
+
+ yield clearDatabase();
+ log.debug("clear");
+
+ yield ensureIndexes();
+ log.debug('indexes');
+
+ yield ensureCapped();
+ log.debug('capped');
+
+}
+
+// not using pow-mongoose-fixtures, becuae it fails with capped collections
+// it calls remove() on them => everything dies
+function *loadDb(dataFile) {
+ yield* createEmptyDb();
+ var modelsData = require(dataFile);
+
+ yield Object.keys(modelsData).map(function(modelName) {
+ return loadModel(modelName, modelsData[modelName]);
+ });
+}
+
+// fixture file must make sure that the model is loaded!
+function *loadModel(name, data) {
+
+ var Model = mongoose.models[name];
+
+ yield data.map(function(itemData) {
+ var model = new Model(itemData);
+ log.debug("save", itemData);
+ return model.persist();
+ });
+
+ log.debug("loadModel is done");
+}
+
+exports.loadDb = loadDb;
+exports.createEmptyDb = createEmptyDb;
+
+/*
+ Usage:
+ co(loadDb('sampleDb'))(function(err) {
+ if (err) throw err;
+ mongoose.connection.close();
+ });*/
diff --git a/modules/lib/debug.js b/modules/lib/debug.js
new file mode 100644
index 000000000..9601c077c
--- /dev/null
+++ b/modules/lib/debug.js
@@ -0,0 +1,18 @@
+
+
+/*
+ some crap to log & isolate steps for stackless errors
+ p() will print next number
+ */
+if (process.env.NODE_ENV == 'development') {
+
+ global.p = function() {
+ var stack = new Error().stack.split("\n")[2].trim();
+ console.log("----> " + global.p.counter++ + " at " + stack);
+ };
+ global.p.counter = 1;
+} else {
+ global.p = function() {
+
+ };
+}
diff --git a/modules/lib/treeUtil.js b/modules/lib/treeUtil.js
new file mode 100644
index 000000000..46ebf9661
--- /dev/null
+++ b/modules/lib/treeUtil.js
@@ -0,0 +1,71 @@
+function walkArray(node, visitor) {
+
+ for (var i = 0; i < node.children.length; i++) {
+ var treeNode = node.children[i];
+ visitor(treeNode);
+ if (treeNode.children) {
+ walkArray(treeNode, visitor);
+ }
+ }
+
+}
+
+function flattenArray(root) {
+
+ const flatten = [];
+
+ walkArray(root, function(node) {
+ flatten.push(node);
+ });
+
+ return flatten;
+
+}
+
+exports.walkArray = walkArray;
+
+
+exports.flattenArray = flattenArray;
+
+exports.findPrevNextById = function(root, id) {
+
+ var flatten = flattenArray(root);
+
+ var node, i;
+
+ for (i = 0; i < flatten.length; i++) {
+ node = flatten[i];
+ if (node._id.toString() == id.toString()) break;
+ }
+
+ if (node._id.toString() != id.toString()) {
+ throw new Error("Id is not in the tree: " + id);
+ }
+
+ var nextNum = i + 1, next;
+ while(true) {
+ if (!flatten[nextNum]) break; // array finished, no next, sorry
+ if (!flatten[nextNum].parent) break; // next item is a root of another tree, search finished
+ if (flatten[nextNum].isFolder) { // next item is a folder, go down
+ nextNum++;
+ continue;
+ }
+ next = flatten[nextNum];
+ break;
+ }
+
+ var prevNum = i - 1, prev;
+ while(true) {
+ if (!flatten[prevNum]) break;
+ if (!flatten[prevNum].parent) break;
+ if (flatten[prevNum].isFolder) {
+ prevNum--;
+ continue;
+ }
+ prev = flatten[prevNum];
+ break;
+ }
+
+
+ return { next: next, prev: prev };
+};
diff --git a/modules/lib/verboseLogger.js b/modules/lib/verboseLogger.js
new file mode 100644
index 000000000..5272e9c11
--- /dev/null
+++ b/modules/lib/verboseLogger.js
@@ -0,0 +1,56 @@
+const pathToRegexp = require('path-to-regexp');
+const log = require('js-log')();
+
+function VerboseLogger() {
+ this.logPaths = [];
+}
+
+
+// csrf.addIgnore adds a path into "disabled csrf" list
+VerboseLogger.prototype.addPath = function(path) {
+ if (path instanceof RegExp) {
+ this.logPaths.push(path);
+ } else if (typeof path == 'string') {
+ this.logPaths.push(pathToRegexp(path));
+ } else {
+ throw new Error("unsupported path type: " + path);
+ }
+};
+
+VerboseLogger.prototype.middleware = function() {
+ var self = this;
+
+ return function*(next) {
+
+ var shouldLog = false;
+ for (var i = 0; i < self.logPaths.length; i++) {
+ var path = self.logPaths[i];
+ log.debug("test " + this.req.url + " against " + path);
+ if (path.test(this.req.url)) {
+ log.debug("match found, will log all");
+ shouldLog = true;
+ break;
+ }
+ }
+
+ if (shouldLog) {
+ self.log(this);
+ }
+
+ yield* next;
+ };
+};
+
+VerboseLogger.prototype.log = function(context) {
+
+ for (var name in context.req.headers) {
+ console.log(name + ": " + context.req.headers[name]);
+ }
+
+ if (context.request.body) {
+ console.log(context.request.body);
+ }
+
+};
+
+exports.VerboseLogger = VerboseLogger;
diff --git a/setup/logger.js b/modules/setup/accessLogger.js
old mode 100755
new mode 100644
similarity index 96%
rename from setup/logger.js
rename to modules/setup/accessLogger.js
index 1674e7e1c..09d0c8c9d
--- a/setup/logger.js
+++ b/modules/setup/accessLogger.js
@@ -3,4 +3,4 @@ const logger = require('koa-logger');
module.exports = function(app) {
app.use(logger());
-};
\ No newline at end of file
+};
diff --git a/modules/setup/csrf.js b/modules/setup/csrf.js
new file mode 100644
index 000000000..01c817737
--- /dev/null
+++ b/modules/setup/csrf.js
@@ -0,0 +1,12 @@
+const csrf = require('koa-csrf');
+const Csrf = require('lib/csrf').Csrf;
+
+// every request gets different this._csrf to use in POST
+// but ALL tokens are valid
+module.exports = function(app) {
+ csrf(app);
+
+ app.csrf = new Csrf();
+
+ app.use(app.csrf.middleware());
+};
diff --git a/modules/setup/errorHandler.js b/modules/setup/errorHandler.js
new file mode 100644
index 000000000..7e41690b7
--- /dev/null
+++ b/modules/setup/errorHandler.js
@@ -0,0 +1,94 @@
+'use strict';
+
+const config = require('config');
+const log = require('js-log')();
+const escapeHtml = require('escape-html');
+
+function renderError(error) {
+ /*jshint -W040 */
+ this.status = error.status;
+
+ var preferredType = this.accepts('html', 'json');
+
+ if (preferredType == 'json') {
+ this.body = error;
+ } else {
+ this.render("error", {error: error});
+ }
+}
+
+function renderDevError(error) {
+ /*jshint -W040 */
+ this.status = 500;
+
+ var preferredType = this.accepts('html', 'json');
+
+ if (preferredType == 'json') {
+ this.body = error;
+ } else {
+ var stack = (error.stack || '')
+ .split('\n').slice(1)
+ .map(function(v){ return '' + escapeHtml(v).replace(/ /g, ' ') + ' '; }).join('');
+
+ this.type = 'text/html; charset=utf-8';
+ this.body = "" + error.message + "
"+stack+ "
";
+ }
+
+}
+
+module.exports = function(app) {
+
+ app.use(function*(next) {
+ this.renderError = renderError;
+ this.renderDevError = renderDevError;
+
+ try {
+ yield* next;
+ } catch (err) {
+
+ if (err.status) {
+ // user-level error
+ if (process.env.NODE_ENV == 'development') {
+ console.log(err);
+ }
+ this.renderError(err);
+ } else {
+
+ // if error is "call stack too long", then log.error(err) is not verbose
+ // so I cast it to string
+ log.error(err.toString());
+ log.error(err.stack);
+
+ this.set('X-Content-Type-Options', 'nosniff');
+
+ if (process.env.NODE_ENV == 'development') {
+ this.renderDevError(err);
+ } else {
+ this.renderError({status: 500, message: "Internal Error"});
+ }
+ }
+ }
+
+ });
+
+ // this middleware handles error BEFORE ^^^
+ // rewrite mongoose wrong mongoose parameter -> 400 (not 500)
+ app.use(function* rewriteCastError(next) {
+
+ try {
+ yield next;
+ } catch (err) {
+
+ if (err.name == 'CastError') {
+ // malformed or absent mongoose params
+ if (process.env.NODE_ENV != 'development') {
+ this.throw(400);
+ }
+ }
+
+ throw err;
+ }
+
+ });
+
+};
diff --git a/modules/setup/httpPostParser.js b/modules/setup/httpPostParser.js
new file mode 100644
index 000000000..d83007ed6
--- /dev/null
+++ b/modules/setup/httpPostParser.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const HttpPostParser = require('httpPostParser');
+const _ = require('lodash');
+
+module.exports = function(app) {
+
+ app.httpPostParser = new HttpPostParser();
+ app.use(app.httpPostParser.middleware());
+
+};
diff --git a/modules/setup/passport.js b/modules/setup/passport.js
new file mode 100644
index 000000000..bee347347
--- /dev/null
+++ b/modules/setup/passport.js
@@ -0,0 +1,11 @@
+const mongoose = require('mongoose');
+const passport = require('koa-passport');
+const config = require('config');
+
+
+module.exports = function(app) {
+
+ app.use(passport.initialize());
+ app.use(passport.session());
+
+};
diff --git a/modules/setup/payments.js b/modules/setup/payments.js
new file mode 100644
index 000000000..16d4d9fbe
--- /dev/null
+++ b/modules/setup/payments.js
@@ -0,0 +1,7 @@
+const mongoose = require('mongoose');
+const payments = require('payments');
+const config = require('config');
+
+module.exports = function(app) {
+ app.use(payments.populateContextMiddleware);
+};
diff --git a/modules/setup/render.js b/modules/setup/render.js
new file mode 100644
index 000000000..076e22829
--- /dev/null
+++ b/modules/setup/render.js
@@ -0,0 +1,125 @@
+'use strict';
+
+const moment = require('moment');
+const Parser = require('jade').Parser;
+const util = require('util');
+const path = require('path');
+const config = require('config');
+const fs = require('fs');
+const log = require('js-log')();
+const jade = require('jade');
+const _ = require('lodash');
+const assert = require('assert');
+
+//log.debugOn();
+
+module.exports = function render(app) {
+ app.use(function *(next) {
+ var ctx = this;
+
+ this.locals = _.assign({}, config.template.options);
+ this.locals.moment = moment;
+
+ // warning!
+ // _.assign does NOT copy defineProperty
+ Object.defineProperty(this.locals, "csrf", {
+ get: function() {
+ var csrf = ctx.csrf;
+ assert(csrf);
+ return csrf;
+ }
+ });
+
+ // this.locals.debug causes jade to dump function
+ /* jshint -W087 */
+ this.locals.deb = function() {
+ debugger;
+ };
+
+ // render(__dirname, 'article', {}) -- 3 args
+ // render(__dirname, 'article') -- 2 args
+ // render('article', {}) -- 2 args
+ // render('article')
+ this.render = function(templateDir, templatePath, locals) {
+ if (arguments.length == 2) {
+ if (typeof templatePath == 'object') {
+ templatePath = arguments[0];
+ templateDir = process.cwd();
+ locals = arguments[1];
+ } else {
+ locals = {};
+ }
+ }
+
+ if (arguments.length == 1) {
+ locals = {};
+ templateDir = process.cwd();
+ }
+
+ // this jade parser knows templateDir through closure
+ function JadeParser(str, filename, options) {
+ Parser.apply(this, arguments);
+ }
+
+ util.inherits(JadeParser, Parser);
+
+ JadeParser.prototype.resolvePath = function(templatePath, purpose) {
+
+ if (templatePath[0] == '.') {
+ if (!this.filename) {
+ throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths');
+ }
+
+ // files like article.html are included in a special way (Filter)
+ templatePath = path.join(path.dirname(this.filename), templatePath) + (path.extname(templatePath) ? '' : '.jade');
+ //console.log("Resolve to ", path);
+ return templatePath;
+ }
+
+ log.debug("resolvePathUp " + templateDir + " " + templatePath);
+
+ return resolvePathUp(templateDir, templatePath + '.jade');
+ };
+
+ var loc = Object.create(this.locals);
+
+ var parseLocals = {
+ parser: JadeParser
+ };
+
+ _.assign(loc, parseLocals, locals);
+
+// console.log(loc);
+ var file = resolvePathUp(templateDir, templatePath + '.jade');
+ if (!file) {
+ throw new Error("Template file not found: " + templatePath + " (in dir " + templateDir + ") ");
+ }
+ log.debug("render file " + file);
+ this.body = jade.renderFile(file, loc);
+ };
+
+
+ yield next;
+ });
+
+};
+
+function resolvePathUp(templateDir, templateName) {
+
+ templateDir = path.resolve(templateDir);
+ var top = path.resolve(process.cwd());
+
+ while (templateDir != path.dirname(top)) {
+ var template = path.join(templateDir, 'templates', templateName);
+ log.debug("-- try path", template);
+ if (fs.existsSync(template)) {
+ log.debug("-- found");
+ return template;
+ }
+ log.debug("-- skipped");
+ templateDir = path.dirname(templateDir);
+ }
+
+ log.debug("-- failed", templateDir, top);
+ return null;
+}
diff --git a/modules/setup/router.js b/modules/setup/router.js
new file mode 100644
index 000000000..33ec2f3af
--- /dev/null
+++ b/modules/setup/router.js
@@ -0,0 +1,55 @@
+'use strict';
+
+var mount = require('koa-mount');
+var compose = require('koa-compose');
+
+module.exports = function(app) {
+
+
+ app.use(mount('/', require('frontpage').middleware));
+
+ if (process.env.NODE_ENV == 'development') {
+ app.use(mount('/markup', require('markup').middleware));
+ }
+
+ app.use(mount('/auth', require('auth').middleware));
+ app.csrf.addIgnorePath('/auth/login/:any*');
+
+ app.use(mount('/getpdf', require('getpdf').middleware));
+
+ app.use(mount('/payments', require('payments').middleware));
+ app.csrf.addIgnorePath('/payments/:any*');
+ app.verboseLogger.addPath('/payments/:any*');
+
+ /*
+ app.use(mount('/webmoney', compose([payment.middleware, require('webmoney').middleware])));
+ app.csrf.addIgnorePath('/webmoney/:any*');
+ app.verboseLogger.addPath('/webmoney/:any*');
+
+ app.use(mount('/yandexmoney', compose([payment.middleware, require('yandexmoney').middleware])));
+ app.csrf.addIgnorePath('/yandexmoney/:any*');
+ app.verboseLogger.addPath('/yandexmoney/:any*');
+
+ app.use(mount('/payanyway', compose([payment.middleware, require('payanyway').middleware])));
+ app.csrf.addIgnorePath('/payanyway/:any*');
+ app.verboseLogger.addPath('/payanyway/:any*');
+
+ app.use(mount('/paypal', compose([payment.middleware, require('paypal').middleware])));
+ app.csrf.addIgnorePath('/paypal/:any*');
+ app.verboseLogger.addPath('/paypal/:any*');
+*/
+
+ // stick to bottom to detect any not-yet-processed /:slug
+ app.use(mount('/', require('tutorial').middleware));
+
+ // by default if the router didn't find anything => it yields to next middleware
+ // so I throw error here manually
+ app.use(function* (next) {
+ yield* next;
+
+ if (this.status == 404) {
+ this.throw(404); // still not found? pass to default errorHandler
+ }
+ });
+
+};
diff --git a/setup/session.js b/modules/setup/session.js
old mode 100755
new mode 100644
similarity index 76%
rename from setup/session.js
rename to modules/setup/session.js
index 518e915e9..ec8b7e6b7
--- a/setup/session.js
+++ b/modules/setup/session.js
@@ -1,5 +1,5 @@
-const mongoose = require('lib/mongoose');
-const session = require('koa-sess');
+const mongoose = require('mongoose');
+const session = require('koa-generic-session');
const mongooseStore = require('koa-session-mongoose');
const config = require('config');
@@ -14,4 +14,4 @@ module.exports = function(app) {
app.keys = config.session.keys; // needed for cookie-signing
-};
\ No newline at end of file
+};
diff --git a/modules/setup/static.js b/modules/setup/static.js
new file mode 100644
index 000000000..d69adc7fe
--- /dev/null
+++ b/modules/setup/static.js
@@ -0,0 +1,36 @@
+'use strict';
+
+
+const favicon = require('koa-favicon');
+const send = require('koa-send');
+const path = require('path');
+
+
+/**
+ * koa-static is a thin wrapper around koa-send
+ * Here we statically send all paths with extension.
+ *
+ * ...And if we fail, there is no big-nice-error-screen which is slow to render
+ * just a simple default error message
+ * @param app
+ */
+module.exports = function(app) {
+
+ app.use(favicon());
+
+ app.use(function*(next) {
+ var opts = {
+ root: 'www',
+ index: 'index.html'
+ };
+
+ if (this.idempotent && path.extname(this.path) !== '') {
+ yield send(this, this.path, opts);
+ return;
+ }
+
+ yield* next;
+
+ });
+
+};
diff --git a/modules/setup/verboseLogger.js b/modules/setup/verboseLogger.js
new file mode 100644
index 000000000..fe35f10ec
--- /dev/null
+++ b/modules/setup/verboseLogger.js
@@ -0,0 +1,9 @@
+const VerboseLogger = require('lib/verboseLogger').VerboseLogger;
+
+
+module.exports = function(app) {
+
+ app.verboseLogger = new VerboseLogger();
+ app.use(app.verboseLogger.middleware());
+
+};
diff --git a/out/native/d8 b/out/native/d8
new file mode 100644
index 000000000..21166c891
Binary files /dev/null and b/out/native/d8 differ
diff --git a/package.json b/package.json
index 76bedfc03..098f65036 100755
--- a/package.json
+++ b/package.json
@@ -3,46 +3,119 @@
"version": "0.0.1",
"private": true,
"scripts": {
- "start": "NODE_ENV=production NODE_PATH=. node --harmony --harmony_generators ./bin/www",
- "dev": "NODE_ENV=development DEBUG=server:* NODE_PATH=. supervisor --harmony --debug --ignore node_modules ./bin/www",
- "test": "NODE_ENV=test DEBUG=server:* NODE_PATH=. supervisor --harmony --debug --ignore node_modules ./bin/www"
+ "prod": "NODE_ENV=production node --harmony ./bin/www",
+ "dev": "NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www",
+ "debug": "NODE_ENV=development supervisor --harmony --debug-brk --ignore node_modules ./bin/www",
+ "test": "NODE_ENV=test supervisor --harmony --debug --ignore node_modules ./bin/www",
+ "fixperms": "sudo chown -R `id -u` . ~/.npm* ~/.node-gyp"
},
+ "precommit": "NODE_ENV=development node --harmony `which gulp` pre-commit",
"dependencies": {
+ "MD5": "*",
"body-parser": "*",
+ "brfs": "*",
"co": "*",
- "koa-views": "*",
+ "escape-html": "*",
+ "event-stream": "*",
+ "factor-bundle": "*",
+ "fs-extra": "*",
+ "glob": "*",
+ "gm": "*",
+ "gulp-cache": "*",
+ "gulp-concat": "*",
+ "gulp-debug": "*",
+ "gulp-dir-sync": "*",
+ "gulp-if": "*",
+ "gulp-ignore": "*",
+ "gulp-jshint-cache": "*",
+ "gulp-load-plugins": "*",
+ "gulp-newer": "*",
+ "gulp-notify": "*",
+ "gulp-plumber": "*",
+ "gulp-rimraf": "*",
+ "gulp-stylus": "*",
+ "gulp-util": "*",
+ "gulp.spritesmith": "*",
+ "imagemin": "*",
+ "imagemin-pngcrush": "*",
+ "imagemin-svgo": "*",
"jade": "*",
+ "javascript-gulp-task-lint": "*",
+ "javascript-parser": "*",
+ "jquery": "*",
+ "js-log": "*",
"koa": "*",
"koa-bodyparser": "*",
+ "koa-compose": "*",
+ "koa-csrf": "*",
"koa-favicon": "*",
+ "koa-formidable": "*",
+ "koa-generic-session": "*",
"koa-logger": "*",
+ "koa-mount": "*",
+ "koa-passport": "*",
+ "koa-request": "*",
"koa-router": "*",
- "koa-sess": "*",
+ "koa-send": "*",
"koa-session-mongoose": "*",
"koa-static": "*",
+ "koa-views": "*",
"lodash": "*",
- "mongoose": "*",
+ "map-stream": "*",
+ "moment": "*",
+ "mongoose": "3.8",
+ "mongoose-auto-increment": "*",
+ "mongoose-troop": "git://github.com/iliakan/mongoose-troop",
+ "nib": "*",
+ "nodemailer": "*",
+ "nodemailer-ses-transport": "*",
"passport": "*",
- "winston": "*"
+ "passport-local": "*",
+ "path-to-regexp": "*",
+ "prismjs": "git://github.com/LeaVerou/prism#gh-pages",
+ "stylus": "*",
+ "svgutils": "*",
+ "through2": "*",
+ "thunkify": "*",
+ "vinyl-fs": "*",
+ "vinyl-source-stream": "*",
+ "winston": "*",
+ "yargs": "*"
},
"devDependencies": {
- "javascript-brunch": "*",
- "css-brunch": "*",
- "uglify-js-brunch": "*",
- "clean-css-brunch": "*",
- "stylus-brunch": "*",
- "autoprefixer-brunch": "*",
- "supervisor": "*"
+ "better-assert": "^1.0.1",
+ "browserify": "*",
+ "clarify": "*",
+ "co-mocha": "*",
+ "gulp": "*",
+ "gulp-autoprefixer": "*",
+ "gulp-flatten": "*",
+ "gulp-jshint": "*",
+ "gulp-livereload": "*",
+ "gulp-mocha": "*",
+ "gulp-sourcemaps": "*",
+ "gulp-stylus-sprite": "*",
+ "gulp-supervisor": "*",
+ "lazypipe": "*",
+ "mocha": "*",
+ "node-notifier": "*",
+ "should": "*",
+ "sinon": "*",
+ "superagent": "*",
+ "supertest": "*",
+ "supervisor": "*",
+ "trace": "*",
+ "watchify": "*"
},
"engines": {
- "node" : "0.11.12"
+ "node": ">=0.11.13"
},
"engineStrict": true,
"repository": {
"type": "git",
"url": "https://github.com/iliakan/javascript-nodejs.git"
},
- "author": "Ilya Kantor and other contributors listed in git repo",
+ "author": "Ilya Kantor and other contributors in git repo",
"license": "CC BY-NC-SA 3.0",
"bugs": {
"url": "https://github.com/iliakan/javascript-nodejs/issues"
diff --git a/routes/index.js b/routes/index.js
deleted file mode 100755
index a4f0a24bc..000000000
--- a/routes/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-var log = require('lib/log')(module);
-var config = require('config');
-
-module.exports = function(app) {
- app.get('/', require('controllers/frontpage').get);
-};
\ No newline at end of file
diff --git a/setup/bodyParser.js b/setup/bodyParser.js
deleted file mode 100755
index a935f5285..000000000
--- a/setup/bodyParser.js
+++ /dev/null
@@ -1,7 +0,0 @@
-'use strict'
-
-const bodyParser = require('koa-bodyparser');
-
-module.exports = function(app) {
- app.use(bodyParser());
-};
\ No newline at end of file
diff --git a/setup/errors.js b/setup/errors.js
deleted file mode 100755
index 36d9633ba..000000000
--- a/setup/errors.js
+++ /dev/null
@@ -1,45 +0,0 @@
-'use strict';
-
-var config = require('config');
-var log = require('lib/log')(module);
-var HttpError = require('error').HttpError;
-
-module.exports = function(app) {
-
- /* TODO: rewrite this express-style error handling in koa
-@see https://github.com/koajs/koa/wiki/Error-Handling
-
- this.use(function(req, res, next) {
- next(404);
- });
-
- this.use(function(err, req, res, next) {
- if (typeof err == 'number') {
- err = new HttpError(err);
- }
-
- if (err.name == 'CastError') {
- // malformed or absent mongoose params
- if (process.env.NODE_ENV == 'development') {
- log.error(err);
- }
- res.sendHttpError(new HttpError(400));
- return;
- }
-
- if (err instanceof HttpError) {
- res.sendHttpError(err);
- } else {
- // if error is "call stack too long", then log.error(err) is not verbose
- // so I cast it to string
- log.error(err.toString());
-
- if (process.env.NODE_ENV == 'development') {
- errorhandler()(err, req, res, next);
- } else {
- res.sendHttpError(new HttpError(500));
- }
- }
- });
-*/
-};
\ No newline at end of file
diff --git a/setup/render.js b/setup/render.js
deleted file mode 100644
index 8e3a82133..000000000
--- a/setup/render.js
+++ /dev/null
@@ -1,9 +0,0 @@
-'use strict';
-
-var views = require('koa-views');
-var config = require('config');
-
-module.exports = function render(app) {
- app.use(views(config.template.path, config.template.options));
-};
-
diff --git a/setup/router.js b/setup/router.js
deleted file mode 100755
index 07707cede..000000000
--- a/setup/router.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-const router = require('koa-router');
-module.exports = function(app) {
-
- app.use(router(app));
-
-};
\ No newline at end of file
diff --git a/setup/static.js b/setup/static.js
deleted file mode 100755
index 34b1ed198..000000000
--- a/setup/static.js
+++ /dev/null
@@ -1,9 +0,0 @@
-'use strict'
-
-const serve = require('koa-static');
-const favicon = require('koa-favicon');
-
-module.exports = function(app) {
- app.use(serve('www'));
- app.use(favicon());
-};
\ No newline at end of file
diff --git a/tasks/browserify.js b/tasks/browserify.js
new file mode 100644
index 000000000..7269b1941
--- /dev/null
+++ b/tasks/browserify.js
@@ -0,0 +1,85 @@
+const gp = require('gulp-load-plugins')();
+const gulp = require('gulp');
+const source = require('vinyl-source-stream');
+const gutil = require('gulp-util');
+const watchify = require('watchify');
+const browserify = require('browserify');
+var Notification = require('node-notifier');
+var assert = require('assert');
+var _ = require('lodash');
+var path = require('path');
+
+// TODO: add uglify if not development
+function makeBundler(options) {
+
+ // dst has same name as (single) src
+ var opts = _.assign({}, options, {
+ debug: (process.env.NODE_ENV === 'development'),
+ cache: {},
+ packageCache: {},
+ fullPaths: true
+ });
+
+ var bundler = browserify(opts);
+ bundler.rebundle = function() {
+ this.bundle()
+ .on('error', function(e) {
+ gutil.log(e.message);
+ new Notification().notify({
+ message: e
+ });
+ })
+ .pipe(source(path.basename(this._options.dst)))
+ .pipe(gulp.dest(path.dirname(this._options.dst)));
+ };
+ bundler.on('update', bundler.rebundle);
+ bundler.on('log', function(msg) {
+ gutil.log("browserify: " + msg);
+ });
+
+ if (options.externals) {
+ for (var i = 0; i < options.externals.length; i++) {
+ var external = options.externals[i];
+ bundler.external(external);
+ }
+ }
+
+ if (process.env.NODE_ENV == 'development') {
+ bundler = watchify(bundler);
+ }
+
+ return bundler;
+}
+
+module.exports = function() {
+
+ return function(callback) {
+
+ var vendor = ['jquery'];
+
+ var bundler = makeBundler({
+ entries: [],
+ dst: './www/js/vendor.js',
+ require: vendor
+ });
+
+ bundler.rebundle();
+
+ var bundler = makeBundler({
+ entries: './app/js/head.js',
+ dst: './www/js/head.js'
+ });
+
+ bundler.rebundle();
+
+
+ var bundler = makeBundler({
+ entries: './app/js/main.js',
+ dst: './www/js/main.js',
+ externals: vendor
+ });
+
+
+ bundler.rebundle();
+ };
+};
diff --git a/tasks/browserifyClean.js b/tasks/browserifyClean.js
new file mode 100644
index 000000000..f27e68a69
--- /dev/null
+++ b/tasks/browserifyClean.js
@@ -0,0 +1,11 @@
+const fse = require('fs-extra');
+
+module.exports = function(options) {
+
+ return function(callback) {
+ fse.removeSync(options.dst);
+ fse.mkdirsSync(options.dst);
+ callback();
+ };
+};
+
diff --git a/tasks/compileCss.js b/tasks/compileCss.js
new file mode 100644
index 000000000..d09c5aeb9
--- /dev/null
+++ b/tasks/compileCss.js
@@ -0,0 +1,19 @@
+const gulp = require('gulp');
+const gp = require('gulp-load-plugins')();
+
+module.exports = function(options) {
+
+ return function() {
+
+ return gulp.src(options.src)
+ // without plumber if stylus emits PluginError, it will disappear at the next step
+ // plumber propagates it down the chain
+ .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")}))
+ .pipe(gp.stylus({use: [require('nib')()]}))
+ .pipe(gp.autoprefixer("last 1 version"))
+ .pipe(gulp.dest(options.dst));
+ };
+
+
+};
+
diff --git a/tasks/linkModules.js b/tasks/linkModules.js
new file mode 100644
index 000000000..2d7f3aacf
--- /dev/null
+++ b/tasks/linkModules.js
@@ -0,0 +1,55 @@
+var fs = require('fs');
+var glob = require('glob');
+var path = require('path');
+var gutil = require('gulp-util');
+
+// Ensures the existance of a symlink linkDst -> linkSrc
+// returns true if link was created
+// returns false if link exists alread (and is correct)
+// throws error if conflict (another file or link by that name)
+function ensureSymlinkSync(linkSrc, linkDst) {
+ var lstat;
+ try {
+ lstat = fs.lstatSync(linkDst);
+ } catch (e) {
+ }
+
+ if (lstat) {
+ if (!lstat.isSymbolicLink()) {
+ throw new Error("Conflict: path exist and is not a link: " + linkDst);
+ }
+
+ var oldDst = fs.readlinkSync(linkDst);
+ if (oldDst == linkSrc) {
+ return false; // already exists
+ }
+ // kill old link!
+ fs.unlinkSync(linkDst);
+ }
+
+ fs.symlinkSync(linkSrc, linkDst);
+ return true;
+}
+
+module.exports = function(options) {
+
+ return function(callback) {
+ var modules = [];
+ options.src.forEach(function(pattern) {
+ modules = modules.concat(glob.sync(pattern));
+ });
+
+ for (var i = 0; i < modules.length; i++) {
+ var moduleToLinkRelPath = modules[i]; // hmvc/auth
+ var moduleToLinkName = path.basename(moduleToLinkRelPath); // auth
+ var linkSrc = path.join('..', moduleToLinkRelPath);
+ var linkDst = path.join('node_modules', moduleToLinkName);
+ if (ensureSymlinkSync(linkSrc, linkDst)) {
+ gutil.log(linkSrc + " -> " + linkDst);
+ }
+ }
+ callback();
+ };
+
+};
+
diff --git a/tasks/lint.js b/tasks/lint.js
new file mode 100644
index 000000000..56f200236
--- /dev/null
+++ b/tasks/lint.js
@@ -0,0 +1,12 @@
+
+const gulp = require('gulp');
+
+module.exports = function(options) {
+ return function(callback) {
+ if (process.env.NODE_ENV == 'development') {
+ gulp.watch(options.src, ['lint']);
+ } else {
+ callback();
+ }
+ };
+};
diff --git a/tasks/lintOnce.js b/tasks/lintOnce.js
new file mode 100644
index 000000000..768fb5ac7
--- /dev/null
+++ b/tasks/lintOnce.js
@@ -0,0 +1,8 @@
+const gp = require('gulp-load-plugins')();
+
+module.exports = function(options) {
+
+ return function(callback) {
+ return gp.jshintCache(options).apply(this, arguments);
+ };
+};
diff --git a/tasks/livereload.js b/tasks/livereload.js
new file mode 100644
index 000000000..5defc1b5f
--- /dev/null
+++ b/tasks/livereload.js
@@ -0,0 +1,20 @@
+var livereload = require('gulp-livereload');
+var gulp = require('gulp');
+var gutil = require('gulp-util');
+
+// options.watch must NOT be www/**, because that breaks (why?!?) supervisor reloading
+// www/**/*.* is fine
+module.exports = function(options) {
+
+ // listen to changes after 7 secs, to let initial jobs finish
+ // no one is going to livereload right now anyway
+ return function(callback) {
+ livereload.listen();
+ setTimeout(function() {
+ gutil.log("livereload: deferred listen on change " + options.watch);
+ gulp.watch(options.watch).on('change', livereload.changed);
+ }, 7000);
+ };
+};
+
+
diff --git a/tasks/loadDb.js b/tasks/loadDb.js
new file mode 100644
index 000000000..51a8361bd
--- /dev/null
+++ b/tasks/loadDb.js
@@ -0,0 +1,33 @@
+var fs = require('fs');
+var co = require('co');
+var path = require('path');
+var gutil = require('gulp-util');
+var dataUtil = require('lib/dataUtil');
+var mongoose = require('config/mongoose');
+
+module.exports = function() {
+ return function(callback) {
+
+ var args = require('yargs')
+ .usage("Path to DB is required.")
+ .demand(['db'])
+ .argv;
+
+ var dbPath = path.join(process.cwd(), args.db);
+
+ gutil.log("loading db " + dbPath);
+
+ co(function*() {
+
+ yield* dataUtil.loadDb(dbPath);
+
+ gutil.log("loaded db " + dbPath);
+ })(function(err) {
+ if (err) throw err;
+ mongoose.disconnect();
+ callback();
+ });
+
+ };
+};
+
diff --git a/tasks/sprite.js b/tasks/sprite.js
new file mode 100644
index 000000000..549adf4af
--- /dev/null
+++ b/tasks/sprite.js
@@ -0,0 +1,9 @@
+const gp = require('gulp-load-plugins')();
+
+module.exports = function(options) {
+
+ return function(callback) {
+ return gp.stylusSprite(options).apply(this, arguments);
+ };
+};
+
diff --git a/tasks/supervisor.js b/tasks/supervisor.js
new file mode 100644
index 000000000..e1c4c50f4
--- /dev/null
+++ b/tasks/supervisor.js
@@ -0,0 +1,16 @@
+
+const gp = require('gulp-load-plugins')();
+
+module.exports = function(options) {
+
+ return function(callback) {
+ gp.supervisor(options.cmd, {
+ args: [],
+ watch: options.watch,
+ pollInterval: 100,
+ extensions: [ "js" ],
+ debug: true,
+ harmony: true
+ });
+ };
+};
diff --git a/tasks/syncCssImages.js b/tasks/syncCssImages.js
new file mode 100644
index 000000000..fd8e06697
--- /dev/null
+++ b/tasks/syncCssImages.js
@@ -0,0 +1,17 @@
+const fse = require('fs-extra');
+const gp = require('gulp-load-plugins')();
+const gulp = require('gulp');
+
+module.exports = function(options) {
+
+ return function(callback) {
+
+ fse.ensureDirSync(options.dst);
+
+ return gulp.src(options.src)
+ .pipe(gp.flatten())
+ .pipe(gp.newer(options.dst))
+ .pipe(gulp.dest(options.dst));
+
+ };
+};
diff --git a/tasks/syncResources.js b/tasks/syncResources.js
new file mode 100644
index 000000000..7e21a24e7
--- /dev/null
+++ b/tasks/syncResources.js
@@ -0,0 +1,23 @@
+const fse = require('fs-extra');
+const gp = require('gulp-load-plugins')();
+
+module.exports = function(resources) {
+
+ return function(callback) {
+
+ for (var src in resources) {
+ var dst = resources[src];
+
+ fse.removeSync(dst);
+
+ if (process.env.NODE_ENV == 'development') {
+ fse.mkdirsSync(dst);
+ gp.dirSync(src, dst);
+ } else {
+ fse.copySync(src, dst);
+ }
+ }
+
+ if (process.env.NODE_ENV != 'development') callback();
+ };
+};
diff --git a/templates/blocks/breadcrumbs.jade b/templates/blocks/breadcrumbs.jade
new file mode 100644
index 000000000..eae4df9c5
--- /dev/null
+++ b/templates/blocks/breadcrumbs.jade
@@ -0,0 +1,9 @@
+ol.breadcrumbs
+ li.breadcrumbs__item
+ a(href='/') Главная
+ li.breadcrumbs__item
+ a(href='/tutorial') Учебник
+ li.breadcrumbs__item
+ a(href='/getting-started') Общая информация
+ li.breadcrumbs__item
+ | Введение в JavaScript
diff --git a/templates/blocks/footer.jade b/templates/blocks/footer.jade
new file mode 100644
index 000000000..4ed5a30c2
--- /dev/null
+++ b/templates/blocks/footer.jade
@@ -0,0 +1 @@
+.page-footer Hello from footer
\ No newline at end of file
diff --git a/templates/blocks/head.jade b/templates/blocks/head.jade
new file mode 100644
index 000000000..52240e5db
--- /dev/null
+++ b/templates/blocks/head.jade
@@ -0,0 +1,10 @@
+doctype html
+html
+ meta(charset='UTF-8')
+ title= title
+ link(href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700|Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic,cyrillic-ext' rel='stylesheet' type='text/css')
+ link(href='/stylesheets/base.css',rel='stylesheet')
+ script(src='/js/head.js')
+ //- link(href='../app/assets/stylesheets/base.css' rel='stylesheet')
+ //if lte IE 9
+ //- link(href='../app/assets/stylesheets/base.ie.css' rel='stylesheet')
diff --git a/templates/blocks/lesson.jade b/templates/blocks/lesson.jade
new file mode 100644
index 000000000..e69de29bb
diff --git a/templates/blocks/lessons.jade b/templates/blocks/lessons.jade
new file mode 100644
index 000000000..cddee8ad3
--- /dev/null
+++ b/templates/blocks/lessons.jade
@@ -0,0 +1,8 @@
+- var lessons = [];
+- lessons.push({title: 'Переменные', })
+- lessons.push({title: 'Переменные'})
+
+.lessons
+ for [1..10]
+ .lessons__lesson-wrap
+ include lesson
diff --git a/templates/blocks/navbar.jade b/templates/blocks/navbar.jade
new file mode 100644
index 000000000..69f41f265
--- /dev/null
+++ b/templates/blocks/navbar.jade
@@ -0,0 +1,40 @@
+nav.navbar
+ ul.navbar__sections
+ li.navbar__sections-item.active.study
+ a(href='/study') Обучение
+ li.navbar__sections-item.blogs
+ a(href='/blogs') Блоги
+ li.navbar__sections-item.qa
+ a(href='/qa') Вопрос / ответ
+ li.navbar__sections-item.dropdown.dropdown_nav.down-right.inherit-min-width.dropdown_open_hover.reference
+ a.dropdown__toggle.dropdown__toggle_nav(href='http://javascript.ru/manual') Справочник
+ ul.dropdown__content.dropdown__content_nav
+ li
+ a(href='http://es5.javascript.ru/') Стандарт ES5
+ li
+ a(href='/referencd') Справочник JS
+ li
+ a(href='/php') PHP-функции
+ li
+ a(href='/book') Книги
+ li.navbar__sections-item.events
+ a(href='/event') События
+ li.navbar__sections-item.job
+ a(href='/job') Работа
+ li.navbar__sections-item.sandbox
+ a(href='/play') Песочница
+ li.navbar__sections-item.chat
+ a(href='/chat') Чат
+ li.navbar__sections-item.search.dropdown.down-left
+ button.dropdown__toggle
+ .dropdown__content.dropdown-search
+ .search-form
+ form#search-block-form(action='/intro', method='post', accept-charset='UTF-8')
+ // TODO: проверить, точно ли нужно? Не ошибка?
+ div
+ .search-form__query
+ input#edit-search-block-form-1.form-text(type='text', maxlength='128', name='search_block_form', value='')
+ button(type='submit') Найти
+ input#w4vSP09jvn-orNjyHpBepTXLSn6qFxyehtYKuZbWWv4(type='hidden', name='form_build_id', value='w4vSP09jvn-orNjyHpBepTXLSn6qFxyehtYKuZbWWv4')
+ input#edit-search-block-form-form-token(type='hidden', name='form_token', value='O9WnzduxDr7Fwfy6eG4XL06_OilwEwBIsJDKhduRG0w')
+ input#edit-search-block-form(type='hidden', name='form_id', value='search_block_form')
diff --git a/templates/blocks/scripts.jade b/templates/blocks/scripts.jade
new file mode 100644
index 000000000..05b76cf1a
--- /dev/null
+++ b/templates/blocks/scripts.jade
@@ -0,0 +1,2 @@
+script(src='/js/vendor.js')
+script(src='/js/main.js')
diff --git a/templates/blocks/social-aside.jade b/templates/blocks/social-aside.jade
new file mode 100644
index 000000000..e6bd981b9
--- /dev/null
+++ b/templates/blocks/social-aside.jade
@@ -0,0 +1,10 @@
+.social.aside
+ a.social__anchor-up(href='#page') Наверх
+ a.social__soc(href='https://plus.google.com/share?url=http://design.javascript.ru/intro', target='_blank')
+ img.soc-icon.google(src='/img/x.gif', width='24', height='24', alt='g+', title='Поделиться в гугл+')
+ a.social__soc(href='http://www.facebook.com/sharer/sharer.php?s=100&p[url]=http://design.javascript.ru/intro', target='_blank')
+ img.soc-icon.facebook(src='/img/x.gif', width='24', height='24', alt='f', title='Поделиться в фейсбуке')
+ a.social__soc(href='https://twitter.com/share?url=http://design.javascript.ru/intro', target='_blank')
+ img.soc-icon.twitter(src='/img/x.gif', width='24', height='24', alt='t', title='Поделиться в твитере')
+ a.social__soc(href='http://vkontakte.ru/share.php?url=http://design.javascript.ru/intro', target='_blank')
+ img.soc-icon.vk(src='/img/x.gif', width='24', height='24', alt='v', title='Поделиться во вконтакте')
\ No newline at end of file
diff --git a/templates/blocks/task-block.jade b/templates/blocks/task-block.jade
new file mode 100644
index 000000000..7903b43ab
--- /dev/null
+++ b/templates/blocks/task-block.jade
@@ -0,0 +1,25 @@
+-var url = "/task/vyzov-na-meste"
+-var title = "Вызов «на месте»"
+-var importance = 4
+-var content = "Content
Content
"
+-var solution = [{title:"",content:"Текст
"}]
+
+section.important.important_ok
+ header.important__header
+ span.important__type Задание:
+ h3.important__title
+ a.important__task-link(href=url)
+ u!=title
+ span.important__importance(title="Насколько эта задача важна для освоения материала, от 1 до 5")
+ | Важность:
+ = ' ' + importance
+
+ .important__content
+ != content
+ each part in solution
+ .spoiler.closed
+ button.spoiler__button
+ u!= part.title || "Решение"
+ .spoiler__content
+ u!= part.content
+
diff --git a/templates/blocks/top-parts.jade b/templates/blocks/top-parts.jade
new file mode 100644
index 000000000..f7b471ba3
--- /dev/null
+++ b/templates/blocks/top-parts.jade
@@ -0,0 +1,9 @@
+
+.top-part
+ .logo
+ a(href='http://javascript.ru/')
+ img(src='/img/logo.png', alt='Javascript.ru')
+ .user.dropdown.down-left.inherit-min-width
+ a.user__entrance(href='/user') Вход на сайт
+
+
diff --git a/templates/blocks/tutorial/bottom-navigation.jade b/templates/blocks/tutorial/bottom-navigation.jade
new file mode 100644
index 000000000..55883defa
--- /dev/null
+++ b/templates/blocks/tutorial/bottom-navigation.jade
@@ -0,0 +1,18 @@
+.book-navigation
+ .page-links.clearfix
+ if prev
+ .book-navigation__prev
+ | Предыдущий урок
+ kbd
+ | (Ctrl +
+ span.book-navigation__arr ←
+ | )
+ a.page-previous(href=prev.url,title="На предыдущую страницу") #{prev.title}
+ if next
+ .book-navigation__next
+ | Следующий урок
+ kbd
+ | (Ctrl +
+ span.book-navigation__arr →
+ | )
+ a.page-next(href=next.url,title="На следующую страницу") #{next.title}
diff --git a/templates/blocks/tutorial/footer.jade b/templates/blocks/tutorial/footer.jade
new file mode 100644
index 000000000..52493c0f3
--- /dev/null
+++ b/templates/blocks/tutorial/footer.jade
@@ -0,0 +1,3 @@
+footer.main__footer
+ time.main__footer-date(datetime=modified.toJSON())
+ != moment(modified).format('DD.MM.YYYY')
diff --git a/templates/blocks/tutorial/top-navigation.jade b/templates/blocks/tutorial/top-navigation.jade
new file mode 100644
index 000000000..a9ec63a47
--- /dev/null
+++ b/templates/blocks/tutorial/top-navigation.jade
@@ -0,0 +1,15 @@
+.main__lesson-nav
+ if prev
+ .main__lesson-nav-prev
+ kbd
+ | (Ctrl +
+ span.main__lesson-nav-arr ←
+ | )
+ a.main__lesson-nav-link(href=prev.url) Предыдущий урок
+ if next
+ .main__lesson-nav-next
+ a.main__lesson-nav-link(href=next.url) Следующий урок
+ kbd
+ | (Ctrl +
+ span.main__lesson-nav-arr →
+ | )
diff --git a/templates/error.jade b/templates/error.jade
new file mode 100644
index 000000000..8ef0aa284
--- /dev/null
+++ b/templates/error.jade
@@ -0,0 +1,6 @@
+extends layouts/base
+
+block content
+ h1 HTTP Error (user-land report)
+ h2= error.message
+ pre #{error.stack}
diff --git a/templates/layouts/base.jade b/templates/layouts/base.jade
new file mode 100644
index 000000000..f3ce7f9cd
--- /dev/null
+++ b/templates/layouts/base.jade
@@ -0,0 +1,12 @@
+block variables
+include ../blocks/head
+body
+ include ../blocks/top-parts
+ .page
+ .page__inner
+ include ../blocks/navbar
+ include ../blocks/social-aside
+ .main-content
+ block content
+ include ../blocks/footer
+ include ../blocks/scripts
\ No newline at end of file
diff --git a/test.sh b/test.sh
deleted file mode 100755
index 20305d22e..000000000
--- a/test.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/bash
-DEBUG=server:* NODE_ENV=test NODE_PATH=. mocha --debug -R list
diff --git a/test/.jshintrc b/test/.jshintrc
new file mode 100644
index 000000000..077663629
--- /dev/null
+++ b/test/.jshintrc
@@ -0,0 +1,23 @@
+{
+ "maxerr": 25,
+ "latedef": "nofunc",
+ "browser": false,
+ "node": true,
+ "indent": 2,
+ "camelcase": true,
+ "newcap": true,
+ "undef": true,
+ "esnext": true,
+ "multistr": true,
+ "noyield": true,
+ "expr": true,
+ "-W004": true,
+ "globals" : {
+ "describe" : false,
+ "it" : false,
+ "before" : false,
+ "beforeEach" : false,
+ "after" : false,
+ "afterEach" : false
+ }
+}
diff --git a/test/data/index.js b/test/data/index.js
deleted file mode 100755
index a10bb4724..000000000
--- a/test/data/index.js
+++ /dev/null
@@ -1,95 +0,0 @@
-var mongoose = require('lib/mongoose');
-var async = require('async');
-var log = require('lib/log')(module);
-
-var db = mongoose.connection.db;
-
-function createEmptyDb(callback) {
-
- async.series([
- function open(callback) {
- if (mongoose.connection.readyState == 1) { // already connected
- return callback();
- } else {
- mongoose.connection.on('open', callback);
- }
- },
- function clearDatabase(callback) {
-
- // db.dropDatabase reallocates file and is slow
- // that's why we drop collections one-by-one
-
- db.collectionNames(function(err, collections) {
-
- async.each(collections, function(collection, callback) {
- var collectionName = collection.name.slice(db.databaseName.length + 1);
- if (collectionName.indexOf('system.') === 0) {
- return callback();
- }
- log.debug("drop ", collectionName);
- db.dropCollection(collectionName, callback);
- }, callback);
-
- });
-
- },
- function ensureIndexes(callback) {
- // models must be recreated
- // but that's problematic
- // so we just make sure all necessary features are in place
- async.each(mongoose.modelNames(), function(modelName, callback) {
- mongoose.models[modelName].ensureIndexes(callback);
- }, callback);
- },
- function ensureCapped(callback) {
- // models must be recreated
- // but that's problematic
- // so we just make sure all necessary features are in place
- async.each(mongoose.modelNames(), function(modelName, callback) {
- var schema = mongoose.models[modelName].schema;
- if (!schema.options.capped) return callback();
- db.command({convertToCapped: mongoose.models[modelName].collection.name, size: schema.options.capped}, callback);
- }, callback);
- }
- ], function(err) {
- callback(err);
- });
-}
-
-exports.createEmptyDb = createEmptyDb;
-
-/**
- * Clear database,
- * require models & wait until indexes are created,
- * then load data from test/data/dataFile.json & callback
- * @param dataFile
- * @param callback
- */
-exports.loadDb = function(dataFile, callback) {
- // warning: pow-mongoose-fixtures fails to work with capped collections
- // it calls remove() on them => everything dies
- async.series([
- createEmptyDb,
- function fillDb(callback) {
- var modelsData = require('test/data/' + dataFile);
-
- async.each(Object.keys(modelsData), function(modelName, callback) {
- loadModel(modelName, modelsData[modelName], callback);
- }, callback);
- }
- ], function(err) {
- callback(err);
- });
-
-};
-
-function loadModel(name, data, callback) {
-
- var Model = mongoose.models[name];
-
- async.each(data, function(itemData, callback) {
- var model = new Model(itemData);
- model.save(callback);
- }, callback);
-
-}
\ No newline at end of file
diff --git a/test/data/sampleDb.js b/test/data/sampleDb.js
deleted file mode 100755
index d385dcf37..000000000
--- a/test/data/sampleDb.js
+++ /dev/null
@@ -1,65 +0,0 @@
-exports.User = [
- { "_id": "000000000000000000000001",
- "created": new Date(2014,0,1),
- "username": "ilya kantor",
- "email": "iliakan@gmail.com",
- "password": "123",
- "avatar": "1.jpg",
- "following": []
- },
- { "_id": "000000000000000000000002",
- "created": new Date(2014,0,1),
- "username": "tester",
- "email": "tester@mail.com",
- "password": "123",
- "avatar": "2.jpg",
- "following": ["000000000000000000000001"]
- },
- { "_id": "000000000000000000000003",
- "created": new Date(2014,0,1),
- "username": "vasya",
- "email": "vasya@mail.com",
- "password": "123",
- "avatar": "3.jpg",
- "following": ["000000000000000000000001", "000000000000000000000002"]
- }
-];
-
-exports.Message = [
- { "_id": "100000000000000000000001",
- "content": "Message 1",
- "created": new Date(2014, 0, 1),
- "user": "000000000000000000000001"
- },
- { "_id": "100000000000000000000002",
- "content": "Message 2",
- "created": new Date(2014, 0, 2),
- "user": "000000000000000000000002"
- },
- { "_id": "100000000000000000000003",
- "content": "Message 3",
- "created": new Date(2014, 0, 3),
- "user": "000000000000000000000002"
- },
- { "_id": "100000000000000000000004",
- "content": "Message 4",
- "created": new Date(2014, 0, 4),
- "user": "000000000000000000000003"
- },
- { "_id": "100000000000000000000005",
- "content": "Message 5",
- "created": new Date(2014, 0, 5),
- "user": "000000000000000000000003"
- },
- { "_id": "100000000000000000000006",
- "content": "Message 6",
- "created": new Date(2014, 0, 6),
- "user": "000000000000000000000003"
- }
-];
-
-exports.Session = [
- { "_id" : "200000000000000000000001", "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":\"000000000000000000000001\"}}", "expires" : new Date("2015-01-24T18:52:24.306Z") },
- { "_id" : "200000000000000000000002", "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":\"000000000000000000000002\"}}", "expires" : ("2015-01-24T18:52:24.306Z") },
- { "_id" : "200000000000000000000003", "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":\"000000000000000000000003\"}}", "expires" : ("2015-01-24T18:52:24.306Z") }
-];
\ No newline at end of file
diff --git a/test/env.js b/test/env.js
deleted file mode 100755
index f7534b31d..000000000
--- a/test/env.js
+++ /dev/null
@@ -1 +0,0 @@
-process.env.NODE_ENV = 'test';
diff --git a/test/mocha.opts b/test/mocha.opts
index 7f3ce0129..790728212 100755
--- a/test/mocha.opts
+++ b/test/mocha.opts
@@ -1,5 +1,9 @@
--reporter spec
--colors
---timeout 10000
+--timeout 10000
+--require config/mongoose
--require should
---require test/env $*
\ No newline at end of file
+--require co
+--require co-mocha
+--recursive
+--ui bdd
diff --git a/test/unit/lib/treeUtil.js b/test/unit/lib/treeUtil.js
new file mode 100644
index 000000000..a9a386a7f
--- /dev/null
+++ b/test/unit/lib/treeUtil.js
@@ -0,0 +1,66 @@
+const treeUtil = require('lib/treeUtil');
+const should = require('should');
+
+describe('treeUtil', function() {
+
+ const tree = {children: [
+ { _id: '1',
+ title: 'Article 1',
+ isFolder: true,
+ weight: 0,
+ children: [
+ { _id: '2',
+ title: 'Article 1.1',
+ weight: 0,
+ parent: '1',
+ isFolder: true,
+ children: [
+ { _id: '3',
+ title: 'Article 1.1.1',
+ weight: 0,
+ isFolder: false,
+ parent: '2' },
+ { _id: '4',
+ title: 'Article 1.1.2',
+ isFolder: false,
+ weight: 1,
+ parent: '2' }
+ ]
+ },
+ { _id: '5',
+ title: 'Article 1.2',
+ weight: 1,
+ isFolder: false,
+ parent: '1' }
+ ] },
+ { _id: '6',
+ title: 'Article 2',
+ weight: 1,
+ isFolder: true,
+ children: [
+ { _id: '7',
+ title: 'Article 2.1',
+ weight: 0,
+ isFolder: false,
+ parent: '6' },
+ { _id: '8',
+ title: 'Article 2.2',
+ weight: 1,
+ isFolder: false,
+ parent: '6' }
+ ] }
+ ]};
+
+ it('finds next', function() {
+ treeUtil.findPrevNextById(tree, 3).next._id.should.be.eql(4);
+ treeUtil.findPrevNextById(tree, 4).next._id.should.be.eql(5);
+ should.not.exist(treeUtil.findPrevNextById(tree, 5).next);
+ });
+
+ it('finds prev', function() {
+ should.not.exist(treeUtil.findPrevNextById(tree, 3).prev);
+ treeUtil.findPrevNextById(tree, 4).prev._id.should.be.eql(3);
+ treeUtil.findPrevNextById(tree, 5).prev._id.should.be.eql(4);
+ });
+
+});
diff --git a/test/unit/model/user.js b/test/unit/model/user.js
deleted file mode 100755
index 7218bd095..000000000
--- a/test/unit/model/user.js
+++ /dev/null
@@ -1,74 +0,0 @@
-var data = require('test/data');
-var mongoose = require('mongoose');
-var async = require('async');
-
-describe('User', function() {
- before(function(done) {
- data.createEmptyDb(done)
- });
-
- var User = mongoose.models.User;
-
- it('errors on save for bad email', function(done) {
-
- var user = new User({
- email: "BAD",
- password: "123"
- });
-
- user.save(function(err) {
- err.name.should.equal('ValidationError');
- err.errors.email.value.should.equal(user.get('email'));
- done();
- });
-
- });
-
- it('errors on save without password', function(done) {
-
- var user = new User({
- email: "BAD"
- });
-
- user.save(function(err) {
- done(err ? undefined : new Error("Password must be set"));
- });
-
- });
-
- it('autogenerates salt and hash', function(done) {
-
- var user = new User({
- email: "a@b.ru",
- password: "pass"
- });
-
- user.get('salt').should.not.be.empty;
- user.get('hash').should.not.be.empty;
- user.checkPassword("pass").should.be.true;
- done();
-
- });
-
- it('requires unique email', function(done) {
-
- function create(callback) {
- new User({
- username: "nonunique",
- email: "nonunique@b.ru",
- password: "pass"
- }).save(callback);
- }
-
- async.series([
- create,
- create
- ], function(err) {
- if (!err) return done(new Error("Same email is saved twice!"));
- err.code.should.equal(11000); // unique index is checked by mongo
- done();
- });
-
- });
-
-});
diff --git a/test/unit/subscribe/socket.js b/test/unit/subscribe/socket.js
deleted file mode 100755
index ad1d21617..000000000
--- a/test/unit/subscribe/socket.js
+++ /dev/null
@@ -1,262 +0,0 @@
-var models = require('mongoose').models;
-var async = require('async');
-var config = require('config');
-var io = require('socket.io-client');
-var querystring = require('querystring');
-var should = require('should');
-
-var socketOptions ={
- transports: ['xhr-polling'],
- 'force new connection': true
-};
-
-var url = 'http://localhost:' + config.get('port');
-
-function openSessionSocket(sid) {
- var clientOptions = {};
- for(var key in socketOptions) {
- clientOptions[key] = socketOptions[key];
- }
- var query = {};
- query[config.get('session:key')] = sid;
-
- clientOptions.query = querystring.stringify(query);
- return io.connect('http://localhost:' + config.get('port'), clientOptions);
-}
-
-describe("Socket",function() {
-
- before(function(done) {
- var testData = require('test/data');
- testData.loadDb('sampleDb', done);
- });
-
- var anon;
- var ilya, tester, vasya;
- var ilyaId = '000000000000000000000001';
- var testerId = '000000000000000000000002';
- var vasyaId = '000000000000000000000003';
-
- it("can open connect with invalid session", function(done) {
- anon = openSessionSocket('FFFFFFFFFFFFFF0000000011');
- anon.on('connect', function() {
- done();
- })
- });
-
- it("anon user may not create a message", function(done) {
- anon.emit('message:create', {content: "ANON MESSAGE"}, function(err) {
- err.status.should.equal(403);
- done();
- });
- });
-
- describe("two users", function() {
-
- before(function(done) { // connect two clients before tests
- async.parallel([
- function(callback) {
- ilya = openSessionSocket('200000000000000000000001');
- ilya.on('connect', callback);
- },
- function(callback) {
- tester = openSessionSocket('200000000000000000000002');
- tester.on('connect', callback);
- },
- function(callback) {
- vasya = openSessionSocket('200000000000000000000003');
- vasya.on('connect', callback);
- }
- ], done);
- });
-
- it('should be able to create a message', function(done){
- ilya.emit('message:create', {content: "NEW ILYA MESSAGE"}, function(err, message) {
- should.equal(err, null);
- message.content.should.equal("NEW ILYA MESSAGE");
- done();
- });
- });
-
- var receivedMessages = [];
- var subscriptions = {};
-
- it('tester subscribes to vasya', function(done) {
- async.waterfall([
- function(callback) {
- tester.emit('message:subscribe', {userId: vasyaId}, callback);
- },
- function getOldMessages(id, callback) {
- subscriptions.vasya = id;
-
- tester.on('message:create', function(message) {
- receivedMessages.push(message);
- if (receivedMessages.length == 3) callback();
- });
-
- },
- function(callback) {
- setTimeout(callback, 20);
- },
- function(callback) {
- // only 3 messages, no more!
- receivedMessages.length.should.equal(3);
-
- vasya.emit('message:create', {content: "NEW VASYA MESSAGE"}, function(err, message) {
- should.equal(err, null);
- message.content.should.equal("NEW VASYA MESSAGE");
- // timeout for subscriber to react
- setTimeout(callback, 30);
- });
- },
- function(callback) {
- receivedMessages.length.should.equal(4);
- tester.removeAllListeners('message:create');
- callback();
- }
-
- ], done);
- });
-
- // NOW tester is subscribed to vasya
-
- it('tester subscribes to ilya', function(done) {
- async.waterfall([
- function(callback) {
- tester.emit('message:subscribe', {userId: ilyaId}, callback);
- },
- function(id, callback) {
- subscriptions.ilya = id;
-
- tester.on('message:create', function(message) {
- receivedMessages.push(message);
- if (receivedMessages.length == 6) callback();
- });
-
- },
- function(callback) {
- setTimeout(callback, 20);
- },
- function(callback) {
- receivedMessages.length.should.equal(6);
-
- ilya.emit('message:create', {content: "NEW ILYA MESSAGE 2"}, function(err, message) {
- should.equal(err, null);
- message.content.should.equal("NEW ILYA MESSAGE 2");
- // timeout for subscriber to react
- setTimeout(callback, 300);
- });
- },
- function(callback) {
- receivedMessages.length.should.equal(7);
- callback();
- }
-
- ], done);
- });
-
- // NOW tester is subscribed to ilya AND vasya
-
- it('tester unsubscribes from vasya', function(done) {
- var oldReceivedCount = receivedMessages.length;
-
- async.waterfall([
- function(callback) {
- tester.emit('message:unsubscribe', subscriptions.vasya, callback);
- },
-
- function(id, callback) {
-
- should.equal(id, subscriptions.vasya);
- delete subscriptions.vasya;
-
- vasya.emit('message:create', {content: "NEW VASYA MESSAGE"}, function(err, message) {
- should.equal(err, null);
- message.content.should.equal("NEW VASYA MESSAGE");
- // timeout for subscriber to react
- setTimeout(callback, 30);
- });
- },
-
- function(callback) {
- // no new messages from vasya (unsubscribed)
- oldReceivedCount.should.equal(receivedMessages.length);
-
- ilya.emit('message:create', {content: "NEW ILYA MESSAGE 3"}, function(err, message) {
- should.equal(err, null);
- message.content.should.equal("NEW ILYA MESSAGE 3");
- // timeout for subscriber to react
- setTimeout(callback, 100);
- });
- },
- function(callback) {
- receivedMessages.length.should.be.equal(oldReceivedCount + 1);
- tester.removeAllListeners('message:create');
- tester.emit('message:unsubscribe', subscriptions.ilya, callback);
- delete subscriptions.ilya;
- }
- ], done);
- });
-
- // NOW tester is not subscribed to anything
-
- it('tester subscribes to vasya & following', function(done) {
- var oldReceivedCount = receivedMessages.length;
- async.waterfall([
- function(callback) {
- tester.emit('message:subscribe', {userId: vasyaId, following: true}, callback);
- },
- function(id, callback) {
- subscriptions.vasya = id;
- // skip old messages
- setTimeout(callback, 60);
- },
- function(callback) {
-
- tester.on('message:create', function(message) {
- receivedMessages.push(message);
- });
-
- async.series([
- function(callback) {
- vasya.emit('message:create', {content: "NEW VASYA MESSAGE"}, callback);
- },
- function(callback) {
- ilya.emit('message:create', {content: "NEW ILYA MESSAGE"}, callback);
- }
- ], function(err) {
- callback(err);
- });
- },
- function(callback) {
- // skip old messages
- setTimeout(callback, 60);
- },
- function(callback) {
- receivedMessages.length.should.equal(oldReceivedCount + 2);
- tester.removeAllListeners('message:create');
- callback();
- }
-
- ], done);
-
- });
-
- // NOW tester is subscribed to vasya & following
-
- });
-
-
-
- after(function(done) {
- async.each([anon, ilya, vasya, tester], function(user, callback) {
- if (!user) return callback(); // if error while opening the socket
- user.on('disconnect', callback);
- user.disconnect();
- }, function() {
- // disconnect sends the packet and triggers event synchronously,
- // we use timeout 10 to let the packet actually leave the client process
- setTimeout(done, 10) });
- });
-
-});
\ No newline at end of file
diff --git a/test/unit/subscribe/subscription.js b/test/unit/subscribe/subscription.js
deleted file mode 100755
index cffc40ca3..000000000
--- a/test/unit/subscribe/subscription.js
+++ /dev/null
@@ -1,105 +0,0 @@
-var async = require('async');
-var config = require('config');
-var models = require('lib/mongoose').models;
-var subscriptionStore = require('lib/subscriptionStore').store;
-
-describe("Subscription",function() {
-
- this.timeout(5000);
-
- before(function(done) {
- var testData = require('test/data');
- testData.loadDb('sampleDb', done);
- });
-
- var subscription, subscription2;
- var receivedMessages = [];
- it("gets old messages", function(done) {
- subscription = subscriptionStore.createSubscription(models.Message.find({user:'000000000000000000000002'}));
- subscription.on('data', function(message) {
- receivedMessages.push(message);
- });
-
- setTimeout(function() {
- receivedMessages.length.should.equal(2);
- done();
- }, 100);
- });
-
- it("gets new messages", function(done) {
-
- async.series([
- function(callback) {
- new models.Message({
- user: '000000000000000000000002',
- content: 'MESSAGE FROM QUERY'
- }).save(callback);
- },
- function(callback) {
- new models.Message({
- user: '000000000000000000000002',
- content: 'MESSAGE FROM QUERY'
- }).save(callback);
- },
- function(callback) {
- new models.Message({
- user: '000000000000000000000001',
- content: 'MESSAGE NOT IN QUERY'
- }).save(callback);
- },
- function(callback) {
- setTimeout(callback, 50);
- },
- function(callback) {
- receivedMessages.length.should.equal(4);
- callback();
- }
- ], done);
-
- });
-
- it("add one more", function(done) {
- subscription2 = subscriptionStore.createSubscription(models.Message.find({user:'000000000000000000000003'}));
- subscription2.on('data', function(message) {
- receivedMessages.push(message);
- });
-
- setTimeout(function() {
- receivedMessages.length.should.equal(7);
- done();
- }, 100);
- });
-
-
- it("receives from both", function(done) {
- async.series([
- function(callback) {
- new models.Message({
- user: '000000000000000000000002',
- content: 'MESSAGE FROM QUERY'
- }).save(callback);
- },
- function(callback) {
- new models.Message({
- user: '000000000000000000000003',
- content: 'MESSAGE FROM QUERY'
- }).save(callback);
- },
- function(callback) {
- new models.Message({
- user: '000000000000000000000001',
- content: 'MESSAGE NOT IN QUERY'
- }).save(callback);
- },
- function(callback) {
- setTimeout(callback, 100);
- },
- function(callback) {
- receivedMessages.length.should.equal(9);
- callback();
- }
- ], done);
- });
-
-
-});
\ No newline at end of file
diff --git a/util/log.js b/util/log.js
deleted file mode 100755
index 465533d13..000000000
--- a/util/log.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * Flexible logging wrapper around windston
- *
- * usage:
- * var log = require('lib/log')(module)
- * log.debug("Winston %s", "syntax");
- *
- * enabling debugdirectly from code:
- * log.debugOn();
- * enabling from CLI:
- * DEBUG=path node app
- * where path is calculated from the project root (where package.json resides)
- * example:
- * DEBUG=models/* node app
- * or
- * DEBUG=-models/user,models/* node app (to log all models except user)
- */
-
-var winston = require('winston');
-var path = require('path');
-var fs = require('fs');
-
-var names = [], skips = [];
-
-(process.env.DEBUG || '')
- .split(/[\s,]+/)
- .forEach(function(name) {
- name = name.replace('*', '.*?');
- if (name[0] === '-') {
- skips.push(new RegExp('^' + name.substr(1) + '$'));
- } else {
- names.push(new RegExp('^' + name + '$'));
- }
- });
-
-function findProjectRoot() {
-
- var dir = __dirname;
- while (!fs.existsSync(path.join(dir, 'package.json'))) {
- dir = path.dirname(dir);
- }
-
- return path.normalize(dir);
-}
-
-var projectRoot = findProjectRoot();
-
-function getLogLevel(module) {
-
- var modulePath = module.filename.slice(projectRoot.length + 1); // models/user.js
- modulePath = modulePath.replace(/\.js$/, ''); // models.user
-
- var logLevel = 'info';
-
- var isSkipped = skips.some(function(re) {
- return re.test(modulePath);
- });
-
- if (!isSkipped) {
- var isIncluded = names.some(function(re) {
- return re.test(modulePath);
- });
-
- if (isIncluded) logLevel = 'debug';
- }
-
- return logLevel;
-}
-
-function getLogger(module) {
-
- var showPath = module.filename.split('/').slice(-2).join('/');
-
- var logLevel = getLogLevel(module);
- var logger = new winston.Logger({
- transports: [
- new winston.transports.Console({
- colorize: true,
- level: logLevel,
- label: showPath
- })
- ]
- });
-
- logger.debugOn = function() {
- Object.keys(this.transports).forEach(function(key) {
- logger.transports[key].level = 'debug';
- }, this);
- };
-
- return logger;
-}
-
-module.exports = getLogger;
\ No newline at end of file
diff --git a/views/error.jade b/views/error.jade
deleted file mode 100755
index 51ec12c6a..000000000
--- a/views/error.jade
+++ /dev/null
@@ -1,6 +0,0 @@
-extends layout
-
-block content
- h1= message
- h2= error.status
- pre #{error.stack}
diff --git a/views/layout.jade b/views/layout.jade
deleted file mode 100755
index 474914e79..000000000
--- a/views/layout.jade
+++ /dev/null
@@ -1,7 +0,0 @@
-doctype html
-html
- head
- title= title
- link(rel='stylesheet', href='/stylesheets/app.css')
- body
- block content
Комментарии 5
+//- Написать +//-+//-- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
+//- - Если ваш комментарий касается задачи — откройте её в отдельном окне и напишите там.
+//- - Для кода внутри строки используйте тег
+//- - Если что-то непонятно — пишите, что именно и с какого места.
+//-
+//-<code>, для блока кода — тег<pre>, если больше 10 строк — ссылку на песочницу.