Merge: Factorize `capitalize` and add option to preserve uppercase letters
authorJean Privat <jean@pryen.org>
Fri, 3 Jun 2016 01:44:04 +0000 (21:44 -0400)
committerJean Privat <jean@pryen.org>
Fri, 3 Jun 2016 01:44:04 +0000 (21:44 -0400)
Merge the duplicated implementation of `String::capitalized` and `Buffer::capitalize` and extend it to accept the `keep_upper` optional parameter. This parameter triggers keeping uppercase letters as is.

It is applied in nitiwiki to preserve acronyms, like the many apparences of FFI at http://nitlanguage.org/FFI/.

Pull-Request: #2085
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

356 files changed:
.gitattributes
.gitignore
NOTICE
README.md
benchmarks/csv/README.md [new file with mode: 0644]
benchmarks/csv/csv_bench.sh [new file with mode: 0755]
benchmarks/csv/scripts/JavaCSV.java [new file with mode: 0644]
benchmarks/csv/scripts/csv_gen.nit [new file with mode: 0644]
benchmarks/csv/scripts/go_csv.go [new file with mode: 0644]
benchmarks/csv/scripts/nit_csv.nit [new file with mode: 0644]
benchmarks/csv/scripts/python_csv.py [new file with mode: 0644]
benchmarks/csv/scripts/python_stdcsv.py [new file with mode: 0644]
benchmarks/csv/scripts/ruby_csv.rb [new file with mode: 0644]
contrib/asteronits/Makefile
contrib/asteronits/src/android.nit
contrib/asteronits/src/touch_ui.nit
contrib/benitlux/.gitignore
contrib/benitlux/Makefile
contrib/benitlux/README.md
contrib/benitlux/android/res/.gitignore [new file with mode: 0644]
contrib/benitlux/android/res/values/styles.xml [new file with mode: 0644]
contrib/benitlux/art/icon.svg [new file with mode: 0644]
contrib/benitlux/art/notif.svg [new file with mode: 0644]
contrib/benitlux/ios/.gitignore [new file with mode: 0644]
contrib/benitlux/net.xymus.benitlux.txt [new file with mode: 0644]
contrib/benitlux/package.ini
contrib/benitlux/src/benitlux_model.nit
contrib/benitlux/src/client/android.nit [new file with mode: 0644]
contrib/benitlux/src/client/android_proto.nit [new file with mode: 0644]
contrib/benitlux/src/client/base.nit [new file with mode: 0644]
contrib/benitlux/src/client/client.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/checkins.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/debug.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/push.nit [new file with mode: 0644]
contrib/benitlux/src/client/features/translations.nit [new file with mode: 0644]
contrib/benitlux/src/client/ios.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/beer_views.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/home_views.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/social_views.nit [new file with mode: 0644]
contrib/benitlux/src/client/views/user_views.nit [new file with mode: 0644]
contrib/benitlux/src/server/benitlux_controller.nit [moved from contrib/benitlux/src/benitlux_controller.nit with 85% similarity]
contrib/benitlux/src/server/benitlux_daily.nit [moved from contrib/benitlux/src/benitlux_daily.nit with 100% similarity]
contrib/benitlux/src/server/benitlux_db.nit [moved from contrib/benitlux/src/benitlux_db.nit with 93% similarity]
contrib/benitlux/src/server/benitlux_social.nit [moved from contrib/benitlux/src/benitlux_social.nit with 100% similarity]
contrib/benitlux/src/server/benitlux_view.nit [moved from contrib/benitlux/src/benitlux_view.nit with 100% similarity]
contrib/benitlux/src/server/server.nit [moved from contrib/benitlux/src/benitlux_web.nit with 80% similarity]
contrib/friendz/src/friendz.nit
contrib/nitiwiki/src/wiki_base.nit
contrib/nitiwiki/src/wiki_html.nit
contrib/nitiwiki/src/wiki_links.nit
contrib/nitrpg/src/test_helper.nit
contrib/refund/src/refund_json.nit
contrib/simplan/simplan.nit
contrib/tinks/src/client/linux_client.nit
contrib/tinks/src/client/tinks_vr.nit [new file with mode: 0644]
contrib/tinks/src/game/framework.nit
contrib/tnitter/package.ini
contrib/tnitter/src/action.nit
contrib/tnitter/src/push.nit
contrib/tnitter/src/tnitter_app.nit
contrib/tnitter/src/tnitter_app_android.nit
contrib/xymus_net/.gitignore [new file with mode: 0644]
contrib/xymus_net/Makefile [new file with mode: 0644]
contrib/xymus_net/README.md [new file with mode: 0644]
contrib/xymus_net/package.ini [new file with mode: 0644]
contrib/xymus_net/xymus_net.nit [moved from lib/nitcorn/examples/src/xymus_net.nit with 96% similarity]
examples/calculator/Makefile
examples/calculator/README.md
examples/calculator/doc/android-scientific.png [new file with mode: 0644]
examples/calculator/doc/ios-scientific.png [new file with mode: 0644]
examples/calculator/doc/linux-scientific.png [new file with mode: 0644]
examples/calculator/package.ini
examples/calculator/src/android_calculator.nit [new file with mode: 0644]
examples/calculator/src/calculator.nit
examples/calculator/src/calculator_logic.nit
examples/calculator/src/calculator_test.nit
examples/calculator/src/ios_calculator.nit [new file with mode: 0644]
examples/calculator/src/scientific_calculator.nit [new file with mode: 0644]
examples/fibonacci.nit
examples/rosettacode/perlin_noise.nit
lib/a_star.nit
lib/android/NitActivity.java
lib/android/README.md
lib/android/android.nit
lib/android/game.nit [new file with mode: 0644]
lib/android/input_events.nit
lib/android/key_event.nit [new file with mode: 0644]
lib/android/landscape.nit
lib/android/nit_activity.nit
lib/android/portrait.nit
lib/android/sensors.nit
lib/android/ui/ui.nit
lib/app/audio.nit
lib/app/data_store.nit
lib/app/ui.nit
lib/base64.nit
lib/bucketed_game.nit
lib/cocoa/foundation.nit
lib/core/collection/abstract_collection.nit
lib/core/collection/array.nit
lib/core/collection/list.nit
lib/core/error.nit
lib/core/exec.nit
lib/core/file.nit
lib/core/math.nit
lib/core/re.nit
lib/core/text/abstract_text.nit
lib/core/text/flat.nit
lib/core/text/ropes.nit
lib/crapto/crapto.nit [new file with mode: 0644]
lib/crapto/english_utils.nit [new file with mode: 0644]
lib/crapto/examples/repeating_key_xor_cipher.txt [new file with mode: 0644]
lib/crapto/examples/repeating_key_xor_solve.nit [new file with mode: 0644]
lib/crapto/package.ini [new file with mode: 0644]
lib/crapto/xor.nit [new file with mode: 0644]
lib/crypto/basic_ciphers.nit [moved from lib/crypto.nit with 89% similarity]
lib/crypto/crypto.nit [new file with mode: 0644]
lib/crypto/package.ini [moved from lib/crypto.ini with 89% similarity]
lib/crypto/xor_ciphers.nit [new file with mode: 0644]
lib/gamnit/cameras.nit
lib/gamnit/depth/depth_core.nit
lib/gamnit/depth/more_materials.nit
lib/gamnit/depth/more_meshes.nit
lib/gamnit/depth/more_models.nit
lib/gamnit/display.nit
lib/gamnit/display_android.nit
lib/gamnit/display_linux.nit
lib/gamnit/egl.nit
lib/gamnit/limit_fps.nit
lib/gamnit/programs.nit
lib/gamnit/texture_atlas_parser.nit [moved from contrib/asteronits/src/texture_atlas_parser.nit with 98% similarity]
lib/gamnit/textures.nit
lib/geometry/polygon.nit
lib/gtk/v3_4/gtk_assistant.nit
lib/gtk/v3_4/gtk_core.nit
lib/gtk/v3_4/gtk_dialogs.nit
lib/gtk/v3_4/gtk_widgets_ext.nit
lib/ios/ui/ui.nit
lib/json/serialization.nit
lib/json/static.nit
lib/linux/ui.nit
lib/mnit/android/android_app.nit
lib/mnit/mnit_fps.nit
lib/nitcorn/README.md
lib/nitcorn/examples/Makefile
lib/nitcorn/file_server.nit
lib/nitcorn/media_types.nit
lib/nitcorn/restful.nit
lib/nitcorn/sessions.nit
lib/nitcorn/vararg_routes.nit
lib/noise.nit
lib/performance_analysis.nit
lib/popcorn/.gitignore [new file with mode: 0644]
lib/popcorn/Makefile [new file with mode: 0644]
lib/popcorn/README.md [new file with mode: 0644]
lib/popcorn/examples/angular/example_angular.nit [new file with mode: 0644]
lib/popcorn/examples/angular/www/index.html [new file with mode: 0644]
lib/popcorn/examples/angular/www/javascripts/ng-example.js [new file with mode: 0644]
lib/popcorn/examples/angular/www/views/index.html [new file with mode: 0644]
lib/popcorn/examples/handlers/example_post_handler.nit [new file with mode: 0644]
lib/popcorn/examples/handlers/example_query_string.nit [new file with mode: 0644]
lib/popcorn/examples/hello_world/example_hello.nit [new file with mode: 0644]
lib/popcorn/examples/middlewares/example_advanced_logger.nit [new file with mode: 0644]
lib/popcorn/examples/middlewares/example_html_error_handler.nit [new file with mode: 0644]
lib/popcorn/examples/middlewares/example_simple_error_handler.nit [moved from tests/test_realtime.nit with 58% similarity]
lib/popcorn/examples/middlewares/example_simple_logger.nit [new file with mode: 0644]
lib/popcorn/examples/mongodb/example_mongodb.nit [new file with mode: 0644]
lib/popcorn/examples/routing/example_glob_route.nit [new file with mode: 0644]
lib/popcorn/examples/routing/example_param_route.nit [new file with mode: 0644]
lib/popcorn/examples/routing/example_router.nit [new file with mode: 0644]
lib/popcorn/examples/sessions/example_session.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/example_static.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/example_static_default.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/example_static_multiple.nit [new file with mode: 0644]
lib/popcorn/examples/static_files/files/index.html [new file with mode: 0644]
lib/popcorn/examples/static_files/public/css/style.css [new file with mode: 0644]
lib/popcorn/examples/static_files/public/default.html [new file with mode: 0644]
lib/popcorn/examples/static_files/public/hello.html [new file with mode: 0644]
lib/popcorn/examples/static_files/public/images/trollface.jpg [new file with mode: 0644]
lib/popcorn/examples/static_files/public/js/app.js [new file with mode: 0644]
lib/popcorn/package.ini [new file with mode: 0644]
lib/popcorn/pop_handlers.nit [new file with mode: 0644]
lib/popcorn/pop_middlewares.nit [new file with mode: 0644]
lib/popcorn/pop_routes.nit [new file with mode: 0644]
lib/popcorn/popcorn.nit [new file with mode: 0644]
lib/popcorn/test_pop_routes.nit [new file with mode: 0644]
lib/popcorn/tests/Makefile [new file with mode: 0644]
lib/popcorn/tests/base_tests.nit [new file with mode: 0644]
lib/popcorn/tests/res/test_example_advanced_logger.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_angular.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_glob_route.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_hello.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_html_error_handler.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_param_route.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_post.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_query_string.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_router.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_session.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_simple_error_handler.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_simple_logger.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_static.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_static_default.res [new file with mode: 0644]
lib/popcorn/tests/res/test_example_static_multiple.res [new file with mode: 0644]
lib/popcorn/tests/res/test_router.res [new file with mode: 0644]
lib/popcorn/tests/res/test_routes.res [new file with mode: 0644]
lib/popcorn/tests/test_example_advanced_logger.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_angular.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_glob_route.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_hello.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_html_error_handler.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_param_route.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_post.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_query_string.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_router.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_session.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_simple_error_handler.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_simple_logger.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_static.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_static_default.nit [new file with mode: 0644]
lib/popcorn/tests/test_example_static_multiple.nit [new file with mode: 0644]
lib/popcorn/tests/test_router.nit [new file with mode: 0644]
lib/popcorn/tests/test_routes.nit [new file with mode: 0644]
lib/popcorn/tests/tests.sh [new file with mode: 0755]
lib/postgresql/native_postgres.nit [new file with mode: 0644]
lib/postgresql/package.ini [new file with mode: 0644]
lib/postgresql/postgres.nit [new file with mode: 0644]
lib/readline.ini [new file with mode: 0644]
lib/readline.nit [new file with mode: 0644]
lib/realtime.nit
lib/rubix.ini [new file with mode: 0644]
lib/rubix.nit
lib/serialization/caching.nit
lib/sqlite3/native_sqlite3.nit
lib/sqlite3/sqlite3.nit
lib/template/macro.nit
misc/docker/Dockerfile [new file with mode: 0644]
misc/docker/README.md [new file with mode: 0644]
misc/docker/full/Dockerfile [new file with mode: 0644]
misc/docker/hello/Dockerfile [new file with mode: 0644]
misc/docker/hello/src/hello.nit [new file with mode: 0644]
share/man/nitc.md
share/man/nitdoc.md
share/man/nitunit.md
share/nitweb/directives/entity/doc.html [new file with mode: 0644]
share/nitweb/directives/entity/link.html [new file with mode: 0644]
share/nitweb/directives/entity/signature.html [new file with mode: 0644]
share/nitweb/directives/group-block.html [new file with mode: 0644]
share/nitweb/index.html [new file with mode: 0644]
share/nitweb/javascripts/entities.js [new file with mode: 0644]
share/nitweb/javascripts/model.js [new file with mode: 0644]
share/nitweb/javascripts/nitweb.js [new file with mode: 0644]
share/nitweb/stylesheets/nitweb.css [new file with mode: 0644]
share/nitweb/stylesheets/nitweb_bootstrap.css [new file with mode: 0644]
share/nitweb/views/class.html [new file with mode: 0644]
share/nitweb/views/classdef.html [new file with mode: 0644]
share/nitweb/views/group.html [new file with mode: 0644]
share/nitweb/views/index.html [new file with mode: 0644]
share/nitweb/views/module.html [new file with mode: 0644]
share/nitweb/views/package.html [new file with mode: 0644]
share/nitweb/views/propdef.html [new file with mode: 0644]
share/nitweb/views/property.html [new file with mode: 0644]
src/catalog.nit
src/doc/doc_phases/doc_html.nit
src/interpreter/dynamic_loading_ffi/on_demand_compiler.nit
src/loader.nit
src/location.nit
src/model/mmodule.nit
src/model/model.nit
src/model/model_base.nit
src/model/model_collect.nit
src/model/model_json.nit [new file with mode: 0644]
src/model/model_views.nit
src/model/model_visitor.nit
src/model/mpackage.nit
src/modelize/modelize_class.nit
src/modelize/modelize_property.nit
src/neo.nit
src/nitcatalog.nit
src/nitlight.nit
src/nitunit.nit
src/nitweb.nit
src/parser/README.md
src/platform/app_annotations.nit
src/rapid_type_analysis.nit
src/semantize/typing.nit
src/test_model_visitor.nit
src/testing/testing_base.nit
src/testing/testing_doc.nit
src/testing/testing_suite.nit
src/toolcontext.nit
src/web/model_api.nit [new file with mode: 0644]
src/web/model_html.nit
src/web/web.nit
src/web/web_actions.nit
src/web/web_base.nit
src/web/web_views.nit
tests/Darwin.skip
tests/base_gen_infinite2.nit [new file with mode: 0644]
tests/base_redef.nit [new file with mode: 0644]
tests/listfull.sh
tests/niti.skip
tests/nitunit.args
tests/nitvm.skip
tests/repeating_key_xor_solve.args [new file with mode: 0644]
tests/sav/base_gen_infinite2.res [new file with mode: 0644]
tests/sav/base_redef.res [new file with mode: 0644]
tests/sav/base_redef_alt1.res [new file with mode: 0644]
tests/sav/base_redef_alt2.res [new file with mode: 0644]
tests/sav/nitcatalog_args1.res
tests/sav/nitce/base_gen_infinite2.res [new file with mode: 0644]
tests/sav/nitcg/fixme/base_gen_infinite2.res [new file with mode: 0644]
tests/sav/nitcs/fixme/base_gen_infinite2.res [new file with mode: 0644]
tests/sav/nitcsg/fixme/base_gen_infinite2.res [new file with mode: 0644]
tests/sav/nitlight_args1.res
tests/sav/nitunit_args1.res
tests/sav/nitunit_args4.res
tests/sav/nitunit_args5.res
tests/sav/nitunit_args6.res
tests/sav/nitunit_args7.res
tests/sav/nitunit_args8.res
tests/sav/nitunit_args9.res
tests/sav/repeating_key_xor_solve.res [new file with mode: 0644]
tests/sav/repeating_key_xor_solve_args1.res [new file with mode: 0644]
tests/sav/test_copy_to_native.res [new file with mode: 0644]
tests/sav/test_copy_to_native_alt1.res [new file with mode: 0644]
tests/sav/test_copy_to_native_alt2.res [new file with mode: 0644]
tests/sav/test_highlight_args1.res
tests/sav/test_json_deserialization_plain.res
tests/sav/test_model_visitor_args1.res
tests/sav/test_model_visitor_args2.res
tests/sav/test_nativestring_fill_from.res [new file with mode: 0644]
tests/sav/test_nativestring_fill_from_alt1.res [new file with mode: 0644]
tests/sav/test_nativestring_fill_from_alt2.res [new file with mode: 0644]
tests/sav/test_nativestring_fill_from_alt3.res [new file with mode: 0644]
tests/sav/test_new_native_alt1.res
tests/sav/test_postgres_native.res [new file with mode: 0644]
tests/sav/test_postgres_nity.res [moved from tests/sav/xymus_net.res with 100% similarity]
tests/sav/test_readline.res [new file with mode: 0644]
tests/sav/test_realtime.res [deleted file]
tests/sav/test_substring.res
tests/test_copy_to_native.nit [new file with mode: 0644]
tests/test_kill_process.nit
tests/test_nativestring_fill_from.nit [new file with mode: 0644]
tests/test_nitcorn.nit
tests/test_nitunit.nit
tests/test_nitunit4/test_nitunit4.nit
tests/test_nitunit4/test_nitunit4.sav/test_bar.res [new file with mode: 0644]
tests/test_nitunit4/test_nitunit4.sav/test_baz.res [new file with mode: 0644]
tests/test_nitunit4/test_nitunit4_base.nit
tests/test_postgres_native.nit [new file with mode: 0644]
tests/test_postgres_nity.nit [new file with mode: 0644]
tests/test_readline.inputs [new file with mode: 0644]
tests/test_readline.nit [new file with mode: 0644]
tests/test_sqlite3_native.nit
tests/test_substring.nit
tests/tests.sh

index ca8864b..02c0677 100644 (file)
@@ -6,4 +6,5 @@ tables_nit.c            -diff
 c_src/**               -diff
 
 tests/sav/**/*.res     -whitespace
+lib/popcorn/tests/res/*.res -whitespace
 *.patch                        -whitespace
index edf0d66..a739d3f 100644 (file)
@@ -1,5 +1,6 @@
 *.bak
 *.swp
+*.swo
 *~
 .project
 EIFGENs
@@ -55,3 +56,4 @@ nitunit.xml
 
 .metadata/*
 .github_data
+.DS_Store
diff --git a/NOTICE b/NOTICE
index 2e08fad..514ed50 100644 (file)
--- a/NOTICE
+++ b/NOTICE
@@ -2,39 +2,49 @@ This product includes software developed as part of the Nit Language
 project ( http://nitlanguage.org ).
 
 Files: *
-Copyright: 2004-2015 Jean Privat <jean@pryen.org>
+Copyright: 2004-2016 Jean Privat <jean@pryen.org>
            2006-2008 Floréal Morandat <morandat@lirmm.fr>
            2009      Julien Chevalier <chevjulien@gmail.com>
            2009-2011 Jean-Sebastien Gelinas <calestar@gmail.com>
-           2009-2015 Alexis Laferrière <alexis.laf@xymus.net>
+           2009-2016 Alexis Laferrière <alexis.laf@xymus.net>
            2011      Matthieu Auger <matthieu.auger@gmail.com>
-           2011-2015 Alexandre Terrasa <alexandre@moz-code.org>
+           2011-2016 Alexandre Terrasa <alexandre@moz-code.org>
            2012      Alexandre Pennetier <alexandre.pennetier@me.com>
-           2013-2015 Lucas Bajolet <r4pass@hotmail.com>
+           2013-2016 Lucas Bajolet <r4pass@hotmail.com>
            2013      Stefan Lage <lagestfan@gmail.com>
            2013      Nathan Heu <heu.nathan@courrier.uqam.ca>
            2013      Matthieu Lucas <lucasmatthieu@gmail.com>
-           2014-2015 Romain Chanoir <romain.chanoir@viacesi.fr>
-           2014      Frédéric Vachon <fredvac@gmail.com>
+           2014-2016 Romain Chanoir <romain.chanoir@viacesi.fr>
+           2014-2015 Frédéric Vachon <fredvac@gmail.com>
            2014      Johan Kayser <johan.kayser@viacesi.fr>
            2014-2015 Julien Pagès <julien.projet@gmail.com>
            2014      Geoffrey Hecht <geoffrey.hecht@gmail.com>
-           2014      Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
+           2014-2016 Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
            2015      Arthur Delamare <arthur.delamare@viacesi.fr>
+           2015-2016 Mehdi Ait Younes <overpex@gmail.com>
+           2015      Renata Carvalho <renatawm@gmail.com>
+           2015      Simon Zeni <simonzeni@gmail.com>
+           2015      Anis Boubaker <anis@boubaker.ca>
+           2015      Istvan SZALAI <szalai972@gmail.com>
+           2015      Hervé Matysiak <herve.matysiak@viacesi.fr>
+           2015      Jean-Philippe Caissy <jean-philippe.caissy@shopify.com>
+           2015      Alexandre Blondin Massé <alexandre.blondin.masse@gmail.com>
+           2015-2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
+           2016      Tony Gaetani <tony.gaetani@gmail.com>
 License: Apache 2.0 (see LICENSE)
 Comment: The main license of the work, except for the following exceptions
 
 Files: lib/*
        clib/*
        share/nitdoc/*
-Copyright: 2004-2015 Jean Privat <jean@pryen.org>
+Copyright: 2004-2016 Jean Privat <jean@pryen.org>
            2006-2008 Floréal Morandat <morandat@lirmm.fr>
            2009-2011 Jean-Sebastien Gelinas <calestar@gmail.com>
-           2009-2015 Alexis Laferrière <alexis.laf@xymus.net>
+           2009-2016 Alexis Laferrière <alexis.laf@xymus.net>
            2009      Julien Chevalier <chevjulien@gmail.com>
-           2011-2015 Alexandre Terrasa <alexandre@moz-concept.com>
+           2011-2016 Alexandre Terrasa <alexandre@moz-concept.com>
            2012      Alexandre Pennetier <alexandre.pennetier@me.com>
-           2013-2015 Lucas Bajolet <r4pass@hotmail.com>
+           2013-2016 Lucas Bajolet <r4pass@hotmail.com>
            2013      Nathan Heu <heu.nathan@courrier.uqam.ca>
            2013      Matthieu Lucas <lucasmatthieu@gmail.com>
            2013      Stefan Lage <lagestfan@gmail.com>
@@ -48,7 +58,11 @@ Copyright: 2004-2015 Jean Privat <jean@pryen.org>
            2014      Maxime Leroy <maxime.leroy76@gmail.com>
            2014      Johann Dubois <johann.dubois@outlook.com>
            2014-2015 Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
-           2014      Alexandre Blondin Massé <alexandre.blondin.masse@gmail.com>
+           2014-2015 Alexandre Blondin Massé <alexandre.blondin.masse@gmail.com>
+           2015      Mehdi Ait Younes <overpex@gmail.com>
+           2015      Budi Kurniawan <budi2020@gmail.com>
+           2015-2016 Philippe Pepos Petitclerc <ppeposp@gmail.com>
+           2015-2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
 Licence: BSD 2 (see LICENSE-BSD)
 Comment: Use of libraries and resources is basically unrestricted. We hold the copyright
          on the compiler and the tools but not on the programs made by the users.
index d84731a..1a5dd3c 100644 (file)
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Requirements:
  * gcc         http://gcc.gnu.org/ (or a compatible C compiler)
  * pkg-config  http://www.freedesktop.org/wiki/Software/pkg-config/
  * ccache      http://ccache.samba.org/        to improve recompilation
- * libgc-dev   http://www.hpl.hp.com/personal/Hans_Boehm/gc/
+ * libgc-dev   http://hboehm.info/gc/
  * graphviz    http://www.graphviz.org/        to enable graphs with the nitdoc tool
  * libunwind   http://nongnu.org/libunwind
 
diff --git a/benchmarks/csv/README.md b/benchmarks/csv/README.md
new file mode 100644 (file)
index 0000000..5f913bd
--- /dev/null
@@ -0,0 +1,11 @@
+# CSV Bench
+
+This is a simple benchmark for CSV parsers between different languages.
+
+Are required for testing (all packages can be apt-get'd):
+
+* Python 2
+* Python 3
+* Java JDK 6+, Apache-commons-csv
+* Ruby
+* Go language compiler
diff --git a/benchmarks/csv/csv_bench.sh b/benchmarks/csv/csv_bench.sh
new file mode 100755 (executable)
index 0000000..badd1e6
--- /dev/null
@@ -0,0 +1,103 @@
+#!/bin/bash
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Shell script to bench json parsers over different documents
+
+source ../bench_common.sh
+source ../bench_plot.sh
+
+## CONFIGURATION OPTIONS ##
+
+# Default number of times a command must be run with bench_command
+# Can be overrided with 'the option -n'
+count=5
+
+## HANDLE OPTIONS ##
+
+function init_repo()
+{
+       mkdir -p inputs
+       nitc --semi-global scripts/csv_gen.nit -o scripts/csv_gen
+       echo "Generating 1000 lines documents"
+       ./scripts/csv_gen 10 1000 inputs/1000_l.csv
+       ./scripts/csv_gen 10 1000 inputs/1000_uni_l.csv --unicode
+       echo "Generating 10000 lines documents"
+       ./scripts/csv_gen 10 10000 inputs/10000_l.csv
+       ./scripts/csv_gen 10 10000 inputs/10000_uni_l.csv --unicode
+       echo "Generating 100000 lines documents"
+       ./scripts/csv_gen 10 100000 inputs/100000_l.csv
+       ./scripts/csv_gen 10 100000 inputs/100000_uni_l.csv --unicode
+       echo "Generating 1000000 lines documents"
+       ./scripts/csv_gen 10 1000000 inputs/1000000_l.csv
+       ./scripts/csv_gen 10 1000000 inputs/1000000_uni_l.csv --unicode
+}
+
+function usage()
+{
+       echo "run_bench: ./csv_bench.sh [options]"
+       echo "  -v: verbose mode"
+       echo "  -n count: number of execution for each bar (default: $count)"
+       echo "  -h: this help"
+}
+
+stop=false
+fast=false
+while [ "$stop" = false ]; do
+       case "$1" in
+               -v) verbose=true; shift;;
+               --fast) fast=true; shift;;
+               -h) usage; exit;;
+               -n) count="$2"; shift; shift;;
+               *) stop=true
+       esac
+done
+
+if [ -z "$fast" ]; then
+       init_repo
+fi
+
+mkdir -p out
+
+echo "Compiling engines"
+
+echo "Java Parser"
+
+javac -cp './scripts/commons-csv-1.3.jar' scripts/JavaCSV.java
+
+echo "Go parser"
+
+go build -o scripts/go_csv scripts/go_csv.go
+
+echo "Nit/Ad-Hoc Parser"
+
+nitc --semi-global scripts/nit_csv.nit -o scripts/nit_csv
+
+declare -a script_names=('Python 3 - Pandas' 'Python 2 - Pandas' 'Go' 'Nit' 'Python 3 - Standard' 'Python 2 - Standard' 'Java - Apache commons' 'Ruby')
+declare -a script_cmds=('python3 scripts/python_csv.py' 'python2 scripts/python_csv.py' './scripts/go_csv' './scripts/nit_csv' 'python3 scripts/python_stdcsv.py' 'python2 scripts/python_stdcsv.py' "java -cp /usr/share/java/commons-csv.jar:. scripts.JavaCSV" 'ruby scripts/ruby_csv.rb')
+
+for script in `seq 1 ${#script_cmds[@]}`; do
+       echo "Preparing res for ${script_names[$script - 1]}"
+       prepare_res "./out/${script_names[$script - 1]}.dat" "${script_names[$script - 1]}" "${script_names[$script - 1]}"
+       for file in inputs/*.csv; do
+               fname=`basename $file .csv`
+               bench_command $file "Benching file $file using ${script_cmds[$script - 1]} parser" ${script_cmds[$script - 1]} $file
+       done;
+done;
+
+rm scripts/nit_csv
+rm scripts/JavaCSV.class
+rm scripts/go_csv
+
+plot out/bench_csv.gnu
diff --git a/benchmarks/csv/scripts/JavaCSV.java b/benchmarks/csv/scripts/JavaCSV.java
new file mode 100644 (file)
index 0000000..f8264ca
--- /dev/null
@@ -0,0 +1,18 @@
+package scripts;
+
+import java.io.File;
+import java.util.List;
+import java.nio.charset.Charset;
+import org.apache.commons.csv.*;
+
+class JavaCSV {
+       public static void main(String[] args) {
+               try {
+                       File csvData = new File(args[0]);
+                       CSVParser parser = CSVParser.parse(csvData, Charset.forName("UTF-8"), CSVFormat.RFC4180);
+                       List<CSVRecord> r = parser.getRecords();
+               } catch(Exception e) {
+                       System.err.println("Major fail");
+               }
+       }
+}
diff --git a/benchmarks/csv/scripts/csv_gen.nit b/benchmarks/csv/scripts/csv_gen.nit
new file mode 100644 (file)
index 0000000..123dbb6
--- /dev/null
@@ -0,0 +1,61 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import csv
+
+if args.length < 3 then
+       print "Usage ./csv_gen record_length record_nb out_filepath [--unicode]"
+       exit 1
+end
+
+var record_length = args[0].to_i
+var record_nb = args[1].to_i
+var outpath = args[2]
+var unicode = false
+
+if args.length == 4 then
+       if not args[3] == "--unicode" then
+               print "Usage ./csv_gen record_length record_nb [--unicode]"
+               exit 1
+       end
+       unicode = true
+end
+
+var ocsv = new CsvDocument
+ocsv.eol = "\r\n"
+
+var sep = ocsv.separator.to_s
+var eol = ocsv.eol
+var del = ocsv.delimiter.to_s
+
+for i in [0 .. record_length[ do ocsv.header.add "Col{i}"
+
+var c = if unicode then "á" else "a"
+for i in [0 .. record_nb[ do
+       var line = new Array[String].with_capacity(record_length)
+       for j in [0 .. record_length[ do
+               var add_sep = 100.rand > 70
+               var add_del = 100.rand > 70
+               var add_eol = 100.rand > 70
+               var ln = 10.rand
+               var s = c * ln
+               if add_sep then s = sep + s
+               if add_del then s += del
+               if add_eol then s += eol
+               line.add s
+       end
+       ocsv.records.add line
+end
+
+ocsv.write_to_file(outpath)
diff --git a/benchmarks/csv/scripts/go_csv.go b/benchmarks/csv/scripts/go_csv.go
new file mode 100644 (file)
index 0000000..5fff932
--- /dev/null
@@ -0,0 +1,18 @@
+package main
+
+import "encoding/csv"
+import "os"
+import "fmt"
+
+func main() {
+       if len(os.Args) == 1 {
+               fmt.Println("Usage ./go_csv file")
+               os.Exit(-1)
+       }
+       file, err := os.Open(os.Args[1])
+       if err != nil { panic(err) }
+
+       var read = csv.NewReader(file)
+       _, r := read.ReadAll()
+       if r != nil { panic(err) }
+}
diff --git a/benchmarks/csv/scripts/nit_csv.nit b/benchmarks/csv/scripts/nit_csv.nit
new file mode 100644 (file)
index 0000000..c8422d1
--- /dev/null
@@ -0,0 +1,25 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import csv
+
+if args.is_empty then
+       print "Usage: ./nit_csv in.csv"
+       exit 1
+end
+
+var csv = new CsvReader(new FileReader.open(args[0]))
+csv.eol = "\r\n"
+
+csv.read_all
diff --git a/benchmarks/csv/scripts/python_csv.py b/benchmarks/csv/scripts/python_csv.py
new file mode 100644 (file)
index 0000000..d8addda
--- /dev/null
@@ -0,0 +1,4 @@
+import sys
+from pandas import read_csv
+
+csv = read_csv(sys.argv[1])
diff --git a/benchmarks/csv/scripts/python_stdcsv.py b/benchmarks/csv/scripts/python_stdcsv.py
new file mode 100644 (file)
index 0000000..b78cb15
--- /dev/null
@@ -0,0 +1,8 @@
+import sys
+import csv
+
+lst = list();
+with open(sys.argv[1], 'r') as f:
+    reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE)
+    for row in reader:
+        list.append(lst, row)
diff --git a/benchmarks/csv/scripts/ruby_csv.rb b/benchmarks/csv/scripts/ruby_csv.rb
new file mode 100644 (file)
index 0000000..6b1fe02
--- /dev/null
@@ -0,0 +1,3 @@
+require 'csv'
+
+CSV.read(ARGV.first)
index f110c83..a5e910a 100644 (file)
@@ -6,17 +6,17 @@ all: bin/asteronits
 bin/asteronits: $(shell ${NITLS} -M src/asteronits.nit linux) ${NITC} pre-build
        ${NITC} src/asteronits.nit -m linux -o $@
 
-bin/texture_atlas_parser: src/texture_atlas_parser.nit
-       ${NITC} src/texture_atlas_parser.nit -o $@
+bin/texture_atlas_parser: ../../lib/gamnit/texture_atlas_parser.nit
+       ${NITC} ../../lib/gamnit/texture_atlas_parser.nit -o $@
 
 src/controls.nit: art/controls.svg
        make -C ../inkscape_tools/
        ../inkscape_tools/bin/svg_to_png_and_nit art/controls.svg -a assets/ -s src/ -x 2.0 -g
 
-src/spritesheet_city.nit: bin/texture_atlas_parser
+src/spritesheet.nit: bin/texture_atlas_parser
        bin/texture_atlas_parser art/sheet.xml --dir src/ -n spritesheet
 
-pre-build: src/controls.nit src/spritesheet_city.nit
+pre-build: src/controls.nit src/spritesheet.nit
 
 check: bin/asteronits
        NIT_TESTING=true bin/asteronits
index 4042f6d..e7ac9df 100644 (file)
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import ::android::platform
+import ::android
 import ::android::vibration
 
 import asteronits
index 3971426..388cd35 100644 (file)
@@ -62,16 +62,16 @@ redef class App
                                        if dy > 0.0 then
                                                # Bottom part of the joystick, turns left or right
                                                if dx < 0.0 then
-                                                       ship.applied_rotation = -1.0
-                                               else
                                                        ship.applied_rotation = 1.0
+                                               else
+                                                       ship.applied_rotation = -1.0
                                                end
                                        else
                                                # Upper part of the joystick, detect action using 45d angles
                                                if dx < dy then
-                                                       ship.applied_rotation = -1.0
-                                               else if dx > -dy then
                                                        ship.applied_rotation = 1.0
+                                               else if dx > -dy then
+                                                       ship.applied_rotation = -1.0
                                                else
                                                        ship.applied_thrust = 1.0
                                                end
@@ -98,20 +98,20 @@ redef class App
 
                # Add the joystick to the UI
                ui_sprites.add new Sprite(spritesheet_controls.forward,
-                       ui_camera.bottom_left.offset(joystick_x, -200.0, 0.0))
+                       ui_camera.bottom_left.offset(joystick_x, 200.0, 0.0))
                ui_sprites.add new Sprite(spritesheet_controls.left,
-                       ui_camera.bottom_left.offset(joystick_x-100.0, -joystick_y, 0.0))
+                       ui_camera.bottom_left.offset(joystick_x-100.0, joystick_y, 0.0))
                ui_sprites.add new Sprite(spritesheet_controls.right,
-                       ui_camera.bottom_left.offset(joystick_x+100.0, -joystick_y, 0.0))
+                       ui_camera.bottom_left.offset(joystick_x+100.0, joystick_y, 0.0))
 
                # Purely cosmetic joystick background
                ui_sprites.add new Sprite(spritesheet_controls.joystick_back,
-                       ui_camera.bottom_left.offset(joystick_x, -joystick_y, -1.0)) # In the back
+                       ui_camera.bottom_left.offset(joystick_x, joystick_y, -1.0)) # In the back
                ui_sprites.add new Sprite(spritesheet_controls.joystick_down,
                        ui_camera.bottom_left.offset(joystick_x, 0.0, 1.0))
 
                # Add the "open fire" button
                ui_sprites.add new Sprite(spritesheet_controls.fire,
-                       ui_camera.bottom_right.offset(-150.0, -150.0, 0.0))
+                       ui_camera.bottom_right.offset(-150.0, 150.0, 0.0))
        end
 end
index 562fb6d..ccbe098 100644 (file)
@@ -1,4 +1,4 @@
-src/benitlux_restful.nit
+src/server/benitlux_restful.nit
 *.db
 *.email
 benitlux_corrections.txt
index c14d648..2142041 100644 (file)
@@ -1,19 +1,19 @@
 SERVER ?= localhost:8080
 
-all: server
+all: server bin/report bin/benitlux
 
 server: bin/benitlux_daily bin/benitlux_web
-bin/benitlux_daily: $(shell ../../bin/nitls -M src/benitlux_daily.nit)
+bin/benitlux_daily: $(shell ../../bin/nitls -M src/server/benitlux_daily.nit)
        mkdir -p bin/
-       ../../bin/nitc -o $@ src/benitlux_daily.nit
+       ../../bin/nitc -o $@ src/server/benitlux_daily.nit
 
-bin/benitlux_web: $(shell ../../bin/nitls -M src/benitlux_web.nit) src/benitlux_restful.nit
+bin/benitlux_web: $(shell ../../bin/nitls -M src/server/server.nit) src/server/benitlux_restful.nit
        mkdir -p bin/
-       ../../bin/nitc -o $@ src/benitlux_web.nit -D iface=$(SERVER)
+       ../../bin/nitc -o $@ src/server/server.nit -D iface=$(SERVER)
 
-pre-build: src/benitlux_restful.nit
-src/benitlux_restful.nit: $(shell ../../bin/nitls -M src/benitlux_controller.nit)
-       ../../bin/nitrestful -o $@ src/benitlux_controller.nit
+pre-build: src/server/benitlux_restful.nit
+src/server/benitlux_restful.nit: $(shell ../../bin/nitls -M src/server/benitlux_controller.nit)
+       ../../bin/nitrestful -o $@ src/server/benitlux_controller.nit
 
 # ---
 # Report
@@ -23,3 +23,64 @@ bin/report: $(shell ../../bin/nitls -M src/report.nit)
 
 report: bin/report
        bin/report
+
+# ---
+# GTK+ client
+
+bin/benitlux: $(shell ../../bin/nitls -M src/client/client.nit)
+       mkdir -p bin/
+       ../../bin/nitc -o bin/benitlux src/client/client.nit -m linux -D benitlux_rest_server_uri=http://$(SERVER)/
+
+# ---
+# Android
+
+# Main icon
+android/res/drawable-hdpi/icon.png:
+       ../inkscape_tools/bin/svg_to_icons art/icon.svg --android --out android/res/
+
+# Notification icon, white only
+android/res/drawable-hdpi/notif.png:
+       ../inkscape_tools/bin/svg_to_icons art/notif.svg --android --out android/res/ --name notif
+
+android-res: android/res/drawable-hdpi/icon.png android/res/drawable-hdpi/notif.png
+
+# Dev / debug app
+android: bin/benitlux.apk
+bin/benitlux.apk: $(shell ../../bin/nitls -M src/client/android.nit) android-res
+       mkdir -p bin/ res/
+       ../../bin/nitc -o $@ src/client/android.nit -m src/client/features/debug.nit \
+               -D benitlux_rest_server_uri=http://$(SERVER)/
+
+# Pure portable prototype, for comparison
+bin/proto.apk: $(shell ../../bin/nitls -M src/client/android_proto.nit) android-res
+       mkdir -p bin/ res/
+       ../../bin/nitc -o $@ src/client/android_proto.nit \
+               -D benitlux_rest_server_uri=http://$(SERVER)/
+
+# Release version
+android-release: $(shell ../../bin/nitls -M src/client/android.nit) android-res
+       mkdir -p bin/ res/
+       ../../bin/nitc -o bin/benitlux.apk src/client/android.nit \
+               -D benitlux_rest_server_uri=http://xymus.net/benitlux/ --release
+
+# ---
+# iOS
+
+ios: bin/benitlux.app
+bin/benitlux.app: $(shell ../../bin/nitls -M src/client/ios.nit) ios/AppIcon.appiconset/Contents.json
+       mkdir -p bin/
+       rm -rf bin/benitlux.app/
+       ../../bin/nitc -o bin/benitlux.app src/client/ios.nit -D benitlux_rest_server_uri=http://$(SERVER)/
+
+bin/proto.app: $(shell ../../bin/nitls -M src/client/ios_proto.nit) ios/AppIcon.appiconset/Contents.json
+       mkdir -p bin/ res/
+       ../../bin/nitc -o $@ src/client/ios_proto.nit \
+               -D benitlux_rest_server_uri=http://$(SERVER)/
+
+ios-release: $(shell ../../bin/nitls -M src/client/ios.nit) ios/AppIcon.appiconset/Contents.json
+       mkdir -p bin/
+       ../../bin/nitc -o bin/benitlux.app src/client/ios.nit -D benitlux_rest_server_uri=http://$(SERVER)/
+
+ios/AppIcon.appiconset/Contents.json: art/icon.svg
+       mkdir -p ios
+       ../inkscape_tools/bin/svg_to_icons art/icon.svg --ios --out ios/AppIcon.appiconset/
index 6f4ce46..b8aed58 100644 (file)
@@ -1,21 +1,45 @@
-An unofficial mailing list and other tools to keep faithful bargoers informed of the beers available at the excellent Brasserie Bénélux.
+An unofficial app and mailing list to keep faithful bargoers informed of the beers available at the excellent Brasserie Bénélux.
 
-This project is composed of two softwares:
+This project is composed of three softwares:
 
-* a Web interface to subscribe and unsubscribe,
-* and a daily background program which updates the BD and send emails.
+* A mobile app and social network,
+* a server with a RESTful API for the mobile app and a web interface to subscribe to the mailing list
+* and a daily background program which updates the DB and send emails.
 
-The web interface is currently published at <http://benitlux.xymus.net/>
+The mobile app is available on the Nit F-Droid repository, see http://nitlanguage.org/fdroid.
+The web interface is currently published at http://benitlux.xymus.net.
 
 # Compile and execute
 
-Make sure all the required packages are installed. Under Debian or Ubuntu, you can use: `apt-get install libevent-dev libsqlite3-dev libcurl4-gnutls-dev sendmail`
+First, choose a server and set the `SERVER` environment variable accordingly.
+It can be localhost, a local development server or the official server.
 
-To compile, run: `make`
+* `SERVER` defaults to `localhost:8080`.
+  This is enough to test running the server and the GNU/Linux client on the same machine.
 
-To launch the daily background program, run: `bin/benitlux_daily` (the argument `-e` activates sending emails)
+* Set `SERVER=192.168.0.1` or to your IP to quickly setup a development server.
+  This allows you to work and test both the clients and the server.
 
-To launch the Web interface, run: `bin/benitlux_web`
+* Set `SERVER=benitlux.xymus.net` to use the official server, it should work with all clients.
+  It is not advised to use the official server with unstable clients.
+
+## Mobile client
+
+Build and run on GNU/Linux with `make bin/benitlux && bin/benitlux`
+
+Build and install for Android with: `make bin/benitlux.apk && adb install -rd bin/benitlux.apk`
+
+Build and simulate for iOS with: `make bin/benitlux.app && ios-sim launch bin/benitlux.app`
+
+## Server
+
+Install all required development packages. Under Debian or Ubuntu, you can use: `apt-get install libevent-dev libsqlite3-dev libcurl4-gnutls-dev sendmail`
+
+Compile with: `make`
+
+Launch the daily background program with: `bin/benitlux_daily` (the argument `-e` sends the emails)
+
+Launch the server with: `bin/benitlux_web`
 
 The Web interface will be accessible at <http://localhost:8080/>
 
@@ -25,10 +49,9 @@ The Web interface will be accessible at <http://localhost:8080/>
 - [x] Daily mailer
 - [x] Web interface
 - [x] Serialization and deserialization of data classes
-- [ ] Android app
-- [ ] iOS app
+- [x] Android app
+- [x] iOS app
 - [ ] Charlevoix location support
-- [ ] Customize mails (daily, on change, per locations)
 - [ ] Authenticate unsubscribe actions over GET
-- [ ] Social network and location updates
+- [x] Social network and location updates
 - [ ] Event updates
diff --git a/contrib/benitlux/android/res/.gitignore b/contrib/benitlux/android/res/.gitignore
new file mode 100644 (file)
index 0000000..46ce728
--- /dev/null
@@ -0,0 +1 @@
+drawable*
diff --git a/contrib/benitlux/android/res/values/styles.xml b/contrib/benitlux/android/res/values/styles.xml
new file mode 100644 (file)
index 0000000..c435c5f
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+       <color name="item_background">#000000</color>
+</resources>
diff --git a/contrib/benitlux/art/icon.svg b/contrib/benitlux/art/icon.svg
new file mode 100644 (file)
index 0000000..779a3e3
--- /dev/null
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="512"
+   height="512"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="icon.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="239.85532"
+     inkscape:cy="187.76324"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1196"
+     inkscape:window-height="1109"
+     inkscape:window-x="2732"
+     inkscape:window-y="1283"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-540.36218)">
+    <path
+       sodipodi:type="arc"
+       style="fill:#000000;fill-opacity:1;stroke:none"
+       id="path2986"
+       sodipodi:cx="270.72089"
+       sodipodi:cy="251.38065"
+       sodipodi:rx="241.42645"
+       sodipodi:ry="241.42645"
+       d="m 512.14734,251.38065 a 241.42645,241.42645 0 1 1 -482.852906,0 241.42645,241.42645 0 1 1 482.852906,0 z"
+       transform="matrix(1.0603643,0,0,1.0603643,-31.062773,529.80711)" />
+    <text
+       xml:space="preserve"
+       style="font-size:394.38067627px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Sans"
+       x="120.70995"
+       y="938.47345"
+       id="text2987"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2989"
+         x="120.70995"
+         y="938.47345"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Webdings;-inkscape-font-specification:Webdings Bold">B</tspan></text>
+    <path
+       transform="matrix(0.96890975,0,0,0.96890975,-6.3041178,552.79701)"
+       d="m 512.14734,251.38065 a 241.42645,241.42645 0 1 1 -482.852906,0 241.42645,241.42645 0 1 1 482.852906,0 z"
+       sodipodi:ry="241.42645"
+       sodipodi:rx="241.42645"
+       sodipodi:cy="251.38065"
+       sodipodi:cx="270.72089"
+       id="path2988"
+       style="fill:none;stroke:#ffffff;stroke-width:10.02402973;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       sodipodi:type="arc" />
+  </g>
+</svg>
diff --git a/contrib/benitlux/art/notif.svg b/contrib/benitlux/art/notif.svg
new file mode 100644 (file)
index 0000000..871b01c
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="512"
+   height="512"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.5 r10040"
+   sodipodi:docname="icon.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="331.40838"
+     inkscape:cy="413.97896"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1196"
+     inkscape:window-height="1109"
+     inkscape:window-x="2732"
+     inkscape:window-y="297"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-540.36218)">
+    <text
+       xml:space="preserve"
+       style="font-size:419.40737915px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Sans"
+       x="112.12482"
+       y="947.49194"
+       id="text2987"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2989"
+         x="112.12482"
+         y="947.49194"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Webdings;-inkscape-font-specification:Webdings Bold">B</tspan></text>
+    <path
+       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10.02402973;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+       d="M 256 2.0625 C 115.82007 2.0625 2.0625 115.82007 2.0625 256 C 2.0625 396.17993 115.82007 509.9375 256 509.9375 C 396.17993 509.9375 509.9375 396.17993 509.9375 256 C 509.9375 115.82007 396.17993 2.0625 256 2.0625 z M 256 37.0625 C 376.96769 37.0625 474.9375 135.03232 474.9375 256 C 474.9375 376.96769 376.96769 474.90625 256 474.90625 C 135.03232 474.90625 37.0625 376.96769 37.0625 256 C 37.0625 135.03232 135.03232 37.0625 256 37.0625 z "
+       transform="translate(0,540.36218)"
+       id="path2988" />
+  </g>
+</svg>
diff --git a/contrib/benitlux/ios/.gitignore b/contrib/benitlux/ios/.gitignore
new file mode 100644 (file)
index 0000000..72e8ffc
--- /dev/null
@@ -0,0 +1 @@
+*
diff --git a/contrib/benitlux/net.xymus.benitlux.txt b/contrib/benitlux/net.xymus.benitlux.txt
new file mode 100644 (file)
index 0000000..fa717a9
--- /dev/null
@@ -0,0 +1,11 @@
+Categories:Nit,Internet
+License:Apache2
+Web Site:http://xymus.net/benitlux
+Source Code:http://nitlanguage.org/nit.git/tree/HEAD:/contrib/benitlux
+Issue Tracker:https://github.com/nitlang/nit/issues
+
+Summary:Mobile client for the Benitlux social network
+Description:
+View the beer menu, rate beers, view community rating, and receive notifications
+of the daily menu changes and when friends are on location.
+.
index e53853c..38eeee8 100644 (file)
@@ -1,12 +1,13 @@
 [package]
 name=benitlux
-tags=network
+tags=mobile,web
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
 browse=https://github.com/nitlang/nit/tree/master/contrib/benitlux/
 git=https://github.com/nitlang/nit.git
 git.directory=contrib/benitlux/
-homepage=http://nitlanguage.org
+homepage=http://xymus.net/benitlux/
 issues=https://github.com/nitlang/nit/issues
-tryit=http://benitlux.xymus.net/
+tryit=http://xymus.net/benitlux/
+apk=http://nitlanguage.org/fdroid/apk/tnitter.apk
index 987047f..48a807a 100644 (file)
@@ -234,6 +234,14 @@ class CheckinReport
        var users = new Array[User]
 end
 
+# Daily menu notifications
+class DailyNotification
+       serialize
+
+       # All beers on the menu today
+       var beers: Array[BeerAndRatings]
+end
+
 # Server or API usage error
 class BenitluxError
        super Error
diff --git a/contrib/benitlux/src/client/android.nit b/contrib/benitlux/src/client/android.nit
new file mode 100644 (file)
index 0000000..f3391bf
--- /dev/null
@@ -0,0 +1,268 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Android variant improved with platform specific services
+module android is
+       android_manifest_activity """android:theme="@android:style/Theme.DeviceDefault" """
+       android_api_min 16 # For BigTextStyle
+       android_api_target 16
+end
+
+import ::android::portrait
+import ::android::toast
+import ::android::wifi
+import ::android::service::at_boot
+
+import client
+import push
+import checkins
+
+redef class App
+
+       redef fun on_create
+       do
+               super
+
+               # Launch service with app, if it wasn't already launched at boot
+               start_service
+       end
+
+       # Use Android toasts if there is an activity, otherwise fallback on the log
+       redef fun feedback(text)
+       do
+               if activities.not_empty then
+                       app.toast(text.to_s, false)
+               else super
+       end
+
+       # Register to callback `async_wifi_scan_available` when a wifi scan is available
+       private fun notify_on_wifi_scan(context: NativeContext)
+       import async_wifi_scan_available in "Java" `{
+
+               android.content.IntentFilter filter = new android.content.IntentFilter();
+               filter.addAction(android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+               final int final_self = self;
+
+               context.registerReceiver(
+                       new android.content.BroadcastReceiver() {
+                               @Override
+                               public void onReceive(android.content.Context context, android.content.Intent intent) {
+                                       if (intent.getAction().equals(android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+                                               App_async_wifi_scan_available(final_self);
+                                       }
+                               }
+                       }, filter);
+       `}
+
+       private fun async_wifi_scan_available do run_on_ui_thread task_on_wifi_scan_available
+
+       private var task_on_wifi_scan_available = new WifiScanAvailable is lazy
+end
+
+redef class Service
+       redef fun on_start_command(intent, flags, id)
+       do
+               app.notify_on_wifi_scan native
+
+               # Check token validity
+               (new PushHttpRequest("push/check_token?token={app.token}")).start
+
+               return start_sticky
+       end
+end
+
+# Task ran on the UI thread when a wifi scan is available
+private class WifiScanAvailable
+       super Task
+
+       redef fun main
+       do
+               jni_env.push_local_frame 4
+               var manager = app.native_context.wifi_manager
+               var networks = manager.get_scan_results
+               var found_ben = false
+               for i in networks.length.times do
+                       jni_env.push_local_frame 4
+                       var net = networks[i]
+                       var ssid = net.ssid.to_s
+
+                       # TODO use BSSID instead
+                       #var bssid = net.bssid.to_s
+                       var target_ssids = ["Benelux"]
+                       if target_ssids.has(ssid) then # and bssid == "C8:F7:33:81:B0:E6" then
+                               found_ben = true
+                               break
+                       end
+                       jni_env.pop_local_frame
+               end
+               jni_env.pop_local_frame
+
+               if found_ben then
+                       app.on_check_in
+               else app.on_check_out
+       end
+end
+
+redef class SectionTitle
+       init do set_text_style(native, app.native_context)
+
+       private fun set_text_style(view: NativeTextView, context: NativeContext) in "Java" `{
+               view.setTextAppearance(context, android.R.style.TextAppearance_Large);
+       `}
+end
+
+redef class ItemView
+       init do set_backgroud(native, app.native_context)
+
+       private fun set_backgroud(view: NativeView, context: NativeContext) in "Java" `{
+               int color = context.getResources().getIdentifier("item_background", "color", context.getPackageName());
+               view.setBackgroundResource(color);
+       `}
+end
+
+# Use Android notifications
+redef fun notify(title, content, id)
+do
+       var service = app.service
+       assert service != null
+       native_notify(service.native, id, title.to_java_string, content.to_java_string)
+end
+
+private fun native_notify(context: NativeService, id: Int, title, content: JavaString)
+in "Java" `{
+       int icon = context.getResources().getIdentifier(
+               "notif", "drawable", context.getPackageName());
+
+       android.app.Notification.BigTextStyle style =
+               new android.app.Notification.BigTextStyle();
+       style.bigText(content);
+
+       android.content.Intent intent = new android.content.Intent(
+               context, nit.app.NitActivity.class);
+       android.app.PendingIntent pendingIntent = android.app.PendingIntent.getActivity(
+               context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);
+
+       android.app.Notification notif = new android.app.Notification.Builder(context)
+               .setContentTitle(title)
+               .setContentText(content)
+               .setSmallIcon(icon)
+               .setAutoCancel(true)
+               .setOngoing(false)
+               .setStyle(style)
+               .setContentIntent(pendingIntent)
+               .setDefaults(android.app.Notification.DEFAULT_SOUND |
+                            android.app.Notification.DEFAULT_LIGHTS)
+               .build();
+
+       android.app.NotificationManager notificationManager =
+         (android.app.NotificationManager)context.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
+
+       notificationManager.notify((int)id, notif);
+`}
+
+
+# Use `RatingBar` as the beer rating control
+redef class BeerView
+       redef fun setup_stars(rating)
+       do
+               var title = "Review %0".t.format(beer_info.beer.name).to_java_string
+               native_setup_stars(app.native_context, top_line_layout.native, rating, title, app.user != null)
+       end
+
+       private fun native_setup_stars(context: NativeContext, layout: NativeViewGroup, rating: Int, title: JavaString, loggedin: Bool)
+       import on_review in "Java" `{
+               // Set an indicator/non-interactive display
+               final android.widget.RatingBar view = new android.widget.RatingBar(
+                       context, null, android.R.attr.ratingBarStyleIndicator);
+               view.setNumStars(5);
+               view.setRating(rating);
+               view.setIsIndicator(true);
+
+               final android.view.ViewGroup.MarginLayoutParams params = new android.view.ViewGroup.MarginLayoutParams(
+                       android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
+                       android.widget.LinearLayout.LayoutParams.FILL_PARENT);
+               layout.addView(view, params);
+
+               // Make some variables final to used in anonymous class and delayed methods
+               final android.content.Context final_context = context;
+               final long final_rating = rating;
+               final String final_title = title;
+               final boolean final_loggedin = loggedin;
+
+               final int final_self = self;
+               BeerView_incr_ref(self); // Nit GC
+
+               view.setOnTouchListener(new android.view.View.OnTouchListener() {
+                       @Override
+                       public boolean onTouch(android.view.View v, android.view.MotionEvent event) {
+                               if (event.getAction() != android.view.MotionEvent.ACTION_UP) return true;
+
+                               // Don't show dialog if not logged in
+                               if (!final_loggedin) {
+                                       android.widget.Toast toast = android.widget.Toast.makeText(
+                                               final_context, "You must login first to post reviews",
+                                               android.widget.Toast.LENGTH_SHORT);
+                                       toast.show();
+                                       return true;
+                               }
+
+                               // Build dialog with a simple interactive RatingBar
+                               final android.app.AlertDialog.Builder dialog_builder = new android.app.AlertDialog.Builder(final_context);
+                               final android.widget.RatingBar rating = new android.widget.RatingBar(final_context);
+                               rating.setNumStars(5);
+                               rating.setStepSize(1.0f);
+                               rating.setRating(final_rating);
+
+                               // Header bar
+                               int icon = final_context.getResources().getIdentifier("notif", "drawable", final_context.getPackageName());
+                               dialog_builder.setIcon(icon);
+                               dialog_builder.setTitle(final_title);
+
+                               // Rating control
+                               android.widget.LinearLayout l = new android.widget.LinearLayout(final_context);
+                               l.addView(rating, params);
+                               l.setHorizontalGravity(android.view.Gravity.CENTER_HORIZONTAL);
+                               dialog_builder.setView(l);
+
+                               // OK button
+                               dialog_builder.setPositiveButton(android.R.string.ok,
+                                       new android.content.DialogInterface.OnClickListener() {
+                                               public void onClick(android.content.DialogInterface dialog, int which) {
+                                                       dialog.dismiss();
+
+                                                       long r = (long)rating.getRating();
+                                                       view.setRating(r); // Update static control
+                                                       view.invalidate(); // For not refreshing bug
+
+                                                       BeerView_on_review(final_self, r); // Callback
+                                                       BeerView_decr_ref(final_self); // Nit GC
+                                               }
+                                       });
+
+                               // Cancel button
+                               dialog_builder.setNegativeButton(android.R.string.cancel,
+                                       new android.content.DialogInterface.OnClickListener() {
+                                               public void onClick(android.content.DialogInterface dialog, int id) {
+                                                       dialog.cancel();
+                                                       BeerView_decr_ref(final_self); // Nit GC
+                                               }
+                                       });
+
+                               dialog_builder.create();
+                               dialog_builder.show();
+                               return true;
+                       }
+               });
+       `}
+end
diff --git a/contrib/benitlux/src/client/android_proto.nit b/contrib/benitlux/src/client/android_proto.nit
new file mode 100644 (file)
index 0000000..22f0953
--- /dev/null
@@ -0,0 +1,28 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Android variant without modification, pure prototype
+#
+# Usually, compiling with `nitc -m android client.nit` is enough.
+# In this case, for research purposes we set a different `app_namespace`.
+# This allows both the proto and the adaptation to be installed on the same device.
+module android_proto is
+       app_name "Ben Proto"
+       app_namespace "net.xymus.benitlux_proto"
+       android_api_target 16
+end
+
+import ::android
+
+import client
diff --git a/contrib/benitlux/src/client/base.nit b/contrib/benitlux/src/client/base.nit
new file mode 100644 (file)
index 0000000..bcc1efd
--- /dev/null
@@ -0,0 +1,155 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Common services for the Benitlux app
+module base
+
+import app::ui
+import app::data_store
+import app::http_request
+import android::aware
+import json::serialization
+
+import benitlux_model
+import translations
+
+# Show debug output?
+fun debug: Bool do return true
+
+# Root URI of the remote RESTfule server
+fun benitlux_rest_server_uri: String do return "http://localhost:8080/"
+
+redef class App
+
+       # Current connection token, or "none"
+       var token: String is lazy, writable do
+               var token = app.data_store["token"]
+               if token isa String then return token
+               return "none"
+       end
+
+       # Name of the currently logged in user
+       var user: nullable String is lazy, writable do
+               var user = app.data_store["user"]
+               if user isa nullable String then return user
+               return null
+       end
+
+       # Event when user logs in or out
+       fun on_log_in do on_save_state
+
+       redef fun on_save_state
+       do
+               app.data_store["user"] = user
+               app.data_store["token"] = token
+               super
+       end
+
+       # Has this app state been restored yet?
+       var restored = false
+
+       redef fun on_restore_state
+       do
+               super
+
+               # TODO this may happen before the lazy loading above
+               restored = true
+
+               if token != "none" then on_log_in
+       end
+
+       # Show simple feedback to the user on important errors
+       fun feedback(text: Text) do print_error text
+end
+
+# Show a notification to the user
+fun notify(title, content: Text, uniqueness_id: Int)
+do print "Notification {uniqueness_id}: {title}; {content}"
+
+# View for an item in a list, like a beer or a person
+abstract class ItemView
+       super View
+end
+
+# Basic async HTTP request for this app
+#
+# Note that connection errors are passed to `on_fail`, and
+# server errors or authentification errors are received by `on_load`
+# and should be passed to `intercept_error`.
+class BenitluxHttpRequest
+       super AsyncHttpRequest
+
+       redef fun rest_server_uri do return benitlux_rest_server_uri
+
+       redef var rest_action
+
+       redef fun on_fail(error)
+       do
+               if error isa IOError then
+                       # This should be a normal network error like being offline.
+                       # Print to log, but don't show to the user.
+                       print_error error.class_name
+               else
+                       # This could be a deserialization error,
+                       # it may be related to an outdated client.
+                       # Report to user.
+                       print_error "Request Error: {rest_server_uri / rest_action} with {error}"
+                       app.feedback "Request Error: {error}"
+               end
+       end
+
+       # Intercept known server side errors
+       fun intercept_error(res: nullable Object): Bool
+       do
+               if res isa BenitluxTokenError then
+                       app.token = "none"
+                       app.user = null
+                       return true
+               else if res isa BenitluxError then
+                       app.feedback((res.user_message or else res.message).t)
+                       return true
+               else if res isa Error then
+                       app.feedback res.message.t
+                       return true
+               end
+               return false
+       end
+end
+
+# Async request with services to act on the windows of the app
+class WindowHttpRequest
+       super BenitluxHttpRequest
+
+       autoinit window, rest_action
+
+       # Type of the related `window`
+       type W: Window
+
+       # `Window` on which to apply the results of this request
+       var window: W
+
+       # `Views` to disable while this request is in progress
+       var affected_views = new Array[View]
+
+       redef fun before do for view in affected_views do view.enabled = false
+
+       redef fun after do for view in affected_views do view.enabled = true
+end
+
+redef class Text
+       # Ellipsize `self` so it fits within `max_length` characters
+       #
+       # FIXME Remove this when labels are correctly ellipsized on iOS.
+       fun ellipsize: Text do return self
+end
diff --git a/contrib/benitlux/src/client/client.nit b/contrib/benitlux/src/client/client.nit
new file mode 100644 (file)
index 0000000..e2fc2a9
--- /dev/null
@@ -0,0 +1,44 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Portable Benitlux app
+module client is
+       app_name "Benitlux"
+       app_version(0, 3, git_revision)
+       app_namespace "net.xymus.benitlux"
+end
+
+import home_views
+import beer_views
+import social_views
+import user_views
+
+# ---
+# Services
+
+redef class Deserializer
+       redef fun deserialize_class(name)
+       do
+               if name == "Array[Beer]" then return new Array[Beer].from_deserializer(self)
+               if name == "Array[User]" then return new Array[User].from_deserializer(self)
+               if name == "Array[BeerBadge]" then return new Array[BeerBadge].from_deserializer(self)
+               if name == "Array[BeerAndRatings]" then return new Array[BeerAndRatings].from_deserializer(self)
+               if name == "Array[String]" then return new Array[String].from_deserializer(self)
+               if name == "Array[UserAndFollowing]" then return new Array[UserAndFollowing].from_deserializer(self)
+               return super
+       end
+end
+
+set_fr
+super
diff --git a/contrib/benitlux/src/client/features/checkins.nit b/contrib/benitlux/src/client/features/checkins.nit
new file mode 100644 (file)
index 0000000..3be087e
--- /dev/null
@@ -0,0 +1,179 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# On location checkin services
+module checkins
+
+import client
+
+redef class App
+
+       # Should we share our checkins with the server and friends?
+       fun share_checkins: Bool
+       do return app.data_store["share_checkins"].as(nullable Bool) or else true
+
+       # Should we share our checkins with the server and friends?
+       fun share_checkins=(value: Bool)
+       do
+               # Notify server
+               if currently_on_location then
+                       if value then
+                               server_check_in
+                       else server_check_out
+               end
+
+               app.data_store["share_checkins"] = value
+       end
+
+       # Are we currently at the location?
+       fun currently_on_location: Bool
+       do return app.data_store["currently_on_location"].as(nullable Bool) or else false
+
+       # Are we currently at the location?
+       fun currently_on_location=(value: Bool) do app.data_store["currently_on_location"] = value
+
+       # Request beer menu from the server
+       #
+       # It includes a diff if `checkins` remembers a previous visit.
+       fun request_menu
+       do
+               var checkins = checkins
+               var since = checkins.latest
+               if since != null then
+                       var today = today
+                       if since == today then
+                               since = checkins.previous
+                       end
+               end
+
+               (new MenuHttpRequest("rest/since?token={token}&date={since or else ""}")).start
+       end
+
+       # User checks in
+       fun on_check_in
+       do
+               if currently_on_location then return
+
+               if share_checkins then server_check_in
+
+               currently_on_location = true
+               request_menu
+               checkins.update today
+       end
+
+       # User checks out
+       fun on_check_out
+       do
+               if not currently_on_location then return
+
+               if share_checkins then server_check_out
+               currently_on_location = false
+       end
+
+       # Notify server of checkin
+       private fun server_check_in do (new BenitluxHttpRequest("rest/checkin?token={app.token}&is_in=true")).start
+
+       # Notify server of checkout
+       private fun server_check_out do (new BenitluxHttpRequest("rest/checkin?token={app.token}&is_in=false")).start
+
+       # History of the last 1 or 2 checkins
+       var checkins = new SimpleMemory
+
+       redef fun on_save_state
+       do
+               super
+               app.data_store["checkins"] = checkins
+       end
+
+       redef fun on_restore_state
+       do
+               var checkins = app.data_store["checkins"]
+               if checkins isa SimpleMemory then self.checkins = checkins
+
+               super
+       end
+end
+
+# Request the menu from the server for a notification
+class MenuHttpRequest
+       super BenitluxHttpRequest
+
+       redef fun on_load(data)
+       do
+               if not data isa Array[BeerAndRatings] then
+                       on_fail new Error("Server sent unexpected data {data or else "null"}")
+                       return
+               end
+
+               var content = data.beers_to_notification
+
+               notify("Passing by the Benelux?".t, content, 2)
+       end
+end
+
+# ---
+# Support services
+
+# Memory of an element and the previous one, avoiding duplication
+#
+# Used to remember the last day at the location,
+# ignoring multiple reports on the same day.
+class SimpleMemory
+       serialize
+
+       # Before latest remembered entry
+       var previous: nullable String = null
+
+       # Last remembered entry
+       var latest: nullable String = null
+
+       # Update `latest` if `value` is different
+       fun update(value: String)
+       do
+               if value == latest then return
+
+               previous = latest
+               latest = value
+       end
+end
+
+# ---
+# UI
+
+redef class UserWindow
+
+       private var lbl_checkins_options_title = new Label(parent=layout,
+               text="Share options".t)
+
+       private var chk_share_checkins = new CheckBox(parent=layout,
+               text="Share checkins with your friends".t)
+
+       init
+       do
+               chk_share_checkins.is_checked = app.share_checkins
+               lbl_checkins_options_title.size = 1.5
+       end
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ToggleEvent then
+                       var sender = event.sender
+                       if sender == chk_share_checkins then
+                               app.share_checkins = sender.is_checked
+                       end
+               end
+       end
+end
diff --git a/contrib/benitlux/src/client/features/debug.nit b/contrib/benitlux/src/client/features/debug.nit
new file mode 100644 (file)
index 0000000..ae7be3c
--- /dev/null
@@ -0,0 +1,67 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Debugging features accessible from the user preference menu
+module debug
+
+import client
+import push
+import checkins
+
+redef class UserWindow
+
+       private var layout_debug = new VerticalLayout(parent=layout)
+
+       private var lbl_debug_title = new Label(parent=layout_debug,
+               text="Debug options".t)
+
+       private var but_test_notif = new Button(parent=layout_debug,
+               text="Test notifications".t)
+
+       private var but_test_checkin = new Button(parent=layout_debug,
+               text="Test checkin".t)
+
+       private var but_test_checkout = new Button(parent=layout_debug,
+               text="Test checkout".t)
+
+       private var but_test_menu = new Button(parent=layout_debug,
+               text="Test menu diff".t)
+
+       init
+       do
+               lbl_debug_title.size = 1.5
+
+               for c in [but_test_notif, but_test_checkin, but_test_checkout, but_test_menu] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_test_notif then
+                               notify("Test Notification", "Some content\nmultiline", 5)
+                       else if sender == but_test_checkin then
+                               app.on_check_in
+                       else if sender == but_test_checkout then
+                               app.on_check_out
+                       else if sender == but_test_menu then
+                               app.request_menu
+                       end
+               end
+       end
+end
diff --git a/contrib/benitlux/src/client/features/push.nit b/contrib/benitlux/src/client/features/push.nit
new file mode 100644 (file)
index 0000000..3006616
--- /dev/null
@@ -0,0 +1,222 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Push notification support
+module push
+
+import app::http_request
+
+import client
+
+redef class App
+       redef fun on_log_in
+       do
+               super
+               #(new PushHttpRequest("push/check_token?token={app.token}")).start
+       end
+
+       # Names of the known users currently on location
+       var users_on_location = new Set[String]
+
+       # Should we show a daily notification when new beers are available?
+       fun notify_on_new_beers: Bool
+       do return app.data_store["notify_on_new_beers"].as(nullable Bool) or else true
+
+       # Should we show a daily notification when new beers are available?
+       fun notify_on_new_beers=(value: Bool) do app.data_store["notify_on_new_beers"] = value
+
+       # Should we show a daily notification of the menu?
+       fun notify_menu_daily: Bool
+       do return app.data_store["notify_menu_daily"].as(nullable Bool) or else false
+
+       # Should we show a daily notification of the menu?
+       fun notify_menu_daily=(value: Bool) do app.data_store["notify_menu_daily"] = value
+
+       # Should we show a notification when friends check in at the location?
+       fun notify_on_checkins: Bool
+       do return app.data_store["notify_on_checkins"].as(nullable Bool) or else true
+
+       # Should we show a notification when friends check in at the location?
+       fun notify_on_checkins=(value: Bool) do app.data_store["notify_on_checkins"] = value
+end
+
+# Open push notification request
+class PushHttpRequest
+       super BenitluxHttpRequest
+
+       redef fun on_fail(error)
+       do
+               if app.user == null then return
+
+               super
+
+               print_error "{class_name}: on_fail {error}"
+
+               var t = new PushHttpRequest("push/?token={app.token}")
+               t.delay = 10.0
+               t.start
+       end
+
+       redef fun on_load(data)
+       do
+               if app.user == null then return
+
+               var delay = 0.0
+               if data isa Pushable then
+                       data.apply_push_if_desired
+               else if data isa BenitluxError then
+                       # TODO if forbidden ask for a new token
+                       delay = 5.0*60.0
+               else
+                       print_error "{class_name}: Received {data or else "null"}"
+               end
+
+               var t = new PushHttpRequest("push/?token={app.token}")
+               t.delay = delay
+               t.start
+       end
+end
+
+# ---
+# Objects sent from the server to the client
+
+# Objects sent as push notifications by the server
+interface Pushable
+       # Act on this push notification
+       fun apply_push do print_error "Unimplemented `apply_push` on {class_name}"
+
+       # Consider to `apply_push` if the user preferences wants to
+       fun apply_push_if_desired do apply_push
+end
+
+redef class CheckinReport
+       super Pushable
+
+       # Flattened array of the name of users
+       var user_names: Array[String] = [for u in users do u.name] is lazy
+
+       redef fun apply_push_if_desired
+       do
+               if not app.notify_on_checkins then return
+
+               var there_is_a_new_user = false
+               for new_users in user_names do
+                       if not app.users_on_location.has(new_users) then
+                               there_is_a_new_user = true
+                               break
+                       end
+               end
+
+               app.users_on_location.clear
+               app.users_on_location.add_all user_names
+
+               # Apply only if there is someone new on location
+               if there_is_a_new_user then super
+       end
+
+       redef fun apply_push
+       do
+               if users.is_empty then
+                       #app.notif_push.cancel
+                       #self.cancel(tag, (int)id);
+                       return
+               end
+
+               var title = "TTB!".t
+               var names = [for user in users do user.name]
+               var content = "From %0".t.format(names.join(", "))
+
+               notify(title, content, 1)
+       end
+end
+
+redef class DailyNotification
+       super Pushable
+
+       redef fun apply_push_if_desired
+       do
+               if app.notify_menu_daily then
+                       super
+                       return
+               end
+
+               if app.notify_on_new_beers then
+                       for beer in beers do
+                               if beer.is_new then
+                                       super
+                                       return
+                               end
+                       end
+               end
+       end
+
+       redef fun apply_push
+       do
+               var title = if beers.has_new_beers then
+                       "New beers are on the menu".t
+               else "Beer Menu".t
+
+               var content = beers.beers_to_notification
+               notify(title, content, 3)
+       end
+end
+
+# ---
+# UI
+
+redef class UserWindow
+
+       private var layout_push_options = new VerticalLayout(parent=layout)
+
+       private var lbl_push_options_title = new Label(parent=layout_push_options,
+               text="Notifications options".t)
+
+       private var chk_notify_on_new_beers = new CheckBox(parent=layout_push_options,
+               text="Notify when there are new beers".t)
+
+       private var chk_notify_menu_daily = new CheckBox(parent=layout_push_options,
+               #text="Show the menu every work day?".t)
+               text="Show the menu every work day".t)
+
+       private var chk_notify_on_checkins = new CheckBox(parent=layout_push_options,
+               text="Notify when a friend checks in".t)
+
+       init
+       do
+               lbl_push_options_title.size = 1.5
+               chk_notify_on_new_beers.is_checked = app.notify_on_new_beers
+               chk_notify_menu_daily.is_checked = app.notify_menu_daily
+               chk_notify_on_checkins.is_checked = app.notify_on_checkins
+
+               for c in [chk_notify_menu_daily, chk_notify_on_new_beers, chk_notify_on_checkins] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ToggleEvent then
+                       var sender = event.sender
+                       if sender == chk_notify_on_new_beers then
+                               app.notify_on_new_beers = sender.is_checked
+                       else if sender == chk_notify_menu_daily then
+                               app.notify_menu_daily = sender.is_checked
+                       else if sender == chk_notify_on_checkins then
+                               app.notify_on_checkins = sender.is_checked
+                       end
+               end
+       end
+end
diff --git a/contrib/benitlux/src/client/features/translations.nit b/contrib/benitlux/src/client/features/translations.nit
new file mode 100644 (file)
index 0000000..02deb61
--- /dev/null
@@ -0,0 +1,103 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+
+# Support for translating the app to different languages, implements French
+module translations
+
+redef class Text
+       # Translate `self` according to the current language `sys.lang`
+       fun t: String
+       do
+               var lang = sys.lang_map
+               if lang == null then return to_s
+
+               if lang.keys.has(self) then return lang[self]
+
+               print "Translation miss ({sys.lang}): {self}"
+               return to_s
+       end
+end
+
+redef class Sys
+       # Name of the language in use
+       var lang = "C"
+
+       # Translation map for the language in use
+       var lang_map: nullable Map[Text, String] = null
+end
+
+# Set French as the current language
+fun set_fr
+do
+       var map = new Map[Text, String]
+
+       # Home views
+       map["Welcome %0"] = "Bienvenue %0"
+       map["Welcome"] = "Bienvenue"
+       map["Beer Menu"] = "Menu de bières"
+       map["View all"] = "Menu complet"
+       map["Preferences"] = "Préférences"
+       map["Friends"] = "Amis"
+       map["Manage"] = "Gérer"
+       map["Events"] = "Événements"
+       map["Loading..."] = "Chargement..."
+       map["Login or signup"] = "S'authentifier"
+       map["On location?"] = "Sur place?"
+       map["Leaving?"] = "Vous quittez?"
+
+       # User/login views
+       map["Account options"] = "Options du compte"
+       map["Share options"] = "Options de partage"
+       map["Notifications options"] = "Options de notification"
+       map["Please login"] = "Veuillez vous identifier"
+       map["Welcome %0!"] = "Bienvenue %0!"
+       map["Logged in as %0"] = "Connecté en tant que %0"
+       map["Username"] = "Nom d'utilisateur"
+       map["Invalid name"] = "Nom d'utilisateur invalide"
+       map["Password"] = "Mot de passe"
+       map["Passwords must be composed of at least 6 characters."] = "Le mot de passe doit avoir au moins 6 charactères."
+       map["Email"] = "Courriel"
+       map["Login"] = "Se connecter"
+       map["Logout"] = "Se déconnecter"
+       map["Signup"] = "Créer un compte"
+
+       # Social views
+       map["Follow"] = "Suivre"
+       map["Unfollow"] = "Ne plus suivre"
+       map["Search"] = "Rechercher"
+       map["Favorites: %0"] = "Favoris: %0"
+       map["No favorites yet"] = "Pas de favoris"
+       map["List followed"] = "Personnes suivies"
+       map["List followers"] = "Personnes vous suivant"
+
+       # Beer views
+       map["Review %0"] = "Évaluer %0"
+       map["%0★ %1 reviews"] = "%0★ %1 avis"
+       map["No reviews yet"] = "Aucun avis"
+       map[", friends: %0☆ %1 reviews"] = ", amis: %0☆ %1 avis"
+       map[" (New)"] = " (Nouveau)"
+       map["Similar to %0."] = "Similaire à %0."
+       map["Favorite beer on the menu."] = "Bière préférée sur le menu."
+       map["Favorite of %0"] = "Préférée de %0"
+
+       # Preferences
+       map["Notify when a friend checks in"] = "Lorsqu'un ami est sur place"
+       map["Show the menu every work day"] = "Menu journalier en semaine"
+       map["Notify when there are new beers"] = "Lorsqu'il y a de nouvelles bières"
+       map["Share checkins with your friends"] = "Partager lorsque vous êtes sur place"
+       map["Passing by the Benelux?"] = "De passage au Bénélux?"
+
+       # Update Sys
+       sys.lang = "fr"
+       sys.lang_map = map
+end
diff --git a/contrib/benitlux/src/client/ios.nit b/contrib/benitlux/src/client/ios.nit
new file mode 100644 (file)
index 0000000..9601202
--- /dev/null
@@ -0,0 +1,147 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# iOS variant using a button to check in/out and local notifications
+module ios
+
+import ::ios
+intrude import app::ui
+
+import client
+import push
+import checkins
+
+redef class HomeWindow
+       init
+       do
+               title = "Benitlux"
+               update_checkin_text
+               checkin_button.observers.add self
+       end
+
+       # TODO hide when not logged in
+       private var layout_login_checkin = new HorizontalLayout(parent=layout_user)
+       private var checkin_label = new Label(parent=layout_login_checkin)
+       private var checkin_button = new Button(parent=layout_login_checkin)
+
+       redef fun on_event(event)
+       do
+               super
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == checkin_button then
+                               if app.currently_on_location then
+                                       app.on_check_out
+                               else app.on_check_in
+                       end
+               end
+       end
+
+       private fun update_checkin_text
+       do
+               if app.currently_on_location then
+                       checkin_label.text = "Leaving?".t
+                       checkin_button.text = "Check out".t
+               else
+                       checkin_label.text = "On location?".t
+                       checkin_button.text = "Check in".t
+               end
+       end
+end
+
+redef class App
+       redef fun on_check_in
+       do
+               super
+               var window = window
+               if window isa HomeWindow then window.update_checkin_text
+       end
+
+       redef fun on_check_out
+       do
+               super
+               var window = window
+               if window isa HomeWindow then window.update_checkin_text
+       end
+
+       redef fun did_finish_launching_with_options
+       do
+               ui_application.register_user_notification_settings
+               return super
+       end
+end
+
+redef class UserWindow
+       init do title = "Preferences".t
+end
+
+redef class BeersWindow
+       init do title = "Beers".t
+end
+
+redef class SocialWindow
+       init do title = "People".t
+end
+
+# --- Notifications
+
+redef fun notify(title, content, id)
+do native_notify(title.to_nsstring, content.to_nsstring)
+
+private fun native_notify(title, content: NSString) in "ObjC" `{
+       UILocalNotification* notif = [[UILocalNotification alloc] init];
+       notif.alertTitle = title;
+       notif.alertBody = content;
+       notif.timeZone = [NSTimeZone defaultTimeZone];
+       [[UIApplication sharedApplication] presentLocalNotificationNow: notif];
+`}
+
+redef class UIApplication
+
+       # Register this app to display notifications
+       private fun register_user_notification_settings
+       in "ObjC" `{
+               if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]){
+                       [self registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
+               }
+       `}
+end
+
+# ---
+# Shorten labels
+
+redef class Label
+       # Ellipsize `text` so it fits within `max_length` characters
+       #
+       # FIXME Remove this when labels are correctly ellipsized on iOS.
+       redef fun text=(text)
+       do
+               if text == null then
+                       super
+                       return
+               end
+
+               var max_length = 50
+               if parent isa HorizontalLayout and parent.parent isa BeerView then
+                       # This is the name of a beer, remember its a hack
+                       max_length = 20
+               end
+
+               if text.length > max_length then
+                       text = text.substring(0, max_length - 3).to_s + "..."
+               end
+               super text
+       end
+end
diff --git a/contrib/benitlux/src/client/views/beer_views.nit b/contrib/benitlux/src/client/views/beer_views.nit
new file mode 100644 (file)
index 0000000..54ad9e9
--- /dev/null
@@ -0,0 +1,329 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Window to list beers and other beer-related views
+module beer_views
+
+import base
+
+# View about a beer, with its name, description and rating
+class BeerView
+       super VerticalLayout
+       super ItemView
+
+       autoinit beer_info, parent
+
+       # Beer information
+       var beer_info: BeerAndRatings
+
+       # Buttons to realize the rating buttons
+       var star_buttons = new Array[StarButton]
+
+       # Layout of the first line with the name and `star_buttons`
+       var top_line_layout = new HorizontalLayout(parent=self)
+
+       init
+       do
+               var lbl_name = new Label(parent=top_line_layout, text=beer_info.beer.name)
+               lbl_name.size = 1.25
+
+               var desc = beer_info.beer.desc
+               if beer_info.is_new then desc += " (New)".t
+               var lbl_desc = new Label(parent=self, text=desc)
+
+               var lbl_stats = new Label(parent=self, text=beer_info.rating_text)
+               lbl_stats.size = 0.5
+
+               var badges = beer_info.badges
+               if badges != null then
+                       var lbl_comment = new Label(parent=self, text=badges.join(" "))
+                       lbl_comment.size = 0.5
+               end
+
+               var rating = beer_info.user_rating or else 0
+               setup_stars rating
+       end
+
+       # Prepare and display the rating controls
+       fun setup_stars(rating: Int)
+       do
+               var l_stars = new HorizontalLayout(parent=top_line_layout)
+
+               for i in [1..5] do
+                       var but = new StarButton(beer_info.beer, i, i <= rating, parent=l_stars)
+                       but.size = 1.5
+                       but.observers.add self
+                       star_buttons.add but
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               assert event isa ButtonPressEvent
+
+               var sender = event.sender
+               if sender isa StarButton then
+                       on_review sender.rating
+               end
+       end
+
+       # Post a user review
+       fun on_review(rating: Int)
+       do
+               var beer_id = beer_info.beer.id
+               (new ReviewAction(app.window, "rest/review?token={app.token}&beer={beer_id}&rating={rating}")).start
+
+               # Update UI
+               var i = 1
+               for but in star_buttons do
+                       but.on = i <= rating
+                       i += 1
+               end
+       end
+end
+
+# Beers pane listing the available beers
+class BeersWindow
+       super Window
+
+       private var layout = new VerticalLayout(parent=self)
+       private var list_beers = new ListLayout(parent=layout)
+
+       init
+       do
+               if debug then print "BenitluxWindow::init"
+
+               action_list_beers
+       end
+
+       # Send HTTP request to list beers
+       fun action_list_beers
+       do (new ListBeersAction(self, "rest/list?token={app.token}")).start
+end
+
+# ---
+# Customized buttons
+
+# View to describe and rate a eer
+class RatingView
+       super View
+
+       autoinit beer, init_rating, parent, enabled
+
+       # Beer id
+       var beer: Beer
+
+       # Previous rating, 0 for none
+       var init_rating: Int
+
+       redef fun parent=(layout) do end
+
+       redef fun enabled=(value) do end
+end
+
+# Button with a star, filled or not, for rating beers
+class StarButton
+       super Button
+
+       autoinit beer, rating, on, parent, enabled
+
+       # Info on the beer to rate
+       var beer: Beer
+
+       # Rating of `beer`
+       var rating: Int
+
+       # Set if the star is filled
+       fun on=(on: Bool) is autoinit do text = if on then "★" else "☆"
+end
+
+redef class BeerAndRatings
+       # Text version of the ratings
+       fun rating_text: String
+       do
+               var txt = new Array[String]
+
+               var global = global
+               if global != null and global.count > 0 then
+                       txt.add "%0★ %1 reviews".t.format(global.average.to_precision(1), global.count)
+               else txt.add "No reviews yet".t
+
+               var local = followed
+               if local != null and local.count > 0 then
+                       txt.add ", friends: %0☆ %1 reviews".t.format(local.average.to_precision(1), local.count)
+               end
+
+               return txt.join
+       end
+end
+
+redef class Beer
+       # Capitalize first letter for a prettier display
+       redef fun desc
+       do
+               var desc = super
+               if desc.length == 0 then return desc
+
+               var first_letter = desc.first.to_upper
+               return first_letter.to_s + desc.substring_from(1)
+       end
+end
+
+# Comparator of beers
+class BeerComparator
+       super Comparator
+
+       redef type COMPARED: BeerAndRatings
+
+       redef fun compare(a, b) do return value_of(a) <=> value_of(b)
+
+       private fun value_of(beer: COMPARED): Float
+       do
+               var max = 0.0
+               var value = 0.0
+
+               var rating = beer.user_rating
+               if rating != null then
+                       max += 20.0
+                       value += rating.to_f * 4.0
+               end
+
+               var followed = beer.followed
+               if followed != null then
+                       max += 10.0
+                       value += followed.average * 2.0
+               end
+
+               var global = beer.global
+               if global != null then
+                       max += 5.0
+                       value += global.average
+               end
+
+               return (max - value)/max
+       end
+end
+
+# Async request to submit a review
+class ReviewAction
+       super WindowHttpRequest
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+       end
+end
+
+# Async request to update the beer list
+class ListBeersAction
+       super WindowHttpRequest
+
+       redef type W: BeersWindow
+
+       redef fun on_load(beers)
+       do
+               window.layout.remove window.list_beers
+               window.list_beers = new ListLayout(parent=window.layout)
+
+               if intercept_error(beers) then return
+
+               if not beers isa Array[BeerAndRatings] then
+                       app.feedback "Communication Error".t
+                       return
+               end
+
+               # Sort beers per preference
+               var comparator = new BeerComparator
+               comparator.sort beers
+
+               # Populate the list
+               for beer_and_rating in beers do
+                       var view = new BeerView(beer_and_rating, parent=window.list_beers)
+               end
+       end
+end
+
+redef class BestBeerBadge
+       redef fun to_s do return "Favorite beer on the menu.".t
+end
+
+redef class FavoriteBeerBadge
+       redef fun to_s do return "Favorite of %0.".t.format(users.join(", ", " & "))
+end
+
+redef class SimilarBeerBadge
+       redef fun to_s do return "Similar to %0.".t.format(beers.join(", ", " & "))
+end
+
+redef class Array[E]
+       # Pretty compressed list of this list of beer as a pseudo diff
+       #
+       # Require: `self isa Array[BeerAndRatings]`
+       fun beers_to_notification: String
+       do
+               assert self isa Array[BeerAndRatings]
+
+               # Sort beers per preference
+               var comparator = new BeerComparator
+               comparator.sort self
+
+               # Organize the notification line per line
+               # First the new beers, then the fixed one.
+               var lines = new Array[String]
+               var fix_beers = new Array[String]
+               for bar in self do
+                       var beer = bar.beer
+                       if bar.is_new then
+                               lines.add "+ {beer.name}: {beer.desc}"
+                       else fix_beers.add beer.name
+               end
+
+               # Show a few fixed beers per line
+               if fix_beers.not_empty then
+                       var line = new FlatBuffer
+                       line.append "= "
+                       for i in fix_beers.length.times, beer in fix_beers do
+
+                               if i > 0 then line.append ", "
+
+                               var l = line.length + beer.length
+                               if l < 42 then # Very approximate width of a notification on Android
+                                       line.append beer
+                                       continue
+                               end
+
+                               lines.add line.to_s
+
+                               line = new FlatBuffer
+                               line.append "= "
+                               line.append beer
+                       end
+
+                       lines.add line.to_s
+               end
+
+               return lines.join("\n")
+       end
+
+       # Does `self` has a new beer?
+       #
+       # Require: `self isa Array[BeerAndRatings]`
+       fun has_new_beers: Bool
+       do
+               assert self isa Array[BeerAndRatings]
+
+               for beer in self do if beer.is_new then return true
+               return false
+       end
+end
diff --git a/contrib/benitlux/src/client/views/home_views.nit b/contrib/benitlux/src/client/views/home_views.nit
new file mode 100644 (file)
index 0000000..c991f3c
--- /dev/null
@@ -0,0 +1,211 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Main home window
+module home_views
+
+import beer_views
+import social_views
+import user_views
+
+redef class App
+       redef fun on_create
+       do
+               if debug then print "App::on_create"
+
+               # Create the main window
+               show_home
+               super
+       end
+
+       # Show the home/main windows
+       fun show_home
+       do
+               var window = new HomeWindow
+               window.refresh
+               push_window window
+       end
+
+       redef fun on_log_in
+       do
+               super
+
+               # Send back to the home window when logging in
+               if not window isa HomeWindow then pop_window
+       end
+end
+
+# Social pane with networking features
+class HomeWindow
+       super Window
+
+       private var layout = new ListLayout(parent=self)
+
+       # Cut-point for the iOS adaptation
+       var layout_user = new VerticalLayout(parent=layout)
+       private var layout_login = new HorizontalLayout(parent=layout_user)
+       private var lbl_login_status = new Label(parent=layout_login, text="Welcome".t, size=1.5)
+       private var but_login = new Button(parent=layout_login, text="Login or signup".t)
+       private var but_preferences = new Button(parent=layout_login, text="Preferences".t)
+
+       private var layout_beers = new VerticalLayout(parent=layout)
+       var layout_beers_title = new HorizontalLayout(parent=layout_beers)
+       var title_beers = new SectionTitle(parent=layout_beers_title, text="Beer Menu".t, size=1.5)
+       private var beer_button = new Button(parent=layout_beers_title, text="View all".t)
+       private var beer_list = new VerticalLayout(parent=layout_beers)
+       private var beer_temp_lbl = new Label(parent=beer_list, text="Loading...".t)
+
+       private var layout_social = new VerticalLayout(parent=layout)
+       private var social_header = new HorizontalLayout(parent=layout_social)
+       private var social_title = new SectionTitle(parent=social_header, text="Friends".t, size=1.5)
+       private var social_button = new Button(parent=social_header, text="Manage".t)
+       private var social_list = new VerticalLayout(parent=layout_social)
+       private var social_temp_lbl = new Label(parent=social_list, text="Loading...".t)
+
+       private var layout_news = new VerticalLayout(parent=layout)
+       private var news_header = new HorizontalLayout(parent=layout_news)
+       private var news_title = new SectionTitle(parent=news_header, text="Events".t, size=1.5)
+       #private var news_button = new Button(parent=news_header, text="Open website") # TODO
+       private var news_label = new Label(parent=layout_news, text="Bière en cask le jeudi!")
+
+       init
+       do
+               for c in [but_login, but_preferences, beer_button, social_button] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_resume do refresh
+
+       # Refresh content of this page
+       fun refresh
+       do
+               if not app.restored then return
+
+               layout_login.clear
+               if app.user != null then
+                       # Logged in
+                       lbl_login_status.parent = layout_login
+                       but_preferences.parent = layout_login
+                       lbl_login_status.set_welcome
+               else
+                       but_login.parent = layout_login
+                       but_preferences.parent = layout_login
+               end
+
+               # Fill beers
+               (new ListDiffAction(self, "rest/since?token={app.token}")).start
+
+               # Fill people
+               (new HomeListPeopleAction(self, "rest/friends?token={app.token}")).start
+
+               # Check if token is still valid
+               (new CheckTokenAction(self, "rest/check_token?token={app.token}")).start
+       end
+
+       redef fun on_event(event)
+       do
+               if debug then print "BenitluxWindow::on_event {event}"
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_preferences then
+                               app.push_window new UserWindow
+                               return
+                       else if sender == but_login then
+                               app.push_window new SignupWindow
+                               return
+                       else if sender == beer_button then
+                               app.push_window new BeersWindow
+                               return
+                       else if sender == social_button then
+                               app.push_window new SocialWindow
+                               return
+                       #else if sender == news_button then
+                               # TODO open browser?
+                       end
+               end
+
+               super
+       end
+end
+
+# `Label` used in section headers
+class SectionTitle super Label end
+
+# Async request to update the beer list on the home screen
+class ListDiffAction
+       super WindowHttpRequest
+
+       redef type W: HomeWindow
+
+       redef fun on_load(beers)
+       do
+               window.layout_beers.remove window.beer_list
+               window.beer_list = new VerticalLayout(parent=window.layout_beers)
+
+               if intercept_error(beers) then return
+
+               if not beers isa Array[BeerAndRatings] then
+                       app.feedback "Communication Error".t
+                       return
+               end
+
+               # Sort beers per preference
+               var comparator = new BeerComparator
+               comparator.sort beers
+
+               var max_beers = 6
+               while beers.length > max_beers do beers.pop
+
+               for bar in beers do
+                       var view = new BeerView(bar, parent=window.beer_list)
+               end
+       end
+end
+
+# Async request to list users
+class HomeListPeopleAction
+       super WindowHttpRequest
+
+       redef type W: HomeWindow
+
+       redef fun on_load(users)
+       do
+               window.layout_social.remove window.social_list
+               window.social_list = new VerticalLayout(parent=window.layout_social)
+
+               if intercept_error(users) then return
+
+               if users isa Array[UserAndFollowing] then for uaf in users do
+                       var view = new PeopleView(uaf, true, parent=window.social_list)
+               end
+       end
+end
+
+# Async request to check if `app.token` is still valid
+class CheckTokenAction
+       super WindowHttpRequest
+
+       redef type W: HomeWindow
+
+       redef fun on_load(res) do intercept_error(res)
+end
+
+# Today's date as a `String`
+fun today: String
+do
+       var tm = new Tm.localtime
+       return "{tm.year+1900}-{tm.mon+1}-{tm.mday}"
+end
diff --git a/contrib/benitlux/src/client/views/social_views.nit b/contrib/benitlux/src/client/views/social_views.nit
new file mode 100644 (file)
index 0000000..35cb6bd
--- /dev/null
@@ -0,0 +1,201 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Window to list beers and other beer-related views
+module social_views
+
+import base
+
+# Social pane with networking features
+class SocialWindow
+       super Window
+
+       private var layout = new VerticalLayout(parent=self)
+
+       private var list_search = new ListLayout(parent=layout)
+
+       private var layout_header = new VerticalLayout(parent=list_search)
+       private var layout_search = new HorizontalLayout(parent=layout_header)
+       private var txt_query = new TextInput(parent=layout_search)
+       private var but_search = new Button(parent=layout_search, text="Search".t)
+
+       private var layout_list = new HorizontalLayout(parent=layout_header)
+       private var but_followed = new Button(parent=layout_list, text="List followed".t)
+       private var but_followers = new Button(parent=layout_list, text="List followers".t)
+
+       init
+       do
+               for c in [but_search, but_followed, but_followers] do
+                       c.observers.add self
+               end
+
+               # Load friends and suggestions
+               (new ListUsersAction(self, "rest/friends?token={app.token}&n=16")).start
+       end
+
+       redef fun on_event(event)
+       do
+               if debug then print "BenitluxWindow::on_event {event}"
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_search then
+                               search
+                       else if sender == but_followed then
+                               var cmd = "rest/followed?token={app.token}"
+                               (new ListUsersAction(self, cmd)).start
+                       else if sender == but_followers then
+                               var cmd = "rest/followers?token={app.token}"
+                               (new ListUsersAction(self, cmd)).start
+                       end
+               end
+
+               super
+       end
+
+       # Execute search with `txt_query.text`
+       fun search
+       do
+               var query = txt_query.text
+               if query == null or query.is_empty then return
+
+               var res = "rest/search?token={app.token}&query={query}&offset=0"
+               (new ListUsersAction(self, res)).start
+       end
+
+       # Fill `list_search` with views for each of `users`
+       fun list_users(users: Array[UserAndFollowing])
+       do
+               for uaf in users do
+                       var view = new PeopleView(uaf, false, parent=list_search)
+               end
+       end
+end
+
+# View to describe, and follow a person
+class PeopleView
+       super VerticalLayout
+       super ItemView
+
+       autoinit user_and_following, home_window_mode, parent
+
+       # Description of the user
+       var user_and_following: UserAndFollowing
+
+       # Toggle tweaks for the home window where the is no "unfollow" buttons
+       var home_window_mode: Bool
+
+       init
+       do
+               var user = user_and_following.user
+
+               var layout_top_line = new HorizontalLayout(parent=self)
+               var lbl_name = new Label(parent=layout_top_line, text=user.name)
+
+               if app.user != null then
+
+                       # Show unfollow button if not on the home screen
+                       if not home_window_mode or not user_and_following.following then
+                               var but = new FollowButton(user.id, user_and_following.following, user_and_following.followed, parent=layout_top_line)
+                               but.observers.add self
+                       end
+               end
+
+               var favs = if not user_and_following.favs.is_empty then
+                       "Favorites: %0".t.format(user_and_following.favs)
+               else "No favorites yet".t
+               var lbl_desc = new Label(parent=self, text=favs, size=0.5)
+       end
+end
+
+# Button to follow or unfollow a user
+class FollowButton
+       super Button
+
+       autoinit followed_id, following, followed_by, parent, enabled, text
+
+       # Id of the user to be followd/unfollow
+       var followed_id: Int
+
+       # Does the local user already follows `followed_id`
+       var following: Bool
+
+       # Does `followed_id` already follows the local user
+       var followed_by: Bool
+
+       # Update the visible text according to `following`
+       fun update_text do text = if following then "Unfollow".t else "Follow".t
+
+       init do update_text
+
+       redef fun on_event(event)
+       do
+               assert event isa ButtonPressEvent
+               var cmd = "rest/follow?token={app.token}&user_to={followed_id}&follow={not following}"
+               enabled = false
+               text = "Updating...".t
+               (new FollowAction(app.window, cmd, self)).start
+       end
+end
+
+# Async request to receive and display a list of users
+#
+# This is used by many features of the social window:
+# search, list followed and list followers.
+class ListUsersAction
+       super WindowHttpRequest
+
+       redef type W: SocialWindow
+
+       init do affected_views.add_all([window.but_search, window.but_followed, window.but_followers])
+
+       redef fun on_load(users)
+       do
+               window.layout.remove window.list_search
+               window.list_search = new ListLayout(parent=window.layout)
+               window.layout_header.parent = window.list_search
+
+               if intercept_error(users) then return
+
+               if users isa Array[UserAndFollowing] then window.list_users users
+       end
+end
+
+# Async request to follow or unfollow a user
+class FollowAction
+       super WindowHttpRequest
+
+       private var button: FollowButton
+       init do affected_views.add(button)
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+       end
+
+       redef fun after
+       do
+               button.following = not button.following
+               button.update_text
+               button.enabled = true
+
+               super
+       end
+
+       redef fun before
+       do
+               button.enabled = false
+               super
+       end
+end
diff --git a/contrib/benitlux/src/client/views/user_views.nit b/contrib/benitlux/src/client/views/user_views.nit
new file mode 100644 (file)
index 0000000..e45fcb5
--- /dev/null
@@ -0,0 +1,215 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# User preference window and other user-related view
+module user_views
+
+import base
+
+redef class Label
+       # Update the content of `lbl_welcome`
+       fun set_user_name
+       do
+               var name = app.user
+               self.text = if name != null then
+                       "Logged in as %0".t.format(name)
+               else "Not logged in".t
+       end
+
+       # Set `text` to welcome an authentified user or invite to authentify
+       fun set_welcome
+       do
+               var name = app.user
+               self.text = if name != null then
+                       "Welcome %0".t.format(name)
+               else ""
+       end
+end
+
+# User preference window
+class UserWindow
+       super Window
+
+       # Main window layout
+       var layout = new ListLayout(parent=self)
+
+       private var layout_user_options = new VerticalLayout(parent=layout)
+
+       private var lbl_user_options_title = new Label(parent=layout_user_options,
+               text="Account options".t)
+
+       private var lbl_welcome = new Label(parent=layout_user_options)
+       private var but_logout = new Button(parent=layout_user_options, text="Logout".t)
+
+       # Refesh displayed text
+       fun refresh
+       do
+               lbl_user_options_title.size = 1.5
+               lbl_welcome.set_user_name
+               but_logout.enabled = app.user != null
+       end
+
+       init
+       do
+               but_logout.observers.add self
+               refresh
+       end
+
+       redef fun on_event(event)
+       do
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_logout then
+                               app.user = null
+                               app.token = "none"
+                               app.on_log_in
+                               refresh
+                       end
+               end
+
+               super
+       end
+end
+
+# Window for signing up a new user or logging in
+class SignupWindow
+       super Window
+
+       # Main window layout
+       var layout = new ListLayout(parent=self)
+
+       private var lbl_welcome = new Label(parent=layout, text="Welcome")
+
+       # Name
+       private var name_line = new HorizontalLayout(parent=layout)
+       private var lbl_name = new Label(parent=name_line, text="Username".t)
+       private var txt_name = new TextInput(parent=name_line, text=app.user)
+
+       # Pass
+       private var pass_line = new HorizontalLayout(parent=layout)
+       private var lbl_pass = new Label(parent=pass_line, text="Password".t)
+       private var txt_pass = new TextInput(parent=pass_line, is_password=true)
+       private var lbl_pass_desc = new Label(parent=layout,
+               text="Passwords must be composed of at least 6 characters.".t)
+
+       private var but_login = new Button(parent=layout, text="Login".t)
+
+       # Email
+       private var email_line = new HorizontalLayout(parent=layout)
+       private var lbl_email = new Label(parent=email_line, text="Email".t)
+       private var txt_email = new TextInput(parent=email_line)
+
+       private var but_signup = new Button(parent=layout, text="Signup".t)
+
+       private var lbl_feedback = new Label(parent=layout, text="")
+
+       init
+       do
+               lbl_pass_desc.size = 0.5
+
+               for c in [but_login, but_signup] do
+                       c.observers.add self
+               end
+       end
+
+       redef fun on_event(event)
+       do
+               if debug then print "BenitluxWindow::on_event {event}"
+
+               if event isa ButtonPressEvent then
+                       var sender = event.sender
+                       if sender == but_login or sender == but_signup then
+
+                               var name = txt_name.text
+                               if name == null or not name.name_is_ok then
+                                       feedback "Invalid name".t
+                                       return
+                               end
+
+                               var pass = txt_pass.text
+                               if pass == null or not pass.pass_is_ok then
+                                       feedback "Invalid password".t
+                                       return
+                               end
+
+                               if sender == but_login then
+                                       (new LoginOrSignupAction(self, "rest/login?name={name}&pass={pass.pass_hash}")).start
+                               else if sender == but_signup then
+                                       var email = txt_email.text
+                                       if email == null or email.is_empty then
+                                               feedback "Invalid email".t
+                                               return
+                                       end
+
+                                       (new LoginOrSignupAction(self, "rest/signup?name={name}&pass={pass.pass_hash}&email={email}")).start
+                               end
+                       end
+               end
+
+               super
+       end
+
+       # Show lasting feedback to the user in a label
+       fun feedback(text: String) do lbl_feedback.text = text
+end
+
+# ---
+# Async RESTful actions
+
+# Async request for login in or signing up
+class LoginOrSignupAction
+       super WindowHttpRequest
+
+       redef type W: SignupWindow
+
+       init do affected_views.add_all([window.but_login, window.but_signup])
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+
+               if not res isa LoginResult then
+                       on_fail new Error("Server sent unexpected data {res or else "null"}")
+                       return
+               end
+
+               app.token = res.token
+               app.user = res.user.name
+
+               app.on_log_in
+       end
+end
+
+# Async request for signing up
+class SignupAction
+       super WindowHttpRequest
+
+       redef type W: SignupWindow
+
+       init do affected_views.add_all([window.but_signup])
+
+       redef fun on_load(res)
+       do
+               if intercept_error(res) then return
+
+               if not res isa LoginResult then
+                       on_fail new Error("Server sent unexpected data {res or else "null"}")
+                       return
+               end
+
+               app.token = res.token
+               app.user = res.user.name
+               app.on_log_in
+       end
+end
@@ -133,6 +133,16 @@ class BenitluxRESTAction
                return new HttpResponse.ok(log)
        end
 
+       # Is `token` valid?
+       #
+       # check_token?token=a -> true | BenitluxError
+       fun check_token(token: String): HttpResponse
+       is restful do
+               var user_id = db.token_to_id(token)
+               if user_id == null then return new HttpResponse.invalid_token
+               return new HttpResponse.ok(true)
+       end
+
        # Search a user
        #
        # search?token=b&query=a&offset=0 -> Array[UserAndFollowing] | BenitluxError
@@ -322,6 +332,57 @@ redef class Sys
 end
 
 # ---
+# Administration
+
+# Path to the secret used to authenticate admin requests
+fun secret_path: String do return "benitlux.secret"
+
+# Services reserved to administrators
+class BenitluxAdminAction
+       super BenitluxAction
+       super RestfulAction
+
+       private fun server_secret: String do return secret_path.to_path.read_all
+
+       # Trigger sending daily menu to connected clients
+       #
+       # This should usually be called by an external cron program.
+       # send_daily_updates?secret=shared_secret -> true | BenitluxError
+       fun send_daily_updates(secret: nullable String): HttpResponse
+       is restful do
+               # Check secrets
+               var server_secret = server_secret
+               if server_secret.is_empty then
+                       print_error "The admin interface needs a secret at '{secret_path}'"
+                       return new HttpResponse.server_error
+               end
+
+               if server_secret != secret then
+                       return new HttpResponse.invalid_token
+               end
+
+               # Load beer menu
+               var list = db.list_beers_and_rating
+               if list == null then return new HttpResponse.server_error
+
+               var msg = new DailyNotification(list)
+
+               # Broadcast updates
+               for conn in push_connections.values.to_a do
+                       if not conn.closed then
+                               conn.respond new HttpResponse.ok(msg)
+                               conn.close
+                       end
+               end
+               push_connections.clear
+
+               return new HttpResponse.ok(true)
+       end
+
+       redef fun answer(request, turi) do return new HttpResponse.bad_request
+end
+
+# ---
 # Misc services
 
 redef class Text
@@ -348,7 +409,7 @@ redef class HttpResponse
        init ok(data: Serializable)
        do
                init 200
-               body = data.to_json_string
+               body = data.serialize_to_json
        end
 
        # Respond with a `BenitluxError` in JSON and a code 403
@@ -356,7 +417,7 @@ redef class HttpResponse
        do
                init 403
                var error = new BenitluxTokenError("Forbidden", "Invalid or outdated token.")
-               body = error.to_json_string
+               body = error.serialize_to_json
        end
 
        # Respond with a `BenitluxError` in JSON and a code 400
@@ -364,7 +425,7 @@ redef class HttpResponse
        do
                init 400
                var error = new BenitluxError("Bad Request", "Application error, or it needs to be updated.")
-               body = error.to_json_string
+               body = error.serialize_to_json
        end
 
        # Respond with a `BenitluxError` in JSON and a code 500
@@ -372,6 +433,6 @@ redef class HttpResponse
        do
                init 500
                var error = new BenitluxError("Internal Server Error", "Server error, try again later.")
-               body = error.to_json_string
+               body = error.serialize_to_json
        end
 end
similarity index 93%
rename from contrib/benitlux/src/benitlux_db.nit
rename to contrib/benitlux/src/server/benitlux_db.nit
index 81ec6bf..c899ede 100644 (file)
@@ -85,7 +85,7 @@ class BenitluxDB
                        last_weekday = "date('now', 'weekday 6', '-7 day')"
                else last_weekday = "date('now', '-1 day')"
 
-               return beer_events_since(last_weekday)
+               return beer_events_since_sql(last_weekday)
        end
 
        # Build and return a `BeerEvents` for today compared to `prev_day`
@@ -93,26 +93,35 @@ class BenitluxDB
        # Return `null` on error
        fun beer_events_since(prev_day: String): nullable BeerEvents
        do
+               prev_day = prev_day.to_sql_date_string
+               return beer_events_since_sql("date({prev_day})")
+       end
+
+       # `BeerEvents` since the SQLite formatted date command `sql_date`
+       #
+       # Return `null` on error
+       private fun beer_events_since_sql(sql_date: String): nullable BeerEvents
+       do
                var events = new BeerEvents
 
                # New
                var stmt = select("ROWID, name, desc FROM beers WHERE " +
                                  "ROWID IN (SELECT beer FROM daily WHERE day=(SELECT MAX(day) FROM daily)) AND " +
-                                 "NOT ROWID IN (SELECT beer FROM daily WHERE date(day) = date({prev_day}))")
+                                 "NOT ROWID IN (SELECT beer FROM daily WHERE date(day) = {sql_date})")
                if stmt == null then return null
                for row in stmt do events.new_beers.add row.to_beer
 
                # Gone
                stmt = select("ROWID, name, desc FROM beers WHERE " +
                              "NOT ROWID IN (SELECT beer FROM daily WHERE day=(SELECT MAX(day) FROM daily)) AND " +
-                             "ROWID IN (SELECT beer FROM daily WHERE date(day) = date({prev_day}))")
+                             "ROWID IN (SELECT beer FROM daily WHERE date(day) = {sql_date})")
                if stmt == null then return null
                for row in stmt do events.gone_beers.add row.to_beer
 
                # Fix
                stmt = select("ROWID, name, desc FROM beers WHERE " +
                              "ROWID IN (SELECT beer FROM daily WHERE day=(SELECT MAX(day) FROM daily)) AND " +
-                             "ROWID IN (SELECT beer FROM daily WHERE date(day) = date({prev_day}))")
+                             "ROWID IN (SELECT beer FROM daily WHERE date(day) = {sql_date})")
                if stmt == null then return null
                for row in stmt do events.fix_beers.add row.to_beer
 
similarity index 80%
rename from contrib/benitlux/src/benitlux_web.nit
rename to contrib/benitlux/src/server/server.nit
index d5bab54..f6d5d41 100644 (file)
@@ -15,7 +15,7 @@
 # limitations under the License.
 
 # Web server for Benitlux
-module benitlux_web
+module server
 
 import benitlux_model
 import benitlux_view
@@ -25,6 +25,9 @@ import benitlux_restful
 # Listening interface
 fun iface: String do return "localhost:8080"
 
+# Listening interface for admin commands
+fun iface_admin: String do return "localhost:8081"
+
 # Sqlite3 database
 var db_path = "benitlux_sherbrooke.db"
 var db = new BenitluxDB.open(db_path)
@@ -40,10 +43,14 @@ vh.routes.add new Route("/rest/", new BenitluxRESTAction(db))
 vh.routes.add new Route("/push/", new BenitluxPushAction(db))
 vh.routes.add new Route(null, new BenitluxSubscriptionAction(db))
 
+var vh_admin = new VirtualHost(iface_admin)
+vh_admin.routes.add new Route(null, new BenitluxAdminAction(db))
+
 var factory = new HttpFactory.and_libevent
 factory.config.virtual_hosts.add vh
+factory.config.virtual_hosts.add vh_admin
 
-print "Launching server on http://{iface}/"
+print "Launching server on http://{iface}/ and http://{iface_admin}/"
 factory.run
 
 db.close
index c0554fe..762eb1d 100644 (file)
@@ -1609,22 +1609,10 @@ redef class App
                game.save
        end
 
-       # Maximum wanted frame per second
-       var max_fps = 30
-
-       # clock used to track FPS
-       private var clock = new Clock
-
        redef fun frame_core(display)
        do
                game.step
                game.draw(display)
-               var dt = clock.lapse
-               var target_dt = 1000000000 / max_fps
-               if dt.sec == 0 and dt.nanosec < target_dt then
-                       var sleep_t = target_dt - dt.nanosec
-                       sys.nanosleep(0, sleep_t)
-               end
        end
 
        redef fun input(input_event)
index e415d7a..d3adc26 100644 (file)
@@ -94,7 +94,7 @@ class Nitiwiki
                                var entry = entries[path]
                                if not entry.is_dirty then continue
                                var name = entry.name
-                               if entry.has_source then name = entry.src_path.to_s
+                               if entry.has_source then name = entry.src_path.as(not null)
                                if entry.is_new then
                                        print " + {name}"
                                else
@@ -141,7 +141,7 @@ class Nitiwiki
        fun need_render(src, target: String): Bool do
                if force_render then return true
                if not target.file_exists then return true
-               return src.file_stat.mtime >= target.file_stat.mtime
+               return src.file_stat.as(not null).mtime >= target.file_stat.as(not null).mtime
        end
 
        # Create a new `WikiSection`.
@@ -283,7 +283,7 @@ abstract class WikiEntry
        # Returns `-1` if not `has_source`.
        fun create_time: Int do
                if not has_source then return -1
-               return src_full_path.file_stat.ctime
+               return src_full_path.as(not null).file_stat.as(not null).ctime
        end
 
        # Entry last modification time.
@@ -291,7 +291,7 @@ abstract class WikiEntry
        # Returns `-1` if not `has_source`.
        fun last_edit_time: Int do
                if not has_source then return -1
-               return src_full_path.file_stat.mtime
+               return src_full_path.as(not null).file_stat.as(not null).mtime
        end
 
        # Entry list rendering time.
@@ -299,7 +299,7 @@ abstract class WikiEntry
        # Returns `-1` if `is_new`.
        fun last_render_time: Int do
                if is_new then return -1
-               return out_full_path.file_stat.mtime
+               return out_full_path.file_stat.as(not null).mtime
        end
 
        # Entries hierarchy
@@ -400,7 +400,7 @@ abstract class WikiEntry
        # then returns the main wiki template file.
        fun template_file: String do
                if is_root then return wiki.config.template_file
-               return parent.template_file
+               return parent.as(not null).template_file
        end
 
        # Header template file for `self`.
@@ -408,7 +408,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun header_file: String do
                if is_root then return wiki.config.header_file
-               return parent.header_file
+               return parent.as(not null).header_file
        end
 
        # Footer template file for `self`.
@@ -416,7 +416,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun footer_file: String do
                if is_root then return wiki.config.footer_file
-               return parent.footer_file
+               return parent.as(not null).footer_file
        end
 
        # Menu template file for `self`.
@@ -424,7 +424,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun menu_file: String do
                if is_root then return wiki.config.menu_file
-               return parent.menu_file
+               return parent.as(not null).menu_file
        end
 
        # Display the entry `name`.
@@ -442,7 +442,7 @@ class WikiSection
 
        redef fun title do
                if has_config then
-                       var title = config.title
+                       var title = config.as(not null).title
                        if title != null then return title
                end
                return super
@@ -452,7 +452,7 @@ class WikiSection
        #
        # Hidden section are rendered but not linked in menus.
        fun is_hidden: Bool do
-               if has_config then return config.is_hidden
+               if has_config then return config.as(not null).is_hidden
                return false
        end
 
@@ -461,7 +461,7 @@ class WikiSection
                if parent == null then
                        return wiki.config.source_dir
                else
-                       return wiki.expand_path(parent.src_path, name)
+                       return wiki.expand_path(parent.as(not null).src_path, name)
                end
        end
 
@@ -486,41 +486,41 @@ class WikiSection
        # Also check custom config.
        redef fun template_file do
                if has_config then
-                       var tpl = config.template_file
+                       var tpl = config.as(not null).template_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.template_file
-               return parent.template_file
+               return parent.as(not null).template_file
        end
 
        # Also check custom config.
        redef fun header_file do
                if has_config then
-                       var tpl = config.header_file
+                       var tpl = config.as(not null).header_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.header_file
-               return parent.header_file
+               return parent.as(not null).header_file
        end
 
        # Also check custom config.
        redef fun footer_file do
                if has_config then
-                       var tpl = config.footer_file
+                       var tpl = config.as(not null).footer_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.footer_file
-               return parent.footer_file
+               return parent.as(not null).footer_file
        end
 
        # Also check custom config.
        redef fun menu_file do
                if has_config then
-                       var tpl = config.menu_file
+                       var tpl = config.as(not null).menu_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.menu_file
-               return parent.menu_file
+               return parent.as(not null).menu_file
        end
 end
 
@@ -534,7 +534,8 @@ class WikiArticle
        # Articles can only have `WikiSection` as parents.
        redef type PARENT: WikiSection
 
-       redef fun title: String do
+       redef fun title do
+               var parent = self.parent
                if name == "index" and parent != null then return parent.title
                return super
        end
@@ -551,7 +552,7 @@ class WikiArticle
                content = md
        end
 
-       redef var src_full_path: nullable String = null
+       redef var src_full_path = null
 
        redef fun src_path do
                var src_full_path = self.src_full_path
@@ -567,7 +568,7 @@ class WikiArticle
        # REQUIRE: `has_source`.
        var md: nullable String is lazy do
                if not has_source then return null
-               var file = new FileReader.open(src_full_path.to_s)
+               var file = new FileReader.open(src_full_path.as(not null))
                var md = file.read_all
                file.close
                return md
@@ -578,7 +579,7 @@ class WikiArticle
        redef fun is_dirty do
                if super then return true
                if has_source then
-                       return wiki.need_render(src_full_path.to_s, out_full_path)
+                       return wiki.need_render(src_full_path.as(not null), out_full_path)
                end
                return false
        end
@@ -779,7 +780,7 @@ class WikiConfig
        var sidebar_blocks: Array[String] is lazy do
                var res = new Array[String]
                if not has_key("wiki.sidebar.blocks") then return res
-               for val in at("wiki.sidebar.blocks").values do
+               for val in at("wiki.sidebar.blocks").as(not null).values do
                        res.add val
                end
                return res
index ab79f09..15ae4d7 100644 (file)
@@ -70,7 +70,8 @@ end
 redef class WikiSection
 
        # Output directory (where to ouput the HTML pages for this section).
-       redef fun out_path: String do
+       redef fun out_path do
+               var parent = self.parent
                if parent == null then
                        return wiki.config.out_dir
                else
@@ -104,7 +105,7 @@ redef class WikiSection
        # Copy attached files from `src_path` to `out_path`.
        private fun copy_files do
                assert has_source
-               var dir = src_full_path.to_s
+               var dir = src_full_path.as(not null).to_s
                for name in dir.files do
                        if name == wiki.config_filename then continue
                        if name.has_suffix(".md") then continue
@@ -167,7 +168,8 @@ end
 
 redef class WikiArticle
 
-       redef fun out_path: String do
+       redef fun out_path do
+               var parent = self.parent
                if parent == null then
                        return wiki.expand_path(wiki.config.out_dir, "{name}.html")
                else
index ac3e171..33dc61f 100644 (file)
@@ -31,7 +31,7 @@ redef class Nitiwiki
        # Returns `null` if no article can be found.
        fun lookup_entry_by_name(context: WikiEntry, name: String): nullable WikiEntry do
                var section: nullable WikiEntry = context.parent or else context
-               var res = section.lookup_entry_by_name(name)
+               var res = section.as(not null).lookup_entry_by_name(name)
                if res != null then return res
                while section != null do
                        if section.name == name then return section
@@ -52,7 +52,7 @@ redef class Nitiwiki
        # Returns `null` if no article can be found.
        fun lookup_entry_by_title(context: WikiEntry, title: String): nullable WikiEntry do
                var section: nullable WikiEntry = context.parent or else context
-               var res = section.lookup_entry_by_title(title)
+               var res = section.as(not null).lookup_entry_by_title(title)
                if res != null then return res
                while section != null do
                        if section.title.to_lower == title.to_lower then return section
@@ -200,6 +200,7 @@ redef class WikiArticle
        fun is_index: Bool do return name == "index"
 
        redef fun href do
+               var parent = self.parent
                if parent == null then
                        return "{name}.html"
                else
index 1ac8314..904862c 100644 (file)
@@ -57,7 +57,8 @@ abstract class NitrpgTestHelper
 
        # Gen a test db with a random name (to avoid race conditions).
        fun gen_test_db: MongoDb do
-               var db_name = "test_nitrpg_{get_time}_{1000.rand}"
+               var testid = "NIT_TESTING_ID".environ.to_i
+               var db_name = "test_nitrpg_{testid}"
                var db = load_db(db_name)
                test_dbs.add db
                return db
index 33032b3..4da71f5 100644 (file)
@@ -72,6 +72,9 @@ redef class RefundProcessor
                if json isa JsonParseError then
                        die("Wrong input file ({json.message})")
                        abort
+               else if json == null then
+                       die("Unable to parse input file as json (got null)")
+                       abort
                else if not json isa JsonObject then
                        die("Wrong input type (expected JsonObject got {json.class_name})")
                        abort
@@ -130,7 +133,7 @@ redef class RefundProcessor
                return new RefundStats.from_json(content)
        end
 
-       redef fun save_stats(stats: RefundStats) do
+       redef fun save_stats(stats) do
                write_output(stats.to_json.to_pretty_json, stats_file)
        end
 end
@@ -165,13 +168,19 @@ redef class ReclamationSheet
                proc.check_key(json, "reclamations")
                var res = new Array[Reclamation]
                var recls = json["reclamations"]
-               if not recls isa JsonArray then
+               if recls == null then
+                       proc.die("Wrong type for `number` (expected JsonArray got null)")
+                       abort
+               else if not recls isa JsonArray then
                        proc.die("Wrong type for `number` (expected JsonArray got {recls.class_name})")
                        abort
                end
                var i = 0
                for obj in recls do
-                       if not obj isa JsonObject then
+                       if obj == null then
+                               proc.die("Wrong type for `reclamations#{i}` (expected JsonObject got null)")
+                               abort
+                       else if not obj isa JsonObject then
                                proc.die("Wrong type for `reclamations#{i}` " +
                                        "(expected JsonObject got {obj.class_name})")
                                abort
@@ -197,7 +206,10 @@ redef class ReclFile
        init from_json(proc: RefundProcessor, json: JsonObject) do
                proc.check_key(json, "dossier")
                var id = json["dossier"]
-               if not id isa String then
+               if id == null then
+                       proc.die("Wrong type for `dossier` (expected String got null)")
+                       abort
+               else if not id isa String then
                        proc.die("Wrong type for `dossier` (expected String got {id.class_name})")
                        abort
                end
@@ -232,7 +244,10 @@ redef class ReclMonth
        init from_json(proc: RefundProcessor, json: JsonObject) do
                proc.check_key(json, "mois")
                var month = json["mois"]
-               if not month isa String then
+               if month == null then
+                       proc.die("Wrong type for `mois` (expected String got null)")
+                       return
+               else if not month isa String then
                        proc.die("Wrong type for `mois` (expected String got {month.class_name})")
                        return
                end
@@ -264,7 +279,10 @@ redef class ReclDate
        init from_json(proc: RefundProcessor, json: JsonObject) do
                proc.check_key(json, "date")
                var date = json["date"]
-               if not date isa String then
+               if date == null then
+                       proc.die("Wrong type for `date` (expected String got null)")
+                       abort
+               else if not date isa String then
                        proc.die("Wrong type for `date` (expected String got {date.class_name})")
                        abort
                end
@@ -302,7 +320,10 @@ redef class Reclamation
        private fun parse_care_id(proc: RefundProcessor, json: JsonObject): Int do
                proc.check_key(json, "soin")
                var id = json["soin"]
-               if not id isa Int then
+               if id == null then
+                       proc.die("Wrong type for `soin` (expected Int got null)")
+                       abort
+               else if not id isa Int then
                        proc.die("Wrong type for `soin` (expected Int got {id.class_name})")
                        abort
                end
@@ -313,7 +334,10 @@ redef class Reclamation
        private fun parse_fees(proc: RefundProcessor, json: JsonObject): Dollar do
                proc.check_key(json, "montant")
                var fees = json["montant"]
-               if not fees isa String then
+               if fees == null then
+                       proc.die("Wrong type for `fees` (expected String got null)")
+                       abort
+               else if not fees isa String then
                        proc.die("Wrong type for `fees` (expected String got {fees.class_name})")
                        abort
                end
index ba32497..301c564 100644 (file)
@@ -80,7 +80,7 @@ class PlanProblem
                        print n.message
                        exit 1
                end
-               n = n.children.first.children.first.as(not null)
+               n = n.children.first.as(not null).children.first.as(not null)
                if n isa Nplan then
                        print "Error: expected a problem, got a plan."
                        exit 1
@@ -88,7 +88,7 @@ class PlanProblem
                assert n isa Nproblem
 
                # Load all locations
-               for n2 in n.n_locations.n_list.children do
+               for n2 in n.n_locations.n_list.as(not null).children do
                        var e = new Location(locations.length, n2.n_name.text, n2.n_x.text.to_f, n2.n_y.text.to_f)
                        assert not locations.has_key(e.name)
                        locations[e.name] = e
@@ -97,7 +97,7 @@ class PlanProblem
 
                # Load all roads
                var nbr = 0
-               for n2 in n.n_roads.n_list.children do
+               for n2 in n.n_roads.n_list.as(not null).children do
                        var o = locations.get_or_null(n2.n_orig.text)
                        var d = locations.get_or_null(n2.n_dest.text)
                        assert o != null and d != null
@@ -132,7 +132,7 @@ class PlanProblem
 
                # Load the robot
                var robot = null
-               for n2 in n.n_robots.n_list.children do
+               for n2 in n.n_robots.n_list.as(not null).children do
                        var name = n2.n_name.text
                        robot = locations.get_or_null(n2.n_emplacement.text)
                        assert name == robot_name and robot != null
@@ -142,7 +142,7 @@ class PlanProblem
 
                # Load the parcels
                var parcel_locations = new Array[nullable Location]
-               for n2 in n.n_parcels.n_list.children do
+               for n2 in n.n_parcels.n_list.as(not null).children do
                        var name = n2.n_name.text
                        var e = locations.get_or_null(n2.n_emplacement.text)
                        assert e != null
@@ -155,7 +155,7 @@ class PlanProblem
                print "# {parcels.length} parcels"
 
                # Load the goal of parcels
-               for n2 in n.n_goal.n_list.children do
+               for n2 in n.n_goal.n_list.as(not null).children do
                        var parcel = parcel_by_name.get_or_null(n2.n_name.text)
                        var e = locations.get_or_null(n2.n_emplacement.text)
                        assert parcel != null and e != null
@@ -179,7 +179,7 @@ class PlanProblem
                        print n.message
                        exit 1
                end
-               n = n.children.first.children.first.as(not null)
+               n = n.children.first.as(not null).children.first.as(not null)
                if n isa Nproblem then
                        print "Error: expected a plan, got a problem."
                        exit 1
@@ -189,7 +189,7 @@ class PlanProblem
                var res = new Plan(self)
                var e = initial_state
                var cost = 0.0
-               for n2 in n.n_actions.children do
+               for n2 in n.n_actions.as(not null).children do
                        if n2 isa Naction_load then
                                var parcel = parcel_by_name.get_or_null(n2.n_parcel.text)
                                assert parcel != null
index 6ef6b15..bd60702 100644 (file)
@@ -54,8 +54,7 @@ redef class App
 
                # Save the default config to pretty Json
                var cc = new ClientConfig
-               var json = cc.to_plain_json
-               json = json.replace(",", ",\n")
+               var json = cc.serialize_to_json(plain=true, pretty=true)
                json.write_to_file config_path
 
                return cc
diff --git a/contrib/tinks/src/client/tinks_vr.nit b/contrib/tinks/src/client/tinks_vr.nit
new file mode 100644 (file)
index 0000000..0e2a300
--- /dev/null
@@ -0,0 +1,22 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# VR mode for Android with Google Cardboard
+#
+# This version is not playable and very laggy as it is not modified
+# or optimized in any way for VR.
+# This module is made available as a minimal example of a VR game.
+module tinks_vr
+
+import gamnit::vr
index 49c74b0..9472da6 100644 (file)
@@ -41,7 +41,7 @@ class TGame
                var dt = clock.lapse
                tick += 1
 
-               var turn = new TTurn(self, tick, dt.to_f, dt.millisec)
+               var turn = new TTurn(self, tick, dt, ((dt-dt.floor)*1000.0).to_i)
                return turn
        end
 
index a01d725..4d7720f 100644 (file)
@@ -1,13 +1,13 @@
 [package]
 name=tnitter
-tags=web
+tags=web,mobile
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
 browse=https://github.com/nitlang/nit/tree/master/contrib/tnitter/
 git=https://github.com/nitlang/nit.git
 git.directory=contrib/tnitter/
-homepage=http://nitlanguage.org
+homepage=http://xymus.net/tnitter/
 issues=https://github.com/nitlang/nit/issues
 tryit=http://tnitter.xymus.net/
 apk=http://nitlanguage.org/fdroid/apk/tnitter.apk
index ad41a71..60ee384 100644 (file)
@@ -266,14 +266,14 @@ class TnitterREST
                        db.close
 
                        var response = new HttpResponse(200)
-                       response.body = posts.to_json_string
+                       response.body = posts.serialize_to_json
                        return response
                end
 
                # Format not recognized
                var error = new Error("Bad Request")
                var response = new HttpResponse(400)
-               response.body = error.to_json_string
+               response.body = error.serialize_to_json
                return response
        end
 end
index cdb2945..bc47789 100644 (file)
@@ -46,7 +46,7 @@ redef class DB
                # Everyone gets the same response
                var posts = list_posts(0, 16)
                var response = new HttpResponse(400)
-               response.body = posts.to_json_string
+               response.body = posts.serialize_to_json
 
                for conn in push_connections do
                        # Complete the answer to `conn`
index fccad93..7a47218 100644 (file)
@@ -42,7 +42,7 @@ redef class App
        redef fun on_create
        do
                # Create the main window
-               window = new TnitterWindow
+               push_window new TnitterWindow
                super
        end
 end
index 74eb1b6..dc24681 100644 (file)
@@ -19,8 +19,7 @@ end
 
 import tnitter_app
 
-import android::ui
-import android::http_request
+import android
 import android::portrait
 
 redef class LabelAuthor
diff --git a/contrib/xymus_net/.gitignore b/contrib/xymus_net/.gitignore
new file mode 100644 (file)
index 0000000..b2285d4
--- /dev/null
@@ -0,0 +1 @@
+xymus.net
diff --git a/contrib/xymus_net/Makefile b/contrib/xymus_net/Makefile
new file mode 100644 (file)
index 0000000..eff98e3
--- /dev/null
@@ -0,0 +1,9 @@
+all: xymus.net
+
+xymus.net: ../benitlux/src/server/benitlux_restful.nit $(shell ../../bin/nitls -M xymus_net.nit)
+       ../../bin/nitc -o $@ xymus_net.nit
+
+../benitlux/src/server/benitlux_restful.nit:
+       make -C ../benitlux src/server/benitlux_restful.nit
+
+pre-build: ../benitlux/src/server/benitlux_restful.nit
diff --git a/contrib/xymus_net/README.md b/contrib/xymus_net/README.md
new file mode 100644 (file)
index 0000000..5bb2c31
--- /dev/null
@@ -0,0 +1,5 @@
+Web server source and config of xymus.net
+
+This module acts also as an example to merge multiple `nitcorn` projects into one server.
+
+See the server online at http://xymus.net/.
diff --git a/contrib/xymus_net/package.ini b/contrib/xymus_net/package.ini
new file mode 100644 (file)
index 0000000..249a39a
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=xymus_net
+tags=web,example
+maintainer=Alexis Laferrière <alexis.laf@xymus.net>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/contrib/xymus.net/
+git=https://github.com/nitlang/nit.git
+git.directory=contrib/xymus.net/
+homepage=http://xymus.net/
+issues=https://github.com/nitlang/nit/issues
similarity index 96%
rename from lib/nitcorn/examples/src/xymus_net.nit
rename to contrib/xymus_net/xymus_net.nit
index 1233db4..726cb6a 100644 (file)
@@ -1,6 +1,6 @@
 # This file is part of NIT ( http://www.nitlanguage.org ).
 #
-# Copyright 2014-2015 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2014-2016 Alexis Laferrière <alexis.laf@xymus.net>
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import privileges
 # Use actions defined by contribs
 import tnitter
 import benitlux::benitlux_controller
+import benitlux::benitlux_restful
 import opportunity::opportunity_controller
 import nitiwiki::wiki_edit
 
@@ -173,6 +174,7 @@ var vps_vh = new VirtualHost("vps.xymus.net:80")
 var tnitter_vh = new VirtualHost("tnitter.xymus.net:80")
 var pep8_vh = new VirtualHost("pep8.xymus.net:80")
 var benitlux_vh = new VirtualHost("benitlux.xymus.net:80")
+var benitlux_admin_vh = new VirtualHost("localhost:8081")
 
 var factory = new HttpFactory.and_libevent
 factory.config.virtual_hosts.add default_vh
@@ -180,6 +182,7 @@ factory.config.virtual_hosts.add vps_vh
 factory.config.virtual_hosts.add tnitter_vh
 factory.config.virtual_hosts.add pep8_vh
 factory.config.virtual_hosts.add benitlux_vh
+factory.config.virtual_hosts.add benitlux_admin_vh
 
 # Ports are open, drop to a low-privileged user if we are root
 var user_group = new UserGroup("nitcorn", "nitcorn")
@@ -228,6 +231,8 @@ benitlux_vh.routes.add new Route("/push/", benitlux_push)
 benitlux_vh.routes.add new Route("/static/", shared_file_server)
 benitlux_vh.routes.add new Route(null, benitlux_sub)
 
+benitlux_admin_vh.routes.add new Route(null, new BenitluxAdminAction(benitlux_db))
+
 # Opportunity service
 var opportunity = new OpportunityWelcome
 var opportunity_rest = new OpportunityRESTAction
index c97809c..8265899 100644 (file)
@@ -7,18 +7,22 @@ bin/calculator: $(shell ${NITLS} -M src/calculator.nit linux) ${NITC}
        mkdir -p bin
        ${NITC} -o $@ src/calculator.nit -m linux
 
+bin/scientific_calculator: $(shell ${NITLS} -M src/scientific_calculator.nit linux) ${NITC}
+       mkdir -p bin
+       ${NITC} -o $@ src/scientific_calculator.nit -m linux
+
 # ---
 # Android
 
 android: bin/calculator.apk
 
-bin/calculator.apk: $(shell ${NITLS} -M src/calculator.nit android) ${NITC} android/res/
+bin/calculator.apk: $(shell ${NITLS} -M src/scientific_calculator.nit src/android_calculator.nit) ${NITC} android/res/
        mkdir -p bin
-       ${NITC} -o $@ src/calculator.nit -m ../../lib/android/ui/ -D debug
+       ${NITC} -o $@ src/scientific_calculator.nit -m src/android_calculator.nit -D debug
 
-android-release: $(shell ${NITLS} -M src/calculator.nit android) ${NITC} android/res/
+android-release: $(shell ${NITLS} -M src/scientific_calculator.nit src/android_calculator.nit) ${NITC} android/res/
        mkdir -p bin
-       ${NITC} -o bin/calculator.apk src/calculator.nit -m ../../lib/android/ui/ --release
+       ${NITC} -o bin/calculator.apk src/scientific_calculator.nit -m src/android_calculator.nit --release
 
 android/res/: art/icon.svg ../../contrib/inkscape_tools/bin/svg_to_icons
        mkdir -p android/res
@@ -33,9 +37,9 @@ android-install: bin/calculator.apk
 # ---
 # iOS
 
-bin/calculator.app: $(shell ${NITLS} -M src/calculator.nit ios) ${NITC} ios/AppIcon.appiconset/Contents.json
+bin/calculator.app: $(shell ${NITLS} -M src/scientific_calculator.nit src/ios_calculator.nit) ${NITC} ios/AppIcon.appiconset/Contents.json
        mkdir -p bin
-       ${NITC} -o $@ src/calculator.nit -m ios -D debug
+       ${NITC} -o $@ src/scientific_calculator.nit -m src/ios_calculator.nit -D debug
 
 ios/AppIcon.appiconset/Contents.json: art/icon-ios.svg
        mkdir -p ios
index 0073c26..aa93fed 100644 (file)
@@ -5,6 +5,9 @@ Portable calculator built using _app.nit_
 * `calculator_logic` defines `CalculatorContext` with all the business logic of a calculator.
   It takes as input operations and numbers, and outputs the text to display.
 * `calculator` implements the portable graphical interface using the _app.nit_ framework
+* `scientific_calculator` refines `calculator` to add scientific operations.
+* `android_calculator` refines `calculator` to get a nicer aesthetic on Android.
+* `ios_calculator` refines `calculator` to get a nicer aesthetic on iOS.
 * `calculator_test` test `CalculatorContext` as a black box.
 
 # Compilation
@@ -29,3 +32,11 @@ Portable calculator built using _app.nit_
        make bin/android.app
        ios-sim launch bin/calculator.app
        ~~~
+
+# Screenshots
+
+![Scientific calculator on Linux with GTK+](doc/linux-scientific.png)
+
+![Scientific calculator on Android](doc/android-scientific.png)
+
+![Scientific calculator on iOS](doc/ios-scientific.png)
diff --git a/examples/calculator/doc/android-scientific.png b/examples/calculator/doc/android-scientific.png
new file mode 100644 (file)
index 0000000..f23d675
Binary files /dev/null and b/examples/calculator/doc/android-scientific.png differ
diff --git a/examples/calculator/doc/ios-scientific.png b/examples/calculator/doc/ios-scientific.png
new file mode 100644 (file)
index 0000000..8505c72
Binary files /dev/null and b/examples/calculator/doc/ios-scientific.png differ
diff --git a/examples/calculator/doc/linux-scientific.png b/examples/calculator/doc/linux-scientific.png
new file mode 100644 (file)
index 0000000..58b653e
Binary files /dev/null and b/examples/calculator/doc/linux-scientific.png differ
index 80b32cb..8237327 100644 (file)
@@ -1,6 +1,6 @@
 [package]
 name=calculator
-tags=example
+tags=example,mobile
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
diff --git a/examples/calculator/src/android_calculator.nit b/examples/calculator/src/android_calculator.nit
new file mode 100644 (file)
index 0000000..1d51073
--- /dev/null
@@ -0,0 +1,36 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Aesthetic adaptations for Android
+module android_calculator
+
+import calculator
+import android
+
+redef class Button
+       init do set_android_style(native, (text or else "?").is_int)
+
+       private fun set_android_style(java_button: NativeButton, is_number: Bool)
+       in "Java" `{
+               // Flatten the background and use a different color for digit buttons
+               int color = is_number? android.graphics.Color.DKGRAY: android.graphics.Color.TRANSPARENT;
+               java_button.setBackgroundColor(color);
+
+               // Center the label on both horizontal and vertical axes
+               java_button.setGravity(android.view.Gravity.CENTER);
+
+               // Set lowercase text to correctly display constants like e and π
+               java_button.setAllCaps(false);
+       `}
+end
index 76c3a0a..c9db316 100644 (file)
@@ -38,7 +38,7 @@ redef class App
                if debug then print "App::on_create"
 
                # Create the main window
-               window = new CalculatorWindow
+               push_window new CalculatorWindow
                super
        end
 end
@@ -51,13 +51,13 @@ class CalculatorWindow
        private var context = new CalculatorContext
 
        # Main window layout
-       private var layout = new VerticalLayout(parent=self)
+       var layout = new VerticalLayout(parent=self)
 
        # Main display, at the top of the screen
        private var display = new TextInput(parent=layout)
 
        # Maps operators as `String` to their `Button`
-       private var buttons = new HashMap[String, Button]
+       var buttons = new HashMap[String, Button]
 
        init
        do
@@ -66,8 +66,8 @@ class CalculatorWindow
                # All the button labels, row by row
                var rows = [["7", "8", "9", "+"],
                            ["4", "5", "6", "-"],
-                           ["1", "2", "3", "*"],
-                           ["0", ".", "C", "/"],
+                           ["1", "2", "3", "×"],
+                           ["0", ".", "C", "÷"],
                            ["="]]
 
                for row in rows do
@@ -94,9 +94,9 @@ class CalculatorWindow
                        else if op.is_numeric then
                                var n = op.to_i
                                context.push_digit n
-                       else
+                       else if op != null then
                                buttons["."].enabled = true
-                               context.push_op op.chars.first
+                               context.push_op op
                        end
 
                        display.text = context.display_text
index e5e492b..840ed84 100644 (file)
@@ -24,33 +24,35 @@ class CalculatorContext
        # Result of the last operation
        var result: nullable Numeric = null
 
-       # Last operation pushed with `push_op`, to be executed on the next push
-       var last_op: nullable Char = null
+       # Last operation pushed with `push_op`
+       var last_op: nullable Text = null
+
+       # Is `last_op` an unary operation or a '='?
+       var last_op_was_unary = false
 
        # Value currently being entered
-       var current: nullable FlatBuffer = null
+       var current: nullable String = null
 
        # Text to display on screen
        fun display_text: String
        do
-               var result = result
-               var last_op = last_op
-               var current = current
-
                var buf = new FlatBuffer
 
-               if result != null and (current == null or last_op != '=') then
-                       if last_op == '=' then buf.append "= "
+               var last_op = last_op
+               var result = result
+               if result != null then
+                       if last_op_was_unary then buf.append "{last_op or else "?"} "
 
                        buf.append result.to_s
                        buf.add ' '
                end
 
-               if last_op != null and last_op != '=' then
-                       buf.add last_op
+               if last_op != null and not last_op_was_unary then
+                       buf.append last_op
                        buf.add ' '
                end
 
+               var current = current
                if current != null then
                        buf.append current.to_s
                        buf.add ' '
@@ -60,40 +62,95 @@ class CalculatorContext
        end
 
        # Push operation `op`, will usually execute the last operation
-       fun push_op(op: Char)
+       fun push_op(op: Text)
        do
+               # Constants
+               # TODO Protect constants to preserve full precision and to forbid appending extra digits
+               if op == "π" then
+                       if last_op_was_unary then clear
+                       current = pi.to_s
+                       return
+               else if op == "e" then
+                       if last_op_was_unary then clear
+                       current = 2.718282.to_s
+                       return
+
+               # Clear screen
+               else if op == "C" then
+                       clear
+                       return
+
+               # Unary -
+               else if op == "-" then
+                       if current == null then
+                               if last_op_was_unary then clear
+                               current = "-"
+                               return
+                       else if current == "-" then
+                               current = null
+                               return
+                       end
+               end
+
+               # For all operators, apply pending operators
                apply_last_op_if_any
-               if op == 'C' then
-                       self.result = null
-                       last_op = null
+
+               var result = self.result or else 0
+
+               last_op = op
+               last_op_was_unary = true
+
+               # Unary operators
+               if op == "√" then
+                       self.result = result.to_f.sqrt
+               else if op == "x²" then
+                       self.result = result.to_f.pow(2.0)
+               else if op == "x!" then
+                       self.result = result.to_i.factorial
+               else if op == "sin" then
+                       self.result = result.to_f.sin
+               else if op == "cos" then
+                       self.result = result.to_f.cos
+               else if op == "tan" then
+                       self.result = result.to_f.tan
+
+               # =
+               else if op == "=" then
+                       current = null
+
+               # Binary operators
                else
-                       last_op = op # store for next push_op
+                       self.result = result # Set as same or 0
+                       last_op_was_unary = false
+                       current = null
                end
+       end
 
-               # prepare next current
-               self.current = null
+       # Clear all state
+       private fun clear
+       do
+               result = null
+               last_op = null
+               current = null
        end
 
        # Push a digit
        fun push_digit(digit: Int)
        do
+               if last_op_was_unary then clear
+
                var current = current
-               if current == null then current = new FlatBuffer
-               current.add digit.to_s.chars.first
+               if current == null then current = ""
+               current += digit.to_s
                self.current = current
-
-               if last_op == '=' then
-                       self.result = null
-                       last_op = null
-               end
        end
 
        # Switch entry mode from integer to decimal
        fun switch_to_decimals
        do
                var current = current
-               if current == null then current = new FlatBuffer.from("0")
-               if not current.chars.has('.') then current.add '.'
+               if current == null then current = "0"
+               if not current.chars.has('.') then current += "."
                self.current = current
        end
 
@@ -110,14 +167,20 @@ class CalculatorContext
                if op == null then
                        result = current.to_n
                else if result != null then
-                       if op == '+' then
+                       if op == "+" then
                                result = result.add(current.to_n)
-                       else if op == '-' then
+                       else if op == "-" then
                                result = result.sub(current.to_n)
-                       else if op == '/' then
+                       else if op == "/" or op == "÷" then
                                result = result.div(current.to_n)
-                       else if op == '*' then
+                       else if op == "*" or op == "×" then
                                result = result.mul(current.to_n)
+                       else if op == "%" then
+                               result = result.to_i % current.to_i
+                       else if op == "xⁿ" then
+                               result = result.to_f.pow(current.to_f)
+                       else if op == "log" then
+                               result = result.to_f.log_base(current.to_f)
                        end
                end
 
index 270854c..610b7a9 100644 (file)
@@ -23,11 +23,11 @@ import calculator_logic
 var context = new CalculatorContext
 context.push_digit( 1 )
 context.push_digit( 2 )
-context.push_op( '+' )
+context.push_op( "+" )
 context.push_digit( 3 )
-context.push_op( '*' )
+context.push_op( "*" )
 context.push_digit( 2 )
-context.push_op( '=' )
+context.push_op( "=" )
 var r = context.result
 assert r == 30 else print r or else "-"
 
@@ -36,16 +36,16 @@ context.push_digit( 1 )
 context.push_digit( 4 )
 context.switch_to_decimals
 context.push_digit( 1 )
-context.push_op( '*' )
+context.push_op( "*" )
 context.push_digit( 3 )
-context.push_op( '=' )
+context.push_op( "=" )
 r = context.result
 assert r == 42.3 else print r or else "-"
 
-context.push_op( '+' )
+context.push_op( "+" )
 context.push_digit( 1 )
 context.push_digit( 1 )
-context.push_op( '=' )
+context.push_op( "=" )
 r = context.result
 assert r == 53.3 else print r or else "-"
 
@@ -54,9 +54,9 @@ context.push_digit( 4 )
 context.push_digit( 2 )
 context.switch_to_decimals
 context.push_digit( 3 )
-context.push_op( '/' )
+context.push_op( "/" )
 context.push_digit( 3 )
-context.push_op( '=' )
+context.push_op( "=" )
 r = context.result
 assert r == 14.1 else print r or else "-"
 
@@ -68,20 +68,20 @@ context.switch_to_decimals
 context.push_digit( 1 )
 context.push_digit( 2 )
 context.push_digit( 3 )
-context.push_op( '+' )
+context.push_op( "+" )
 context.push_digit( 1 )
-context.push_op( '=' )
+context.push_op( "=" )
 r = context.result
 assert r == 51.123 else print r or else "-"
 
-#test 'C' button
+#test "C" button
 context = new CalculatorContext
 context.push_digit( 1 )
 context.push_digit( 0 )
-context.push_op( '+' )
+context.push_op( "+" )
 context.push_digit( 1 )
 context.push_digit( 0 )
-context.push_op( '=' )
-context.push_op( 'C' )
+context.push_op( "=" )
+context.push_op( "C" )
 r = context.result
 assert r == null else print r
diff --git a/examples/calculator/src/ios_calculator.nit b/examples/calculator/src/ios_calculator.nit
new file mode 100644 (file)
index 0000000..23fba4c
--- /dev/null
@@ -0,0 +1,32 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Aesthetic adaptations for iOS
+module ios_calculator
+
+import calculator
+import ios
+
+redef class CalculatorWindow
+       init do title = "app.nit Calculator"
+end
+
+redef class TextInput
+       init do set_ios_style(native)
+
+       private fun set_ios_style(objc_text_field: UITextField)
+       in "ObjC" `{
+               objc_text_field.textAlignment = NSTextAlignmentCenter;
+       `}
+end
diff --git a/examples/calculator/src/scientific_calculator.nit b/examples/calculator/src/scientific_calculator.nit
new file mode 100644 (file)
index 0000000..c22e44e
--- /dev/null
@@ -0,0 +1,40 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Extends the portable calculator app with scientific operations
+module scientific_calculator
+
+import calculator
+
+redef class CalculatorWindow
+       init
+       do
+               # All the button labels, row by row
+               var rows = [["√",  "x²", "xⁿ", "e"  ],
+                           ["log","ln", "%",  "x!" ],
+                           ["π",  "sin","cos","tan"]]
+
+               for row in rows do
+                       var row_layout = new HorizontalLayout(parent=layout)
+
+                       for op in row do
+                               var but = new Button(parent=row_layout, text=op)
+                               but.observers.add self
+                               buttons[op] = but
+                       end
+               end
+
+               super
+       end
+end
index d4330ba..66cbfb0 100644 (file)
@@ -21,12 +21,9 @@ redef class Int
        # Calculate the self-th element of the fibonacci sequence.
        fun fibonacci: Int
        do
-               if self < 2 then
-                       return 1
-               else
-                       return (self-2).fibonacci + (self-1).fibonacci
-               end
-       end 
+               if self < 2 then return self
+               return (self-2).fibonacci + (self-1).fibonacci
+       end
 end
 
 # Print usage and exit.
index 0733bbf..a5cd314 100644 (file)
@@ -8,69 +8,7 @@
 # See: <http://rosettacode.org/wiki/Perlin_noise>
 module perlin_noise
 
-redef universal Float
-       # Smoothened `self`
-       fun fade: Float do return self*self*self*(self*(self*6.0-15.0)+10.0)
-end
-
-# Improved noise
-class ImprovedNoise
-       # Permutations
-       var p: Array[Int] = [151,160,137,91,90,15,
-               131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
-               190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
-               88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
-               77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
-               102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
-               135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
-               5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
-               223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
-               129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
-               251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
-               49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
-               138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]
-
-       # Noise value in [-1..1] at 3d coordinates `x, y, z`
-       fun noise(x, y, z: Float): Float
-       do
-               var xx = x.to_i & 255
-               var yy = y.to_i & 255
-               var zz = z.to_i & 255
-
-               x -= x.floor
-               y -= y.floor
-               z -= z.floor
-
-               var u = x.fade
-               var v = y.fade
-               var w = z.fade
-
-               var a  = p[xx  ] + yy
-               var aa = p[a   ] + zz
-               var ab = p[a+1 ] + zz
-               var b  = p[xx+1] + yy
-               var ba = p[b   ] + zz
-               var bb = p[b+1 ] + zz
-
-               return w.lerp(v.lerp(u.lerp(grad(p[aa  ], x,     y,     z    ),
-                                           grad(p[ba  ], x-1.0, y,     z    )),
-                                    u.lerp(grad(p[ab  ], x,     y-1.0, z    ),
-                                           grad(p[bb  ], x-1.0, y-1.0, z    ))),
-                      v.lerp(u.lerp(grad(p[aa+1], x,     y,     z-1.0),
-                                           grad(p[ba+1], x-1.0, y,     z-1.0)),
-                                    u.lerp(grad(p[ab+1], x,     y-1.0, z-1.0),
-                                           grad(p[bb+1], x-1.0, y-1.0, z-1.0))))
-       end
-
-       # Value at a corner of the grid
-       fun grad(hash: Int, x, y, z: Float): Float
-       do
-               var h = hash & 15
-               var u = if h < 8 then x else y
-               var v = if h < 4 then y else if h == 12 or h == 14 then x else z
-               return (if h.is_even then u else -u) + (if h & 2 == 0 then v else -v)
-       end
-end
+import noise
 
 var map = new ImprovedNoise
 print map.noise(3.14, 42.0, 7.0).to_precision(17)
index 7b7ce02..cc0cba3 100644 (file)
@@ -188,6 +188,14 @@ class Node
                end
        end
 
+       # Find the closest node accepted by `cond` under `max_cost`
+       fun find_closest(max_cost: Int, context: PathContext, cond: nullable TargetCondition[N]): nullable N
+       do
+               var path = path_to_alts(null, max_cost, context, cond)
+               if path == null then return null
+               return path.nodes.last
+       end
+
        # We customize the serialization process to avoid problems with recursive
        # serialization engines. These engines, such as `JsonSerializer`,
        # are at danger to serialize the graph as a very deep tree.
index 3744988..cbe6ad4 100644 (file)
@@ -17,6 +17,7 @@ package nit.app;
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.view.KeyEvent;
 
 /*
  * Entry point to Nit applications on Android, redirect most calls to Nit
@@ -47,6 +48,11 @@ public class NitActivity extends Activity {
        protected native void nitOnDestroy(int activity);
        protected native void nitOnSaveInstanceState(int activity, Bundle savedInstanceState);
        protected native void nitOnRestoreInstanceState(int activity, Bundle savedInstanceState);
+       protected native boolean nitOnBackPressed(int activity);
+       protected native boolean nitOnKeyDown(int activity, int keyCode, KeyEvent event);
+       protected native boolean nitOnKeyLongPress(int activity, int keyCode, KeyEvent event);
+       protected native boolean nitOnKeyMultiple(int activity, int keyCode, int count, KeyEvent event);
+       protected native boolean nitOnKeyUp(int activity, int keyCode, KeyEvent event);
 
        /*
         * Implementation of OS callbacks
@@ -108,4 +114,34 @@ public class NitActivity extends Activity {
                super.onRestoreInstanceState(savedInstanceState);
                nitOnRestoreInstanceState(nitActivity, savedInstanceState);
        }
+
+       @Override
+       public void onBackPressed() {
+               if (!nitOnBackPressed(nitActivity))
+                       super.onBackPressed();
+       }
+
+       @Override
+       public boolean onKeyDown(int keyCode, KeyEvent event) {
+               return nitOnKeyDown(nitActivity, keyCode, event)
+                       || super.onKeyDown(keyCode, event);
+       }
+
+       @Override
+       public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+               return nitOnKeyLongPress(nitActivity, keyCode, event)
+                       || super.onKeyLongPress(keyCode, event);
+       }
+
+       @Override
+       public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
+               return nitOnKeyMultiple(nitActivity, keyCode, count, event)
+                       || super.onKeyMultiple(keyCode, count, event);
+       }
+
+       @Override
+       public boolean onKeyUp(int keyCode, KeyEvent event) {
+               return nitOnKeyUp(nitActivity, keyCode, event)
+                       || super.onKeyUp(keyCode, event);
+       }
 }
index 47e0cec..b6bcf7e 100644 (file)
@@ -64,7 +64,16 @@ integer as argument. They are applied in the Android manifest as
   only be used by low-level implementations of Nit on Android.
   Its usefulness will be extended in the future to customize user applications.
 
-## Project entry points
+## Android implementation
+
+There is two core implementation for Nit apps on Android.
+`android::nit_activity` is used by apps with standard windows and native UI controls.
+`android::game` is used by, well, games and the game frameworks `mnit` and `gamnit`.
+
+Clients don't have to select the core implementation, it is imported by other relevant modules.
+For example, a module importing `app::ui` and `android` will trigger the importation of `android::nit_activity`.
+
+## Lock app orientation
 
 Importing `android::landscape` or `android::portrait` locks the generated
 application in the specified orientation. This can be useful for games and
index b5aa04f..58e296a 100644 (file)
 module android
 
 import platform
-import native_app_glue
 import dalvik
 private import log
-private import assets
-
-redef class App
-       redef fun init_window
-       do
-               super
-               on_create
-               on_restore_state
-               on_start
-       end
-
-       redef fun term_window
-       do
-               super
-               on_stop
-       end
-
-       # Is the application currently paused?
-       var paused = true
-
-       redef fun pause
-       do
-               paused = true
-               on_pause
-               super
-       end
-
-       redef fun resume
-       do
-               paused = false
-               on_resume
-               super
-       end
-
-       redef fun save_state do on_save_state
-
-       redef fun lost_focus
-       do
-               paused = true
-               super
-       end
-
-       redef fun gained_focus
-       do
-               paused = false
-               super
-       end
-
-       redef fun destroy do on_destroy
-end
diff --git a/lib/android/game.nit b/lib/android/game.nit
new file mode 100644 (file)
index 0000000..2d17054
--- /dev/null
@@ -0,0 +1,71 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Android services and implementation of app.nit for gamnit and mnit
+module game
+
+import platform
+import native_app_glue
+import dalvik
+private import log
+private import assets
+
+redef class App
+       redef fun init_window
+       do
+               super
+               on_create
+               on_restore_state
+               on_start
+       end
+
+       redef fun term_window
+       do
+               super
+               on_stop
+       end
+
+       # Is the application currently paused?
+       var paused = true
+
+       redef fun pause
+       do
+               paused = true
+               on_pause
+               super
+       end
+
+       redef fun resume
+       do
+               paused = false
+               on_resume
+               super
+       end
+
+       redef fun save_state do on_save_state
+
+       redef fun lost_focus
+       do
+               paused = true
+               super
+       end
+
+       redef fun gained_focus
+       do
+               paused = false
+               super
+       end
+
+       redef fun destroy do on_destroy
+end
index 9597eea..5ee9d87 100644 (file)
@@ -18,7 +18,7 @@
 module input_events
 
 import mnit::input
-import android
+import android::game
 
 in "C header" `{
        #include <android/log.h>
@@ -70,6 +70,8 @@ private extern class NativeAndroidMotionEvent `{AInputEvent *`}
        `}
 
        fun action: AMotionEventAction `{ return AMotionEvent_getAction(self); `}
+
+       fun native_down_time: Int `{ return AMotionEvent_getDownTime(self); `}
 end
 
 private extern class AMotionEventAction `{ int32_t `}
@@ -149,6 +151,11 @@ class AndroidMotionEvent
                        return null
                end
        end
+
+       # Time when the user originally pressed down to start a stream of position events
+       #
+       # The return value is in the `java.lang.System.nanoTime()` time base.
+       fun down_time: Int do return native.native_down_time
 end
 
 # A pointer event
diff --git a/lib/android/key_event.nit b/lib/android/key_event.nit
new file mode 100644 (file)
index 0000000..b5b39e4
--- /dev/null
@@ -0,0 +1,192 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module key_event
+
+import platform
+
+# Java class: android.view.KeyEvent
+extern class NativeKeyEvent in "Java" `{ android.view.KeyEvent `}
+       super JavaObject
+
+       # Java implementation: boolean android.view.KeyEvent.isSystem()
+       fun is_system: Bool in "Java" `{
+               return self.isSystem();
+       `}
+
+       # Java implementation:  android.view.KeyEvent.setSource(int)
+       fun set_source(arg0: Int) in "Java" `{
+               self.setSource((int)arg0);
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getMetaState()
+       fun meta_state: Int in "Java" `{
+               return self.getMetaState();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getModifiers()
+       fun modifiers: Int in "Java" `{
+               return self.getModifiers();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getFlags()
+       fun flags: Int in "Java" `{
+               return self.getFlags();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.hasNoModifiers()
+       fun has_no_modifiers: Bool in "Java" `{
+               return self.hasNoModifiers();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.hasModifiers(int)
+       fun has_modifiers(arg0: Int): Bool in "Java" `{
+               return self.hasModifiers((int)arg0);
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isAltPressed()
+       fun is_alt_pressed: Bool in "Java" `{
+               return self.isAltPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isShiftPressed()
+       fun is_shift_pressed: Bool in "Java" `{
+               return self.isShiftPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isSymPressed()
+       fun is_sym_pressed: Bool in "Java" `{
+               return self.isSymPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCtrlPressed()
+       fun is_ctrl_pressed: Bool in "Java" `{
+               return self.isCtrlPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isMetaPressed()
+       fun is_meta_pressed: Bool in "Java" `{
+               return self.isMetaPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isFunctionPressed()
+       fun is_function_pressed: Bool in "Java" `{
+               return self.isFunctionPressed();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCapsLockOn()
+       fun is_caps_lock_on: Bool in "Java" `{
+               return self.isCapsLockOn();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isNumLockOn()
+       fun is_num_lock_on: Bool in "Java" `{
+               return self.isNumLockOn();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isScrollLockOn()
+       fun is_scroll_lock_on: Bool in "Java" `{
+               return self.isScrollLockOn();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getAction()
+       fun action: Int in "Java" `{
+               return self.getAction();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isCanceled()
+       fun is_canceled: Bool in "Java" `{
+               return self.isCanceled();
+       `}
+
+       # Java implementation:  android.view.KeyEvent.startTracking()
+       fun start_tracking in "Java" `{
+               self.startTracking();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isTracking()
+       fun is_tracking: Bool in "Java" `{
+               return self.isTracking();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isLongPress()
+       fun is_long_press: Bool in "Java" `{
+               return self.isLongPress();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getKeyCode()
+       fun key_code: Int in "Java" `{
+               return self.getKeyCode();
+       `}
+
+       # Java implementation: java.lang.String android.view.KeyEvent.getCharacters()
+       fun characters: JavaString in "Java" `{
+               return self.getCharacters();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getScanCode()
+       fun scan_code: Int in "Java" `{
+               return self.getScanCode();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getRepeatCount()
+       fun repeat_count: Int in "Java" `{
+               return self.getRepeatCount();
+       `}
+
+       # Java implementation: long android.view.KeyEvent.getDownTime()
+       fun down_time: Int in "Java" `{
+               return self.getDownTime();
+       `}
+
+       # Java implementation: long android.view.KeyEvent.getEventTime()
+       fun event_time: Int in "Java" `{
+               return self.getEventTime();
+       `}
+
+       # Java implementation: char android.view.KeyEvent.getDisplayLabel()
+       fun display_label: Char in "Java" `{
+               return self.getDisplayLabel();
+       `}
+
+       # Java implementation: int android.view.KeyEvent.getUnicodeChar()
+       fun unicode_char: Int in "Java" `{
+               return self.getUnicodeChar();
+       `}
+
+       # Java implementation: char android.view.KeyEvent.getNumber()
+       fun number: Char in "Java" `{
+               return self.getNumber();
+       `}
+
+       # Java implementation: boolean android.view.KeyEvent.isPrintingKey()
+       fun is_printing_key: Bool in "Java" `{
+               return self.isPrintingKey();
+       `}
+
+       redef fun new_global_ref import sys, Sys.jni_env `{
+               Sys sys = NativeKeyEvent_sys(self);
+               JNIEnv *env = Sys_jni_env(sys);
+               return (*env)->NewGlobalRef(env, self);
+       `}
+
+       redef fun pop_from_local_frame_with_env(jni_env) `{
+               return (*jni_env)->PopLocalFrame(jni_env, self);
+       `}
+end
+
+# Java getter: android.view.KeyEvent.KEYCODE_BACK
+fun android_view_key_event_keycode_back: Int in "Java" `{
+       return android.view.KeyEvent.KEYCODE_BACK;
+`}
index be79eb5..c313efb 100644 (file)
@@ -20,4 +20,4 @@ module landscape is
        android_manifest_activity """android:screenOrientation="sensorLandscape" """
 end
 
-import platform
+import android
index 87d9d22..1668147 100644 (file)
@@ -43,6 +43,7 @@ end
 import platform
 import log
 import activities
+import key_event
 import bundle
 import dalvik
 
@@ -133,6 +134,36 @@ in "C body" `{
        {
                Activity_on_restore_instance_state((Activity)nit_activity, saved_state);
        }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnBackPressed
+         (JNIEnv *env, jobject java_activity, jint nit_activity)
+       {
+               return (jboolean)Activity_on_back_pressed((Activity)nit_activity);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyDown
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_down((Activity)nit_activity, keyCode, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyLongPress
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_long_press((Activity)nit_activity, keyCode, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyMultiple
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jint count, jobject event)
+       {
+               return (jboolean)Activity_on_key_multiple((Activity)nit_activity, keyCode, count, event);
+       }
+
+       JNIEXPORT jboolean JNICALL Java_nit_app_NitActivity_nitOnKeyUp
+         (JNIEnv *env, jobject java_activity, jint nit_activity, jint keyCode, jobject event)
+       {
+               return (jboolean)Activity_on_key_up((Activity)nit_activity, keyCode, event);
+       }
 `}
 
 # Wrapper to our Java `NitActivity`
@@ -158,7 +189,10 @@ redef class App
        Activity.on_create, Activity.on_destroy,
        Activity.on_start, Activity.on_restart, Activity.on_stop,
        Activity.on_pause, Activity.on_resume,
-       Activity.on_save_instance_state, Activity.on_restore_instance_state `{
+       Activity.on_save_instance_state, Activity.on_restore_instance_state,
+       Activity.on_back_pressed,
+       Activity.on_key_down, Activity.on_key_long_press,
+       Activity.on_key_multiple, Activity.on_key_up `{
                App_incr_ref(self);
                global_app = self;
        `}
@@ -251,6 +285,31 @@ class Activity
 
        # Notification from Android, the current device configuration has changed
        fun on_configuration_changed do end
+
+       # The back key has been pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_back_pressed: Bool do return false
+
+       # A key has been pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_down(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+       # A key has been long pressed
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_long_press(key_code: Int, event: NativeKeyEvent): Bool do return false
+
+       # Multiple down/up pairs of the same key have occurred in a row
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_multiple(key_code, count: Int, event: NativeKeyEvent): Bool do return false
+
+       # A key has been released
+       #
+       # Return `true` if the event has been handled.
+       fun on_key_up(key_code: Int, event: NativeKeyEvent): Bool do return false
 end
 
 # Set up global data in C and leave it to Android to callback Java, which we relay to Nit
index e8f5720..0759c40 100644 (file)
@@ -17,4 +17,4 @@ module portrait is android_manifest_activity """
                android:screenOrientation="portrait"
 """
 
-import platform
+import android
index b969389..0681e4a 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This module is used to manipulate android sensors
-# The sensor support is implemented in android_app module, so the user can enable the type of sensor he wants to use.
-# There is an example of how you can use the android sensors in nit/examples/mnit_ballz :
+# Access Android sensors
+#
+# Sensors are to be enabled when `App` is created.
+# The following example enables all sensors.
+# The events (`SensorEvent`, `ASensorAccelerometer`, `ASensorMagneticField`...)
+# are sent to the `input` callback of `App`
 #
 # ~~~~nitish
-# #FIXME rewrite the example
 # redef class App
-#      sensors_support_enabled = true
-#      accelerometer.enabled = true
-#      accelerometer.eventrate = 10000
-#      magnetic_field.enabled = true
-#      gyroscope.enabled = true
-#      light.enabled = true
-#      proximity.enabled = true
+#     init
+#     do
+#         sensors_support_enabled = true
+#         accelerometer.enabled = true
+#         accelerometer.eventrate = 10000
+#         magnetic_field.enabled = true
+#         gyroscope.enabled = true
+#         light.enabled = true
+#         proximity.enabled = true
+#     end
 # end
 # ~~~~
-#
-# In this example, we enable the sensor support, then enable all types of sensors supported by the API, directly with `App` attributes
-# As a result, you get all type of SensorEvent (ASensorAccelerometer, ASensorMagneticField ...) in the `input` callback of `App`
 module sensors
 
-import android
+import game
 import mnit
 
 in "C header" `{
index 65535e8..cd9f4ea 100644 (file)
 # limitations under the License.
 
 # Views and services to use the Android native user interface
-module ui
+module ui is
+       # `adjustPan` allows to use EditText in a ListLayout
+       android_manifest_activity """android:windowSoftInputMode="adjustPan""""
+end
 
 # Implementation note:
 #
@@ -78,6 +81,19 @@ redef class App
        end
 end
 
+redef class Activity
+       redef fun on_back_pressed
+       do
+               var window = app.window
+               if window.enable_back_button then
+                       window.on_back_button
+                       return true
+               end
+
+               return false
+       end
+end
+
 # On Android, a window is implemented with the fragment `native`
 redef class Window
        redef var native = (new Android_app_Fragment(self)).new_global_ref
@@ -228,7 +244,7 @@ redef class TextView
                else // if (align > 0.5d)
                        g = android.view.Gravity.RIGHT;
 
-               view.setGravity(g);
+               view.setGravity(g | android.view.Gravity.CENTER_VERTICAL);
        `}
 end
 
@@ -240,9 +256,26 @@ end
 redef class CheckBox
        redef type NATIVE: Android_widget_CompoundButton
        redef var native do return (new Android_widget_CheckBox(app.native_activity)).new_global_ref
+       init do set_callback_on_toggle(native)
 
        redef fun is_checked do return native.is_checked
        redef fun is_checked=(value) do native.set_checked(value)
+
+       private fun on_toggle do notify_observers new ToggleEvent(self)
+
+       private fun set_callback_on_toggle(view: NATIVE)
+       import on_toggle in "Java" `{
+               final int final_sender_object = self;
+               CheckBox_incr_ref(final_sender_object);
+
+               view.setOnCheckedChangeListener(
+                       new android.widget.CompoundButton.OnCheckedChangeListener() {
+                               @Override
+                               public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) {
+                                       CheckBox_on_toggle(final_sender_object);
+                               }
+                       });
+       `}
 end
 
 redef class TextInput
index 42d90ae..7f22c78 100644 (file)
@@ -27,7 +27,6 @@ import app_base
 import core::error
 
 # Platform variations
-# TODO: move on the platform once qualified names are understand in the condition
 import linux::audio is conditional(linux)
 import android::audio is conditional(android)
 
index 4db4b22..ebebe83 100644 (file)
@@ -23,7 +23,6 @@ import app_base
 import serialization
 
 # Platform variations
-# TODO: move on the platform once qualified names are understand in the condition
 import linux::data_store is conditional(linux)
 import android::data_store is conditional(android)
 import ios::data_store is conditional(ios)
index 4efbb87..0afe7aa 100644 (file)
@@ -18,9 +18,8 @@ module ui
 import app_base
 
 # Platform variations
-# TODO: move on the platform once qualified names are understand in the condition
 import linux::ui is conditional(linux)
-import android::ui is conditional(android) # FIXME it should be conditional to `android::platform`
+import android::ui is conditional(android)
 import ios::ui is conditional(ios)
 
 redef class App
@@ -28,8 +27,32 @@ redef class App
 
        # The current `Window` of this activity
        #
-       # This attribute must be set by refinements of `App`.
-       var window: Window is writable
+       # This attribute is set by `push_window`.
+       var window: Window is noinit
+
+       # Make visible and push `window` on the top of `pop_window`
+       #
+       # This method must be called at least once within `App::on_create`.
+       # It can be called at any times while the app is active.
+       fun push_window(window: Window)
+       do
+               window_stack.add window
+               self.window = window
+       end
+
+       # Pop the current `window` from the stack and show the previous one
+       #
+       # Require: `window_stack.not_empty`
+       fun pop_window
+       do
+               assert window_stack.not_empty
+               window_stack.pop
+               window = window_stack.last
+               window.on_resume
+       end
+
+       # Stack of active windows
+       var window_stack = new Array[Window]
 
        redef fun on_create do window.on_create
 
@@ -141,6 +164,12 @@ end
 # A window, root of the `Control` tree
 class Window
        super CompositeControl
+
+       # Should the back button be shown and used to go back to a previous window?
+       fun enable_back_button: Bool do return app.window_stack.length > 1
+
+       # The back button has been pressed, usually to open the previous window
+       fun on_back_button do app.pop_window
 end
 
 # A viewable `Control`
@@ -211,12 +240,29 @@ class CheckBox
        var is_checked = false is writable
 end
 
+# Event sent from a `VIEW`
+class ViewEvent
+       super AppEvent
+
+       # The `VIEW` that raised this event
+       var sender: VIEW
+
+       # Type of the `sender`
+       type VIEW: View
+end
+
 # A `Button` press event
 class ButtonPressEvent
-       super AppEvent
+       super ViewEvent
+
+       redef type VIEW: Button
+end
+
+# The `CheckBox` `sender` has been toggled
+class ToggleEvent
+       super ViewEvent
 
-       # The `Button` that raised this event
-       var sender: Button
+       redef type VIEW: CheckBox
 end
 
 # A layout to visually organize `Control`s
index cb73b3b..8f6565b 100644 (file)
@@ -78,15 +78,22 @@ redef class NativeString
        # Decodes `self` from base64
        #
        #      assert "c3RyaW5n".decode_base64 == "string"
+       #      assert "c3Rya\nW5n".decode_base64 == "string"
        #
        # REQUIRE: `length % 4 == 0`
        private fun decode_base64(length: Int, padding: nullable Byte): Bytes do
                if padding == null then padding = '='.ascii
                var inv = once inverted_base64_chars
                if length == 0 then return new Bytes.empty
-               assert length % 4 == 0 else print "base64::decode_base64 only supports strings of length multiple of 4"
 
-               var bytes = self
+               # Remove non-base64 chars
+               var bytes = new Bytes.with_capacity(length)
+               for k in [0 .. length[ do
+                       var byte = self[k]
+                       if inv.has_key(byte) or byte == padding then bytes.add(byte)
+               end
+               length = bytes.length
+
                var steps = length / 4
                var result_length = steps * 3
 
index b5542e4..28129a5 100644 (file)
@@ -26,6 +26,7 @@
 module bucketed_game is serialize
 
 import serialization
+import counter
 
 # Something acting on the game
 abstract class Turnable[G: Game]
@@ -65,6 +66,9 @@ class Buckets[G: Game]
        private var buckets: Array[BUCKET] =
                [for b in n_buckets.times do new HashSet[Bucketable[G]]] is lazy
 
+       # Stats on delays asked when adding an event with `act_in` and `act_next`
+       private var delays = new Counter[Int]
+
        # Add the Bucketable event `e` at `at_tick`.
        fun add_at(e: Bucketable[G], at_tick: Int)
        do
@@ -106,6 +110,26 @@ class Buckets[G: Game]
                        end
                end
        end
+
+       # Get some statistics on both the current held events and historic expired events
+       fun stats: String
+       do
+               var entries = 0
+               var instances = new HashSet[Bucketable[G]]
+               var max = 0
+               var min = 100000
+               for bucket in buckets do
+                       var len = bucket.length
+                       entries += len
+                       instances.add_all bucket
+                       min = min.min(len)
+                       max = max.max(len)
+               end
+               var avg = entries.to_f / buckets.length.to_f
+
+               return "{buckets.length} buckets; uniq/tot:{instances.length}/{entries}, avg:{avg.to_precision(1)}, min:{min}, max:{max}\n" +
+                       "history:{delays.sum}, avg:{delays.avg}, min:{delays[delays.min.as(not null)]}, max:{delays[delays.max.as(not null)]}"
+       end
 end
 
 # Game related event
@@ -123,16 +147,16 @@ end
 # Game logic on the client
 class ThinGame
 
-       # Game tick when `self` should act.
+       # Current game tick
        #
        # Default is 0.
-       var tick: Int = 0 is protected writable
+       var tick: Int = 0 is writable
 end
 
 # Game turn on the client
 class ThinGameTurn[G: ThinGame]
 
-       # Game tick when `self` should act.
+       # Game tick when happened this turn
        var tick: Int is protected writable
 
        # Game events occurred for `self`.
@@ -153,10 +177,18 @@ class GameTurn[G: Game]
        end
 
        # Insert the Bucketable event `e` to be executed at next tick.
-       fun act_next(e: Bucketable[G]) do game.buckets.add_at(e, tick + 1)
+       fun act_next(e: Bucketable[G])
+       do
+               game.buckets.add_at(e, tick + 1)
+               game.buckets.delays.inc(1)
+       end
 
        # Insert the Bucketable event `e` to be executed at tick `t`.
-       fun act_in(e: Bucketable[G], t: Int) do game.buckets.add_at(e, tick + t)
+       fun act_in(e: Bucketable[G], t: Int)
+       do
+               game.buckets.add_at(e, tick + t)
+               game.buckets.delays.inc(t)
+       end
 
        # Add and `apply` a game `event`.
        fun add_event( event : GameEvent )
index 33114e4..3a6fa98 100644 (file)
@@ -37,7 +37,7 @@ extern class NSString in "ObjC" `{ NSString * `}
        # Get an UTF8 encoded `char*` copy of `self`
        fun utf8_string: NativeString in "ObjC" `{ return (char *)[self UTF8String]; `}
 
-       redef fun to_s do return utf8_string.to_s
+       redef fun to_s do return utf8_string.to_s_with_copy
 end
 
 redef class NativeString
index 5cffbec..5ed911f 100644 (file)
@@ -457,7 +457,9 @@ interface Set[E]
                var res = 23 + length
                # Note: the order of the elements must not change the hash value.
                # So, unlike usual hash functions, the accumulator is not combined with itself.
-               for e in self do res += e.hash
+               for e in self do
+                       if e != null then res += e.hash
+               end
                return res
        end
 
index 79e7a70..f01bbb5 100644 (file)
@@ -321,7 +321,7 @@ class Array[E]
        redef fun [](index)
        do
                assert index: index >= 0 and index < _length
-               return _items[index]
+               return _items.as(not null)[index]
        end
 
        redef fun []=(index, item)
@@ -333,7 +333,7 @@ class Array[E]
                if _length <= index then
                        _length = index + 1
                end
-               _items[index] = item
+               _items.as(not null)[index] = item
        end
 
        redef fun add(item)
@@ -343,7 +343,7 @@ class Array[E]
                        enlarge(l + 1)
                end
                _length = l + 1
-               _items[l] = item
+               _items.as(not null)[l] = item
        end
 
        # Slight optimization for arrays
@@ -358,13 +358,13 @@ class Array[E]
                if items isa Array[E] then
                        var k = 0
                        while l < nl do
-                               _items[l] = items._items[k]
+                               _items.as(not null)[l] = items._items.as(not null)[k]
                                l += 1
                                k += 1
                        end
                else
                        for item in items do
-                               _items[l] = item
+                               _items.as(not null)[l] = item
                                l += 1
                        end
                end
@@ -404,7 +404,7 @@ class Array[E]
                if cap <= c then return
                while c <= cap do c = c * 2 + 2
                var a = new NativeArray[E](c)
-               if _capacity > 0 then _items.copy_to(a, _length)
+               if _capacity > 0 then _items.as(not null).copy_to(a, _length)
                _items = a
                _capacity = c
        end
@@ -474,9 +474,10 @@ class Array[E]
                # Efficient implementation
                var l = length
                if l != o.length then return false
+               if l == 0 then return true
                var i = 0
-               var it = _items
-               var oit = o._items
+               var it = _items.as(not null)
+               var oit = o._items.as(not null)
                while i < l do
                        if it[i] != oit[i] then return false
                        i += 1
@@ -921,10 +922,11 @@ class ArrayCmp[E: nullable Comparable]
 
        redef fun <=>(o)
        do
-               var it = _items
-               var oit = o._items
                var i = 0
                var l = length
+               if l == 0 then return 0
+               var it = _items.as(not null)
+               var oit = o._items.as(not null)
                var ol = o.length
                var len
                if l < ol then len = l else len = ol
index ab81a37..241238b 100644 (file)
@@ -21,21 +21,21 @@ class List[E]
 
 # Access
 
-       redef fun [](index) do return get_node(index).item
+       redef fun [](index) do return get_node(index).as(not null).item
 
-       redef fun []=(index, item) do get_node(index).item = item
+       redef fun []=(index, item) do get_node(index).as(not null).item = item
 
        # O(1)
-       redef fun first do return _head.item
+       redef fun first do return _head.as(not null).item
 
        # O(1)
-       redef fun first=(e) do _head.item = e
+       redef fun first=(e) do _head.as(not null).item = e
 
        # O(1)
-       redef fun last do return _tail.item
+       redef fun last do return _tail.as(not null).item
 
        # O(1)
-       redef fun last=(e) do _tail.item = e
+       redef fun last=(e) do _tail.as(not null).item = e
 
 # Queries
 
@@ -87,11 +87,12 @@ class List[E]
        redef fun push(e)
        do
                var node = new ListNode[E](e)
-               if _tail == null then
+               var tail = _tail
+               if tail == null then
                        _head = node
                else
-                       _tail.next = node
-                       node.prev = _tail
+                       tail.next = node
+                       node.prev = tail
                end
                _tail = node
                length += 1
@@ -101,11 +102,12 @@ class List[E]
        redef fun unshift(e)
        do
                var node = new ListNode[E](e)
-               if _head == null then
+               var head = _head
+               if head == null then
                        _tail = node
                else
-                       node.next = _head
-                       _head.prev = node
+                       node.next = head
+                       head.prev = node
                end
                _head = node
                length += 1
@@ -127,11 +129,12 @@ class List[E]
        # O(1)
        fun link(l: List[E])
        do
-               if _tail == null then
+               var tail = _tail
+               if tail == null then
                        _head = l._head
                else if l._head != null then
-                       _tail.next = l._head
-                       _tail.next.prev = _tail
+                       tail.next = l._head
+                       tail.next.as(not null).prev = tail
                end
                _tail = l._tail
                length += l.length
@@ -143,13 +146,13 @@ class List[E]
        # O(1)
        redef fun pop
        do
-               var node = _tail
+               var node = _tail.as(not null)
                _tail = node.prev
                node.prev = null
                if _tail == null then
                        _head = null
                else
-                       _tail.next = null
+                       _tail.as(not null).next = null
                end
                length -= 1
                return node.item
@@ -158,13 +161,13 @@ class List[E]
        # O(1)
        redef fun shift
        do
-               var node = _head
+               var node = _head.as(not null)
                _head = node.next
                node.next = null
                if _head == null then
                        _tail = null
                else
-                       _head.prev = null
+                       _head.as(not null).prev = null
                end
                length -= 1
                return node.item
@@ -236,14 +239,14 @@ class List[E]
                        if node.next == null then
                                _tail = null
                        else
-                               node.next.prev = null
+                               node.next.as(not null).prev = null
                        end
                else if node.next == null then
                        _tail = node.prev
-                       node.prev.next = null
+                       node.prev.as(not null).next = null
                else
-                       node.prev.next = node.next
-                       node.next.prev = node.prev
+                       node.prev.as(not null).next = node.next
+                       node.next.as(not null).prev = node.prev
                end
        end
 
@@ -266,16 +269,16 @@ end
 # This is the iterator class of List
 class ListIterator[E]
        super IndexedIterator[E]
-       redef fun item do return _node.item
+       redef fun item do return _node.as(not null).item
 
        # Set item `e` at self `index`.
-       fun item=(e: E) do _node.item = e
+       fun item=(e: E) do _node.as(not null).item = e
 
        redef fun is_ok do return not _node == null
 
        redef fun next
        do
-               _node = _node.next
+               _node = _node.as(not null).next
                _index += 1
        end
 
@@ -312,7 +315,7 @@ private class ListReverseIterator[E]
 
        redef fun next
        do
-               _node = _node.prev
+               _node = _node.as(not null).prev
                _index -= 1
        end
 
index ac13301..6a58480 100644 (file)
@@ -86,6 +86,6 @@ class MaybeError[V, E: Error]
        redef fun to_s do
                var e = maybe_error
                if e != null then return e.to_s
-               return value.to_s
+               return value.as(not null).to_s
        end
 end
index 10c5374..6c03c7f 100644 (file)
@@ -98,6 +98,7 @@ class Process
                var args = new FlatBuffer
                var l = 1 # Number of elements in args
                args.append(command)
+               var arguments = self.arguments
                if arguments != null then
                        for a in arguments do
                                args.add('\0')
@@ -324,6 +325,9 @@ redef class Sys
        do
                return command.to_cstring.system
        end
+
+       # The pid of the program
+       fun pid: Int `{ return getpid(); `}
 end
 
 redef class NativeString
index def1cd2..8bf8078 100644 (file)
@@ -49,23 +49,24 @@ abstract class FileStream
        # Return null in case of error
        fun file_stat: nullable FileStat
        do
-               var stat = _file.file_stat
+               var stat = _file.as(not null).file_stat
                if stat.address_is_null then return null
                return new FileStat(stat)
        end
 
        # File descriptor of this file
-       fun fd: Int do return _file.fileno
+       fun fd: Int do return _file.as(not null).fileno
 
        redef fun close
        do
-               if _file == null then return
-               if _file.address_is_null then
+               var file = _file
+               if file == null then return
+               if file.address_is_null then
                        if last_error != null then return
                        last_error = new IOError("Cannot close unopened file")
                        return
                end
-               var i = _file.io_close
+               var i = file.io_close
                if i != 0 then
                        last_error = new IOError("Close failed due to error {sys.errno.strerror}")
                end
@@ -83,7 +84,7 @@ abstract class FileStream
        # * `buffer_mode_none`
        fun set_buffering_mode(buf_size, mode: Int) do
                if buf_size <= 0 then buf_size = 512
-               if _file.set_buffering_type(buf_size, mode) != 0 then
+               if _file.as(not null).set_buffering_type(buf_size, mode) != 0 then
                        last_error = new IOError("Error while changing buffering type for FileStream, returned error {sys.errno.strerror}")
                end
        end
@@ -105,10 +106,10 @@ class FileReader
        #     assert l == f.read_line
        fun reopen
        do
-               if not eof and not _file.address_is_null then close
+               if not eof and not _file.as(not null).address_is_null then close
                last_error = null
-               _file = new NativeFile.io_open_read(path.to_cstring)
-               if _file.address_is_null then
+               _file = new NativeFile.io_open_read(path.as(not null).to_cstring)
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Cannot open `{path.as(not null)}`: {sys.errno.strerror}")
                        end_reached = true
                        return
@@ -126,8 +127,8 @@ class FileReader
 
        redef fun fill_buffer
        do
-               var nb = _file.io_read(_buffer, _buffer_capacity)
-               if last_error == null and _file.ferror then
+               var nb = _file.as(not null).io_read(_buffer, _buffer_capacity)
+               if last_error == null and _file.as(not null).ferror then
                        last_error = new IOError("Cannot read `{path.as(not null)}`: {sys.errno.strerror}")
                        end_reached = true
                end
@@ -158,7 +159,7 @@ class FileReader
                self.path = path
                prepare_buffer(100)
                _file = new NativeFile.io_open_read(path.to_cstring)
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
                        end_reached = true
                end
@@ -171,7 +172,7 @@ class FileReader
                self.path = ""
                prepare_buffer(1)
                _file = fd.fd_to_stream(read_only)
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Error: Converting fd {fd} to stream failed with '{sys.errno.strerror}'")
                        end_reached = true
                end
@@ -223,13 +224,13 @@ class FileWriter
                        last_error = new IOError("Cannot write to non-writable stream")
                        return
                end
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Writing on a null stream")
                        _is_writable = false
                        return
                end
 
-               var err = _file.write_byte(value)
+               var err = _file.as(not null).write_byte(value)
                if err != 1 then
                        # Big problem
                        last_error = new IOError("Problem writing a byte: {err}")
@@ -251,12 +252,12 @@ class FileWriter
                        last_error = new IOError("Cannot write to non-writable stream")
                        return
                end
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Writing on a null stream")
                        _is_writable = false
                        return
                end
-               var err = _file.io_write(native, from, len)
+               var err = _file.as(not null).io_write(native, from, len)
                if err != len then
                        # Big problem
                        last_error = new IOError("Problem in writing : {err} {len} \n")
@@ -269,7 +270,7 @@ class FileWriter
                _file = new NativeFile.io_open_write(path.to_cstring)
                self.path = path
                _is_writable = true
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
                        is_writable = false
                end
@@ -280,7 +281,7 @@ class FileWriter
                self.path = ""
                _file = fd.fd_to_stream(wipe_write)
                _is_writable = true
-                if _file.address_is_null then
+                if _file.as(not null).address_is_null then
                         last_error = new IOError("Error: Opening stream from file descriptor {fd} failed with '{sys.errno.strerror}'")
                        _is_writable = false
                end
@@ -663,6 +664,19 @@ class Path
                return res
        end
 
+       # Is `self` the path to an existing directory ?
+       #
+       # ~~~nit
+       # assert ".".to_path.is_dir
+       # assert not "/etc/issue".to_path.is_dir
+       # assert not "/should/not/exist".to_path.is_dir
+       # ~~~
+       fun is_dir: Bool do
+               var st = stat
+               if st == null then return false
+               return st.is_dir
+       end
+
        # Delete a directory and all of its content
        #
        # Does not go through symbolic links and may get stuck in a cycle if there
index 0f8325f..7b51e2a 100644 (file)
@@ -169,6 +169,16 @@ redef class Int
                end
                return res
        end
+
+       # Is `self` a power of two ?
+       #
+       # ~~~nit
+       # assert not 3.is_pow2
+       # assert 2.is_pow2
+       # assert 1.is_pow2
+       # assert not 0.is_pow2
+       # ~~~
+       fun is_pow2: Bool do return self != 0 and (self & self - 1) == 0
 end
 
 redef class Byte
index 917ec4f..0ac109c 100644 (file)
@@ -183,7 +183,7 @@ class Regex
        # Cache of a single `regmatch_t` to prevent many calls to `malloc`
        private var native_match: NativeMatchArray is lazy do
                native_match_is_init = true
-               return new NativeMatchArray.malloc(native.re_nsub+1)
+               return new NativeMatchArray.malloc(native.as(not null).re_nsub+1)
        end
 
        private var native_match_is_init = false
index c699d50..8fefb24 100644 (file)
@@ -2114,7 +2114,12 @@ end
 # see `alpha_comparator`
 private class AlphaComparator
        super Comparator
-       redef fun compare(a, b) do return a.to_s <=> b.to_s
+       redef fun compare(a, b) do
+               if a == b then return 0
+               if a == null then return -1
+               if b == null then return 1
+               return a.to_s <=> b.to_s
+       end
 end
 
 # Stateless comparator that naively use `to_s` to compare things.
@@ -2172,6 +2177,11 @@ redef class NativeString
        # SEE: `abstract_text::Text` for more info on the difference
        # between `Text::bytelen` and `Text::length`.
        fun to_s_full(bytelen, unilen: Int): String is abstract
+
+       # Copies the content of `src` to `self`
+       #
+       # NOTE: `self` must be large enough to withold `self.bytelen` bytes
+       fun fill_from(src: Text) do src.copy_to_native(self, src.bytelen, 0, 0)
 end
 
 redef class NativeArray[E]
index b7e8e61..9e70321 100644 (file)
@@ -369,6 +369,10 @@ redef class FlatText
                end
                return res
        end
+
+       redef fun copy_to_native(dst, n, src_off, dst_off) do
+               _items.copy_to(dst, n, first_byte + src_off, dst_off)
+       end
 end
 
 # Immutable strings of characters.
@@ -666,15 +670,15 @@ private class ASCIIFlatString
        end
 
        redef fun substring(from, count) do
+               var ln = _length
+               if count <= 0 then return ""
+               if (count + from) > ln then count = ln - from
                if count <= 0 then return ""
-
                if from < 0 then
                        count += from
-                       if count < 0 then return ""
+                       if count <= 0 then return ""
                        from = 0
                end
-               var ln = _length
-               if (count + from) > ln then count = ln - from
                return new ASCIIFlatString.full_data(_items, count, from + _first_byte, count)
        end
 
index f370e49..f57b33d 100644 (file)
@@ -221,23 +221,16 @@ private class Concat
        end
 
        redef fun copy_to_native(dest, n, src_offset, dest_offset) do
-               var subs = new RopeSubstrings.from(self, src_offset)
-               var st = src_offset - subs.pos
-               var off = dest_offset
-               while n > 0 do
-                       var it = subs.item
-                       if n > it.length then
-                               var cplen = it.length - st
-                               it._items.copy_to(dest, cplen, st, off)
-                               off += cplen
-                               n -= cplen
-                       else
-                               it._items.copy_to(dest, n, st, off)
-                               n = 0
-                       end
-                       subs.next
-                       st = 0
+               var l = _left
+               if src_offset < l.bytelen then
+                       var lcopy = l.bytelen - src_offset
+                       lcopy = if lcopy > n then n else lcopy
+                       l.copy_to_native(dest, lcopy, src_offset, dest_offset)
+                       dest_offset += lcopy
+                       n -= lcopy
+                       src_offset = 0
                end
+               _right.copy_to_native(dest, n, src_offset, dest_offset)
        end
 
        # Returns a balanced version of `self`
@@ -828,7 +821,7 @@ private class ReverseRopeSubstrings
        redef fun next do
                if pos < 0 then return
                var curr = iter.prev
-               var currit = curr.node
+               var currit = curr.as(not null).node
                while curr != null do
                        currit = curr.node
                        if not currit isa Concat then
@@ -935,14 +928,14 @@ private class RopeSubstrings
        redef fun next do
                pos += str.length
                if pos > max then return
-               var it = iter.prev
+               var it = iter.prev.as(not null)
                var rnod = it.node
                loop
                        if not rnod isa Concat then
                                it.ldone = true
                                it.rdone = true
                                str = rnod.as(FlatString)
-                               iter = it.as(not null)
+                               iter = it
                                break
                        end
                        if not it.ldone then
@@ -954,7 +947,7 @@ private class RopeSubstrings
                                rnod = rnod._right
                                it = new RopeCharIteratorPiece(rnod, false, false, it)
                        else
-                               it = it.prev
+                               it = it.prev.as(not null)
                                rnod = it.node
                                continue
                        end
diff --git a/lib/crapto/crapto.nit b/lib/crapto/crapto.nit
new file mode 100644 (file)
index 0000000..5f3f017
--- /dev/null
@@ -0,0 +1,19 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Cryptographic attacks and utilities.
+module crapto
+
+import english_utils
+import xor
diff --git a/lib/crapto/english_utils.nit b/lib/crapto/english_utils.nit
new file mode 100644 (file)
index 0000000..9f39e18
--- /dev/null
@@ -0,0 +1,80 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Philippe Pépos Petitclerc <ppeposp@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# English language utilities for cryptographic purposes.
+module english_utils
+
+redef class Sys
+       # English letter frequency map
+       var english_freqs: HashMap[Char, Float] is lazy do
+               var freqs = new HashMap[Char, Float]
+
+               freqs['a'] = 0.0651738
+               freqs['b'] = 0.0124248
+               freqs['c'] = 0.0217339
+               freqs['d'] = 0.0349835
+               freqs['e'] = 0.1041442
+               freqs['f'] = 0.0197881
+               freqs['g'] = 0.0158610
+               freqs['h'] = 0.0492888
+               freqs['i'] = 0.0558094
+               freqs['j'] = 0.0009033
+               freqs['k'] = 0.0050529
+               freqs['l'] = 0.0331490
+               freqs['m'] = 0.0202124
+               freqs['n'] = 0.0564513
+               freqs['o'] = 0.0596302
+               freqs['p'] = 0.0137645
+               freqs['q'] = 0.0008606
+               freqs['r'] = 0.0497563
+               freqs['s'] = 0.0515760
+               freqs['t'] = 0.0729357
+               freqs['u'] = 0.0225134
+               freqs['v'] = 0.0082903
+               freqs['w'] = 0.0171272
+               freqs['x'] = 0.0013692
+               freqs['y'] = 0.0145984
+               freqs['z'] = 0.0007836
+               freqs[' '] = 0.1918182
+
+               return freqs
+       end
+end
+
+redef class Text
+
+       # Score `self` according to english's letter frequency.
+       # This function is useful mainly for cryptography but could happen to be helpful
+       # elsewhere.
+       #
+       #     assert "aaaa".english_scoring > "bbbb".english_scoring
+       fun english_scoring: Float do
+
+               var freqs = english_freqs
+               var score = 0.0
+
+               for c in self do
+                       c = c.to_lower
+                       var points = freqs.get_or_null(c)
+                       if points != null then
+                               score += points
+                       end
+               end
+
+               return score
+
+       end
+end
diff --git a/lib/crapto/examples/repeating_key_xor_cipher.txt b/lib/crapto/examples/repeating_key_xor_cipher.txt
new file mode 100644 (file)
index 0000000..cecdb81
--- /dev/null
@@ -0,0 +1,64 @@
+HUIfTQsPAh9PE048GmllH0kcDk4TAQsHThsBFkU2AB4BSWQgVB0dQzNTTmVS
+BgBHVBwNRU0HBAxTEjwMHghJGgkRTxRMIRpHKwAFHUdZEQQJAGQmB1MANxYG
+DBoXQR0BUlQwXwAgEwoFR08SSAhFTmU+Fgk4RQYFCBpGB08fWXh+amI2DB0P
+QQ1IBlUaGwAdQnQEHgFJGgkRAlJ6f0kASDoAGhNJGk9FSA8dDVMEOgFSGQEL
+QRMGAEwxX1NiFQYHCQdUCxdBFBZJeTM1CxsBBQ9GB08dTnhOSCdSBAcMRVhI
+CEEATyBUCHQLHRlJAgAOFlwAUjBpZR9JAgJUAAELB04CEFMBJhAVTQIHAh9P
+G054MGk2UgoBCVQGBwlTTgIQUwg7EAYFSQ8PEE87ADpfRyscSWQzT1QCEFMa
+TwUWEXQMBk0PAg4DQ1JMPU4ALwtJDQhOFw0VVB1PDhxFXigLTRkBEgcKVVN4
+Tk9iBgELR1MdDAAAFwoFHww6Ql5NLgFBIg4cSTRWQWI1Bk9HKn47CE8BGwFT
+QjcEBx4MThUcDgYHKxpUKhdJGQZZVCFFVwcDBVMHMUV4LAcKQR0JUlk3TwAm
+HQdJEwATARNFTg5JFwQ5C15NHQYEGk94dzBDADsdHE4UVBUaDE5JTwgHRTkA
+Umc6AUETCgYAN1xGYlUKDxJTEUgsAA0ABwcXOwlSGQELQQcbE0c9GioWGgwc
+AgcHSAtPTgsAABY9C1VNCAINGxgXRHgwaWUfSQcJABkRRU8ZAUkDDTUWF01j
+OgkRTxVJKlZJJwFJHQYADUgRSAsWSR8KIgBSAAxOABoLUlQwW1RiGxpOCEtU
+YiROCk8gUwY1C1IJCAACEU8QRSxORTBSHQYGTlQJC1lOBAAXRTpCUh0FDxhU
+ZXhzLFtHJ1JbTkoNVDEAQU4bARZFOwsXTRAPRlQYE042WwAuGxoaAk5UHAoA
+ZCYdVBZ0ChQLSQMYVAcXQTwaUy1SBQsTAAAAAAAMCggHRSQJExRJGgkGAAdH
+MBoqER1JJ0dDFQZFRhsBAlMMIEUHHUkPDxBPH0EzXwArBkkdCFUaDEVHAQAN
+U29lSEBAWk44G09fDXhxTi0RAk4ITlQbCk0LTx4cCjBFeCsGHEETAB1EeFZV
+IRlFTi4AGAEORU4CEFMXPBwfCBpOAAAdHUMxVVUxUmM9ElARGgZBAg4PAQQz
+DB4EGhoIFwoKUDFbTCsWBg0OTwEbRSonSARTBDpFFwsPCwIATxNOPBpUKhMd
+Th5PAUgGQQBPCxYRdG87TQoPD1QbE0s9GkFiFAUXR0cdGgkADwENUwg1DhdN
+AQsTVBgXVHYaKkg7TgNHTB0DAAA9DgQACjpFX0BJPQAZHB1OeE5PYjYMAg5M
+FQBFKjoHDAEAcxZSAwZOBREBC0k2HQxiKwYbR0MVBkVUHBZJBwp0DRMDDk5r
+NhoGACFVVWUeBU4MRREYRVQcFgAdQnQRHU0OCxVUAgsAK05ZLhdJZChWERpF
+QQALSRwTMRdeTRkcABcbG0M9Gk0jGQwdR1ARGgNFDRtJeSchEVIDBhpBHQlS
+WTdPBzAXSQ9HTBsJA0UcQUl5bw0KB0oFAkETCgYANlVXKhcbC0sAGgdFUAIO
+ChZJdAsdTR0HDBFDUk43GkcrAAUdRyonBwpOTkJEUyo8RR8USSkOEENSSDdX
+RSAdDRdLAA0HEAAeHQYRBDYJC00MDxVUZSFQOV1IJwYdB0dXHRwNAA9PGgMK
+OwtTTSoBDBFPHU54W04mUhoPHgAdHEQAZGU/OjV6RSQMBwcNGA5SaTtfADsX
+GUJHWREYSQAnSARTBjsIGwNOTgkVHRYANFNLJ1IIThVIHQYKAGQmBwcKLAwR
+DB0HDxNPAU94Q083UhoaBkcTDRcAAgYCFkU1RQUEBwFBfjwdAChPTikBSR0T
+TwRIEVIXBgcURTULFk0OBxMYTwFUN0oAIQAQBwkHVGIzQQAGBR8EdCwRCEkH
+ElQcF0w0U05lUggAAwANBxAAHgoGAwkxRRMfDE4DARYbTn8aKmUxCBsURVQf
+DVlOGwEWRTIXFwwCHUEVHRcAMlVDKRsHSUdMHQMAAC0dCAkcdCIeGAxOazkA
+BEk2HQAjHA1OAFIbBxNJAEhJBxctDBwKSRoOVBwbTj8aQS4dBwlHKjUECQAa
+BxscEDMNUhkBC0ETBxdULFUAJQAGARFJGk9FVAYGGlMNMRcXTRoBDxNPeG43
+TQA7HRxJFUVUCQhBFAoNUwctRQYFDE43PT9SUDdJUydcSWRtcwANFVAHAU5T
+FjtFGgwbCkEYBhlFeFsABRcbAwZOVCYEWgdPYyARNRcGAQwKQRYWUlQwXwAg
+ExoLFAAcARFUBwFOUwImCgcDDU5rIAcXUj0dU2IcBk4TUh0YFUkASEkcC3QI
+GwMMQkE9SB8AMk9TNlIOCxNUHQZCAAoAHh1FXjYCDBsFABkOBkk7FgALVQRO
+D0EaDwxOSU8dGgI8EVIBAAUEVA5SRjlUQTYbCk5teRsdRVQcDhkDADBFHwhJ
+AQ8XClJBNl4AC1IdBghVEwARABoHCAdFXjwdGEkDCBMHBgAwW1YnUgAaRyon
+B0VTGgoZUwE7EhxNCAAFVAMXTjwaTSdSEAESUlQNBFJOZU5LXHQMHE0EF0EA
+Bh9FeRp5LQdFTkAZREgMU04CEFMcMQQAQ0lkay0ABwcqXwA1FwgFAk4dBkIA
+CA4aB0l0PD1MSQ8PEE87ADtbTmIGDAILAB0cRSo3ABwBRTYKFhROHUETCgZU
+MVQHYhoGGksABwdJAB0ASTpFNwQcTRoDBBgDUkksGioRHUkKCE5THEVCC08E
+EgF0BBwJSQoOGkgGADpfADETDU5tBzcJEFMLTx0bAHQJCx8ADRJUDRdMN1RH
+YgYGTi5jMURFeQEaSRAEOkURDAUCQRkKUmQ5XgBIKwYbQFIRSBVJGgwBGgtz
+RRNNDwcVWE8BT3hJVCcCSQwGQx9IBE4KTwwdASEXF01jIgQATwZIPRpXKwYK
+BkdEGwsRTxxDSToGMUlSCQZOFRwKUkQ5VEMnUh0BR0MBGgAAZDwGUwY7CBdN
+HB5BFwMdUz0aQSwWSQoITlMcRUILTxoCEDUXF01jNw4BTwVBNlRBYhAIGhNM
+EUgIRU5CRFMkOhwGBAQLTVQOHFkvUkUwF0lkbXkbHUVUBgAcFA0gRQYFCBpB
+PU8FQSsaVycTAkJHYhsRSQAXABxUFzFFFggICkEDHR1OPxoqER1JDQhNEUgK
+TkJPDAUAJhwQAg0XQRUBFgArU04lUh0GDlNUGwpOCU9jeTY1HFJARE4xGA4L
+ACxSQTZSDxsJSw1ICFUdBgpTNjUcXk0OAUEDBxtUPRpCLQtFTgBPVB8NSRoK
+SREKLUUVAklkERgOCwAsUkE2Ug8bCUsNSAhVHQYKUyI7RQUFABoEVA0dWXQa
+Ry1SHgYOVBFIB08XQ0kUCnRvPgwQTgUbGBwAOVREYhAGAQBJEUgETgpPGR8E
+LUUGBQgaQRIaHEshGk03AQANR1QdBAkAFwAcUwE9AFxNY2QxGA4LACxSQTZS
+DxsJSw1ICFUdBgpTJjsIF00GAE1ULB1NPRpPLF5JAgJUVAUAAAYKCAFFXjUe
+DBBOFRwOBgA+T04pC0kDElMdC0VXBgYdFkU2CgtNEAEUVBwTWXhTVG5SGg8e
+AB0cRSo+AwgKRSANExlJCBQaBAsANU9TKxFJL0dMHRwRTAtPBRwQMAAATQcB
+FlRlIkw5QwA2GggaR0YBBg5ZTgIcAAw3SVIaAQcVEU8QTyEaYy0fDE4ITlhI
+Jk8DCkkcC3hFMQIEC0EbAVIqCFZBO1IdBgZUVA4QTgUWSR4QJwwRTWM=
diff --git a/lib/crapto/examples/repeating_key_xor_solve.nit b/lib/crapto/examples/repeating_key_xor_solve.nit
new file mode 100644 (file)
index 0000000..f88e2d9
--- /dev/null
@@ -0,0 +1,38 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import crapto
+
+# Check usage
+if args.length != 1 then
+       print "Usage: repeating_key_xor_solve <cipher_file>"
+       exit 1
+end
+
+# Read the cipher from the file
+var cipher_bytes = args[0].to_path.read_all_bytes.decode_base64
+
+# Create a RepeatingKeyXorCipher object to manipulate your ciphertext
+var xorcipher = new RepeatingKeyXorCipher
+xorcipher.ciphertext = cipher_bytes
+
+# Try to find the best candidate key
+xorcipher.find_key
+
+# Decrypt the cipher according to the found key
+xorcipher.decrypt
+
+# Check the resulting plaintext out...
+print xorcipher.plaintext
diff --git a/lib/crapto/package.ini b/lib/crapto/package.ini
new file mode 100644 (file)
index 0000000..2f9d180
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=crapto
+tags=crypto
+maintainer=Philippe Pépos Petitclerc <ppeposp@gmail.com>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/crapto
+git=https://github.com/nitlang/nit.git
+git.directory=lib/crapto/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
diff --git a/lib/crapto/xor.nit b/lib/crapto/xor.nit
new file mode 100644 (file)
index 0000000..8745c1a
--- /dev/null
@@ -0,0 +1,150 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Cryptographic attacks and utilities for XOR-based algorithms.
+module xor
+
+import combinations
+import crypto
+import english_utils
+
+redef class SingleByteXorCipher
+       # Tries to find key using frequency analysis on all possible plaintexts.
+       # Populates `self.key`
+       fun find_key do
+
+               # Accumulate best result
+               var max = 0.0
+               var best = 0.to_b
+
+               # Iterate on possible values for a byte
+               var xor_b = new Bytes.with_capacity(1)
+               for b in [0 .. 255] do
+                       # Need `Bytes` to pass to xor
+                       xor_b[0] = b.to_b
+
+                       # Xor and evaluate result
+                       var xored = ciphertext.xorcipher(xor_b)
+                       var result = xored.to_s.english_scoring
+                       if result > max then
+                               max = result
+                               best = b.to_b
+                       end
+               end
+
+               self.key = best
+
+       end
+end
+
+redef class RepeatingKeyXorCipher
+       # Attempts to find the key by using frequency analysis on the resulting plaintexts.
+       # Best key lengths are estimated using the hamming distance of blocks of keylength bytes.
+       # Ciphertext is then transposed in such a way that it can be solved by sequences of
+       # SingleByteXor (one for each offset in the key).
+       #
+       # `min_keylength` and `max_keylength` are limits as to what key lengths are tested.
+       # `considered_keylength_count` is the number of best key lengths that are kept to be
+       # analysed by the SingleByteXor frequency analysis.
+       fun find_key(min_keylength, max_keylength, considered_keylength_count: nullable Int)  do
+
+               # Set default values
+               if min_keylength == null then min_keylength = 2
+               if max_keylength == null then max_keylength = 40
+               if considered_keylength_count == null then considered_keylength_count = 3
+
+               # Find the best candidate key lengths based on the normalized hamming distances
+               var best_sizes = get_normalized_hamming_distances(min_keylength, max_keylength, considered_keylength_count)
+
+               var best = 0.0
+               var best_key: nullable Bytes = null
+               for ks in best_sizes do
+
+                       # Rearrange ciphertext to be in SingleByteXORable blocks
+                       var transposed = transpose_ciphertext(ks)
+
+                       var key = new Bytes.empty
+                       for slot in transposed do
+                               var sbx = new SingleByteXorCipher
+                               sbx.ciphertext = slot
+                               sbx.find_key
+                               key.add sbx.key
+                       end
+
+                       # Evaluate the resulting plaintext based on english frequency analysis
+                       var eng_score = ciphertext.xorcipher(key).to_s.english_scoring
+                       if eng_score > best then
+                               best_key = key
+                               best = eng_score
+                       end
+
+                       assert best_key != null
+                       self.key = best_key
+
+               end
+       end
+
+       # Computes the normalized hamming distances between blocks of ciphertext of length between `min_length` and `max_length`.
+       # The `considered_keylength_count` smallest results are returned
+       private fun get_normalized_hamming_distances(min_keylength, max_keylength, considered_keylength_count: Int): Array[Int] do
+
+               var normalized_distances = new HashMap[Float, Int]
+
+               # Iterate over all given key lengths
+               for ks in [min_keylength .. max_keylength[ do
+
+                       # Initialize the blocks of size `ks`
+                       var blocks = new Array[Bytes]
+                       while (blocks.length + 1) * ks < ciphertext.length do
+                               blocks.add(ciphertext.slice(blocks.length * ks, ks))
+                       end
+
+                       # Compute the normalized hamming distance with all block combinations
+                       var pairs = new CombinationCollection[Bytes](blocks, 2)
+                       var hamming_dists = new Array[Float]
+                       for p in pairs do
+                               hamming_dists.add(p[0].hamming_distance(p[1]).to_f / ks.to_f)
+                       end
+
+                       # Normalize the results based on the number of blocks
+                       var normalized = 0.0
+                       for dist in hamming_dists do normalized += dist
+                       normalized /= hamming_dists.length.to_f
+                       normalized_distances[normalized] = ks
+
+               end
+
+               # Collect the best candidates
+               var distances = normalized_distances.keys.to_a
+               default_comparator.sort(distances)
+               var best_distances = distances.subarray(0, considered_keylength_count)
+               var best_sizes = [for d in best_distances do normalized_distances[d]]
+
+               return best_sizes
+       end
+
+       # Returns a rearranged format of the ciphertext where every byte of a slot will be XORed with the same offset of a key of length `keylength`.
+       private fun transpose_ciphertext(keylength: Int): Array[Bytes] do
+               var transposed = new Array[Bytes]
+               for i in [0 .. keylength[ do
+                       transposed.add(new Bytes.empty)
+               end
+
+               for byte_idx in [0 .. ciphertext.length[ do
+                       transposed[byte_idx % keylength].add(ciphertext[byte_idx])
+               end
+
+               return transposed
+       end
+end
similarity index 89%
rename from lib/crypto.nit
rename to lib/crypto/basic_ciphers.nit
index 357eb99..e4a133c 100644 (file)
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Mix of all things cryptography-related
-module crypto
+# Basic cryptographic ciphers and utilities.
+module basic_ciphers
 
 redef class Char
        # Rotates self of `x`
@@ -154,32 +154,24 @@ redef class Text
                end
                return arr.to_s
        end
+end
 
-       # Returns `self` xored with `key`
-       #
-       # The shortest of the two is cycled through until the longest has been
-       # completely xored.
-       #
-       #     assert "goodmorning".xor(" ".to_bytes) == "GOODMORNING"
-       fun xor(key: SequenceRead[Byte]): Text do
-               var xored = new Bytes.with_capacity(bytelen.max(key.length))
-
-               var shortest: SequenceRead[Byte]
-               var longest: SequenceRead[Byte]
-
-               if key.length > self.length then
-                       shortest = self.to_bytes
-                       longest = key
-               else
-                       shortest = key
-                       longest = self.to_bytes
-               end
-
-               for i in longest.length.times do
-                       xored.add(longest[i] ^ shortest[i % shortest.length])
+redef class Bytes
+       # Computes the edit/hamming distance of two sequences of bytes.
+       #
+       #     assert "this is a test".to_bytes.hamming_distance("wokka wokka!!!".bytes) == 37
+       #     assert "this is a test".to_bytes.hamming_distance("this is a test".bytes) == 0
+       #
+       fun hamming_distance(other: SequenceRead[Byte]): Int do
+               var diff = 0
+               for idx in self.length.times do
+                       var res_byte = self[idx] ^ other[idx]
+                       for bit in [0..8[ do
+                               if res_byte & 1u8 == 1u8 then diff += 1
+                               res_byte = res_byte >> 1
+                       end
                end
-
-               return xored.to_s
+               return diff
        end
 end
 
diff --git a/lib/crypto/crypto.nit b/lib/crypto/crypto.nit
new file mode 100644 (file)
index 0000000..4709403
--- /dev/null
@@ -0,0 +1,19 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Mix of all things cryptography-related
+module crypto
+
+import basic_ciphers
+import xor_ciphers
similarity index 89%
rename from lib/crypto.ini
rename to lib/crypto/package.ini
index e8b4cf1..24f6b14 100644 (file)
@@ -6,6 +6,6 @@ license=Apache-2.0
 [upstream]
 browse=https://github.com/nitlang/nit/tree/master/lib/crypto.nit
 git=https://github.com/nitlang/nit.git
-git.directory=lib/crypto.nit
+git.directory=lib/crypto/crypto.nit
 homepage=http://nitlanguage.org
 issues=https://github.com/nitlang/nit/issues
diff --git a/lib/crypto/xor_ciphers.nit b/lib/crypto/xor_ciphers.nit
new file mode 100644 (file)
index 0000000..1c4807c
--- /dev/null
@@ -0,0 +1,88 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# XOR oriented cryptographic ciphers and utilities.
+module xor_ciphers
+
+redef class Bytes
+       # Returns `self` xored with `key`
+       #
+       # The key is cycled through until the `self` has been completely xored.
+       #
+       #     assert "goodmorning".to_bytes.xorcipher(" ".to_bytes) == "GOODMORNING".bytes
+       fun xorcipher(key: Bytes): Bytes do
+               var xored = new Bytes.with_capacity(self.length)
+
+               for i in self.length.times do
+                       xored.add(self[i] ^ key[i % key.length])
+               end
+
+               return xored
+       end
+end
+
+# Base class to modelize cryptographic ciphers
+abstract class Cipher
+
+       # Encrypted data
+       var ciphertext = new Bytes.empty is writable
+
+       # Unencrypted data
+       var plaintext = new Bytes.empty is writable
+
+       # Encrypt plaintext and populate `self.ciphertext`
+       fun encrypt is abstract
+
+       # Decrypt ciphertext and populate `self.plaintext`
+       fun decrypt is abstract
+
+end
+
+# Simple XOR cipher where the whole plaintext is XORed with a single byte.
+class SingleByteXorCipher
+       super Cipher
+
+       # Cryptographic key used in encryption and decryption.
+       var key: Byte = 0.to_b
+
+       redef fun encrypt do
+               var key_bytes = new Bytes.with_capacity(1)
+               key_bytes.add(key)
+               ciphertext = plaintext.xorcipher(key_bytes)
+       end
+
+       redef fun decrypt do
+               var key_bytes = new Bytes.with_capacity(1)
+               key_bytes.add(key)
+               plaintext = ciphertext.xorcipher(key_bytes)
+       end
+end
+
+# XOR cipher where the key is repeated to match the length of the message.
+class RepeatingKeyXorCipher
+       super Cipher
+
+       # Cryptographic key used in encryption and decryption.
+       var key = new Bytes.empty
+
+       redef fun encrypt do
+               assert key.length > 0
+               ciphertext = plaintext.xorcipher(key)
+       end
+
+       redef fun decrypt do
+               assert key.length > 0
+               plaintext = ciphertext.xorcipher(key)
+       end
+end
index a1eed2f..245d071 100644 (file)
@@ -162,6 +162,35 @@ class EulerCamera
                yaw = 0.0
                roll = 0.0
        end
+
+       # Convert the position `x, y` on screen, to world coordinates on the plane at `target_z`
+       #
+       # `target_z` defaults to `0.0` and specifies the Z coordinates of the plane
+       # on which to project the screen position `x, y`.
+       #
+       # This method assumes that the camera is looking along the Z axis towards higher values.
+       # Using it in a different orientation can be useful, but won't result in valid
+       # world coordinates.
+       fun camera_to_world(x, y: Numeric, target_z: nullable Float): Point[Float]
+       do
+               # TODO, this method could be tweaked to support projecting the 2D point,
+               # on the near plane (x,y) onto a given distance no matter to orientation
+               # of the camera.
+
+               target_z = target_z or else 0.0
+
+               # Convert from pixel units / window resolution to
+               # units on the near clipping wall to
+               # units on the target wall at Z = 0
+               var near_height = (field_of_view_y/2.0).tan * near
+               var cross_screen_to_near = near_height / (display.height.to_f/2.0)
+               var cross_near_to_target = (position.z - target_z) / near
+               var mod = cross_screen_to_near * cross_near_to_target * 1.72 # FIXME drop the magic number
+
+               var wx = position.x + (x.to_f-display.width.to_f/2.0) * mod
+               var wy = position.y - (y.to_f-display.height.to_f/2.0) * mod
+               return new Point[Float](wx, wy)
+       end
 end
 
 # Orthogonal camera to draw UI objects with services to work with screens of different sizes
index 03a3970..f1e5803 100644 (file)
@@ -103,6 +103,9 @@ class Mesh
        # Coordinates on the texture per vertex
        var texture_coords = new Array[Float] is lazy, writable
 
+       # `GLDrawMode` used to display this mesh, defaults to `gl_TRIANGLES`
+       fun draw_mode: GLDrawMode do return gl_TRIANGLES
+
        # Create an UV sphere of `radius` with `n_meridians` and `n_parallels`
        init uv_sphere(radius: Float, n_meridians, n_parallels: Int)
        do
index 613ff1a..b77c85e 100644 (file)
@@ -75,9 +75,9 @@ class SmoothMaterial
 
                # Execute draw
                if mesh.indices.is_empty then
-                       glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+                       glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
                else
-                       glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+                       glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
                end
        end
 end
@@ -154,11 +154,13 @@ class TexturedMaterial
                                var xd = sample_used_texture.offset_right - xa
                                var ya = sample_used_texture.offset_top
                                var yd = sample_used_texture.offset_bottom - ya
+                               xd *= 0.999
+                               yd *= 0.999
 
                                var tex_coords = new Array[Float].with_capacity(mesh.texture_coords.length)
                                for i in [0..mesh.texture_coords.length/2[ do
                                        tex_coords[i*2]   = xa + xd * mesh.texture_coords[i*2]
-                                       tex_coords[i*2+1] = ya + yd * mesh.texture_coords[i*2+1]
+                                       tex_coords[i*2+1] = 1.0 - (ya + yd * mesh.texture_coords[i*2+1])
                                end
 
                                program.tex_coord.array(tex_coords, 2)
@@ -180,9 +182,9 @@ class TexturedMaterial
                program.camera.uniform(app.world_camera.position.x, app.world_camera.position.y, app.world_camera.position.z)
 
                if mesh.indices.is_empty then
-                       glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+                       glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
                else
-                       glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+                       glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
                end
        end
 end
@@ -218,9 +220,9 @@ class NormalsMaterial
                program.normal.array(mesh.normals, 3)
 
                if mesh.indices.is_empty then
-                       glDrawArrays(gl_TRIANGLES, 0, mesh.vertices.length/3)
+                       glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
                else
-                       glDrawElements(gl_TRIANGLES, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+                       glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
                end
        end
 end
index 9f2318a..569973b 100644 (file)
@@ -124,7 +124,8 @@ class Cube
                var d = [1.0, 0.0]
 
                var texture_coords = new Array[Float]
-               for v in [c, d, a, a, d, b] do for i in 6.times do texture_coords.add_all v
+               var face = [a, c, d, a, d, b]
+               for i in 6.times do for v in face do texture_coords.add_all v
                return texture_coords
        end
 
index a126223..c2b9664 100644 (file)
@@ -50,7 +50,7 @@ class ModelAsset
 
                if leaves.is_empty then
                        # Nothing was loaded, use a cube with the default material
-                       var leaf = new LeafModel(new Cube, new SmoothMaterial.default)
+                       var leaf = placeholder_model
                        leaves.add leaf
                end
        end
@@ -168,7 +168,7 @@ private class ModelFromObj
                # Load textures need for these materials
                for name in texture_names do
                        if not asset_textures_by_name.keys.has(name) then
-                               var tex = new GamnitAssetTexture(name)
+                               var tex = new TextureAsset(name)
                                asset_textures_by_name[name] = tex
                        end
                end
@@ -379,8 +379,13 @@ end
 
 redef class Sys
        # Textures loaded from .mtl files for models
-       var asset_textures_by_name = new Map[String, GamnitAssetTexture]
+       var asset_textures_by_name = new Map[String, TextureAsset]
 
        # All instantiated asset models
        var models = new Set[ModelAsset]
+
+       # Blue cube of 1 unit on each side, acting as placeholder for models failing to load
+       #
+       # This model can be freely used by any `Actor` as placeholder or for debugging.
+       var placeholder_model = new LeafModel(new Cube, new SmoothMaterial.default) is lazy
 end
index 69ddb41..6e7c58c 100644 (file)
@@ -44,6 +44,15 @@ class GamnitDisplay
        # Only affects the desktop implementations.
        var show_cursor: Bool = true is writable
 
+       # Number of bits used for the red value in the color buffer
+       fun red_bits: Int do return 8
+
+       # Number of bits used for the green value in the color buffer
+       fun green_bits: Int do return 8
+
+       # Number of bits used for the blue value in the color buffer
+       fun blue_bits: Int do return 8
+
        # Prepare this display
        #
        # The implementation varies per platform.
index 3997c4a..e9d1b4a 100644 (file)
@@ -21,7 +21,7 @@ module display_android is
        android_manifest """<uses-feature android:glEsVersion="0x00020000"/>"""
 end
 
-import ::android
+import ::android::game
 intrude import android::load_image
 
 private import gamnit::egl
@@ -37,7 +37,7 @@ redef class GamnitDisplay
                setup_egl_display native_display
 
                # We need 8 bits per color for selection by color
-               select_egl_config(8, 8, 8, 0, 8, 0, 0)
+               select_egl_config(red_bits, green_bits, blue_bits, 0, 8, 0, 0)
 
                var format = egl_config.attribs(egl_display).native_visual_id
                native_window.set_buffers_geometry(0, 0, format)
@@ -48,7 +48,7 @@ redef class GamnitDisplay
        redef fun close do close_egl
 end
 
-redef class GamnitAssetTexture
+redef class TextureAsset
 
        redef fun load_from_platform
        do
index 8f115e7..1dbee21 100644 (file)
@@ -48,7 +48,7 @@ redef class GamnitDisplay
                setup_egl_display x11_display
 
                if debug_gamnit then print "Setting up EGL context"
-               select_egl_config(8, 8, 8, 8, 8, 0, 0)
+               select_egl_config(red_bits, green_bits, blue_bits, 8, 8, 0, 0)
                setup_egl_context window_handle
        end
 
@@ -95,7 +95,7 @@ redef class GamnitDisplay
        end
 end
 
-redef class GamnitAssetTexture
+redef class TextureAsset
 
        redef fun load_from_platform
        do
index 3fcb8c8..b451362 100644 (file)
@@ -46,14 +46,14 @@ redef class GamnitDisplay
        end
 
        # Select an EGL config
-       protected fun select_egl_config(blue, green, red, alpha, depth, stencil, sample: Int)
+       protected fun select_egl_config(red, green, blue, alpha, depth, stencil, sample: Int)
        do
                var config_chooser = new EGLConfigChooser
                config_chooser.renderable_type_egl
                config_chooser.surface_type_egl
-               config_chooser.blue_size = blue
-               config_chooser.green_size = green
                config_chooser.red_size = red
+               config_chooser.green_size = green
+               config_chooser.blue_size = blue
                if alpha > 0 then config_chooser.alpha_size = alpha
                if depth > 0 then config_chooser.depth_size = depth
                if stencil > 0 then config_chooser.stencil_size = stencil
index a300b54..896f07f 100644 (file)
@@ -29,8 +29,8 @@ redef class App
 
        # Current frame-rate
        #
-       # Updated each 5 seconds.
-       var current_fps = 0.0
+       # Updated each 5 seconds, initialized at the value of `maximum_fps`.
+       var current_fps: Float = maximum_fps is lazy
 
        redef fun frame_full
        do
@@ -47,7 +47,7 @@ redef class App
        private var frame_count = 0
 
        # Deadline used to compute `current_fps`
-       private var frame_count_deadline = 0
+       private var frame_count_deadline = 5.0
 
        # Check and sleep to maintain a frame-rate bellow `maximum_fps`
        #
@@ -55,12 +55,12 @@ redef class App
        # Is automatically called at the end of `full_frame`.
        fun limit_fps
        do
-               var t = clock.total.sec
+               var t = clock.total
                if t >= frame_count_deadline then
                        var cfps = frame_count.to_f / 5.0
                        self.current_fps = cfps
                        frame_count = 0
-                       frame_count_deadline = t + 5
+                       frame_count_deadline = t + 5.0
                end
                frame_count += 1
 
index 1a995ef..6859c76 100644 (file)
@@ -428,6 +428,23 @@ abstract class GamnitProgram
                end
        end
 
+       # Diagnose possible problems with the shaders of the program
+       #
+       # Lists to the console inactive uniforms and attributes.
+       # These may not be problematic but they can help to debug the program.
+       fun diagnose
+       do
+               if gl_program == null then compile_and_link
+
+               print "# Diagnose {class_name}"
+               for k,v in uniforms do
+                       if not v.is_active then print "* Uniform {v.name} is inactive"
+               end
+               for k,v in attributes do
+                       if not v.is_active then print "* Attribute {v.name} is inactive"
+               end
+       end
+
        # Attributes of this program organized by name
        #
        # Active attributes are gathered at `compile_and_link`.
similarity index 98%
rename from contrib/asteronits/src/texture_atlas_parser.nit
rename to lib/gamnit/texture_atlas_parser.nit
index 113b887..a0e4d5f 100644 (file)
@@ -44,7 +44,7 @@ var attributes = new Array[String]
 # Insert the first attribute, to load the root texture
 var png_file = "images" / xml_file.basename("xml") + "png"
 attributes.add """
-       var root_texture = new Texture("{{{png_file}}}")"""
+       var root_texture = new TextureAsset("{{{png_file}}}")"""
 
 # Read XML file
 var content = xml_file.to_path.read_all
index 415aa90..d644d60 100644 (file)
@@ -21,7 +21,7 @@ import display
 abstract class Texture
 
        # Prepare a texture located at `path` within the `assets` folder
-       new (path: Text) do return new GamnitAssetTexture(path.to_s)
+       new (path: Text) do return new TextureAsset(path.to_s)
 
        # Root texture of which `self` is derived
        fun root: GamnitRootTexture is abstract
@@ -41,7 +41,7 @@ abstract class Texture
        # OpenGL handle to this texture
        fun gl_texture: Int do return root.gl_texture
 
-       # Prepare a subtexture from this texture
+       # Prepare a subtexture from this texture, from the given pixel offsets
        fun subtexture(left, top, width, height: Numeric): GamnitSubtexture
        do
                # Setup the subtexture
@@ -143,7 +143,7 @@ class GamnitRootTexture
 end
 
 # Texture loaded from the assets folder
-class GamnitAssetTexture
+class TextureAsset
        super GamnitRootTexture
 
        # Path to this texture within the `assets` folder
index 94ad1c9..1be832e 100644 (file)
@@ -441,31 +441,40 @@ fun turn_left(p1, p2, p3: Point[Float]): Bool do
 end
 
 # Split a polygon into triangles
-# Useful for converting a concave polygon into multiple convex ones
-fun triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do
-       var poly = new Polygon(pts)
-       pts = poly.points
-       recursive_triangulate(pts, results)
+#
+# Useful for converting a concave polygon into multiple convex ones.
+#
+# See: the alternative `triangulate_recursive` uses arrays in-place.
+fun triangulate(points: Array[Point[Float]]): Array[ConvexPolygon]
+do
+       var results = new Array[ConvexPolygon]
+       triangulate_recursive(points.clone, results)
+       return results
 end
 
-private fun recursive_triangulate(pts: Array[Point[Float]], results: Array[ConvexPolygon]) do
-       if pts.length == 3 then
-               results.add(new ConvexPolygon(pts))
+# Split a polygon into triangles using arrays in-place
+#
+# Consumes the content of `points` and add the triangles to `results`.
+#
+# See: the alternative `triangulate` which does not modify `points` and returns a new array.
+fun triangulate_recursive(points: Array[Point[Float]], results: Array[ConvexPolygon]) do
+       if points.length == 3 then
+               results.add(new ConvexPolygon(points))
                return
        end
-       var prev = pts[pts.length - 1]
-       var curr = pts[0]
-       var next = pts[1]
-       for i in [1..pts.length[ do
+       var prev = points[points.length - 1]
+       var curr = points[0]
+       var next = points[1]
+       for i in [1..points.length[ do
                if turn_left(prev, curr, next) then
-                       prev = pts[i-1]
+                       prev = points[i-1]
                        curr = next
-                       if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1]
+                       if i+1 == points.length then next = points[points.length - 1] else next = points[i+1]
                        continue
                end
                var contains = false
                var triangle = new ConvexPolygon(new Array[Point[Float]].with_items(prev, curr, next))
-               for p in pts do
+               for p in points do
                        if p != prev and p != curr and p != next then
                                if triangle.contain(p) then
                                        contains = true
@@ -475,12 +484,12 @@ private fun recursive_triangulate(pts: Array[Point[Float]], results: Array[Conve
                end
                if not contains then
                        results.add(triangle)
-                       pts.remove(curr)
-                       recursive_triangulate(pts, results)
+                       points.remove(curr)
+                       triangulate_recursive(points, results)
                        break
                end
-               prev = pts[i-1]
+               prev = points[i-1]
                curr = next
-               if i+1 == pts.length then next = pts[pts.length - 1] else next = pts[i+1]
+               if i+1 == points.length then next = points[points.length - 1] else next = points[i+1]
        end
 end
index 0d43784..e472143 100644 (file)
@@ -72,8 +72,8 @@ extern class GtkAssistant `{GtkAssistant *`}
                gtk_assistant_set_page_type(self, page, t);
        `}
 
-       fun get_page_title(page: GtkWidget): String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_assistant_get_page_title(self, page));
+       fun get_page_title(page: GtkWidget): String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_assistant_get_page_title(self, page));
        `}
 
        fun set_page_title(page: GtkWidget, title: String) import String.to_cstring `{
index ae6b915..b6fec7c 100644 (file)
@@ -331,8 +331,8 @@ extern class GtkFrame `{GtkFrame *`}
                return (GtkFrame *)gtk_frame_new(String_to_cstring(lbl));
        `}
 
-       fun frame_label: String `{
-               return NativeString_to_s((char *)gtk_frame_get_label(self));
+       fun frame_label: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_frame_get_label(self));
        `}
 
        fun frame_label=(lbl: String) import String.to_cstring `{
@@ -682,8 +682,8 @@ extern class GtkLabel `{GtkLabel *`}
        `}
 
        # Returns the text of the label
-       fun text: String import NativeString.to_s `{
-               return NativeString_to_s((char*)gtk_label_get_text(self));
+       fun text: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char*)gtk_label_get_text(self));
        `}
 
        # Sets the angle of rotation for the label.
@@ -803,8 +803,8 @@ extern class GtkButton `{GtkButton *`}
                return (GtkButton *)gtk_button_new_from_stock(String_to_cstring(stock_id));
        `}
 
-       fun text: String `{
-               return NativeString_to_s((char *)gtk_button_get_label(self));
+       fun text: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_button_get_label(self));
        `}
 
        fun text=(value: String) import String.to_cstring `{
@@ -876,8 +876,8 @@ extern class GtkExpander `{GtkExpander *`}
                gtk_expander_set_spacing(self, pixels);
        `}
 
-       fun label_text: String `{
-               return NativeString_to_s((char *)gtk_expander_get_label(self));
+       fun label_text: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_expander_get_label(self));
        `}
 
        fun label_text=(lbl: String) import String.to_cstring `{
@@ -995,8 +995,8 @@ extern class GtkComboBox `{GtkComboBox *`}
                gtk_combo_box_set_id_column(self, id_column);
        `}
 
-       fun active_id: String `{
-               return NativeString_to_s((char *)gtk_combo_box_get_active_id(self));
+       fun active_id: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_combo_box_get_active_id(self));
        `}
 
        fun active_id=(id_active: String) import String.to_cstring `{
@@ -1019,8 +1019,8 @@ extern class GtkComboBox `{GtkComboBox *`}
                gtk_combo_box_popdown(self);
        `}
 
-       fun title: String `{
-               return NativeString_to_s((char *)gtk_combo_box_get_title(self));
+       fun title: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_combo_box_get_title(self));
        `}
 
        fun title=(t: String) import String.to_cstring `{
index d4ff95f..f28a91b 100644 (file)
@@ -52,40 +52,40 @@ extern class GtkAboutDialog `{GtkAboutDialog *`}
                return (GtkAboutDialog *)gtk_about_dialog_new();
        `}
 
-       fun program_name: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_about_dialog_get_program_name(self));
+       fun program_name: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_about_dialog_get_program_name(self));
        `}
 
        fun program_name=(name: String) import String.to_cstring `{
                gtk_about_dialog_set_program_name(self, String_to_cstring(name));
        `}
 
-       fun version: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_about_dialog_get_version(self));
+       fun version: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_about_dialog_get_version(self));
        `}
 
        fun version=(v: String) import String.to_cstring `{
                gtk_about_dialog_set_version(self, String_to_cstring(v));
        `}
 
-       fun copyright: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_about_dialog_get_copyright(self));
+       fun copyright: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_about_dialog_get_copyright(self));
        `}
 
        fun copyright=(c: String) import String.to_cstring `{
                gtk_about_dialog_set_copyright(self, String_to_cstring(c));
        `}
 
-       fun comments: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_about_dialog_get_comments(self));
+       fun comments: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_about_dialog_get_comments(self));
        `}
 
        fun comments=(com: String) import String.to_cstring `{
                gtk_about_dialog_set_comments(self, String_to_cstring(com));
        `}
 
-       fun license: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_about_dialog_get_license(self));
+       fun license: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_about_dialog_get_license(self));
        `}
 
        fun license=(li: String) import String.to_cstring `{
@@ -94,16 +94,16 @@ extern class GtkAboutDialog `{GtkAboutDialog *`}
 
        # license_type
 
-       fun website: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_about_dialog_get_website(self));
+       fun website: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_about_dialog_get_website(self));
        `}
 
        fun website=(link: String) import String.to_cstring `{
                gtk_about_dialog_set_website(self, String_to_cstring(link));
        `}
 
-       fun website_label: String import NativeString.to_s `{
-               return NativeString_to_s((char *) gtk_about_dialog_get_website_label(self));
+       fun website_label: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *) gtk_about_dialog_get_website_label(self));
        `}
 
        fun website_label=(link_label: String) import String.to_cstring `{
@@ -111,8 +111,8 @@ extern class GtkAboutDialog `{GtkAboutDialog *`}
        `}
 
        # TODO
-       # fun authors: String`{
-       #               return NativeString_to_s(gtk_about_dialog_get_authors(self));
+       # fun authors: String  import NativeString.to_s_with_copy `{
+       #               return NativeString_to_s_with_copy(gtk_about_dialog_get_authors(self));
        # `}
 
        # TODO
@@ -144,8 +144,8 @@ extern class GtkAppChooserDialog `{GtkAppChooserDialog *`}
 
        fun widget: GtkWidget `{ return gtk_app_chooser_dialog_get_widget(self); `}
 
-       fun heading: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_app_chooser_dialog_get_heading(self));
+       fun heading: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_app_chooser_dialog_get_heading(self));
        `}
 
        fun heading=(text: String) import String.to_cstring `{
index 25265c4..1cf98ee 100644 (file)
@@ -131,8 +131,8 @@ extern class GtkProgressBar `{GtkProgressBar *`}
                gtk_progress_bar_set_show_text(self, show);
        `}
 
-       fun text: String import NativeString.to_s `{
-               return NativeString_to_s((char *)gtk_progress_bar_get_text(self));
+       fun text: String import NativeString.to_s_with_copy `{
+               return NativeString_to_s_with_copy((char *)gtk_progress_bar_get_text(self));
        `}
 
        fun text=(value: String) import String.to_cstring `{
index a4a0000..48c7e12 100644 (file)
@@ -25,16 +25,16 @@ in "ObjC" `{
 @interface NitCallbackReference: NSObject
 
        // Nit object target of the callbacks from UI events
-       @property (nonatomic) Button nit_button;
+       @property (nonatomic) View nit_view;
 
        // Actual callback method
-       -(void) nitOnEvent: (UIButton*) sender;
+       -(void) nitOnEvent: (UIView*) sender;
 @end
 
 @implementation NitCallbackReference
 
-       -(void) nitOnEvent: (UIButton*) sender {
-               Button_on_click(self.nit_button);
+       -(void) nitOnEvent: (UIView*) sender {
+               View_on_ios_event(self.nit_view);
        }
 @end
 
@@ -112,6 +112,19 @@ redef class App
                set_view_controller(app_delegate.window, window.native)
                super
        end
+
+       # Use iOS ` popViewControllerAnimated`
+       redef fun pop_window
+       do
+               window_stack.pop
+               pop_view_controller app_delegate.window
+               window.on_resume
+       end
+
+       private fun pop_view_controller(window: UIWindow) in "ObjC" `{
+               UINavigationController *navController = (UINavigationController*)window.rootViewController;
+               [navController popViewControllerAnimated: YES];
+       `}
 end
 
 redef class AppDelegate
@@ -136,6 +149,8 @@ redef class View
        redef type NATIVE: UIView
 
        redef var enabled = null is lazy
+
+       private fun on_ios_event do end
 end
 
 redef class CompositeControl
@@ -180,7 +195,6 @@ redef class Layout
        init
        do
                native.alignment = new UIStackViewAlignment.fill
-               native.distribution = new UIStackViewDistribution.fill_equally
 
                # TODO make customizable
                native.spacing = 4.0
@@ -197,11 +211,19 @@ redef class Layout
 end
 
 redef class HorizontalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.horizontal
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.horizontal
+               native.distribution = new UIStackViewDistribution.fill_equally
+       end
 end
 
 redef class VerticalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.vertical
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.vertical
+               native.distribution = new UIStackViewDistribution.equal_spacing
+       end
 end
 
 redef class Label
@@ -253,15 +275,20 @@ redef class CheckBox
        # `UISwitch` acting as the real check box
        var ui_switch: UISwitch is noautoinit
 
-       init do
+       redef fun on_ios_event do notify_observers new ToggleEvent(self)
+
+       init
+       do
                # Tweak the layout so it is centered
-               layout.native.distribution = new UIStackViewDistribution.fill_proportionally
-               layout.native.alignment = new UIStackViewAlignment.center
+               layout.native.distribution = new UIStackViewDistribution.equal_spacing
+               layout.native.alignment = new UIStackViewAlignment.fill
                layout.native.layout_margins_relative_arrangement = true
 
                var s = new UISwitch
                native.add_arranged_subview s
                ui_switch = s
+
+               ui_switch.set_callback self
        end
 
        redef fun text=(text) do lbl.text = text
@@ -271,6 +298,23 @@ redef class CheckBox
        redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
 end
 
+redef class UISwitch
+       # Register callbacks on this switch to be relayed to `sender`
+       private fun set_callback(sender: View)
+       import View.on_ios_event in "ObjC" `{
+
+               NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
+               ncr.nit_view = sender;
+
+               // Pin the objects in both Objective-C and Nit GC
+               View_incr_ref(sender);
+               ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
+
+               [self addTarget:ncr action:@selector(nitOnEvent:)
+                       forControlEvents:UIControlEventValueChanged];
+       `}
+end
+
 redef class TextInput
 
        redef type NATIVE: UITextField
@@ -293,25 +337,25 @@ redef class Button
 
        init do native.set_callback self
 
+       redef fun on_ios_event do notify_observers new ButtonPressEvent(self)
+
        redef fun text=(text) do if text != null then native.title = text.to_nsstring
        redef fun text do return native.current_title.to_s
 
-       private fun on_click do notify_observers new ButtonPressEvent(self)
-
        redef fun enabled=(enabled) do native.enabled = enabled or else true
        redef fun enabled do return native.enabled
 end
 
 redef class UIButton
        # Register callbacks on this button to be relayed to `sender`
-       private fun set_callback(sender: Button)
-       import Button.on_click in "ObjC" `{
+       private fun set_callback(sender: View)
+       import View.on_ios_event in "ObjC" `{
 
                NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
-               ncr.nit_button = sender;
+               ncr.nit_view = sender;
 
                // Pin the objects in both Objective-C and Nit GC
-               Button_incr_ref(sender);
+               View_incr_ref(sender);
                ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
 
                [self addTarget:ncr action:@selector(nitOnEvent:)
@@ -333,7 +377,7 @@ redef class ListLayout
                native_stack_view.translates_autoresizing_mask_into_constraits = false
                native_stack_view.axis = new UILayoutConstraintAxis.vertical
                native_stack_view.alignment = new UIStackViewAlignment.fill
-               native_stack_view.distribution = new UIStackViewDistribution.fill_equally
+               native_stack_view.distribution = new UIStackViewDistribution.equal_spacing
                native_stack_view.spacing = 4.0
 
                native.add_subview native_stack_view
index 4d00782..45578f2 100644 (file)
 
 # Handles serialization and deserialization of objects to/from JSON
 #
-# ## Nity JSON
+# ## Writing JSON with metadata
 #
 # `JsonSerializer` write Nit objects that subclass `Serializable` to JSON,
-# and `JsonDeserializer` can read them. They both use meta-data added to the
+# and `JsonDeserializer` can read them. They both use metadata added to the
 # generated JSON to recreate the Nit instances with the exact original type.
 #
 # For more information on Nit serialization, see: ../serialization/README.md
 #
-# ## Plain JSON
+# ## Writing plain JSON
 #
 # The attribute `JsonSerializer::plain_json` triggers generating plain and
 # clean JSON. This format is easier to read for an human and a non-Nit program,
 # but it cannot be fully deserialized. It can still be read by services from
 # `json::static` and `json::dynamic`.
 #
-# A shortcut to this service is provided by `Serializable::to_plain_json`.
+# A shortcut to these writing services is provided by `Serializable::serialize_to_json`.
 #
 # ### Usage Example
 #
 # var bob = new Person("Bob", 1986)
 # var alice = new Person("Alice", 1978, bob)
 #
-# assert bob.to_plain_json == """
-# {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}"""
+# assert bob.serialize_to_json(pretty=true, plain=true) == """
+#{
+#      "name": "Bob",
+#      "year_of_birth": 1986,
+#      "next_of_kin": null
+#}"""
 #
-# assert alice.to_plain_json == """
-# {"name": "Alice", "year_of_birth": 1978, "next_of_kin": {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}}"""
+# assert alice.serialize_to_json(pretty=true, plain=true) == """
+#{
+#      "name": "Alice",
+#      "year_of_birth": 1978,
+#      "next_of_kin": {
+#              "name": "Bob",
+#              "year_of_birth": 1986,
+#              "next_of_kin": null
+#      }
+#}"""
 # ~~~
 #
 # ## JSON to Nit objects
 #
-# The `JsonDeserializer` support reading JSON code with minimal meta-data
+# The `JsonDeserializer` support reading JSON code with minimal metadata
 # to easily create Nit object from client-side code or configuration files.
 # Each JSON object must define the `__class` attribute with the corresponding
 # Nit class and the expected attributes with its name in Nit followed by its value.
 # var deserializer = new JsonDeserializer(json_code)
 #
 # var meet = deserializer.deserialize
+#
+# # Check for errors
+# assert deserializer.errors.is_empty
+#
 # assert meet isa MeetupConfig
 # assert meet.description == "My Awesome Meetup"
 # assert meet.max_participants == null
@@ -90,7 +106,8 @@ module serialization
 
 import ::serialization::caching
 private import ::serialization::engine_tools
-import static
+private import static
+private import string_parser
 
 # Serializer of Nit objects to Json string.
 class JsonSerializer
@@ -103,14 +120,14 @@ class JsonSerializer
        #
        # If `false`, the default, serialize to support deserialization:
        #
-       # * Write meta-data, including the types of the serialized objects so they can
+       # * Write metadata, including the types of the serialized objects so they can
        #   be deserialized to their original form using `JsonDeserializer`.
        # * Use references when an object has already been serialized so to not duplicate it.
        # * Support cycles in references.
        # * Preserve the Nit `Char` type as an object because it does not exist in JSON.
        # * The generated JSON is standard and can be read by non-Nit programs.
        #   However, some Nit types are not represented by the simplest possible JSON representation.
-       #   With the added meta-data, it can be complex to read.
+       #   With the added metadata, it can be complex to read.
        #
        # If `true`, serialize for other programs:
        #
@@ -119,7 +136,7 @@ class JsonSerializer
        # * Nit objects are serialized for every references, so they can be duplicated.
        #   It is easier to read but it creates a larger output.
        # * Does not support cycles, will replace the problematic references by `null`.
-       # * Does not serialize the meta-data needed to deserialize the objects
+       # * Does not serialize the metadata needed to deserialize the objects
        #   back to regular Nit objects.
        # * Keys of Nit `HashMap` are converted to their string representation using `to_s`.
        var plain_json = false is writable
@@ -162,7 +179,7 @@ class JsonSerializer
                        end
 
                        first_attribute = true
-                       object.serialize_to_json self
+                       object.accept_json_serializer self
                        first_attribute = false
 
                        if plain_json then open_objects.pop
@@ -220,10 +237,10 @@ class JsonDeserializer
        private var text: Text
 
        # Root json object parsed from input text.
-       private var root: nullable Jsonable is noinit
+       private var root: nullable Object is noinit
 
        # Depth-first path in the serialized object tree.
-       private var path = new Array[JsonObject]
+       private var path = new Array[Map[String, nullable Object]]
 
        # Last encountered object reference id.
        #
@@ -232,7 +249,7 @@ class JsonDeserializer
 
        init do
                var root = text.parse_json
-               if root isa JsonObject then path.add(root)
+               if root isa Map[String, nullable Object] then path.add(root)
                self.root = root
        end
 
@@ -268,7 +285,7 @@ class JsonDeserializer
                        return null
                end
 
-               if object isa JsonObject then
+               if object isa Map[String, nullable Object] then
                        var kind = null
                        if object.keys.has("__kind") then
                                kind = object["__kind"]
@@ -467,7 +484,7 @@ class JsonDeserializer
        # deserialized = deserializer.deserialize
        # assert deserialized isa MyError
        # ~~~
-       protected fun class_name_heuristic(json_object: JsonObject): nullable String
+       protected fun class_name_heuristic(json_object: Map[String, nullable Object]): nullable String
        do
                return null
        end
@@ -492,11 +509,44 @@ redef class Text
                return res
        end
 
-       redef fun serialize_to_json(v) do v.stream.write(to_json)
+       redef fun accept_json_serializer(v) do v.stream.write(to_json)
 end
 
 redef class Serializable
-       private fun serialize_to_json(v: JsonSerializer)
+
+       # Serialize `self` to JSON
+       #
+       # Set `plain = true` to generate standard JSON, without deserialization metadata.
+       # Use this option if the generated JSON will be read by other programs or humans.
+       # Use the default, `plain = false`, if the JSON is to be deserialized by a Nit program.
+       #
+       # Set `pretty = true` to generate pretty JSON for human eyes.
+       # Use the default, `pretty = false`, to generate minified JSON.
+       #
+       # This method should not be refined by subclasses,
+       # instead `accept_json_serializer` can customize the serialization of an object.
+       #
+       # See: `JsonSerializer`
+       fun serialize_to_json(plain, pretty: nullable Bool): String
+       do
+               var stream = new StringWriter
+               var serializer = new JsonSerializer(stream)
+               serializer.plain_json = plain or else false
+               serializer.pretty_json = pretty or else false
+               serializer.serialize self
+               stream.close
+               return stream.to_s
+       end
+
+       # Refinable service to customize the serialization of this class to JSON
+       #
+       # This method can be refined to customize the serialization by either
+       # writing pure JSON directly on the stream `v.stream` or
+       # by using other services of `JsonSerializer`.
+       #
+       # Most of the time, it is preferable to refine the method `core_serialize_to`
+       # which is used by all the serialization engines, not just JSON.
+       protected fun accept_json_serializer(v: JsonSerializer)
        do
                var id = v.cache.new_id_for(self)
                v.stream.write "\{"
@@ -515,59 +565,35 @@ redef class Serializable
                v.new_line_and_indent
                v.stream.write "\}"
        end
-
-       # Serialize this object to a JSON string with metadata for deserialization
-       fun to_json_string: String
-       do
-               var stream = new StringWriter
-               var serializer = new JsonSerializer(stream)
-               serializer.serialize self
-               stream.close
-               return stream.to_s
-       end
-
-       # Serialize this object to plain JSON
-       #
-       # This is a shortcut using `JsonSerializer::plain_json`,
-       # see its documentation for more information.
-       fun to_plain_json: String
-       do
-               var stream = new StringWriter
-               var serializer = new JsonSerializer(stream)
-               serializer.plain_json = true
-               serializer.serialize self
-               stream.close
-               return stream.to_s
-       end
 end
 
 redef class Int
-       redef fun serialize_to_json(v) do v.stream.write(to_s)
+       redef fun accept_json_serializer(v) do v.stream.write to_s
 end
 
 redef class Float
-       redef fun serialize_to_json(v) do v.stream.write(to_s)
+       redef fun accept_json_serializer(v) do v.stream.write to_s
 end
 
 redef class Bool
-       redef fun serialize_to_json(v) do v.stream.write(to_s)
+       redef fun accept_json_serializer(v) do v.stream.write to_s
 end
 
 redef class Char
-       redef fun serialize_to_json(v)
+       redef fun accept_json_serializer(v)
        do
                if v.plain_json then
-                       v.stream.write to_s.to_json
+                       to_s.accept_json_serializer v
                else
                        v.stream.write "\{\"__kind\": \"char\", \"__val\": "
-                       v.stream.write to_s.to_json
+                       to_s.accept_json_serializer v
                        v.stream.write "\}"
                end
        end
 end
 
 redef class NativeString
-       redef fun serialize_to_json(v) do to_s.serialize_to_json(v)
+       redef fun accept_json_serializer(v) do to_s.accept_json_serializer(v)
 end
 
 redef class Collection[E]
@@ -595,7 +621,7 @@ redef class Collection[E]
 end
 
 redef class SimpleCollection[E]
-       redef fun serialize_to_json(v)
+       redef fun accept_json_serializer(v)
        do
                # Register as pseudo object
                if not v.plain_json then
@@ -638,7 +664,7 @@ redef class SimpleCollection[E]
 end
 
 redef class Map[K, V]
-       redef fun serialize_to_json(v)
+       redef fun accept_json_serializer(v)
        do
                # Register as pseudo object
                var id = v.cache.new_id_for(self)
@@ -654,7 +680,7 @@ redef class Map[K, V]
                                v.new_line_and_indent
 
                                var k = key or else "null"
-                               v.stream.write k.to_s.to_json
+                               k.to_s.accept_json_serializer v
                                v.stream.write ": "
                                if not v.try_to_serialize(val) then
                                        assert val != null # null would have been serialized
index 565da1f..e333b85 100644 (file)
@@ -31,6 +31,9 @@ private import json_lexer
 interface Jsonable
        # Encode `self` in JSON.
        #
+       # This is a recursive method which can be refined by any subclasses.
+       # To write any `Serializable` object to JSON, see `serialize_to_json`.
+       #
        # SEE: `append_json`
        fun to_json: String is abstract
 
index ca004a0..1e26f17 100644 (file)
@@ -43,7 +43,7 @@ redef class App
                bar.title = "app.nit" # TODO offer a portable API to name windows
                bar.show_close_button = true
 
-               # TODO add back button
+               bar.add back_button.native
 
                return bar
        end
@@ -55,6 +55,9 @@ redef class App
                return stack
        end
 
+       # Button on the header bar to go back
+       var back_button = new BackButton is lazy
+
        # On GNU/Linux, we go through all the callbacks once,
        # there is no complex life-cycle.
        redef fun run
@@ -64,7 +67,6 @@ redef class App
                app.on_start
                app.on_resume
 
-               native_window.show_all
                gtk_main
 
                app.on_pause
@@ -88,7 +90,13 @@ redef class App
                # improved with GTK 3.18 and interpolate_size.
                native_window.resizable = false
 
+               native_window.show_all
+
                super
+
+               if window.enable_back_button then
+                       back_button.native.show
+               else back_button.native.hide
        end
 end
 
@@ -243,6 +251,21 @@ redef class Button
        init do native.signal_connect("clicked", self, null)
 end
 
+# Button to go back between windows
+class BackButton
+       super Button
+
+       # TODO i18n
+       redef fun text=(value) do super(value or else "Back")
+
+       redef fun signal(sender, data)
+       do
+               super
+
+               app.window.on_back_button
+       end
+end
+
 redef class Label
        redef type NATIVE: GtkLabel
        redef var native = new GtkLabel("")
@@ -294,6 +317,9 @@ redef class CheckBox
        redef type NATIVE: GtkCheckButton
        redef var native = new GtkCheckButton
 
+       redef fun signal(sender, data) do notify_observers new ToggleEvent(self)
+       init do native.signal_connect("toggled", self, null)
+
        redef fun text do return native.text
        redef fun text=(value) do native.text = (value or else "").to_s
 
index c8f02e3..e78da16 100644 (file)
@@ -22,7 +22,7 @@ module android_app is android_manifest_activity """
 
 import mnit
 import mnit::opengles1
-import ::android
+import ::android::game
 intrude import ::android::input_events
 
 in "C" `{
index 3841bc6..f4abd92 100644 (file)
@@ -44,19 +44,19 @@ redef class App
        private var frame_count = 0
 
        # Deadline used to compute `current_fps`
-       private var frame_count_deadline = 0
+       private var frame_count_deadline = 0.0
 
        # Check and sleep to maitain a frame-rate bellow `maximum_fps`
        # Also periodically uptate `current_fps`
        # Is automatically called at the end of `full_frame`.
        fun limit_fps
        do
-               var t = clock.total.sec
+               var t = clock.total
                if t >= frame_count_deadline then
                        var cfps = frame_count.to_f / 5.0
                        self.current_fps = cfps
                        frame_count = 0
-                       frame_count_deadline = t + 5
+                       frame_count_deadline = t + 5.0
                end
                frame_count += 1
 
index 6bc8b36..2db66b8 100644 (file)
@@ -26,7 +26,8 @@ and the FFI enables calling services in different languages.
 A minimal example follows with a custom `Action` and using `FileServer`.
 
 More general examples are available at `lib/nitcorn/examples/`.
-It includes the configuration of `http://xymus.net/` which merges many other _nitcorn_ applications.
+For an example of a larger project merging many _nitcorn_ applications into one server,
+take a look at the configuration of `http://xymus.net/` at `../contrib/xymus_net/xymus_net.nit`.
 
 Larger projects using _nitcorn_ can be found in the `contrib/` folder:
 * _opportunity_ is a meetup planner heavily based on _nitcorn_.
index d1bdeae..b1bbed9 100644 (file)
@@ -2,10 +2,6 @@ all: bin/restful_annot
        mkdir -p bin/
        ../../../bin/nitc --dir bin src/nitcorn_hello_world.nit src/simple_file_server.nit
 
-xymus.net:
-       mkdir -p bin/
-       ../../../bin/nitc --dir bin/ src/xymus_net.nit
-
 pre-build: src/restful_annot_gen.nit
 src/restful_annot_gen.nit:
        ../../../bin/nitrestful -o $@ src/restful_annot.nit
index ea3212d..a9ef5f6 100644 (file)
@@ -71,6 +71,15 @@ class FileServer
        # Caching attributes of served files, used as the `cache-control` field in response headers
        var cache_control = "public, max-age=360" is writable
 
+       # Show directory listing?
+       var show_directory_listing = true is writable
+
+       # Default file returned when no static file matches the requested URI.
+       #
+       # If no `default_file` is provided, the FileServer responds 404 error to
+       # unmatched queries.
+       var default_file: nullable String = null is writable
+
        redef fun answer(request, turi)
        do
                var response
@@ -83,15 +92,14 @@ class FileServer
                # This make sure that the requested file is within the root folder.
                if (local_file + "/").has_prefix(root) then
                        # Does it exists?
-                       if local_file.file_exists then
-                               if local_file.file_stat.is_dir then
+                       var file_stat = local_file.file_stat
+                       if file_stat != null then
+                               if file_stat.is_dir then
                                        # If we target a directory without an ending `/`,
                                        # redirect to the directory ending with `/`.
-                                       if not request.uri.is_empty and
-                                          request.uri.chars.last != '/' then
-                                               response = new HttpResponse(303)
-                                               response.header["Location"] = request.uri + "/"
-                                               return response
+                                       var uri = request.uri
+                                       if not uri.is_empty and uri.chars.last != '/' then
+                                               return answer_redirection(request.uri + "/")
                                        end
 
                                        # Show index file instead of the directory listing
@@ -105,33 +113,94 @@ class FileServer
                                        end
                                end
 
-                               response = new HttpResponse(200)
-                               if local_file.file_stat.is_dir then
-                                       # Show the directory listing
-                                       var title = turi
-                                       var files = local_file.files
+                               file_stat = local_file.file_stat
+                               if show_directory_listing and file_stat != null and file_stat.is_dir then
+                                       response = answer_directory_listing(request, turi, local_file)
+                               else if file_stat != null and not file_stat.is_dir then # It's a single file
+                                       response = answer_file(local_file)
+                               else response = answer_default
+                       else response = answer_default
+               else response = new HttpResponse(403)
 
-                                       alpha_comparator.sort files
+               if response.status_code != 200 then
+                       var tmpl = error_page(response.status_code)
+                       if header != null and tmpl isa ErrorTemplate then tmpl.header = header
+                       response.body = tmpl.to_s
+               end
 
-                                       var links = new Array[String]
-                                       if turi.length > 1 then
-                                               var path = (request.uri + "/..").simplify_path
-                                               links.add "<a href=\"{path}/\">..</a>"
-                                       end
-                                       for file in files do
-                                               var local_path = local_file.join_path(file).simplify_path
-                                               var web_path = file.simplify_path
-                                               if local_path.file_stat.is_dir then web_path = web_path + "/"
-                                               links.add "<a href=\"{web_path}\">{file}</a>"
-                                       end
+               return response
+       end
+
+       # Answer the `default_file` if any.
+       fun answer_default: HttpResponse do
+               var default_file = self.default_file
+               if default_file == null then
+                       return new HttpResponse(404)
+               end
+
+               var local_file = (root / default_file).simplify_path
+               return answer_file(local_file)
+       end
 
-                                       var header = self.header
-                                       var header_code
-                                       if header != null then
-                                               header_code = header.write_to_string
-                                       else header_code = ""
+       # Answer a 303 redirection to `location`.
+       fun answer_redirection(location: String): HttpResponse do
+               var response = new HttpResponse(303)
+               response.header["Location"] = location
+               return response
+       end
+
+       # Build a reponse containing a single `local_file`.
+       #
+       # Returns a 404 error if local_file does not exists.
+       fun answer_file(local_file: String): HttpResponse do
+               if not local_file.file_exists then return new HttpResponse(404)
+
+               var response = new HttpResponse(200)
+               response.files.add local_file
+
+               # Set Content-Type depending on the file extension
+               var ext = local_file.file_extension
+               if ext != null then
+                       var media_type = media_types[ext]
+                       if media_type != null then
+                               response.header["Content-Type"] = media_type
+                       else response.header["Content-Type"] = "application/octet-stream"
+               end
 
-                                       response.body = """
+               # Cache control
+               response.header["cache-control"] = cache_control
+               return response
+       end
+
+       # Answer with a directory listing for files within `local_files`.
+       fun answer_directory_listing(request: HttpRequest, turi, local_file: String): HttpResponse do
+               # Show the directory listing
+               var title = turi
+               var files = local_file.files
+
+               alpha_comparator.sort files
+
+               var links = new Array[String]
+               if turi.length > 1 then
+                       var path = (request.uri + "/..").simplify_path
+                       links.add "<a href=\"{path}/\">..</a>"
+               end
+               for file in files do
+                       var local_path = local_file.join_path(file).simplify_path
+                       var web_path = file.simplify_path
+                       var file_stat = local_path.file_stat
+                       if file_stat != null and file_stat.is_dir then web_path = web_path + "/"
+                       links.add "<a href=\"{web_path}\">{file}</a>"
+               end
+
+               var header = self.header
+               var header_code
+               if header != null then
+                       header_code = header.write_to_string
+               else header_code = ""
+
+               var response = new HttpResponse(200)
+               response.body = """
 <!DOCTYPE html>
 <head>
        <meta charset="utf-8">
@@ -153,32 +222,7 @@ class FileServer
 </body>
 </html>"""
 
-                                       response.header["Content-Type"] = media_types["html"].as(not null)
-                               else
-                                       # It's a single file
-                                       response.files.add local_file
-
-                                       var ext = local_file.file_extension
-                                       if ext != null then
-                                               var media_type = media_types[ext]
-                                               if media_type != null then
-                                                       response.header["Content-Type"] = media_type
-                                               else response.header["Content-Type"] = "application/octet-stream"
-                                       end
-
-                                       # Cache control
-                                       response.header["cache-control"] = cache_control
-                               end
-
-                       else response = new HttpResponse(404)
-               else response = new HttpResponse(403)
-
-               if response.status_code != 200 then
-                       var tmpl = error_page(response.status_code)
-                       if header != null and tmpl isa ErrorTemplate then tmpl.header = header
-                       response.body = tmpl.to_s
-               end
-
+               response.header["Content-Type"] = media_types["html"].as(not null)
                return response
        end
 end
index 73c9626..37dd9db 100644 (file)
@@ -51,6 +51,7 @@ class MediaTypes
                types["jar"]        = "application/java-archive"
                types["war"]        = "application/java-archive"
                types["ear"]        = "application/java-archive"
+               types["json"]       = "application/json"
                types["hqx"]        = "application/mac-binhex40"
                types["pdf"]        = "application/pdf"
                types["cco"]        = "application/x-cocoa"
index bccfd2c..158643a 100644 (file)
@@ -35,11 +35,13 @@ class RestfulAction
                if val == null then return null
 
                var deserializer = new JsonDeserializer(val)
+               var obj = deserializer.deserialize
+
                if deserializer.errors.not_empty then
                        print_error deserializer.errors.join("\n")
                        return null
                end
 
-               return deserializer.deserialize
+               return obj
        end
 end
index 4d54aa9..005a4dd 100644 (file)
@@ -72,7 +72,7 @@ end
 
 redef class HttpRequest
        # The `Session` associated to this request
-       var session: nullable Session = null
+       var session: nullable Session = null is writable
 end
 
 redef class HttpRequestParser
index 07664f6..3ec5dc1 100644 (file)
@@ -226,6 +226,8 @@ private class UriParam
 
        # Parameters match everything.
        redef fun match(part) do return true
+
+       redef fun to_s do return name
 end
 
 # A static uri string like `users`.
@@ -237,6 +239,8 @@ private class UriString
 
        # Empty strings match everything otherwise matching is based on string equality.
        redef fun match(part) do return string.is_empty or string == part
+
+       redef fun to_s do return string
 end
 
 redef class Routes
index ba09bc1..cf4d0fa 100644 (file)
@@ -359,3 +359,72 @@ redef universal Int
                return self & 0x3FFF_FFFF
        end
 end
+
+redef universal Float
+       # Smoothened `self`, used by `ImprovedNoise`
+       private fun fade: Float do return self*self*self*(self*(self*6.0-15.0)+10.0)
+end
+
+# Direct translation of Ken Perlin's improved noise Java implementation
+#
+# This implementation differs from `PerlinNoise` on two main points.
+# This noise is calculated for a 3D point, vs 2D in `PerlinNoise`.
+# `PerlinNoise` is based off a customizable seed, while this noise has a static data source.
+class ImprovedNoise
+
+       # Permutations
+       private var p: Array[Int] = [151,160,137,91,90,15,
+               131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
+               190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
+               88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
+               77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
+               102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
+               135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
+               5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
+               223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
+               129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
+               251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
+               49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
+               138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180] * 2
+
+       # Noise value in [-1..1] at 3D coordinates `x, y, z`
+       fun noise(x, y, z: Float): Float
+       do
+               var xx = x.floor.to_i & 255
+               var yy = y.floor.to_i & 255
+               var zz = z.floor.to_i & 255
+
+               x -= x.floor
+               y -= y.floor
+               z -= z.floor
+
+               var u = x.fade
+               var v = y.fade
+               var w = z.fade
+
+               var a  = p[xx  ] + yy
+               var aa = p[a   ] + zz
+               var ab = p[a+1 ] + zz
+               var b  = p[xx+1] + yy
+               var ba = p[b   ] + zz
+               var bb = p[b+1 ] + zz
+
+               return w.lerp(v.lerp(u.lerp(grad(p[aa  ], x,     y,     z    ),
+                                           grad(p[ba  ], x-1.0, y,     z    )),
+                                    u.lerp(grad(p[ab  ], x,     y-1.0, z    ),
+                                           grad(p[bb  ], x-1.0, y-1.0, z    ))),
+                      v.lerp(u.lerp(grad(p[aa+1], x,     y,     z-1.0),
+                                           grad(p[ba+1], x-1.0, y,     z-1.0)),
+                                    u.lerp(grad(p[ab+1], x,     y-1.0, z-1.0),
+                                           grad(p[bb+1], x-1.0, y-1.0, z-1.0))))
+       end
+
+       # Value at a corner of the grid
+       private fun grad(hash: Int, x, y, z: Float): Float
+       do
+               var h = hash & 15
+               var u = if h < 8 then x else y
+               var v = if h < 4 then y else if h == 12 or h == 14 then x else z
+               return (if h.is_even then u else -u) + (if h & 2 == 0 then v else -v)
+       end
+end
index 4763061..b9e436a 100644 (file)
@@ -122,11 +122,8 @@ class PerfEntry
        # Total execution time of this event
        var sum = 0.0
 
-       # Register a new event execution time with a `Timespec`
-       fun add(lapse: Timespec) do add_float lapse.to_f
-
-       # Register a new event execution time in seconds using a `Float`
-       fun add_float(time: Float)
+       # Register a new event execution time in seconds
+       fun add(time: Float)
        do
                if time.to_f < min.to_f or count == 0 then min = time
                if time.to_f > max.to_f then max = time
diff --git a/lib/popcorn/.gitignore b/lib/popcorn/.gitignore
new file mode 100644 (file)
index 0000000..c32211a
--- /dev/null
@@ -0,0 +1 @@
+tests/out
diff --git a/lib/popcorn/Makefile b/lib/popcorn/Makefile
new file mode 100644 (file)
index 0000000..12db3df
--- /dev/null
@@ -0,0 +1,24 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+NITUNIT=../../bin/nitunit
+
+check:
+       $(NITUNIT) README.md
+       $(NITUNIT) pop_routes.nit
+       $(NITUNIT) pop_handlers.nit
+       $(NITUNIT) popcorn.nit
+       cd tests; make check
diff --git a/lib/popcorn/README.md b/lib/popcorn/README.md
new file mode 100644 (file)
index 0000000..2bb6646
--- /dev/null
@@ -0,0 +1,855 @@
+# Popcorn
+
+**Why endure plain corn when you can pop it?!**
+
+Popcorn is a minimal yet powerful nit web application framework that provides cool
+features for lazy developpers.
+
+Popcorn is built over nitcorn to provide a clean and user friendly interface
+without all the boiler plate code.
+
+## What does it taste like?
+
+Set up is quick and easy as 10 lines of code.
+Create a file `app.nit` and add the following code:
+
+~~~
+import popcorn
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
+~~~
+
+The Popcorn app listens on port 3000 for connections.
+The app responds with "Hello World!" for requests to the root URL (`/`) or **route**.
+For every other path, it will respond with a **404 Not Found**.
+
+The `req` (request) and `res` (response) parameters are the same that nitcorn provides
+so you can do anything else you would do in your route without Popcorn involved.
+
+Run the app with the following command:
+
+~~~bash
+$ nitc app.nit && ./app
+~~~
+
+Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output.
+
+Here the output using the `curl` command:
+
+~~~bash
+$ curl localhost:3000
+<h1>Hello World!</h1>
+
+$ curl localhost:3000/wrong_uri
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Not Found</title>
+</head>
+<body>
+<h1>404 Not Found</h1>
+</body>
+</html>
+~~~
+
+This is why we love popcorn!
+
+## Basic routing
+
+**Routing** refers to determining how an application responds to a client request
+to a particular endpoint, which is a URI (or path) and a specific HTTP request
+method GET, POST, PUT or DELETE (other methods are not suported yet).
+
+Each route can have one or more handler methods, which are executed when the route is matched.
+
+Route handlers definition takes the following form:
+
+~~~nitish
+import popcorn
+
+class MyHandler
+       super Handler
+
+       redef fun METHOD(req, res) do end
+end
+~~~
+
+Where:
+* `MyHandler` is the name of the handler you will add to the app.
+* `METHOD` can be replaced by `get`, `post`, `put` or `delete`.
+
+The following example responds to GET and POST requests:
+
+~~~
+import popcorn
+
+class MyHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Got a GET request"
+       redef fun post(req, res) do res.send "Got a POST request"
+end
+~~~
+
+To make your handler responds to a specific route, you have to add it to the app.
+
+Respond to POST request on the root route (`/`), the application's home page:
+
+~~~
+var app = new App
+app.use("/", new MyHandler)
+~~~
+
+Respond to a request to the `/user` route:
+
+~~~
+app.use("/user", new MyHandler)
+~~~
+
+For more details about routing, see the routing section.
+
+## Serving static files with Popcorn
+
+To serve static files such as images, CSS files, and JavaScript files, use the
+Popcorn built-in handler `StaticHandler`.
+
+Pass the name of the directory that contains the static assets to the StaticHandler
+init method to start serving the files directly.
+For example, use the following code to serve images, CSS files, and JavaScript files
+in a directory named `public`:
+
+~~~
+app.use("/", new StaticHandler("public/"))
+~~~
+
+Now, you can load the files that are in the `public` directory:
+
+~~~raw
+http://localhost:3000/images/trollface.jpg
+http://localhost:3000/css/style.css
+http://localhost:3000/js/app.js
+http://localhost:3000/hello.html
+~~~
+
+Popcorn looks up the files relative to the static directory, so the name of the
+static directory is not part of the URL.
+To use multiple static assets directories, add the `StaticHandler` multiple times:
+
+~~~
+app.use("/", new StaticHandler("public/"))
+app.use("/", new StaticHandler("files/"))
+~~~
+
+Popcorn looks up the files in the order in which you set the static directories
+with the `use` method.
+
+To create a virtual path prefix (where the path does not actually exist in the file system)
+for files that are served by the `StaticHandler`, specify a mount path for the
+static directory, as shown below:
+
+~~~
+app.use("/static/", new StaticHandler("public/"))
+~~~
+
+Now, you can load the files that are in the public directory from the `/static`
+path prefix.
+
+~~~raw
+http://localhost:3000/static/images/trollface.jpg
+http://localhost:3000/static/css/style.css
+http://localhost:3000/static/js/app.js
+http://localhost:3000/static/hello.html
+~~~
+
+However, the path that you provide to the `StaticHandler` is relative to the
+directory from where you launch your app.
+If you run the app from another directory, it’s safer to use the absolute path of
+the directory that you want to serve.
+
+In some cases, you can want to redirect request to static files to a default file
+instead of returning a 404 error.
+This can be achieved by specifying a default file in the StaticHandler:
+
+~~~
+app.use("/static/", new StaticHandler("public/", "default.html"))
+~~~
+
+This way all non-matched queries to the StaticHandler will be answered with the
+`default.html` file.
+
+## Advanced Routing
+
+**Routing** refers to the definition of application end points (URIs) and how
+they respond to client requests. For an introduction to routing, see the Basic routing
+section.
+
+The following code is an example of a very basic route.
+
+~~~
+import popcorn
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+~~~
+
+### Route methods
+
+A **route method** is derived from one of the HTTP methods, and is attached to an
+instance of the Handler class.
+
+The following code is an example of routes that are defined for the GET and the POST
+methods to the root of the app.
+
+~~~
+import popcorn
+
+class GetPostHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "GET request to the homepage"
+       redef fun post(req, res) do res.send "POST request to the homepage"
+end
+
+var app = new App
+app.use("/", new GetPostHandler)
+~~~
+
+Popcorn supports the following routing methods that correspond to HTTP methods:
+get, post, put, and delete.
+
+The request query string is accessed through the `req` parameter:
+
+~~~
+import popcorn
+import template
+
+class QueryStringHandler
+       super Handler
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Query string: {req.query_string}"
+               for name, arg in req.get_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new QueryStringHandler)
+app.listen("localhost", 3000)
+~~~
+
+Post parameters can also be accessed through the `req` parameter:
+
+~~~
+import popcorn
+import template
+
+class PostHandler
+       super Handler
+
+       redef fun post(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Body: {req.body}"
+               for name, arg in req.post_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new PostHandler)
+app.listen("localhost", 3000)
+~~~
+
+There is a special routing method, `all(res, req)`, which is not derived from any
+HTTP method. This method is used to respond at a path for all request methods.
+
+In the following example, the handler will be executed for requests to "/user"
+whether you are using GET, POST, PUT, DELETE, or any other HTTP request method.
+
+~~~
+import popcorn
+
+class AllHandler
+       super Handler
+
+       redef fun all(req, res) do res.send "Every request to the homepage"
+end
+~~~
+
+Using the `all` method you can also implement other HTTP request methods.
+
+~~~
+import popcorn
+
+class MergeHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if req.method == "MERGE" then
+                       # handle that method
+               else super # keep handle GET, POST, PUT and DELETE methods
+       end
+end
+~~~
+
+### Route paths
+
+**Route paths**, in combination with a request handlers, define the endpoints at
+which requests can be made.
+Route paths can be strings, parameterized strings or glob patterns.
+Query strings such as `?q=foo`are not part of the route path.
+
+Popcorn uses the `Handler::match(uri)` method to match the route paths.
+
+Here are some examples of route paths based on strings.
+
+This route path will match requests to the root route, `/`.
+
+~~~
+import popcorn
+
+class MyHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Got a GET request"
+end
+
+var app = new App
+app.use("/", new MyHandler)
+~~~
+
+This route path will match requests to `/about`.
+
+~~~
+app.use("/about", new MyHandler)
+~~~
+
+This route path will match requests to `/random.text`.
+
+~~~
+app.use("/random.text", new MyHandler)
+~~~
+
+During the query/response process, routes are matched by order of declaration
+through the `App::use` method.
+
+The app declared in this example will try to match the routes in this order:
+
+1. `/`
+2. `/about`
+3. `/random.text`
+
+### Route parameters
+
+**Route parameters** are variable parts of a route path. They can be used to path
+arguments within the URI.\13
+Parameters in a route are prefixed with a colon `:` like in `:userId`, `:year`.
+
+The following example declares a handler `UserHome` that responds with the `user`
+name.
+
+~~~
+import popcorn
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               if user != null then
+                       res.send "Hello {user}"
+               else
+                       res.send("Nothing received", 400)
+               end
+       end
+end
+
+var app = new App
+app.use("/:user", new UserHome)
+app.listen("localhost", 3000)
+~~~
+
+The `UserHome` handler listen to every path matching `/:user`. This can be `/Morriar`,
+`/10`, ... but not `/Morriar/profile` since route follow the strict matching rule.
+
+### Glob routes
+
+**Glob routes** are routes that match only on a prefix, thus accepting a wider range
+of URI.
+Glob routes end with the symbol `*`.
+
+Here we define a `UserItem` handler that will respond to any URI matching the prefix
+`/user/:user/item/:item`.
+Note that glob route are compatible with route parameters.
+
+~~~
+import popcorn
+
+class UserItem
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               var item = req.param("item")
+               if user == null or item == null then
+                       res.send("Nothing received", 400)
+               else
+                       res.send "Here the item {item} of the use {user}."
+               end
+       end
+end
+
+var app = new App
+app.use("/user/:user/item/:item/*", new UserItem)
+app.listen("localhost", 3000)
+~~~
+
+## Response methods
+
+The methods on the response object (`res`), can is used to manipulate the
+request-response cycle.
+If none of these methods are called from a route handler, the client request will
+receive a `404 Not found` error.
+
+* `res.html()` Send a HTML response.
+* `res.json()` Send a JSON response.
+* `res.redirect()` Redirect a request.
+* `res.send()` Send a response of various types.
+* `res.error()` Set the response status code and send its message as the response body.
+
+## Response cycle
+
+When the popcorn `App` receives a request, the response cycle is the following:
+
+1. `pre-middlewares` lookup matching middlewares registered with `use_before(pre_middleware)`:
+       1. execute matching middleware by registration order
+       2. if a middleware send a response then let the `pre-middlewares` loop continue
+          with the next middleware
+2. `response-handlers` lookup matching handlers registered with `use(handler)`:
+       1. execute matching middleware by registration order
+       2. if a middleware send a response then stop the `response-handlers` loop
+       3. if no hander matches or sends a response, generate a 404 response
+3. `post-middlewares` lookup matching handlers registered with `use_after(post_handler)`:
+       1. execute matching middleware by registration order
+       2. if a middleware send a response then let the `post-middlewares` loop continue
+          with the next middleware
+
+## Middlewares
+
+### Overview
+
+**Middleware** handlers are handlers that typically do not send `HttpResponse` responses.
+Middleware handlers can perform the following tasks:
+
+* Execute any code.
+* Make changes to the request and the response objects.
+* End its action and pass to the next handler in the stack.
+
+If a middleware handler makes a call to `res.send()`, it provoques the end the
+request-response cycle and the response is sent to the client.
+
+### Ultra simple logger example
+
+Here is an example of a simple “Hello World” Popcorn application.
+We add a middleware handler to the application called MyLogger that prints a simple
+log message in the app stdout.
+
+~~~
+import popcorn
+
+class MyLogger
+       super Handler
+
+       redef fun all(req, res) do print "Request Logged!"
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+
+var app = new App
+app.use_before("/*", new MyLogger)
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
+~~~
+
+By using the `MyLogger` handler to the route `/*` we ensure that every requests
+(even 404 ones) pass through the middleware handler.
+This handler just prints “Request Logged!” when a request is received.
+
+Be default, the order of middleware execution is that are loaded first are also executed first.
+To ensure our middleware `MyLogger` will be executed before all the other, we add it
+with the `use_before` method.
+
+### Ultra cool, more advanced logger example
+
+Next, we’ll create a middleware handler called “LogHandler” that prints the requested
+uri, the response status and the time it took to Popcorn to process the request.
+
+This example gives a simplified version of the `RequestClock` and `ConsoleLog` middlewares.
+
+~~~
+import popcorn
+import realtime
+
+redef class HttpRequest
+       # Time that request was received by the Popcorn app.
+       var timer: nullable Clock = null
+end
+
+class RequestTimeHandler
+       super Handler
+
+       redef fun all(req, res) do req.timer = new Clock
+end
+
+class LogHandler
+       super Handler
+
+       redef fun all(req, res) do
+               var timer = req.timer
+               if timer != null then
+                       print "{req.method} {req.uri} {res.color_status} ({timer.total}s)"
+               else
+                       print "{req.method} {req.uri} {res.color_status}"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use_before("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use_after("/*", new LogHandler)
+app.listen("localhost", 3000)
+~~~
+
+First, we attach a new attribute `timer` to every `HttpRequest`.
+Doing so we can access our data from all handlers that import our module, directly
+from the `req` parameter.
+
+We use the new middleware called `RequestTimeHandler` to initialize the request timer.
+Because of the `use_before` method, the `RequestTimeHandler` middleware will be executed
+before all the others.
+
+We then let the `HelloHandler` produce the response.
+
+Finally, our `LogHandler` will display a bunch of data and use the request `timer`
+to display the time it took to process the request.
+Because of the `use_after` method, the `LogHandler` middleware will be executed after
+all the others.
+
+The app now uses the `RequestTimeHandler` middleware for every requests received
+by the Popcorn app.
+The page is processed the `HelloHandler` to display the index page.
+And, before every response is sent, the `LogHandler` is activated to display the
+log line.
+
+Because you have access to the request object, the response object, and all the
+Popcorn API, the possibilities with middleware functions are endless.
+
+### Built-in middlewares
+
+Starting with version 0.1, Popcorn provide a set of built-in middleware that can
+be used to develop your app faster.
+
+* `RequestClock`: initializes requests clock.
+* `ConsoleLog`: displays resquest and response status in console (can be used with `RequestClock`).
+* `SessionInit`: initializes requests session (see the `Sessions` section).
+* `StaticServer`: serves static files (see the `Serving static files with Popcorn` section).
+* `Router`: a mountable mini-app (see the `Mountable routers` section).
+
+## Mountable routers
+
+Use the `Router` class to create modular, mountable route handlers.
+A Router instance is a complete middleware and routing system; for this reason,
+it is often referred to as a “mini-app”.
+
+The following example creates a router as a module, loads a middleware handler in it,
+defines some routes, and mounts the router module on a path in the main app.
+
+~~~
+import popcorn
+
+class AppHome
+       super Handler
+
+       redef fun get(req, res) do res.send "Site Home"
+end
+
+class UserLogger
+       super Handler
+
+       redef fun all(req, res) do print "User logged"
+end
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do res.send "User Home"
+end
+
+class UserProfile
+       super Handler
+
+       redef fun get(req, res) do res.send "User Profile"
+end
+
+var user_router = new Router
+user_router.use("/*", new UserLogger)
+user_router.use("/", new UserHome)
+user_router.use("/profile", new UserProfile)
+
+var app = new App
+app.use("/", new AppHome)
+app.use("/user", user_router)
+app.listen("localhost", 3000)
+~~~
+
+The app will now be able to handle requests to /user and /user/profile, as well
+as call the `Time` middleware handler that is specific to the route.
+
+## Error handling
+
+**Error handling** is based on middleware handlers.
+
+Define error-handling middlewares in the same way as other middleware handlers:
+
+~~~
+import popcorn
+
+class SimpleErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       print "An error occurred! {res.status_code})"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.use("/*", new SimpleErrorHandler)
+app.listen("localhost", 3000)
+~~~
+
+In this example, every non-200 response is caught by the `SimpleErrorHandler`
+that print an error in stdout.
+
+By defining multiple middleware error handlers, you can take multiple action depending
+on the kind of error or the kind of interface you provide (HTML, XML, JSON...).
+
+Here an example of the 404 custom error page in HTML:
+
+~~~
+import popcorn
+import template
+
+class HtmlErrorTemplate
+       super Template
+
+       var status: Int
+       var message: nullable String
+
+       redef fun rendering do add """
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>{{{message or else status}}}</title>
+               </head>
+               <body>
+               <h1>{{{status}}} {{{message or else ""}}}</h1>
+               </body>
+               </html>"""
+end
+
+class HtmlErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       res.send(new HtmlErrorTemplate(res.status_code, "An error occurred!"))
+               end
+       end
+end
+
+var app = new App
+app.use("/*", new HtmlErrorHandler)
+app.listen("localhost", 3000)
+~~~
+
+## Sessions
+
+**Sessions** can be used thanks to the built-in `SessionMiddleware`.
+
+Here a simple example of login button that define a value in the `req` session.
+
+~~~
+import popcorn
+
+redef class Session
+       var is_logged = false
+end
+
+class AppLogin
+       super Handler
+
+       redef fun get(req, res) do
+               res.html """
+               <p>Is logged: {{{req.session.as(not null).is_logged}}}</p>
+               <form action="/" method="POST">
+                       <input type="submit" value="Login" />
+               </form>"""
+       end
+
+       redef fun post(req, res) do
+               req.session.as(not null).is_logged = true
+               res.redirect("/")
+       end
+end
+
+var app = new App
+app.use("/*", new SessionInit)
+app.use("/", new AppLogin)
+app.listen("localhost", 3000)
+~~~
+
+Notice the use of the `SessionInit` on the `/*` route. You must use the
+`SessionInit` first to initialize the request session.
+Without that, your request session will be set to `null`.
+If you don't use sessions in your app, you do not need to include that middleware.
+
+## Database integration
+
+### Mongo DB
+
+If you want to persist your data, Popcorn works well with MongoDB.
+
+In this example, we will show how to store and list user with a Mongo database.
+
+First let's define a handler that access the database to list all the user.
+The mongo database reference is passed to the UserList handler through the `db` attribute.
+
+Then we define a handler that displays the user creation form on GET requests.
+POST requests are used to save the user data.
+
+~~~
+import popcorn
+import mongodb
+import template
+
+class UserList
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var users = db.collection("users").find_all(new JsonObject)
+
+               var tpl = new Template
+               tpl.add "<h1>Users</h1>"
+               tpl.add "<table>"
+               for user in users do
+                       tpl.add """<tr>
+                               <td>{{{user["login"] or else "null"}}}</td>
+                               <td>{{{user["password"] or else "null"}}}</td>
+                       </tr>"""
+               end
+               tpl.add "</table>"
+               res.html tpl
+       end
+end
+
+class UserForm
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.add """<h1>Add a new user</h1>
+               <form action="/new" method="POST">
+                       <input type="text" name="login" />
+                       <input type="password" name="password" />
+                       <input type="submit" value="save" />
+               </form>"""
+               res.html tpl
+       end
+
+       redef fun post(req, res) do
+               var json = new JsonObject
+               json["login"] = req.post_args["login"]
+               json["password"] = req.post_args["password"]
+               db.collection("users").insert(json)
+               res.redirect "/"
+       end
+end
+
+var mongo = new MongoClient("mongodb://localhost:27017/")
+var db = mongo.database("mongo_example")
+
+var app = new App
+app.use("/", new UserList(db))
+app.use("/new", new UserForm(db))
+app.listen("localhost", 3000)
+~~~
+
+## Angular.JS integration
+
+Loving [AngularJS](https://angularjs.org/)? Popcorn is made for Angular and for you!
+
+Using the StaticHandler with a glob route, you can easily redirect all HTTP requests
+to your angular controller:
+
+~~~
+import popcorn
+
+var app = new App
+app.use("/*", new StaticHandler("my-ng-app/", "index.html"))
+app.listen("localhost", 3000)
+~~~
+
+Because the StaticHandler will not find the angular routes as static files,
+you must specify the path to the default angular controller.
+In this example, the StaticHandler will redirect any unknown requests to the `index.html`
+angular controller.
+
+See the examples for a more detailed use case working with a JSON API.
diff --git a/lib/popcorn/examples/angular/example_angular.nit b/lib/popcorn/examples/angular/example_angular.nit
new file mode 100644 (file)
index 0000000..9212a8e
--- /dev/null
@@ -0,0 +1,44 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+class CounterAPI
+       super Handler
+
+       var counter = 0
+
+       fun json_counter: JsonObject do
+               var json = new JsonObject
+               json["label"] = "Visitors"
+               json["value"] = counter
+               return json
+       end
+
+       redef fun get(req, res) do
+               res.json(json_counter)
+       end
+
+       redef fun post(req, res) do
+               counter += 1
+               res.json(json_counter)
+       end
+end
+
+var app = new App
+app.use("/counter", new CounterAPI)
+app.use("/*", new StaticHandler("www/", "index.html"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/angular/www/index.html b/lib/popcorn/examples/angular/www/index.html
new file mode 100644 (file)
index 0000000..59bc4d1
--- /dev/null
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang='en' ng-app='ng-example'>
+       <head>
+               <base href='/'>
+               <title>ng-example</title>
+       </head>
+       <body>
+               <div ng-view></div>
+
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.min.js'></script>
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-route.js'></script>
+               <script src='/javascripts/ng-example.js'></script>
+       </body>
+</html>
diff --git a/lib/popcorn/examples/angular/www/javascripts/ng-example.js b/lib/popcorn/examples/angular/www/javascripts/ng-example.js
new file mode 100644 (file)
index 0000000..f9270d2
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       angular
+
+       .module('ng-example', ['ngRoute'])
+
+       .factory('CounterModel', ['$http', function($http) {
+               return {
+                       load: function(cb, cbErr) {
+                               $http.get('/counter')
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       increment: function(cb, cbErr) {
+                               $http.post('/counter')
+                                       .success(cb)
+                                       .error(cbErr);
+                       }
+               };
+       }])
+
+       .controller('CounterCtrl', ['CounterModel', function(CounterModel) {
+               var $this = this;
+
+               this.loadCounter = function() {
+                       CounterModel.load(
+                               function(data) {
+                                       $this.counter = data;
+                               }, function(err) {
+                                       $this.error = err;
+                               });
+               };
+
+               this.incrementCounter = function() {
+                       CounterModel.increment(
+                               function(data) {
+                                       $this.counter = data;
+                               }, function(err) {
+                                       $this.error = err;
+                               });
+               };
+
+               this.loadCounter();
+       }])
+
+       .config(function($routeProvider, $locationProvider) {
+               $routeProvider
+                       .when('/', {
+                               templateUrl: 'views/index.html',
+                               controller: 'CounterCtrl',
+                               controllerAs: 'counterCtrl'
+                       })
+                       .otherwise({
+                               redirectTo: '/'
+                       });
+               $locationProvider.html5Mode(true);
+       });
+})();
diff --git a/lib/popcorn/examples/angular/www/views/index.html b/lib/popcorn/examples/angular/www/views/index.html
new file mode 100644 (file)
index 0000000..058e799
--- /dev/null
@@ -0,0 +1,9 @@
+<h1>Nit &hearts; Angular.JS</h1>
+
+<p>Click the button to increment the counter.</p>
+
+<form>
+       <label>{{counterCtrl.counter.label}}</label>
+       <input type="number" ng-model="counterCtrl.counter.value" readonly />
+       <button ng-click="counterCtrl.incrementCounter()">Increment!</button>
+</form>
diff --git a/lib/popcorn/examples/handlers/example_post_handler.nit b/lib/popcorn/examples/handlers/example_post_handler.nit
new file mode 100644 (file)
index 0000000..826c165
--- /dev/null
@@ -0,0 +1,36 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+import template
+
+class PostHandler
+       super Handler
+
+       redef fun post(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Body: {req.body}"
+               for name, arg in req.post_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new PostHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/handlers/example_query_string.nit b/lib/popcorn/examples/handlers/example_query_string.nit
new file mode 100644 (file)
index 0000000..e8c6187
--- /dev/null
@@ -0,0 +1,36 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+import template
+
+class QueryStringHandler
+       super Handler
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Query string: {req.query_string}"
+               for name, arg in req.get_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new QueryStringHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/hello_world/example_hello.nit b/lib/popcorn/examples/hello_world/example_hello.nit
new file mode 100644 (file)
index 0000000..fc9f3e5
--- /dev/null
@@ -0,0 +1,27 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/middlewares/example_advanced_logger.nit b/lib/popcorn/examples/middlewares/example_advanced_logger.nit
new file mode 100644 (file)
index 0000000..4ca9120
--- /dev/null
@@ -0,0 +1,54 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+import realtime
+
+redef class HttpRequest
+       # Time that request was received by the Popcorn app.
+       var timer: nullable Clock = null
+end
+
+class RequestTimeHandler
+       super Handler
+
+       redef fun all(req, res) do req.timer = new Clock
+end
+
+class LogHandler
+       super Handler
+
+       redef fun all(req, res) do
+               var timer = req.timer
+               if timer != null then
+                       print "{req.method} {req.uri} {res.color_status} ({timer.total}s)"
+               else
+                       print "{req.method} {req.uri} {res.color_status}"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use_before("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use_after("/*", new LogHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/middlewares/example_html_error_handler.nit b/lib/popcorn/examples/middlewares/example_html_error_handler.nit
new file mode 100644 (file)
index 0000000..5bd399c
--- /dev/null
@@ -0,0 +1,51 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+import template
+
+class HtmlErrorTemplate
+       super Template
+
+       var status: Int
+       var message: nullable String
+
+       redef fun rendering do add """
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>{{{message or else status}}}</title>
+               </head>
+               <body>
+               <h1>{{{status}}} {{{message or else ""}}}</h1>
+               </body>
+               </html>"""
+end
+
+class HtmlErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       res.send(new HtmlErrorTemplate(res.status_code, "An error occurred!"))
+               end
+       end
+end
+
+var app = new App
+app.use("/*", new HtmlErrorHandler)
+app.listen("localhost", 3000)
similarity index 58%
rename from tests/test_realtime.nit
rename to lib/popcorn/examples/middlewares/example_simple_error_handler.nit
index 6101a3d..16a5977 100644 (file)
@@ -1,6 +1,6 @@
 # This file is part of NIT ( http://www.nitlanguage.org ).
 #
-# Copyright 2012 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import realtime
+import popcorn
 
-redef extern class Timespec
-       fun simplify : Int
-       do
-               return sec*1000000 + nanosec/1000
+class SimpleErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       res.send("An error occurred!", res.status_code)
+               end
        end
 end
 
-var c = new Clock
-var t0 = c.total.simplify
-
-print "sleeping 1s"
-nanosleep(1, 0)
-print c.total.sec >= 1
-print c.lapse.sec >= 1
+class HelloHandler
+       super Handler
 
-var t1 = c.total.simplify
-
-print "sleeping 5000ns"
-nanosleep(0, 5000)
-print c.lapse.nanosec >= 5000
+       redef fun get(req, res) do res.send "Hello World!"
+end
 
-var t2 = c.total.simplify
 
-print t0 <= t1
-print t1 <= t2
+var app = new App
+app.use("/", new HelloHandler)
+app.use("/*", new SimpleErrorHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/middlewares/example_simple_logger.nit b/lib/popcorn/examples/middlewares/example_simple_logger.nit
new file mode 100644 (file)
index 0000000..4052169
--- /dev/null
@@ -0,0 +1,35 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+class LogHandler
+       super Handler
+
+       redef fun all(req, res) do print "Request Logged"
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+
+var app = new App
+app.use_before("/*", new LogHandler)
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/mongodb/example_mongodb.nit b/lib/popcorn/examples/mongodb/example_mongodb.nit
new file mode 100644 (file)
index 0000000..4f89f5b
--- /dev/null
@@ -0,0 +1,66 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+import mongodb
+import template
+
+class UserList
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var users = db.collection("users").find_all(new JsonObject)
+
+               var tpl = new Template
+               tpl.add """
+               <h1>Users</h1>
+
+               <h2>Add a new user</h2>
+               <form action="/" method="POST">
+                       <input type="text" name="login" />
+                       <input type="password" name="password" />
+                       <input type="submit" value="save" />
+               </form>
+
+               <h2>All users</h2>
+               <table>"""
+               for user in users do
+                       tpl.add """<tr>
+                       <td>{{{user["login"] or else "null"}}}</td>
+                       <td>{{{user["password"] or else "null"}}}</td>
+                       </tr>"""
+               end
+               tpl.add "</table>"
+               res.html(tpl)
+       end
+
+       redef fun post(req, res) do
+               var json = new JsonObject
+               json["login"] = req.post_args["login"]
+               json["password"] = req.post_args["password"]
+               db.collection("users").insert(json)
+               res.redirect("/")
+       end
+end
+
+var mongo = new MongoClient("mongodb://localhost:27017/")
+var db = mongo.database("mongo_example")
+
+var app = new App
+app.use("/", new UserList(db))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/routing/example_glob_route.nit b/lib/popcorn/examples/routing/example_glob_route.nit
new file mode 100644 (file)
index 0000000..b33caec
--- /dev/null
@@ -0,0 +1,35 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+class UserItem
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               var item = req.param("item")
+               if user == null or item == null then
+                       res.send("Nothing received", 400)
+               else
+                       res.send "Here the item {item} of the use {user}."
+               end
+       end
+end
+
+var app = new App
+app.use("/user/:user/item/:item/*", new UserItem)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/routing/example_param_route.nit b/lib/popcorn/examples/routing/example_param_route.nit
new file mode 100644 (file)
index 0000000..f536771
--- /dev/null
@@ -0,0 +1,34 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               if user != null then
+                       res.send "Hello {user}"
+               else
+                       res.send("Nothing received", 400)
+               end
+       end
+end
+
+var app = new App
+app.use("/:user", new UserHome)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/routing/example_router.nit b/lib/popcorn/examples/routing/example_router.nit
new file mode 100644 (file)
index 0000000..91aa3ab
--- /dev/null
@@ -0,0 +1,51 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+class AppHome
+       super Handler
+
+       redef fun get(req, res) do res.send "Site Home"
+end
+
+class UserLogger
+       super Handler
+
+       redef fun all(req, res) do print "User logged"
+end
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do res.send "User Home"
+end
+
+class UserProfile
+       super Handler
+
+       redef fun get(req, res) do res.send "User Profile"
+end
+
+var user_router = new Router
+user_router.use("/*", new UserLogger)
+user_router.use("/", new UserHome)
+user_router.use("/profile", new UserProfile)
+
+var app = new App
+app.use("/", new AppHome)
+app.use("/user", user_router)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/sessions/example_session.nit b/lib/popcorn/examples/sessions/example_session.nit
new file mode 100644 (file)
index 0000000..be79fb1
--- /dev/null
@@ -0,0 +1,43 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+redef class Session
+       var is_logged = false
+end
+
+class AppLogin
+       super Handler
+
+       redef fun get(req, res) do
+               res.html """
+               <p>Is logged: {{{req.session.as(not null).is_logged}}}</p>
+               <form action="/" method="POST">
+                       <input type="submit" value="Login" />
+               </form>"""
+       end
+
+       redef fun post(req, res) do
+               req.session.as(not null).is_logged = true
+               res.redirect("/")
+       end
+end
+
+var app = new App
+app.use("/*", new SessionInit)
+app.use("/", new AppLogin)
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/example_static.nit b/lib/popcorn/examples/static_files/example_static.nit
new file mode 100644 (file)
index 0000000..c3da6d0
--- /dev/null
@@ -0,0 +1,21 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+var app = new App
+app.use("/", new StaticHandler("public/"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/example_static_default.nit b/lib/popcorn/examples/static_files/example_static_default.nit
new file mode 100644 (file)
index 0000000..6aa5b34
--- /dev/null
@@ -0,0 +1,21 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+var app = new App
+app.use("/", new StaticHandler("public/", "default.html"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/example_static_multiple.nit b/lib/popcorn/examples/static_files/example_static_multiple.nit
new file mode 100644 (file)
index 0000000..1c730c6
--- /dev/null
@@ -0,0 +1,24 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+
+var app = new App
+app.use("/", new StaticHandler("public/"))
+app.use("/", new StaticHandler("files/"))
+app.use("/static", new StaticHandler("public/"))
+app.use("/static", new StaticHandler("files/"))
+app.listen("localhost", 3000)
diff --git a/lib/popcorn/examples/static_files/files/index.html b/lib/popcorn/examples/static_files/files/index.html
new file mode 100644 (file)
index 0000000..bb850bd
--- /dev/null
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+       <body>
+               <h1>Another Index</h1>
+       </body>
+</html>
diff --git a/lib/popcorn/examples/static_files/public/css/style.css b/lib/popcorn/examples/static_files/public/css/style.css
new file mode 100644 (file)
index 0000000..5ba0171
--- /dev/null
@@ -0,0 +1,4 @@
+body {
+       color: blue;
+       padding: 20px;
+}
diff --git a/lib/popcorn/examples/static_files/public/default.html b/lib/popcorn/examples/static_files/public/default.html
new file mode 100644 (file)
index 0000000..2abb789
--- /dev/null
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
diff --git a/lib/popcorn/examples/static_files/public/hello.html b/lib/popcorn/examples/static_files/public/hello.html
new file mode 100644 (file)
index 0000000..61521ac
--- /dev/null
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
diff --git a/lib/popcorn/examples/static_files/public/images/trollface.jpg b/lib/popcorn/examples/static_files/public/images/trollface.jpg
new file mode 100644 (file)
index 0000000..0c733bc
Binary files /dev/null and b/lib/popcorn/examples/static_files/public/images/trollface.jpg differ
diff --git a/lib/popcorn/examples/static_files/public/js/app.js b/lib/popcorn/examples/static_files/public/js/app.js
new file mode 100644 (file)
index 0000000..4a1510d
--- /dev/null
@@ -0,0 +1 @@
+alert("Hello World!");
diff --git a/lib/popcorn/package.ini b/lib/popcorn/package.ini
new file mode 100644 (file)
index 0000000..8f99b9c
--- /dev/null
@@ -0,0 +1,12 @@
+[package]
+name=popcorn
+tags=web,lib
+maintainer=Alexandre Terrasa <alexandre@moz-code.org>
+license=Apache-2.0
+version=1.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/popcorn/
+git=https://github.com/nitlang/nit.git
+git.directory=lib/popcorn/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
diff --git a/lib/popcorn/pop_handlers.nit b/lib/popcorn/pop_handlers.nit
new file mode 100644 (file)
index 0000000..5d84961
--- /dev/null
@@ -0,0 +1,477 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Route handlers.
+module pop_handlers
+
+import pop_routes
+import json
+
+# Class handler for a route.
+#
+# **Routing** refers to determining how an application responds to a client request
+# to a particular endpoint, which is a URI (or path) and a specific HTTP request
+# method GET, POST, PUT or DELETE (other methods are not suported yet).
+#
+# Each route can have one or more handler methods, which are executed when the route is matched.
+#
+# Route handlers definition takes the following form:
+#
+# ~~~nitish
+# class MyHandler
+#      super Handler
+#
+#      redef fun METHOD(req, res) do end
+# end
+# ~~~
+#
+# Where:
+# * `MyHandler` is the name of the handler you will add to the app.
+# * `METHOD` can be replaced by `get`, `post`, `put` or `delete`.
+#
+# The following example responds with `Hello World!` to GET and POST requests:
+#
+# ~~~
+# class MyHandler
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "Got a GET request"
+#      redef fun post(req, res) do res.send "Got a POST request"
+# end
+# ~~~
+#
+# To make your handler responds to a specific route, you have to add it to the app.
+#
+# Respond to POST request on the root route (`/`), the application's home page:
+#
+# ~~~
+# var app = new App
+# app.use("/", new MyHandler)
+# ~~~
+#
+# Respond to a request to the `/user` route:
+#
+# ~~~
+# app.use("/user", new MyHandler)
+# ~~~
+abstract class Handler
+
+       # Call `all(req, res)` if `route` matches `uri`.
+       private fun handle(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               if route.match(uri) then
+                       if route isa AppParamRoute then
+                               req.uri_params = route.parse_uri_parameters(uri)
+                       end
+                       all(req, res)
+               end
+       end
+
+       # Handler to all kind of HTTP request methods.
+       #
+       # `all` is a special request handler, which is not derived from any
+       # HTTP method. This method is used to respond at a path for all request methods.
+       #
+       # In the following example, the handler will be executed for requests to "/user"
+       # whether you are using GET, POST, PUT, DELETE, or any other HTTP request method.
+       #
+       # ~~~
+       # class AllHandler
+       #       super Handler
+       #
+       #       redef fun all(req, res) do res.send "Every request to the homepage"
+       # end
+       # ~~~
+       #
+       # Using the `all` method you can also implement other HTTP request methods.
+       #
+       # ~~~
+       # class MergeHandler
+       #       super Handler
+       #
+       #       redef fun all(req, res) do
+       #               if req.method == "MERGE" then
+       #                       # handle that method
+       #               else super # keep handle GET, POST, PUT and DELETE methods
+       #       end
+       # end
+       # ~~~
+       fun all(req: HttpRequest, res: HttpResponse) do
+               if req.method == "GET" then
+                       get(req, res)
+               else if req.method == "POST" then
+                       post(req, res)
+               else if req.method == "PUT" then
+                       put(req, res)
+               else if req.method == "DELETE" then
+                       delete(req, res)
+               else
+                       res.status_code = 405
+               end
+       end
+
+       # GET handler.
+       #
+       # Exemple of route responding to GET requests.
+       # ~~~
+       # class GetHandler
+       #       super Handler
+       #
+       #       redef fun get(req, res) do res.send "GETrequest received"
+       # end
+       # ~~~
+       fun get(req: HttpRequest, res: HttpResponse) do end
+
+       # POST handler.
+       #
+       # Exemple of route responding to POST requests.
+       # ~~~
+       # class PostHandler
+       #       super Handler
+       #
+       #       redef fun post(req, res) do res.send "POST request received"
+       # end
+       # ~~~
+       fun post(req: HttpRequest, res: HttpResponse) do end
+
+       # PUT handler.
+       #
+       # Exemple of route responding to PUT requests.
+       # ~~~
+       # class PutHandler
+       #       super Handler
+       #
+       #       redef fun put(req, res) do res.send "PUT request received"
+       # end
+       # ~~~
+       fun put(req: HttpRequest, res: HttpResponse) do end
+
+       # DELETE handler.
+       #
+       # Exemple of route responding to PUT requests.
+       # ~~~
+       # class DeleteHandler
+       #       super Handler
+       #
+       #       redef fun delete(req, res) do res.send "DELETE request received"
+       # end
+       # ~~~
+       fun delete(req: HttpRequest, res: HttpResponse) do end
+end
+
+# Static files server.
+#
+# To serve static files such as images, CSS files, and JavaScript files, use the
+# Popcorn built-in handler `StaticHandler`.
+#
+# Pass the name of the directory that contains the static assets to the StaticHandler
+# init method to start serving the files directly.
+# For example, use the following code to serve images, CSS files, and JavaScript files
+# in a directory named `public`:
+#
+# ~~~
+# var app = new App
+# app.use("/", new StaticHandler("public/"))
+# ~~~
+#
+# Now, you can load the files that are in the `public` directory:
+#
+# ~~~raw
+# http://localhost:3000/images/trollface.jpg
+# http://localhost:3000/css/style.css
+# http://localhost:3000/js/app.js
+# http://localhost:3000/hello.html
+# ~~~
+#
+# Popcorn looks up the files relative to the static directory, so the name of the
+# static directory is not part of the URL.
+# To use multiple static assets directories, add the `StaticHandler` multiple times:
+#
+# ~~~
+# app.use("/", new StaticHandler("public/"))
+# app.use("/", new StaticHandler("files/"))
+# ~~~
+#
+# Popcorn looks up the files in the order in which you set the static directories
+# with the `use` method.
+#
+# To create a virtual path prefix (where the path does not actually exist in the file system)
+# for files that are served by the `StaticHandler`, specify a mount path for the
+# static directory, as shown below:
+#
+# ~~~
+# app.use("/static/", new StaticHandler("public/"))
+# ~~~
+#
+# Now, you can load the files that are in the public directory from the `/static`
+# path prefix.
+#
+# ~~~raw
+# http://localhost:3000/static/images/trollface.jpg
+# http://localhost:3000/static/css/style.css
+# http://localhost:3000/static/js/app.js
+# http://localhost:3000/static/hello.html
+# ~~~
+#
+# However, the path that you provide to the `StaticHandler` is relative to the
+# directory from where you launch your app.
+# If you run the app from another directory, it’s safer to use the absolute path of
+# the directory that you want to serve.
+class StaticHandler
+       super Handler
+
+       # Static files directory to serve.
+       var static_dir: String
+
+       # Default file to serve if nothing matches the request.
+       #
+       # `null` for no default file.
+       var default_file: nullable String
+
+       # Internal file server used to lookup and render files.
+       var file_server: FileServer is lazy do
+               var srv = new FileServer(static_dir)
+               srv.show_directory_listing = false
+               srv.default_file = default_file
+               return srv
+       end
+
+       redef fun handle(route, uri, req, res) do
+               var answer = file_server.answer(req, route.uri_root(uri))
+               if answer.status_code == 200 then
+                       res.status_code = answer.status_code
+                       res.header.add_all answer.header
+                       res.files.add_all answer.files
+                       res.send
+               else if answer.status_code != 404 then
+                       res.status_code = answer.status_code
+               end
+       end
+end
+
+# Mountable routers
+#
+# Use the `Router` class to create modular, mountable route handlers.
+# A Router instance is a complete middleware and routing system; for this reason,
+# it is often referred to as a “mini-app”.
+#
+# The following example creates a router as a module, loads a middleware handler in it,
+# defines some routes, and mounts the router module on a path in the main app.
+#
+# ~~~
+# class AppHome
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "Site Home"
+# end
+#
+# class UserLogger
+#      super Handler
+#
+#      redef fun all(req, res) do print "User logged"
+# end
+#
+# class UserHome
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "User Home"
+# end
+#
+# class UserProfile
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "User Profile"
+# end
+#
+# var user_router = new Router
+# user_router.use("/*", new UserLogger)
+# user_router.use("/", new UserHome)
+# user_router.use("/profile", new UserProfile)
+#
+# var app = new App
+# app.use("/", new AppHome)
+# app.use("/user", user_router)
+# ~~~
+#
+# The app will now be able to handle requests to /user and /user/profile, as well
+# as call the `Time` middleware handler that is specific to the route.
+class Router
+       super Handler
+
+       # List of handlers to match with requests.
+       private var handlers = new Map[AppRoute, Handler]
+
+       # List of handlers to match before every other.
+       private var pre_handlers = new Map[AppRoute, Handler]
+
+       # List of handlers to match after every other.
+       private var post_handlers = new Map[AppRoute, Handler]
+
+       # Register a `handler` for a route `path`.
+       #
+       # Route paths are matched in registration order.
+       fun use(path: String, handler: Handler) do
+               var route = build_route(handler, path)
+               handlers[route] = handler
+       end
+
+       # Register a pre-handler for a route `path`.
+       #
+       # Prehandlers are matched before every other handlers in registrastion order.
+       fun use_before(path: String, handler: Handler) do
+               var route = build_route(handler, path)
+               pre_handlers[route] = handler
+       end
+
+       # Register a post-handler for a route `path`.
+       #
+       # Posthandlers are matched after every other handlers in registrastion order.
+       fun use_after(path: String, handler: Handler) do
+               var route = build_route(handler, path)
+               post_handlers[route] = handler
+       end
+
+       redef fun handle(route, uri, req, res) do
+               if not route.match(uri) then return
+               handle_pre(route, uri, req, res)
+               handle_in(route, uri, req, res)
+               handle_post(route, uri, req, res)
+       end
+
+       private fun handle_pre(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               for hroute, handler in pre_handlers do
+                       handler.handle(hroute, route.uri_root(uri), req, res)
+               end
+       end
+
+       private fun handle_in(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               for hroute, handler in handlers do
+                       handler.handle(hroute, route.uri_root(uri), req, res)
+                       if res.sent then break
+               end
+       end
+
+       private fun handle_post(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               for hroute, handler in post_handlers do
+                       handler.handle(hroute, route.uri_root(uri), req, res)
+               end
+       end
+
+       private fun build_route(handler: Handler, path: String): AppRoute do
+               if handler isa Router or handler isa StaticHandler then
+                       return new AppGlobRoute(path)
+               else if path.has_suffix("*") then
+                       return new AppGlobRoute(path)
+               else
+                       return new AppParamRoute(path)
+               end
+       end
+end
+
+# Popcorn application.
+#
+# The `App`  is the main point of the application.
+# It acts as a `Router` that holds the top level route handlers.
+#
+# Here an example to create a simple web app with Popcorn:
+#
+# ~~~
+# import popcorn
+#
+# class HelloHandler
+#      super Handler
+#
+#      redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
+# end
+#
+# var app = new App
+# app.use("/", new HelloHandler)
+# # app.listen("localhost", 3000)
+# ~~~
+#
+# The Popcorn app listens on port 3000 for connections.
+# The app responds with "Hello World!" for request to the root URL (`/`) or **route**.
+# For every other path, it will respond with a **404 Not Found**.
+#
+# The `req` (request) and `res` (response) parameters are the same that nitcorn provides
+# so you can do anything else you would do in your route without Popcorn involved.
+#
+# Run the app with the following command:
+#
+# ~~~bash
+# nitc app.nit && ./app
+# ~~~
+#
+# Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output.
+class App
+       super Router
+end
+
+redef class HttpResponse
+
+       # Was this request sent by a handler?
+       var sent = false
+
+       private fun check_sent do
+               if sent then print "Warning: Headers already sent!"
+       end
+
+       # Write data in body response and send it.
+       fun send(raw_data: nullable Writable, status: nullable Int) do
+               if raw_data != null then
+                       body += raw_data.write_to_string
+               end
+               if status != null then
+                       status_code = status
+               else
+                       status_code = 200
+               end
+               check_sent
+               sent = true
+       end
+
+       # Write data as HTML and set the right content type header.
+       fun html(html: nullable Writable, status: nullable Int) do
+               header["Content-Type"] = media_types["html"].as(not null)
+               send(html, status)
+       end
+
+       # Write data as JSON and set the right content type header.
+       fun json(json: nullable Jsonable, status: nullable Int) do
+               header["Content-Type"] = media_types["json"].as(not null)
+               if json == null then
+                       send(null, status)
+               else
+                       send(json.to_json, status)
+               end
+       end
+
+       # Redirect response to `location`
+       fun redirect(location: String, status: nullable Int) do
+               header["Location"] = location
+               if status != null then
+                       status_code = status
+               else
+                       status_code = 302
+               end
+               check_sent
+               sent = true
+       end
+
+       # TODO The error message should be parameterizable.
+       fun error(status: Int) do
+               html("Error", status)
+       end
+end
diff --git a/lib/popcorn/pop_middlewares.nit b/lib/popcorn/pop_middlewares.nit
new file mode 100644 (file)
index 0000000..518d9dc
--- /dev/null
@@ -0,0 +1,77 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module pop_middlewares
+
+import pop_handlers
+import console
+import realtime
+
+# Initialize session in request if non existent.
+#
+# Should be called before any use of the session.
+class SessionInit
+       super Handler
+
+       redef fun all(req, res) do if req.session == null then req.session = new Session
+end
+
+# Initialize a clock for the resquest.
+#
+# Can be used to compute the time passed to respond that request.
+class RequestClock
+       super Handler
+
+       redef fun all(req, res) do req.clock = new Clock
+end
+
+# Display log info about request processing.
+class ConsoleLog
+       super Handler
+
+       # Do we want colors in the console output?
+       var colors = true
+
+       redef fun all(req, res) do
+               var clock = req.clock
+               if clock != null then
+                       print "{req.method} {req.uri} {status(res)} ({clock.total}s)"
+               else
+                       print "{req.method} {req.uri} {status(res)}"
+               end
+       end
+
+       # Colorize the request status.
+       private fun status(res: HttpResponse): String do
+               if colors then return res.color_status
+               return res.status_code.to_s
+       end
+end
+
+redef class HttpRequest
+       # Time that request was received by the Popcorn app.
+       var clock: nullable Clock = null
+end
+
+redef class HttpResponse
+       # Return `self` status colored for console.
+       fun color_status: String do
+               if status_code == 200 then return status_code.to_s.green
+               if status_code == 304 then return status_code.to_s.blue
+               if status_code == 404 then return status_code.to_s.yellow
+               return status_code.to_s.red
+       end
+end
diff --git a/lib/popcorn/pop_routes.nit b/lib/popcorn/pop_routes.nit
new file mode 100644 (file)
index 0000000..68c3d37
--- /dev/null
@@ -0,0 +1,263 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Internal routes representation.
+module pop_routes
+
+import nitcorn
+
+# AppRoute provide services for path and uri manipulation and matching..
+#
+# Default strict routes like `/` or `/user` match the same URI string.
+# An exception is done for the trailing `/`, which is always omitted during the
+# parsing.
+#
+# ~~~
+# var route = new AppRoute("/")
+# assert route.match("")
+# assert route.match("/")
+# assert not route.match("/user")
+# assert not route.match("user")
+#
+# route = new AppRoute("/user")
+# assert not route.match("/")
+# assert route.match("/user")
+# assert route.match("/user/")
+# assert not route.match("/user/10")
+# assert not route.match("/foo")
+# assert not route.match("user")
+# assert not route.match("/username")
+# ~~~
+class AppRoute
+
+       # Route relative path from server root.
+       var path: String
+
+       # Does self match the `req`?
+       fun match(uri: String): Bool do
+               uri = uri.simplify_path
+               var path = resolve_path(uri)
+               if uri.is_empty and path == "/" then return true
+               return uri == path
+       end
+
+       # Replace path parameters with concrete values from the `uri`.
+       #
+       # For strict routes, it returns the path unchanged:
+       # ~~~
+       # var route = new AppRoute("/")
+       # assert route.resolve_path("/user/10/profile") == "/"
+       #
+       # route = new AppRoute("/user")
+       # assert route.resolve_path("/user/10/profile") == "/user"
+       # ~~~
+       fun resolve_path(uri: String): String do return path.simplify_path
+
+       # Remove `resolved_path` prefix from `uri`.
+       #
+       # Mainly used to resolve and match mountable routes.
+       #
+       # ~~~
+       # var route = new AppRoute("/")
+       # assert route.uri_root("/user/10/profile") == "/user/10/profile"
+       #
+       # route = new AppRoute("/user")
+       # assert route.uri_root("/user/10/profile") == "/10/profile"
+       # ~~~
+       fun uri_root(uri: String): String do
+               var path = resolve_path(uri)
+               if path == "/" then return uri
+               return uri.substring(path.length, uri.length).simplify_path
+       end
+end
+
+# Parameterizable routes.
+#
+# Routes that can contains variables parts that will be resolved during the
+# matching process.
+#
+# Route parameters are marked with a colon `:`
+# ~~~
+# var route = new AppParamRoute("/:id")
+# assert not route.match("/")
+# assert route.match("/user")
+# assert route.match("/user/")
+# assert not route.match("/user/10")
+# ~~~
+#
+# It is possible to use more than one parameter in the same route:
+# ~~~
+# route = new AppParamRoute("/user/:userId/items/:itemId")
+# assert not route.match("/user/10/items/")
+# assert route.match("/user/10/items/e895346")
+# assert route.match("/user/USER/items/0/")
+# assert not route.match("/user/10/items/10/profile")
+# ~~~
+class AppParamRoute
+       super AppRoute
+
+       init do parse_path_parameters(path)
+
+       # Cut `path` into `UriParts`.
+       fun parse_path_parameters(path: String) do
+               for part in path.split("/") do
+                       if not part.is_empty and part.first == ':' then
+                               # is an uri param
+                               path_parts.add new UriParam(part.substring(1, part.length))
+                       else
+                               # is a standard string
+                               path_parts.add new UriString(part)
+                       end
+               end
+       end
+
+       # For parameterized routes, parameter names are replaced by their value in the URI.
+       # ~~~
+       # var route = new AppParamRoute("/user/:id")
+       # assert route.resolve_path("/user/10/profile") == "/user/10"
+       #
+       # route = new AppParamRoute("/user/:userId/items/:itemId")
+       # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
+       # ~~~
+       redef fun resolve_path(uri) do
+               var uri_params = parse_uri_parameters(uri)
+               var path = "/"
+               for part in path_parts do
+                       if part isa UriString then
+                               path /= part.string
+                       else if part isa UriParam then
+                               path /= uri_params.get_or_default(part.name, part.name)
+                       end
+               end
+               return path.simplify_path
+       end
+
+       # Extract parameter values from `uri`.
+       # ~~~
+       # var route = new AppParamRoute("/user/:userId/items/:itemId")
+       # var params = route.parse_uri_parameters("/user/10/items/i125/desc")
+       # assert params["userId"] == "10"
+       # assert params["itemId"] == "i125"
+       # assert params.length == 2
+       #
+       # params = route.parse_uri_parameters("/")
+       # assert params.is_empty
+       # ~~~
+       fun parse_uri_parameters(uri: String): Map[String, String] do
+               var res = new HashMap[String, String]
+               if path_parts.is_empty then return res
+               var parts = uri.split("/")
+               for i in [0 .. path_parts.length[ do
+                       if i >= parts.length then return res
+                       var ppart = path_parts[i]
+                       var part = parts[i]
+                       if not ppart.match(part) then return res
+                       if ppart isa UriParam then
+                               res[ppart.name] = part
+                       end
+               end
+               return res
+       end
+
+       private var path_parts = new Array[UriPart]
+end
+
+# Route with glob.
+#
+# Route variable part is suffixed with a star `*`:
+# ~~~
+# var route = new AppGlobRoute("/*")
+# assert route.match("/")
+# assert route.match("/user")
+# assert route.match("/user/10")
+# ~~~
+#
+# Glob routes can be combined with parameters:
+# ~~~
+# route = new AppGlobRoute("/user/:id/*")
+# assert not route.match("/user")
+# assert route.match("/user/10")
+# assert route.match("/user/10/profile")
+# ~~~
+#
+# Note that the star can be used directly on the end of an URI part:
+# ~~~
+# route = new AppGlobRoute("/user*")
+# assert route.match("/user")
+# assert route.match("/username")
+# assert route.match("/user/10/profile")
+# assert not route.match("/foo")
+# ~~~
+#
+# For now, stars cannot be used inside a route, use URI parameters instead.
+class AppGlobRoute
+       super AppParamRoute
+
+       # Path without the trailing `*`.
+       # ~~~
+       # var route = new AppGlobRoute("/user/:id/*")
+       # assert route.resolve_path("/user/10/profile") == "/user/10"
+       #
+       # route = new AppGlobRoute("/user/:userId/items/:itemId*")
+       # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
+       # ~~~
+       redef fun resolve_path(uri) do
+               var path = super
+               if path.has_suffix("*") then
+                       return path.substring(0, path.length - 1).simplify_path
+               end
+               return path.simplify_path
+       end
+
+       redef fun match(uri) do
+               var path = resolve_path(uri)
+               return uri.has_prefix(path.substring(0, path.length - 1))
+       end
+end
+
+# A String that compose an URI.
+#
+# In practice, UriPart can be parameters or static strings.
+private interface UriPart
+       # Does `self` matches a part of the uri?
+       fun match(uri_part: String): Bool is abstract
+end
+
+# An uri parameter string like `:id`.
+private class UriParam
+       super UriPart
+
+       # Param `name` in the route uri.
+       var name: String
+
+       # Parameters match everything.
+       redef fun match(part) do return not part.is_empty
+
+       redef fun to_s do return name
+end
+
+# A static uri string like `users`.
+private class UriString
+       super UriPart
+
+       # Uri part string.
+       var string: String
+
+       # Empty strings match everything otherwise matching is based on string equality.
+       redef fun match(part) do return string.is_empty or string == part
+
+       redef fun to_s do return string
+end
diff --git a/lib/popcorn/popcorn.nit b/lib/popcorn/popcorn.nit
new file mode 100644 (file)
index 0000000..64e4459
--- /dev/null
@@ -0,0 +1,96 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Application server abstraction on top of nitcorn.
+module popcorn
+
+import nitcorn
+import pop_middlewares
+intrude import pop_handlers
+
+# App acts like a wrapper around a nitcorn `Action`.
+redef class App
+       super Action
+
+       # Do not show anything on console
+       var quiet = false is writable
+
+       # Start listening on `host:port`.
+       fun listen(host: String, port: Int) do
+               var iface = "{host}:{port}"
+               var vh = new VirtualHost(iface)
+
+               vh.routes.add new Route("/", self)
+
+               var fac = new HttpFactory.and_libevent
+               fac.config.virtual_hosts.add vh
+
+               if not quiet then
+                       print "Launching server on http://{iface}/"
+               end
+               fac.run
+       end
+
+       # Handle request from nitcorn
+       redef fun answer(req, uri) do
+               uri = uri.simplify_path
+               var res = new HttpResponse(404)
+               for route, handler in pre_handlers do
+                       handler.handle(route, uri, req, res)
+               end
+               for route, handler in handlers do
+                       handler.handle(route, uri, req, res)
+                       if res.sent then break
+               end
+               if not res.sent then
+                       res.send(error_tpl(res.status_code, res.status_message), 404)
+               end
+               for route, handler in post_handlers do
+                       handler.handle(route, uri, req, res)
+               end
+               res.session = req.session
+               return res
+       end
+
+       #
+       fun error_tpl(status: Int, message: nullable String): Template do
+               return new ErrorTpl(status, message)
+       end
+end
+
+#
+class ErrorTpl
+       super Template
+
+       #
+       var status: Int
+
+       #
+       var message: nullable String
+
+       redef fun rendering do add """
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>{{{message or else status}}}</title>
+               </head>
+               <body>
+               <h1>{{{status}}} {{{message or else ""}}}</h1>
+               </body>
+               </html>"""
+
+end
diff --git a/lib/popcorn/test_pop_routes.nit b/lib/popcorn/test_pop_routes.nit
new file mode 100644 (file)
index 0000000..9ad37a5
--- /dev/null
@@ -0,0 +1,166 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module test_pop_routes is test_suite
+
+import pop_routes
+import test_suite
+
+class TestAppRoute
+       super TestSuite
+
+       fun test_root_match_only_one_uri do
+               var r = new AppRoute("/")
+               assert r.match("")
+               assert r.match("/")
+               assert not r.match("/user")
+       end
+
+       fun test_strict_route_match_only_one_uri do
+               var r = new AppRoute("/user")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/")
+               assert not r.match("/user/10")
+               assert not r.match("/foo")
+       end
+end
+
+class TestAppParamRoute
+       super TestSuite
+
+       fun test_param_route_match_good_uri_params_1 do
+               var r = new AppParamRoute("/:id")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/")
+               assert not r.match("/user/10")
+       end
+
+       fun test_param_route_match_good_uri_params_2 do
+               var r = new AppParamRoute("/user/:id")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/user/")
+               assert r.match("/user/10")
+               assert r.match("/user/10/")
+               assert not r.match("/user/10/profile")
+       end
+
+       fun test_param_route_match_good_uri_params_3 do
+               var r = new AppParamRoute("/user/:id/profile")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/user/")
+               assert not r.match("/user/10")
+               assert not r.match("/user/10/")
+               assert r.match("/user/10/profile")
+               assert r.match("/user/10/profile/")
+               assert not r.match("/user/10/profile/settings")
+               assert not r.match("/user/10/foo")
+       end
+
+       fun test_param_route_match_good_uri_params_4 do
+               var r = new AppParamRoute("/:id/:foo")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/user/")
+               assert r.match("/user/10")
+               assert r.match("/user/10/")
+               assert not r.match("/user/10/10")
+       end
+
+       fun test_param_route_match_good_uri_params_5 do
+               var r = new AppParamRoute("/user/:id/:foo")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/foo")
+               assert not r.match("/user/10")
+               assert r.match("/user/10/10")
+               assert r.match("/user/10/10/")
+               assert not r.match("/user/10/10/profile")
+       end
+
+       fun test_param_route_match_good_uri_params_6 do
+               var r = new AppParamRoute("/user/:id/settings/:foo")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert not r.match("/foo")
+               assert not r.match("/user/10")
+               assert not r.match("/user/10/10")
+               assert not r.match("/user/10/10/")
+               assert not r.match("/user/10/10/profile")
+               assert r.match("/user/10/settings/profile")
+               assert r.match("/user/10/settings/profile/")
+               assert not r.match("/user/10/settings/profile/10")
+       end
+end
+
+class TestRouteMatching
+       super TestSuite
+
+       fun test_glob_route_match_good_uri_prefix1 do
+               var r = new AppGlobRoute("/*")
+               assert r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_match_good_uri_prefix2 do
+               var r = new AppGlobRoute("/user/*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_match_good_uri_prefix3 do
+               var r = new AppGlobRoute("/user*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_work_with_parameters_1 do
+               var r = new AppGlobRoute("/:id/*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+               assert r.match("/user/10/profile")
+       end
+
+       fun test_glob_route_work_with_parameters_2 do
+               var r = new AppGlobRoute("/:id*")
+               assert not r.match("/")
+               assert r.match("/user")
+               assert r.match("/user/10")
+       end
+
+       fun test_glob_route_work_with_parameters_3 do
+               var r = new AppGlobRoute("/user/:id/*")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert r.match("/user/10")
+               assert r.match("/user/10/profile")
+       end
+
+       fun test_glob_route_work_with_parameters_4 do
+               var r = new AppGlobRoute("/user/:id*")
+               assert not r.match("/")
+               assert not r.match("/user")
+               assert r.match("/user/10")
+               assert r.match("/user/10/profile")
+       end
+end
diff --git a/lib/popcorn/tests/Makefile b/lib/popcorn/tests/Makefile
new file mode 100644 (file)
index 0000000..7e78098
--- /dev/null
@@ -0,0 +1,21 @@
+# Copyright 2013 Alexandre Terrasa <alexandre@moz-code.org>.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+all: tests
+
+check: clean
+       ./tests.sh
+
+clean:
+       rm -rf out/
diff --git a/lib/popcorn/tests/base_tests.nit b/lib/popcorn/tests/base_tests.nit
new file mode 100644 (file)
index 0000000..a56a33d
--- /dev/null
@@ -0,0 +1,63 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import popcorn
+import pthreads
+
+redef class Sys
+       var test_host = "localhost"
+
+       # Return a new port for each instance
+       fun test_port: Int do
+               srand
+               return 10000+20000.rand
+       end
+end
+
+class AppThread
+       super Thread
+
+       var host: String
+       var port: Int
+       var app: App
+
+       redef fun main
+       do
+               # Hide testing concept to force nitcorn to actually run
+               "NIT_TESTING".setenv("false")
+               app.quiet = true
+               app.listen(host, port)
+               return null
+       end
+end
+
+class ClientThread
+       super Thread
+
+       var host: String
+       var port: Int
+
+       redef fun main do return null
+
+       # Regex to catch and hide the port from the output to get consistent results
+       var host_re: Regex = "localhost:\[0-9\]+".to_re
+
+       fun system(cmd: String, title: nullable String)
+       do
+               title = title or else cmd
+               title = title.replace(host_re, "localhost:*****")
+               print "\n[Client] {title}"
+               sys.system cmd
+       end
+end
diff --git a/lib/popcorn/tests/res/test_example_advanced_logger.res b/lib/popcorn/tests/res/test_example_advanced_logger.res
new file mode 100644 (file)
index 0000000..7e56183
--- /dev/null
@@ -0,0 +1,16 @@
+
+[Client] curl -s localhost:*****/
+GET / \e[32m200\e[m (0.0s)
+Hello World!
+[Client] curl -s localhost:*****/about
+GET /about \e[33m404\e[m (0.0s)
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_angular.res b/lib/popcorn/tests/res/test_example_angular.res
new file mode 100644 (file)
index 0000000..bb94961
--- /dev/null
@@ -0,0 +1,22 @@
+
+[Client] curl -s localhost:*****/counter
+{"label":"Visitors","value":0}
+[Client] curl -s localhost:*****/counter -X POST
+{"label":"Visitors","value":1}
+[Client] curl -s localhost:*****/counter
+{"label":"Visitors","value":1}
+[Client] curl -s localhost:*****/not_found
+<!DOCTYPE html>
+<html lang='en' ng-app='ng-example'>
+       <head>
+               <base href='/'>
+               <title>ng-example</title>
+       </head>
+       <body>
+               <div ng-view></div>
+
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.min.js'></script>
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-route.js'></script>
+               <script src='/javascripts/ng-example.js'></script>
+       </body>
+</html>
diff --git a/lib/popcorn/tests/res/test_example_glob_route.res b/lib/popcorn/tests/res/test_example_glob_route.res
new file mode 100644 (file)
index 0000000..1e91986
--- /dev/null
@@ -0,0 +1,42 @@
+
+[Client] curl -s localhost:*****/user/Morriar/item/10
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/user/Morriar/item/10/
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/user/Morriar/item/10/profile
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/user/Morriar/item/10/profile/settings
+Here the item 10 of the use Morriar.
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_hello.res b/lib/popcorn/tests/res/test_example_hello.res
new file mode 100644 (file)
index 0000000..194fb36
--- /dev/null
@@ -0,0 +1,29 @@
+
+[Client] curl -s localhost:*****
+<h1>Hello World!</h1>
+[Client] curl -s localhost:*****/
+<h1>Hello World!</h1>
+[Client] curl -s localhost:*****///////////
+<h1>Hello World!</h1>
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_html_error_handler.res b/lib/popcorn/tests/res/test_example_html_error_handler.res
new file mode 100644 (file)
index 0000000..3015280
--- /dev/null
@@ -0,0 +1,23 @@
+
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>An error occurred!</title>
+               </head>
+               <body>
+               <h1>404 An error occurred!</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/about
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>An error occurred!</title>
+               </head>
+               <body>
+               <h1>404 An error occurred!</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_param_route.res b/lib/popcorn/tests/res/test_example_param_route.res
new file mode 100644 (file)
index 0000000..f0b898f
--- /dev/null
@@ -0,0 +1,38 @@
+
+[Client] curl -s localhost:*****/Morriar
+Hello Morriar
+[Client] curl -s localhost:*****//
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found
+Hello not_found
+[Client] curl -s localhost:*****/not_found/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_post.res b/lib/popcorn/tests/res/test_example_post.res
new file mode 100644 (file)
index 0000000..72589e2
--- /dev/null
@@ -0,0 +1,38 @@
+
+[Client] curl -s localhost:*****/ -X POST
+URI: /
+Body: 
+
+[Client] curl -s localhost:*****/ --data 'user'
+POST Error: user format error on user
+URI: /
+Body: user
+
+[Client] curl -s localhost:*****/ --data 'user=Morriar'
+URI: /
+Body: user=Morriar
+user: Morriar
+
+[Client] curl -s localhost:*****/ --data 'user=&order=desc'
+URI: /
+Body: user=&order=desc
+user: 
+order: desc
+
+[Client] curl -s localhost:*****/ --data 'user=Morriar&order=desc'
+URI: /
+Body: user=Morriar&order=desc
+user: Morriar
+order: desc
+
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_query_string.res b/lib/popcorn/tests/res/test_example_query_string.res
new file mode 100644 (file)
index 0000000..699b0db
--- /dev/null
@@ -0,0 +1,24 @@
+
+[Client] curl -s localhost:*****/
+URI: /
+Query string: 
+
+[Client] curl -s localhost:*****/?user=Morriar
+URI: /
+Query string: user=Morriar
+user: Morriar
+
+[Client] curl -s localhost:*****/?reload
+URI: /
+Query string: reload
+
+[Client] curl -s localhost:*****/?foo\&bar=baz
+URI: /
+Query string: foo&bar=baz
+bar: baz
+
+[Client] curl -s localhost:*****/?items=10\&order=asc
+URI: /
+Query string: items=10&order=asc
+items: 10
+order: asc
diff --git a/lib/popcorn/tests/res/test_example_router.res b/lib/popcorn/tests/res/test_example_router.res
new file mode 100644 (file)
index 0000000..fb222c3
--- /dev/null
@@ -0,0 +1,48 @@
+
+[Client] curl -s localhost:*****
+Site Home
+[Client] curl -s localhost:*****/
+Site Home
+[Client] curl -s localhost:*****/user
+User logged
+User Home
+[Client] curl -s localhost:*****/user/
+User logged
+User Home
+[Client] curl -s localhost:*****/user/profile
+User logged
+User Profile
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/not_found
+User logged
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/products/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_session.res b/lib/popcorn/tests/res/test_example_session.res
new file mode 100644 (file)
index 0000000..4ca758e
--- /dev/null
@@ -0,0 +1,41 @@
+
+[Client] curl -s localhost:*****/
+               <p>Is logged: false</p>
+               <form action="/" method="POST">
+                       <input type="submit" value="Login" />
+               </form>
+[Client] curl -s localhost:*****/ -X POST
+
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/products/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_simple_error_handler.res b/lib/popcorn/tests/res/test_example_simple_error_handler.res
new file mode 100644 (file)
index 0000000..67e5a72
--- /dev/null
@@ -0,0 +1,5 @@
+
+[Client] curl -s localhost:*****/
+Hello World!
+[Client] curl -s localhost:*****/about
+An error occurred!
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_simple_logger.res b/lib/popcorn/tests/res/test_example_simple_logger.res
new file mode 100644 (file)
index 0000000..9bc50e3
--- /dev/null
@@ -0,0 +1,16 @@
+
+[Client] curl -s localhost:*****/
+Request Logged
+Hello World!
+[Client] curl -s localhost:*****/about
+Request Logged
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_static.res b/lib/popcorn/tests/res/test_example_static.res
new file mode 100644 (file)
index 0000000..7f07ed8
--- /dev/null
@@ -0,0 +1,73 @@
+
+[Client] curl -s localhost:*****/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/static/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_example_static_default.res b/lib/popcorn/tests/res/test_example_static_default.res
new file mode 100644 (file)
index 0000000..59392f6
--- /dev/null
@@ -0,0 +1,88 @@
+
+[Client] curl -s localhost:*****/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/css/not_found.nit
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/static/css/not_found.nit
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/not_found.nit
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Default Page</h1>
+       </body>
+</html>
diff --git a/lib/popcorn/tests/res/test_example_static_multiple.res b/lib/popcorn/tests/res/test_example_static_multiple.res
new file mode 100644 (file)
index 0000000..3a6692f
--- /dev/null
@@ -0,0 +1,106 @@
+
+[Client] curl -s localhost:*****/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/
+<!DOCTYPE html>
+<html>
+       <body>
+               <h1>Another Index</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/static/css/style.css
+body {
+       color: blue;
+       padding: 20px;
+}
+
+[Client] curl -s localhost:*****/static/js/app.js
+alert("Hello World!");
+
+[Client] curl -s localhost:*****/static/hello.html
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+
+               <title>Some Popcorn love</title>
+
+               <link rel="stylesheet" type="text/css" href="/css/style.css">
+       </head>
+       <body>
+               <h1>Hello Popcorn!</h1>
+
+               <img src="/images/trollface.jpg" alt="maybe it's a kitten?" />
+
+               <script src="/js/app.js"></script>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/static/
+<!DOCTYPE html>
+<html>
+       <body>
+               <h1>Another Index</h1>
+       </body>
+</html>
+
+[Client] curl -s localhost:*****/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/static/css/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/not_found.nit
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_router.res b/lib/popcorn/tests/res/test_router.res
new file mode 100644 (file)
index 0000000..37be3a6
--- /dev/null
@@ -0,0 +1,50 @@
+
+[Client] curl -s localhost:*****
+/
+[Client] curl -s localhost:*****/
+/
+[Client] curl -s localhost:*****/user
+/user
+[Client] curl -s localhost:*****/user/
+/user
+[Client] curl -s localhost:*****/user/settings
+/user/settings
+[Client] curl -s localhost:*****/products
+/products
+[Client] curl -s localhost:*****/products/
+/products
+[Client] curl -s localhost:*****/products/list
+/products/list
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/products/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/res/test_routes.res b/lib/popcorn/tests/res/test_routes.res
new file mode 100644 (file)
index 0000000..0c1abfa
--- /dev/null
@@ -0,0 +1,49 @@
+
+[Client] curl -s localhost:*****
+/
+[Client] curl -s localhost:*****/
+/
+[Client] curl -s localhost:*****/misc
+/misc/everything
+[Client] curl -s localhost:*****/misc/foo
+/misc/everything
+[Client] curl -s localhost:*****/misc/foo/bar
+/misc/everything
+[Client] curl -s localhost:*****/misc/foo/baz
+/misc/everything
+[Client] curl -s localhost:*****/user
+/user
+[Client] curl -s localhost:*****/user/
+/user
+[Client] curl -s localhost:*****/user/id
+/user/id
+[Client] curl -s localhost:*****/user/id/profile
+/user/id/profile
+[Client] curl -s localhost:*****/user/id/misc/foo
+/user/id/misc/everything
+[Client] curl -s localhost:*****/user/id/misc/foo/bar
+/user/id/misc/everything
+[Client] curl -s localhost:*****/user/id/misc/foo/bar/baz
+/user/id/misc/everything
+[Client] curl -s localhost:*****/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
+[Client] curl -s localhost:*****/user/id/not_found
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>Not Found</title>
+               </head>
+               <body>
+               <h1>404 Not Found</h1>
+               </body>
+               </html>
\ No newline at end of file
diff --git a/lib/popcorn/tests/test_example_advanced_logger.nit b/lib/popcorn/tests/test_example_advanced_logger.nit
new file mode 100644 (file)
index 0000000..29b3fc2
--- /dev/null
@@ -0,0 +1,47 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_advanced_logger
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use_before("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use_after("/*", new LogHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_angular.nit b/lib/popcorn/tests/test_example_angular.nit
new file mode 100644 (file)
index 0000000..821aa32
--- /dev/null
@@ -0,0 +1,48 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_angular
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/counter"
+               system "curl -s {host}:{port}/counter -X POST"
+               system "curl -s {host}:{port}/counter"
+               system "curl -s {host}:{port}/not_found" # handled by angular controller
+               return null
+       end
+end
+
+var app = new App
+app.use("/counter", new CounterAPI)
+app.use("/*", new StaticHandler("../examples/angular/www/", "index.html"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_glob_route.nit b/lib/popcorn/tests/test_example_glob_route.nit
new file mode 100644 (file)
index 0000000..d1ca6c8
--- /dev/null
@@ -0,0 +1,51 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_glob_route
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/user/Morriar/item/10"
+               system "curl -s {host}:{port}/user/Morriar/item/10/"
+               system "curl -s {host}:{port}/user/Morriar/item/10/profile"
+               system "curl -s {host}:{port}/user/Morriar/item/10/profile/settings"
+
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/not_found/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/user/:user/item/:item/*", new UserItem)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_hello.nit b/lib/popcorn/tests/test_example_hello.nit
new file mode 100644 (file)
index 0000000..d7327e3
--- /dev/null
@@ -0,0 +1,48 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_hello
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}///////////"
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/not_found/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_html_error_handler.nit b/lib/popcorn/tests/test_example_html_error_handler.nit
new file mode 100644 (file)
index 0000000..95abb7a
--- /dev/null
@@ -0,0 +1,45 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_html_error_handler
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use("/*", new HtmlErrorHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_param_route.nit b/lib/popcorn/tests/test_example_param_route.nit
new file mode 100644 (file)
index 0000000..541fde0
--- /dev/null
@@ -0,0 +1,49 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_param_route
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/Morriar"
+               system "curl -s {host}:{port}//"
+
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/not_found/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/:user", new UserHome)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_post.nit b/lib/popcorn/tests/test_example_post.nit
new file mode 100644 (file)
index 0000000..bf5330e
--- /dev/null
@@ -0,0 +1,50 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_post_handler
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/ -X POST"
+               system "curl -s {host}:{port}/ --data 'user'"
+               system "curl -s {host}:{port}/ --data 'user=Morriar'"
+               system "curl -s {host}:{port}/ --data 'user=\&order=desc'"
+               system "curl -s {host}:{port}/ --data 'user=Morriar\&order=desc'"
+
+               system "curl -s {host}:{port}/"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new PostHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_query_string.nit b/lib/popcorn/tests/test_example_query_string.nit
new file mode 100644 (file)
index 0000000..6c489ba
--- /dev/null
@@ -0,0 +1,48 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_query_string
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/?user=Morriar"
+               system "curl -s {host}:{port}/?reload"
+               system "curl -s {host}:{port}/?foo\\&bar=baz"
+               system "curl -s {host}:{port}/?items=10\\&order=asc"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new QueryStringHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_router.nit b/lib/popcorn/tests/test_example_router.nit
new file mode 100644 (file)
index 0000000..f9ab8df
--- /dev/null
@@ -0,0 +1,58 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_router
+import base_tests
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/user"
+               system "curl -s {host}:{port}/user/"
+               system "curl -s {host}:{port}/user/profile"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/not_found"
+               system "curl -s {host}:{port}/products/not_found"
+               return null
+       end
+end
+
+var user_router = new Router
+user_router.use("/*", new UserLogger)
+user_router.use("/", new UserHome)
+user_router.use("/profile", new UserProfile)
+
+var app = new App
+app.use("/", new AppHome)
+app.use("/user", user_router)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_session.nit b/lib/popcorn/tests/test_example_session.nit
new file mode 100644 (file)
index 0000000..b7bedc3
--- /dev/null
@@ -0,0 +1,50 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_session
+import base_tests
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/ -X POST"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/not_found"
+               system "curl -s {host}:{port}/products/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/*", new SessionInit)
+app.use("/", new AppLogin)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_simple_error_handler.nit b/lib/popcorn/tests/test_example_simple_error_handler.nit
new file mode 100644 (file)
index 0000000..ce671f0
--- /dev/null
@@ -0,0 +1,46 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_simple_error_handler
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.use("/*", new SimpleErrorHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_simple_logger.nit b/lib/popcorn/tests/test_example_simple_logger.nit
new file mode 100644 (file)
index 0000000..e8aab41
--- /dev/null
@@ -0,0 +1,46 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import example_simple_logger
+import base_tests
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/about"
+               return null
+       end
+end
+
+var app = new App
+app.use_before("/*", new LogHandler)
+app.use("/", new HelloHandler)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_static.nit b/lib/popcorn/tests/test_example_static.nit
new file mode 100644 (file)
index 0000000..e23a7be
--- /dev/null
@@ -0,0 +1,52 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base_tests
+import example_static
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/css/style.css"
+               system "curl -s {host}:{port}/js/app.js"
+               system "curl -s {host}:{port}/hello.html"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/css/not_found.nit"
+               system "curl -s {host}:{port}/static/css/not_found.nit"
+               system "curl -s {host}:{port}/not_found.nit"
+
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new StaticHandler("../examples/static_files/public/"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_static_default.nit b/lib/popcorn/tests/test_example_static_default.nit
new file mode 100644 (file)
index 0000000..ced04e2
--- /dev/null
@@ -0,0 +1,52 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base_tests
+import example_static_default
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/css/style.css"
+               system "curl -s {host}:{port}/js/app.js"
+               system "curl -s {host}:{port}/hello.html"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/css/not_found.nit"
+               system "curl -s {host}:{port}/static/css/not_found.nit"
+               system "curl -s {host}:{port}/not_found.nit"
+
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new StaticHandler("../examples/static_files/public/", "default.html"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_example_static_multiple.nit b/lib/popcorn/tests/test_example_static_multiple.nit
new file mode 100644 (file)
index 0000000..5e99ae1
--- /dev/null
@@ -0,0 +1,60 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base_tests
+import example_static_multiple
+
+class TestClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}/css/style.css"
+               system "curl -s {host}:{port}/js/app.js"
+               system "curl -s {host}:{port}/hello.html"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/static/css/style.css"
+               system "curl -s {host}:{port}/static/js/app.js"
+               system "curl -s {host}:{port}/static/hello.html"
+               system "curl -s {host}:{port}/static/"
+
+               system "curl -s {host}:{port}/css/not_found.nit"
+               system "curl -s {host}:{port}/static/css/not_found.nit"
+               system "curl -s {host}:{port}/not_found.nit"
+
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new StaticHandler("../examples/static_files/public/"))
+app.use("/", new StaticHandler("../examples/static_files/files/"))
+app.use("/static", new StaticHandler("../examples/static_files/public/"))
+app.use("/static", new StaticHandler("../examples/static_files/files/"))
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new TestClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_router.nit b/lib/popcorn/tests/test_router.nit
new file mode 100644 (file)
index 0000000..915ce4a
--- /dev/null
@@ -0,0 +1,73 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base_tests
+
+class TestHandler
+       super Handler
+
+       var marker: String
+
+       redef fun get(req, res) do res.send marker
+end
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+               system "curl -s {host}:{port}/user"
+               system "curl -s {host}:{port}/user/"
+               system "curl -s {host}:{port}/user/settings"
+               system "curl -s {host}:{port}/products"
+               system "curl -s {host}:{port}/products/"
+               system "curl -s {host}:{port}/products/list"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/not_found"
+               system "curl -s {host}:{port}/products/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new TestHandler("/"))
+app.use("/about", new TestHandler("/about"))
+
+var router1 = new App
+router1.use("/", new TestHandler("/user"))
+router1.use("/settings", new TestHandler("/user/settings"))
+app.use("/user", router1)
+
+var router2 = new App
+router2.use("/", new TestHandler("/products"))
+router2.use("/list", new TestHandler("/products/list"))
+app.use("/products", router2)
+
+var host = test_host
+var port = test_port
+
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+exit 0
diff --git a/lib/popcorn/tests/test_routes.nit b/lib/popcorn/tests/test_routes.nit
new file mode 100644 (file)
index 0000000..2db660a
--- /dev/null
@@ -0,0 +1,76 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base_tests
+
+class TestHandler
+       super Handler
+
+       var marker: String
+
+       redef fun get(req, res) do res.send marker
+end
+
+class HelloClient
+       super ClientThread
+
+       redef fun main do
+               system "curl -s {host}:{port}"
+               system "curl -s {host}:{port}/"
+
+               system "curl -s {host}:{port}/misc"
+               system "curl -s {host}:{port}/misc/foo"
+               system "curl -s {host}:{port}/misc/foo/bar"
+               system "curl -s {host}:{port}/misc/foo/baz"
+
+               system "curl -s {host}:{port}/user"
+               system "curl -s {host}:{port}/user/"
+               system "curl -s {host}:{port}/user/id"
+               system "curl -s {host}:{port}/user/id/profile"
+               system "curl -s {host}:{port}/user/id/misc/foo"
+               system "curl -s {host}:{port}/user/id/misc/foo/bar"
+               system "curl -s {host}:{port}/user/id/misc/foo/bar/baz"
+
+               system "curl -s {host}:{port}/not_found"
+               system "curl -s {host}:{port}/user/id/not_found"
+               return null
+       end
+end
+
+var app = new App
+app.use("/", new TestHandler("/"))
+app.use("/user", new TestHandler("/user"))
+app.use("/misc/*", new TestHandler("/misc/everything"))
+app.use("/user/:id", new TestHandler("/user/id"))
+app.use("/user/:id/profile", new TestHandler("/user/id/profile"))
+app.use("/user/:id/misc/*", new TestHandler("/user/id/misc/everything"))
+
+var host = test_host
+var port = test_port
+
+# First, launch a server in the background
+var server = new AppThread(host, port, app)
+server.start
+0.1.sleep
+
+# Then, launch a client running test requests
+var client = new HelloClient(host, port)
+client.start
+client.join
+0.1.sleep
+
+# Force quit the server
+exit 0
diff --git a/lib/popcorn/tests/tests.sh b/lib/popcorn/tests/tests.sh
new file mode 100755 (executable)
index 0000000..4406058
--- /dev/null
@@ -0,0 +1,108 @@
+#!/bin/bash
+
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+BIN=../bin
+OUT=./out
+RES=./res
+
+NITC=../../../bin/nitc
+
+compile() {
+       local test="$1"
+       $NITC $test.nit -o $OUT/$test.bin 1>&2 2> $OUT/$test.cmp_err
+}
+
+test_prog()
+{
+       local test="$1"
+
+       chmod +x $OUT/$test.bin 2> $OUT/$test.err
+       $OUT/$test.bin > $OUT/$test.res 2> $OUT/$test.err
+
+       diff $OUT/$test.res $RES/$test.res > $OUT/$test.diff 2> /dev/null
+}
+
+# return
+#  0 if the sav not exists
+#  1 if the file does match
+#  2 if the file does not match
+check_result() {
+       local test="$1"
+
+       if [ -s "$OUT/$test.cmp_err" ]; then
+               return 0
+       elif [ -s "$OUT/$test.err" ]; then
+               return 1
+       elif [ ! -r "$RES/$test.res" ]; then
+               return 2
+       elif [ -s "$OUT/$test.diff" ]; then
+               return 3
+       else
+               return 4
+       fi
+}
+
+echo "Testing..."
+echo ""
+
+rm -rf $OUT 2>/dev/null
+mkdir $OUT 2>/dev/null
+
+all=0
+ok=0
+ko=0
+sk=0
+
+for file in `ls test_*.nit`; do
+       ((all++))
+       test="${file%.*}"
+       echo -n "* $test: "
+
+       compile $test
+       test_prog $test
+       check_result $test
+
+       case "$?" in
+               0)
+                       echo "compile error (cat $OUT/$test.cmp_err)"
+                       ((ko++))
+                       ;;
+               1)
+                       echo "error (cat $OUT/$test.cmp_err)"
+                       ((ko++))
+                       ;;
+               2)
+                       echo "skip ($test.res not found)"
+                       ((sk++))
+                       continue;;
+               3)
+                       echo "error (diff $OUT/$test.res $RES/$test.res)"
+                       ((ko++))
+                       ;;
+               4)
+                       echo "success"
+                       ((ok++))
+                       ;;
+
+       esac
+done
+echo ""
+echo "==> success $ok/$all ($ko tests failed, $sk skipped)"
+
+# return result
+test "$ok" == "$all"
diff --git a/lib/postgresql/native_postgres.nit b/lib/postgresql/native_postgres.nit
new file mode 100644 (file)
index 0000000..d32c908
--- /dev/null
@@ -0,0 +1,145 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2015-2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A native wrapper ove the postgres c api
+module native_postgres is pkgconfig("libpq")
+
+in "C header" `{
+  #include <libpq-fe.h>
+`}
+
+extern class ExecStatusType `{int`}
+  new empty           `{ return PGRES_EMPTY_QUERY; `}
+  new command_ok      `{ return PGRES_COMMAND_OK; `}
+  new tuples_ok       `{ return PGRES_TUPLES_OK; `}
+  new copy_out        `{ return PGRES_COPY_OUT; `}
+  new copy_in         `{ return PGRES_COPY_IN; `}
+  new bad_response    `{ return PGRES_BAD_RESPONSE; `}
+  new nonfatal_error  `{ return PGRES_NONFATAL_ERROR; `}
+  new fatal_error     `{ return PGRES_FATAL_ERROR; `}
+
+  fun is_ok: Bool `{
+    return !(self == PGRES_BAD_RESPONSE || self == PGRES_NONFATAL_ERROR || self == PGRES_FATAL_ERROR);
+  `}
+
+  redef fun to_s import NativeString.to_s `{
+    char * err = PQresStatus(self);
+    if(err == NULL) err = "";
+    return NativeString_to_s(err);
+  `}
+end
+
+extern class ConnStatusType `{int`}
+  new connection_ok `{ return CONNECTION_OK; `}
+  new connection_bad `{ return CONNECTION_BAD; `}
+
+  fun is_ok: Bool `{return self == CONNECTION_OK; `}
+end
+
+extern class NativePGResult `{PGresult *`}
+  # Frees the memory block associated with the result
+  fun clear `{PQclear(self); `}
+
+  # Returns the number of rows in the query result
+  fun ntuples:Int `{ return PQntuples(self); `}
+
+  # Returns the number of columns in each row of the query result
+  fun nfields:Int `{return PQnfields(self); `}
+
+  # Returns the ExecStatusType of a result
+  fun status: ExecStatusType `{ return PQresultStatus(self); `}
+
+  # Returns the field name of a given column_number
+  fun fname(column_number:Int):String import NativeString.to_s `{
+    return NativeString_to_s( PQfname(self, column_number));
+  `}
+
+  # Returns the column number associated with the column name
+  fun fnumber(column_name:String):Int import String.to_cstring `{
+    return PQfnumber(self, String_to_cstring(column_name));
+  `}
+
+  # Returns a single field value of one row of the result at row_number, column_number
+  fun value(row_number:Int, column_number:Int):String import NativeString.to_s `{
+    return NativeString_to_s(PQgetvalue(self, row_number, column_number));
+  `}
+
+  # Tests wether a field is a null value
+  fun is_null(row_number:Int, column_number: Int): Bool `{
+    return PQgetisnull(self, row_number, column_number);
+  `}
+
+end
+extern class NativePostgres `{PGconn *`}
+
+  # Connect to a new database using the conninfo string as a parameter
+  new connectdb(conninfo: Text) import Text.to_cstring `{
+    PGconn * self = NULL;
+    self = PQconnectdb(Text_to_cstring(conninfo));
+    return self;
+  `}
+
+  # Submits a query to the server and waits for the result returns the ExecStatustype of the query
+  fun exec(query: Text): NativePGResult import Text.to_cstring `{
+    PGresult *res = PQexec(self, Text_to_cstring(query));
+    return res;
+  `}
+
+  # Prepares a statement with the given parameters
+  fun prepare(stmt: String, query: String, nParams: Int): NativePGResult import String.to_cstring `{
+    const char * stmtName = String_to_cstring(stmt);
+    const char * queryStr = String_to_cstring(query);
+    PGresult * res = PQprepare(self, stmtName, queryStr, nParams, NULL);
+    return res;
+  `}
+
+  fun exec_prepared(stmt: String, nParams: Int, values: Array[String], pLengths: Array[Int], pFormats: Array[Int], resultFormat: Int): NativePGResult import String.to_cstring, Array[String].[], Array[Int].[] `{
+    const char * stmtName = String_to_cstring(stmt);
+    const char * paramValues[nParams];
+    int paramLengths[nParams];
+    int paramFormats[nParams];
+    int i;
+    for(i = 0; i < nParams; i++)
+      paramValues[i] = String_to_cstring(Array_of_String__index(values, i));
+    for(i = 0; i < nParams; i++)
+      paramLengths[i] = Array_of_Int__index(pLengths, i);
+    for(i = 0; i < nParams; i++)
+      paramFormats[i] = Array_of_Int__index(pFormats, i);
+    PGresult * res = PQexecPrepared(self, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat);
+    return res;
+  `}
+
+  # Returns the error message of the last operation on the connection
+  fun error: String import NativeString.to_s `{
+    char * error = PQerrorMessage(self);
+    return NativeString_to_s(error);
+  `}
+
+  # Returns the status of this connection
+  fun status: ConnStatusType `{
+    return PQstatus(self);
+  `}
+
+  # Closes the connection to the server
+  fun finish  `{
+    PQfinish(self);
+  `}
+
+  # Closes the connection to the server and attempts to reconnect with the previously used params
+  fun reset `{
+    PQreset(self);
+  `}
+end
diff --git a/lib/postgresql/package.ini b/lib/postgresql/package.ini
new file mode 100644 (file)
index 0000000..dc82d85
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=postgresql
+tags=database,lib
+maintainer=Guilherme Mansur <guilhermerpmansur@gmail.com>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/postgresql/
+git=https://github.com/nitlang/nit.git
+git.directory=lib/postgresql/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
\ No newline at end of file
diff --git a/lib/postgresql/postgres.nit b/lib/postgresql/postgres.nit
new file mode 100644 (file)
index 0000000..22e498f
--- /dev/null
@@ -0,0 +1,144 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Guilherme Mansur<guilhermerpmansur@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Services to manipulate a Postgres database
+#
+# For more information, refer to the documentation of http://www.postgresql.org/docs/manuals/
+#
+# ### Usage example
+#
+# ~~~
+# class Animal
+#   var name: String
+#   var kind: String
+#   var age: Int
+# end
+#
+# var animals = new Array[Animal]
+# var dog = new Animal("Lassy", "dog", 10)
+# var cat = new Animal("Garfield", "cat", 3)
+# var turtle = new Animal("George", "turtle", 123)
+#
+# animals.add(dog)
+# animals.add(cat)
+# animals.add(turtle)
+#
+# var db = new Postgres.open("dbname=postgres")
+#
+# assert db_is_open: not db.is_closed
+# assert create_table: db.create_table("IF NOT EXISTS animals (aname TEXT PRIMARY KEY, kind TEXT NOT NULL, age INT NOT NULL)") else print db.error
+#
+# for animal in animals do
+#   assert insert: db.insert("INTO animals VALUES('{animal.name}', '{animal.kind}', {animal.age})") else print db.error
+# end
+#
+# var result = db.raw_execute("SELECT * FROM animals")
+# assert  result.is_ok
+# assert drop_table: db.execute("DROP TABLE animals")
+# db.finish
+# assert db_is_closed: db.is_closed
+# ~~~
+module postgres
+
+private import native_postgres
+
+# A connection to a Postgres database
+class Postgres
+  private var native_connection: NativePostgres
+
+  var is_closed = true
+
+  # Open the connnection with the database using the `conninfo`
+  init open(conninfo: Text)
+  do
+    init(new NativePostgres.connectdb(conninfo))
+    if native_connection.status.is_ok then is_closed = false
+  end
+
+  # Close this connection with the database
+  fun finish
+  do
+    if is_closed then return
+
+    is_closed = true
+
+    native_connection.finish
+  end
+
+  fun prepare(stmt_name:String, query:String, num_params: Int):PGResult do return new PGResult(native_connection.prepare(stmt_name, query, num_params))
+
+  fun exec_prepared(stmt_name: String, num_params: Int, values: Array[String], param_lengths: Array[Int], param_formats: Array[Int], result_format: Int):PGResult do
+    return new PGResult(native_connection.exec_prepared(stmt_name, num_params, values, param_lengths, param_formats, result_format))
+  end
+
+  # Executes a `query` and returns the raw `PGResult`
+  fun raw_execute(query: Text): PGResult do return new PGResult(native_connection.exec(query))
+
+  # Execute the `sql` statement and returns `true` on success
+  fun execute(query: Text): Bool do return native_connection.exec(query).status.is_ok
+
+  # Create a table on the DB with a statement beginning with "CREATE TABLE ", followed by `rest`
+  #
+  # This method does not escape special characters.
+  fun create_table(rest: Text): Bool do return execute("CREATE TABLE " + rest)
+
+  # Insert in the DB with a statement beginning with "INSERT ", followed by `rest`
+  #
+  # This method does not escape special characters.
+  fun insert(rest: Text): Bool do return execute("INSERT " + rest)
+
+  # Replace in the DB with a statement beginning with "REPLACE", followed by `rest`
+  #
+  # This method does not escape special characters.
+  fun replace(rest: Text): Bool do return execute("REPLACE " + rest)
+
+  # The latest error message on the connection an empty string if none
+  fun error: String do return native_connection.error
+
+  # The status of this connection
+  fun is_valid: Bool do return native_connection.status.is_ok
+
+  # Resets the connection to the database
+  fun reset do native_connection.reset
+end
+
+# The result of a given query
+class PGResult
+  private var pg_result: NativePGResult
+
+  fun clear do pg_result.clear
+
+  # Returns the number of rows in the query result
+  fun ntuples:Int do return pg_result.ntuples
+
+  # Returns the number of columns in each row of the query result
+  fun nfields:Int do return pg_result.nfields
+
+  # Returns the ExecStatusType of a result
+  fun is_ok:Bool do return pg_result.status.is_ok
+
+  # Returns the field name of a given `column_number`
+  fun fname(column_number:Int):String do return pg_result.fname(column_number)
+
+  # Returns the column number associated with the `column_name`
+  fun fnumber(column_name:String):Int do return pg_result.fnumber(column_name)
+
+  # Returns a single field value of one row of the result at `row_number`, `column_number`
+  fun value(row_number:Int, column_number:Int):String  do return pg_result.value(row_number, column_number)
+
+  # Tests wether a field specified by the `row_number` and `column_number` is null.
+  fun is_null(row_number:Int, column_number: Int): Bool do return pg_result.is_null(row_number, column_number)
+end
diff --git a/lib/readline.ini b/lib/readline.ini
new file mode 100644 (file)
index 0000000..c7e2ee9
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=readline
+tags=lib
+maintainer=Frédéric Vachon <fredvac@gmail.com>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/readline.nit
+git=https://github.com/nitlang/nit.git
+git.directory=lib/readline.nit
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
diff --git a/lib/readline.nit b/lib/readline.nit
new file mode 100644 (file)
index 0000000..783e4e4
--- /dev/null
@@ -0,0 +1,58 @@
+# This file is part of NIT (http://www.nitlanguage.org).
+#
+# Copyright 2016 Frédéric Vachon <fredvac@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# GNU readline library wrapper
+module readline is ldflags "-lreadline"
+
+in "C" `{
+       #include <readline/readline.h>
+       #include <readline/history.h>
+`}
+
+private fun native_readline(prompt: NativeString): NativeString `{
+       return readline(prompt);
+`}
+
+private fun native_add_history(data: NativeString) `{
+       if (data == NULL) return;
+       add_history(data);
+`}
+
+# Set emacs keybindings mode
+fun set_vi_mode `{ rl_editing_mode = 0; `}
+
+# Set emacs keybindings mode
+fun set_emacs_mode `{ rl_editing_mode = 1; `}
+
+# Use the GNU Library readline function
+# Returns `null` if EOF is read
+# If `with_history` is true, it will save all commands in the history except
+# empty strings and white characters strings
+fun readline(message: String, with_history: nullable Bool): nullable String do
+       var line = native_readline(message.to_cstring)
+       if line.address_is_null then return null
+
+       var nit_str = line.to_s
+
+       if with_history != null and with_history then
+               if nit_str.trim != "" then native_add_history(line)
+       end
+
+       return nit_str
+end
+
+# Adds the data String to the history no matter what it contains
+fun add_history(data: String) do native_add_history data.to_cstring
index 8cfa4da..243b5d8 100644 (file)
@@ -73,13 +73,16 @@ extern class Timespec `{struct timespec*`}
                clock_gettime(CLOCK_MONOTONIC, self);
        `}
 
-       # Substract a Timespec from `self`.
-       fun - ( o : Timespec ) : Timespec
+       # Subtract `other` from `self`
+       fun -(other: Timespec): Timespec
        do
-               var s = sec - o.sec
-               var ns = nanosec - o.nanosec
-               if ns > nanosec then s += 1
-               return new Timespec( s, ns )
+               var s = sec - other.sec
+               var ns = nanosec - other.nanosec
+               if ns < 0 then
+                       s -= 1
+                       ns += 1000000000
+               end
+               return new Timespec(s, ns)
        end
 
        # Number of whole seconds of elapsed time.
@@ -117,15 +120,33 @@ extern class Timespec `{struct timespec*`}
 end
 
 # Keeps track of real time
+#
+# ~~~
+# var clock = new Clock
+#
+# # sleeping at least 1s
+# 1.0.sleep
+# assert clock.total >= 1.0
+# assert clock.lapse >= 1.0
+#
+# # sleeping at least 5ms
+# 0.005.sleep
+# assert clock.total >= 1.005
+# assert clock.lapse >= 0.005
+# ~~~
 class Clock
-       # Time at instanciation
+       super FinalizableOnce
+
+       # TODO use less mallocs
+
+       # Time at creation
        protected var time_at_beginning = new Timespec.monotonic_now
 
        # Time at last time a lapse method was called
        protected var time_at_last_lapse = new Timespec.monotonic_now
 
        # Smallest time frame reported by clock
-       fun resolution : Timespec `{
+       fun resolution: Timespec `{
                struct timespec* tv = malloc( sizeof(struct timespec) );
 #ifdef __MACH__
                clock_serv_t cclock;
@@ -142,18 +163,43 @@ class Clock
                return tv;
        `}
 
-       # Return timelapse since instanciation of this instance
-       fun total : Timespec
+       # Seconds since the creation of this instance
+       fun total: Float
        do
-               return new Timespec.monotonic_now - time_at_beginning
+               var now = new Timespec.monotonic_now
+               var diff = now - time_at_beginning
+               var r = diff.to_f
+               diff.free
+               now.free
+               return r
        end
 
-       # Return timelapse since last call to lapse
-       fun lapse : Timespec
+       # Seconds since the last call to `lapse`
+       fun lapse: Float
        do
                var nt = new Timespec.monotonic_now
                var dt = nt - time_at_last_lapse
+               var r = dt.to_f
+               dt.free
+               time_at_last_lapse.free
                time_at_last_lapse = nt
-               return dt
+               return r
+       end
+
+       # Seconds since the last call to `lapse`, without resetting the lapse counter
+       fun peek_lapse: Float
+       do
+               var nt = new Timespec.monotonic_now
+               var dt = nt - time_at_last_lapse
+               var r = dt.to_f
+               nt.free
+               dt.free
+               return r
+       end
+
+       redef fun finalize_once
+       do
+               time_at_beginning.free
+               time_at_last_lapse.free
        end
 end
diff --git a/lib/rubix.ini b/lib/rubix.ini
new file mode 100644 (file)
index 0000000..c644c2e
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name=rubix
+tags=algo,lib
+maintainer=Lucas Bajolet<r4pass@hotmail.com>
+license=Apache-2.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/rubix.nit
+git=https://github.com/nitlang/nit.git
+git.directory=lib/rubix.nit
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
index 618a26f..8e63784 100644 (file)
@@ -8,6 +8,20 @@
 # You  are  allowed  to  redistribute it and sell it, alone or is a part of
 # another product.
 
+# Rubix-cube modelization library
+#
+# As for now the library supports basic representation of a Rubix-cube
+# The library is built for speed, most operations cost no allocations to perform.
+# This does however mean that the library is NOT thread-safe and should be handled
+# with the appropriate mutual-exclusion mechanisms to avoid memory corruption
+# or unintended side-effects on a single cube.
+#
+# The library supports the singmaster notation as a way to perform operations
+# on a Rubix cube.
+#
+# No solver is (yet) provided along with the library.
+module rubix
+
 import console
 
 private fun array1d_copy_to(fromarr: Array[Int], oarr: Array[Int]) do
index 4541428..3554d22 100644 (file)
@@ -55,7 +55,7 @@ end
 # then using a reference.
 class SerializerCache
        # Map of already serialized objects to the reference id
-       private var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int]
+       protected var sent: Map[Serializable, Int] = new StrictHashMap[Serializable, Int]
 
        # Is `object` known?
        fun has_object(object: Serializable): Bool do return sent.keys.has(object)
@@ -88,7 +88,7 @@ end
 # Used by `Deserializer` to find already deserialized objects by their reference.
 class DeserializerCache
        # Map of references to already deserialized objects.
-       private var received: Map[Int, Object] = new StrictHashMap[Int, Object]
+       protected var received: Map[Int, Object] = new StrictHashMap[Int, Object]
 
        # Is there an object associated to `id`?
        fun has_id(id: Int): Bool do return received.keys.has(id)
index f441545..4b92786 100644 (file)
@@ -22,9 +22,14 @@ in "C header" `{
        #include <sqlite3.h>
 `}
 
+in "C" `{
+       // Return code of the last call to the constructor of `NativeSqlite3`
+       static int nit_sqlite_open_error = SQLITE_OK;
+`}
+
 redef class Sys
        # Last error raised when calling `Sqlite3::open`
-       var sqlite_open_error: nullable Sqlite3Code = null
+       fun sqlite_open_error: Sqlite3Code `{ return nit_sqlite_open_error; `}
 end
 
 extern class Sqlite3Code `{int`}
@@ -75,11 +80,10 @@ extern class Sqlite3Code `{int`}
 
        private fun native_to_s: NativeString `{
 #if SQLITE_VERSION_NUMBER >= 3007015
-               char *err = (char *)sqlite3_errstr(self);
+               return (char *)sqlite3_errstr(self);
 #else
-               char *err = "sqlite3_errstr supported only by version >= 3.7.15";
+               return "sqlite3_errstr is not supported in version < 3.7.15";
 #endif
-               return err;
        `}
 end
 
@@ -91,13 +95,8 @@ extern class NativeStatement `{sqlite3_stmt*`}
                return sqlite3_step(self);
        `}
 
-       fun column_name(i: Int) : String import NativeString.to_s `{
-               const char * name = (sqlite3_column_name(self, i));
-               if(name == NULL){
-                       name = "";
-               }
-               char * ret = (char *) name;
-               return NativeString_to_s(ret);
+       fun column_name(i: Int): NativeString `{
+               return (char *)sqlite3_column_name(self, i);
        `}
 
        # Number of bytes in the blob or string at row `i`
@@ -139,25 +138,18 @@ end
 extern class NativeSqlite3 `{sqlite3 *`}
 
        # Open a connection to a database in UTF-8
-       new open(filename: NativeString) import set_sys_sqlite_open_error `{
+       new open(filename: NativeString) `{
                sqlite3 *self = NULL;
                int err = sqlite3_open(filename, &self);
-               NativeSqlite3_set_sys_sqlite_open_error(self, (void*)(long)err);
-               // The previous cast is a hack, using non pointers in extern classes is not
-               // yet in the spec of the FFI.
+               nit_sqlite_open_error = err;
                return self;
        `}
 
-       # Utility method to set `Sys.sqlite_open_error`
-       private fun set_sys_sqlite_open_error(err: Sqlite3Code) do sys.sqlite_open_error = err
-
        # Has this DB been correctly opened?
        #
        # To know if it has been closed or interrupted, you must check for errors with `error`.
        fun is_valid: Bool do return not address_is_null
 
-       fun destroy do close
-
        # Close this connection
        fun close `{
 #if SQLITE_VERSION_NUMBER >= 3007014
@@ -171,18 +163,18 @@ extern class NativeSqlite3 `{sqlite3 *`}
        `}
 
        # Execute a SQL statement
-       fun exec(sql: String): Sqlite3Code import String.to_cstring `{
-               return sqlite3_exec(self, String_to_cstring(sql), 0, 0, 0);
+       fun exec(sql: NativeString): Sqlite3Code `{
+               return sqlite3_exec(self, sql, NULL, NULL, NULL);
        `}
 
        # Prepare a SQL statement
-       fun prepare(sql: String): nullable NativeStatement import String.to_cstring, NativeStatement.as nullable `{
+       fun prepare(sql: NativeString): NativeStatement `{
                sqlite3_stmt *stmt;
-               int res = sqlite3_prepare_v2(self, String_to_cstring(sql), -1, &stmt, 0);
+               int res = sqlite3_prepare_v2(self, sql, -1, &stmt, 0);
                if (res == SQLITE_OK)
-                       return NativeStatement_as_nullable(stmt);
+                       return stmt;
                else
-                       return null_NativeStatement();
+                       return NULL;
        `}
 
        fun last_insert_rowid: Int `{
index 62fe8e5..322e7d7 100644 (file)
@@ -57,8 +57,8 @@ class Sqlite3DB
        # Prepare and return a `Statement`, return `null` on error
        fun prepare(sql: Text): nullable Statement
        do
-               var native_stmt = native_connection.prepare(sql.to_s)
-               if native_stmt == null then return null
+               var native_stmt = native_connection.prepare(sql.to_cstring)
+               if native_stmt.address_is_null then return null
 
                var stmt = new Statement(native_stmt)
                open_statements.add stmt
@@ -68,7 +68,7 @@ class Sqlite3DB
        # Execute the `sql` statement and return `true` on success
        fun execute(sql: Text): Bool
        do
-               var err = native_connection.exec(sql.to_s)
+               var err = native_connection.exec(sql.to_cstring)
                return err.is_ok
        end
 
@@ -99,7 +99,7 @@ class Sqlite3DB
        do
                if not native_connection.is_valid then
                        var err = sys.sqlite_open_error
-                       if err == null then return null
+                       if err.is_ok then return null
                        return err.to_s
                end
 
@@ -182,7 +182,9 @@ class StatementEntry
        var name: String is lazy do
                assert statement_closed: statement.is_open
 
-               return statement.native_statement.column_name(index)
+               var cname = statement.native_statement.column_name(index)
+               assert not cname.address_is_null
+               return cname.to_s
        end
 
        # Get the value of this entry according to its Sqlite type
@@ -305,8 +307,9 @@ redef universal Int super Sqlite3Data end
 
 redef universal Float super Sqlite3Data end
 
-redef class String
-       super Sqlite3Data
+redef class String super Sqlite3Data end
+
+redef class Text
 
        # Return `self` between `'`s, escaping `\` and `'`
        #
@@ -315,6 +318,24 @@ redef class String
        do
                return "'{self.replace('\\', "\\\\").replace('\'', "''")}'"
        end
+
+       # Format the date represented by `self` into an escaped string for SQLite
+       #
+       # `self` must be composed of 1 to 3 integers separated by '-'.
+       # An incompatible format will result in an invalid date string.
+       #
+       #     assert "2016-5-1".to_sql_date_string == "'2016-05-01'"
+       #     assert "2016".to_sql_date_string == "'2016-01-01'"
+       fun to_sql_date_string: String
+       do
+               var parts = self.split("-")
+               for i in [parts.length .. 3[ do parts[i] = "1"
+
+               var year = parts[0].justify(4, 1.0, '0')
+               var month = parts[1].justify(2, 1.0, '0')
+               var day = parts[2].justify(2, 1.0, '0')
+               return "{year}-{month}-{day}".to_sql_string
+       end
 end
 
 # A Sqlite3 blob
index d0bc77d..615c45f 100644 (file)
@@ -37,7 +37,7 @@ import template
 # A macro identifier is valid if:
 #
 # * starts with an uppercase letter
-# * contains only numers, uppercase letters or '_'
+# * contains only numbers, uppercase letters or '_'
 #
 # See `String::is_valid_macro_name` for more details.
 #
diff --git a/misc/docker/Dockerfile b/misc/docker/Dockerfile
new file mode 100644 (file)
index 0000000..95995bf
--- /dev/null
@@ -0,0 +1,35 @@
+# This is a basic install of Nit on a debian base.
+
+FROM debian:jessie
+MAINTAINER Jean Privat <jean@pryen.org>
+
+# Install dependencies
+RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+               # Recomanded builds pakages
+               build-essential \
+               ccache \
+               libgc-dev \
+               graphviz \
+               libunwind-dev \
+               pkg-config \
+               # Get the code!
+               git \
+               ca-certificates \
+               curl \
+               # For nit manpages :)
+               man \
+       && rm -rf /var/lib/apt/lists/*
+
+# Clone and compile
+RUN git clone https://github.com/nitlang/nit.git /root/nit \
+       && cd /root/nit \
+       && make \
+       && . misc/nit_env.sh install \
+       # Clean and reduce size
+       && strip c_src/nitc bin/nit* \
+       && ccache -C \
+       && rm -rf .git
+
+ENV NIT_DIR /root/nit
+ENV PATH $NIT_DIR/bin:$PATH
+WORKDIR $NIT_DIR
diff --git a/misc/docker/README.md b/misc/docker/README.md
new file mode 100644 (file)
index 0000000..d255285
--- /dev/null
@@ -0,0 +1,64 @@
+# Supported tags and respective Dockerfile links
+
+* [latest](https://github.com/nitlang/nit/blob/master/misc/docker/Dockerfile)
+* [full](https://github.com/nitlang/nit/blob/master/misc/docker/full/Dockerfile)
+
+## What is Nit?
+
+Nit is an expressive language with a script-like syntax, a friendly type-system and aims at elegance, simplicity and intuitiveness.
+
+Nit has a simple straightforward style and can usually be picked up quickly, particularly by anyone who has programmed before.
+While object-oriented, it allows procedural styles.
+
+More information on
+
+* Website <http://www.nitlanguage.org>
+* Github <https://github.com/nitlang/nit>
+* Chatroom <https://gitter.im/nitlang/nit>
+
+## How to use this image
+
+You can use these images to build then run your programs.
+
+### Experimenting with Nit
+
+~~~
+host$ docker run -ti nitlang/nit
+root@ce9b671dd9fc:/root/nit# nitc examples/hello_world.nit
+root@ce9b671dd9fc:/root/nit# ./hello_world
+hello world
+~~~
+
+### Build and Run Programs
+
+In your Dockerfile, write something like:
+
+~~~Dockerfile
+FROM nitlang/nit
+
+# Create a workdir
+RUN mkdir -p /root/work
+WORKDIR /root/work
+
+# Copy the source code in /root/work/
+COPY . /root/work/
+
+# Compile
+RUN nitc src/hello.nit --dir . \
+       # Clear disk space
+       && ccache -C
+# You can also use a Makefile or any build system you want.
+
+# Run
+CMD ["./hello"]
+~~~
+
+Then, build and execute
+
+~~~
+host$ docker build -t nithello .
+host$ docker run --rm nithello
+hello!
+~~~
+
+See the full example at <https://github.com/nitlang/nit/blob/master/misc/docker/hello/Dockerfile>
diff --git a/misc/docker/full/Dockerfile b/misc/docker/full/Dockerfile
new file mode 100644 (file)
index 0000000..df0d214
--- /dev/null
@@ -0,0 +1,76 @@
+# This is a full install of Nit on a debian base.
+# Full because most dependencies are installed so that most tests can be run
+
+FROM nitlang/nit:latest
+MAINTAINER Jean Privat <jean@pryen.org>
+
+# Dependencies for more libs and tests
+RUN dpkg --add-architecture i386 \
+       && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+               # Packages needed for lib/
+               libcurl4-openssl-dev \
+               libegl1-mesa-dev \
+               libevent-dev \
+               libgles1-mesa-dev \
+               libgles2-mesa-dev \
+               libgtk-3-dev \
+               libncurses5-dev \
+               libpq-dev \
+               libsdl-image1.2-dev \
+               libsdl-ttf2.0-dev \
+               libsdl1.2-dev \
+               libsdl2-dev \
+               libsqlite3-dev \
+               libx11-dev \
+               libxdg-basedir-dev \
+               postgresql \
+               # Packages needed for contrib, platforms and FFI
+               ant \
+               clang \
+               default-jdk \
+               file \
+               inkscape \
+               libopenmpi-dev \
+               unzip \
+               # Android
+               libc6:i386 \
+               libstdc++6:i386 \
+               zlib1g:i386 \
+               # TODO neo4j emscripten test_glsl_validation
+       && rm -rf /var/lib/apt/lists/*
+
+# Install android sdk/ndk
+RUN mkdir -p /opt \
+       && cd /opt \
+       # Android SDK
+       && curl https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz -o android-sdk-linux.tgz \
+       && tar xzf android-sdk-linux.tgz \
+       && rm android-sdk-linux.tgz \
+       && echo y | android-sdk-linux/tools/android update sdk -a --no-ui --filter \
+               # Hardcode minimal known working things
+               platform-tools,build-tools-22.0.1,android-22,android-10 \
+       # Android NDK
+       && curl http://dl.google.com/android/repository/android-ndk-r11c-linux-x86_64.zip -o android-ndk.zip \
+       && unzip -q android-ndk.zip \
+       && ln -s android-ndk-r11c android-ndk \
+       && rm android-ndk.zip \
+       && printf "PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_NDK\nexport PATH\n" >> "/etc/profile.d/android.sh"
+
+# Setup environment variables
+
+ENV ANDROID_HOME /opt/android-sdk-linux
+ENV ANDROID_NDK /opt/android-ndk
+ENV PATH $PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_NDK
+
+# Run tests
+RUN cd /root/nit/tests \
+       # Basic tests
+       && ./testfull.sh || true \
+       && rm -rf out/ alt/*.nit \
+       # Nitunits
+       && ../bin/nitunit ../lib ../contrib || true \
+       && rm -rf .nitunit \
+       && ccache -C
+
+WORKDIR /root/nit
+ENTRYPOINT [ "bash" ]
diff --git a/misc/docker/hello/Dockerfile b/misc/docker/hello/Dockerfile
new file mode 100644 (file)
index 0000000..c0a018b
--- /dev/null
@@ -0,0 +1,17 @@
+FROM nitlang/nit
+
+# Create a workdir
+RUN mkdir -p /root/work
+WORKDIR /root/work
+
+# Copy the source code in /root/work/
+COPY . /root/work/
+
+# Compile
+RUN nitc src/hello.nit --dir . \
+        # Clear disk space
+        && ccache -C
+# You can also use a Makefile or what you want
+
+# Say what to run
+CMD ["./hello"]
diff --git a/misc/docker/hello/src/hello.nit b/misc/docker/hello/src/hello.nit
new file mode 100644 (file)
index 0000000..b732142
--- /dev/null
@@ -0,0 +1 @@
+print "hello"
index 2927a24..a316782 100644 (file)
@@ -386,7 +386,7 @@ Put primitive attributes in a box instead of an union.
 ### `--no-shortcut-equal`
 Always call == in a polymorphic way.
 
-### `--no-tag-primitive`
+### `--no-tag-primitives`
 Use only boxes for primitive types.
 
 The separate compiler uses tagged values to encode common primitive types like Int, Bool and Char.
@@ -410,9 +410,6 @@ Use an indirection when calling.
 
 Just add the trampolines of `--substitute-monomorph` without doing any additionnal optimizations.
 
-### `--no-tag-primitives`
-Use only boxes for primitive types.
-
 ## INTERNAL OPTIONS
 
 These options can be used to control the fine behavior of the tool.
index cd5472a..0b42d2f 100644 (file)
@@ -66,8 +66,8 @@ Also generate private API.
 
 ## CUSTOMIZATION
 
-### `--sharedir`
-Directory containing nitdoc assets.
+### `--share-dir`
+Directory containing tools assets.
 
 By default `$NIT_DIR/share/nitdoc/` is used.
 
index e01dbd2..6f09402 100644 (file)
@@ -118,6 +118,15 @@ Finally, standard markdown documents can be checked with:
 
     $ nitunit foo.md
 
+When testing, the environment variable `NIT_TESTING` is set to `true`.
+This flag can be used by libraries and program to prevent (or limit) the execution of dangerous pieces of code.
+
+~~~~~
+# NIT_TESTING is automatically set.
+#
+#     assert "NIT_TESTING".environ == "true"
+~~~~
+
 ## Working with `TestSuites`
 
 TestSuites are Nit files that define a set of TestCases for a particular module.
@@ -160,6 +169,34 @@ class TestFoo
 end
 ~~~~
 
+## Black Box Testing
+
+Sometimes, it is easier to validate a `TestCase` by comparing its output with a text file containing the expected result.
+
+For each TestCase `test_bar` of a TestSuite `test_mod.nit`, if the corresponding file `test_mod.sav/test_bar.res` exists, then the output of the test is compared with the file.
+
+The `diff(1)` command is used to perform the comparison.
+The test is failed if non-zero is returned by `diff`.
+
+~~~
+module test_mod is test_suite
+class TestFoo
+       fun test_bar do
+               print "Hello!"
+       end
+end
+~~~
+
+Where `test_mod.sav/test_bar.res` contains
+
+~~~raw
+Hello!
+~~~
+
+If no corresponding `.res` file exists, then the output of the TestCase is ignored.
+
+## Configuring TestSuites
+
 `TestSuites` also provide methods to configure the test run:
 
 `before_test` and `after_test`: methods called before/after each test case.
@@ -228,13 +265,22 @@ With the `--full` option, all imported modules (even those in standard) are also
 ### `-o`, `--output`
 Output name (default is 'nitunit.xml').
 
-### `nitunit` produces a XML file comatible with JUnit.
+`nitunit` produces a XML file compatible with JUnit.
 
 ### `--dir`
-Working directory (default is '.nitunit').
+Working directory (default is 'nitunit.out').
 
 In order to execute the tests, nit files are generated then compiled and executed in the giver working directory.
 
+In case of success, the directory is removed.
+In case of failure, it is kept as is so files can be investigated.
+
+### `--nitc`
+nitc compiler to use.
+
+By default, nitunit tries to locate the `nitc` program with the environment variable `NITC` or heuristics.
+The option is used to indicate a specific nitc binary.
+
 ### `--no-act`
 Does not compile and run tests.
 
@@ -262,6 +308,36 @@ Also generate test case for private methods.
 ### `--only-show`
 Only display the skeleton, do not write any file.
 
+
+# ENVIRONMENT VARIABLES
+
+### `NITC`
+
+Indicate the specific Nit compiler executable to use. See `--nitc`.
+
+### `NIT_TESTING`
+
+The environment variable `NIT_TESTING` is set to `true` during the execution of program tests.
+Some libraries of programs can use it to produce specific reproducible results; or just to exit their executions.
+
+Unit-tests may unset this environment variable to retrieve the original behavior of such piece of software.
+
+### `SRAND`
+
+In order to maximize reproducibility, `SRAND` is set to 0.
+This make the pseudo-random generator no random at all.
+See `Sys::srand` for details.
+
+To retrieve the randomness, unit-tests may unset this environment variable then call `srand`.
+
+### `NIT_TESTING_ID`
+
+Parallel executions can cause some race collisions on named resources (e.g. DB table names).
+To solve this issue, `NIT_TESTING_ID` is initialized with a distinct integer identifier that can be used to give unique names to resources.
+
+Note: `rand` is not a recommended way to get a distinct identifier because its randomness is disabled by default. See `SRAND`.
+
+
 # SEE ALSO
 
 The Nit language documentation and the source code of its tools and libraries may be downloaded from <http://nitlanguage.org>
diff --git a/share/nitweb/directives/entity/doc.html b/share/nitweb/directives/entity/doc.html
new file mode 100644 (file)
index 0000000..9d0ffb0
--- /dev/null
@@ -0,0 +1,5 @@
+<div class='card' ng-if='mentity.mdoc'>
+       <div class='card-body'>
+               <div ng-bind-html='mentity.mdoc.html_documentation'></div>
+       </div>
+</div>
diff --git a/share/nitweb/directives/entity/link.html b/share/nitweb/directives/entity/link.html
new file mode 100644 (file)
index 0000000..740ea8b
--- /dev/null
@@ -0,0 +1,3 @@
+<span>
+       <a ng-href='{{mentity.web_url}}'>{{mentity.name}}</a>
+</span>
diff --git a/share/nitweb/directives/entity/signature.html b/share/nitweb/directives/entity/signature.html
new file mode 100644 (file)
index 0000000..f3f2dc6
--- /dev/null
@@ -0,0 +1,51 @@
+<span class='signature'>
+       <span ng-repeat='modifier in mentity.modifiers'>
+               <span ng-if='modifier != "public"' class='modifier'>{{modifier}}</span>
+       </span>
+       <span class='name'>
+               <entity-link mentity='mentity' />
+       </span>
+       <span ng-if='mentity.mparameters'>
+               <span ng-if='mentity.mparameters.length > 0'>
+                       <span>[</span>
+                       <span ng-repeat='mparam in mentity.mparameters'>
+                               <span>
+                                       <span>{{mparam.name}}</span>
+                                       <span>: </span>
+                                       <entity-signature mentity='mparam.mtype' />
+                               </span>
+                               <span ng-if='$middle'>, </span>
+                       </span>
+                       <span>]</span>
+               </span>
+       </span>
+       <span ng-if='mentity.msignature'>
+               <span ng-if='mentity.msignature.arity > 0'>
+                       <span>(</span>
+                       <span ng-repeat='mparam in mentity.msignature.mparams'>
+                               <span>
+                                       <span>{{mparam.name}}</span>
+                                       <span ng-if='mentity.is_intro !== false'>
+                                               <span>: </span>
+                                               <entity-signature mentity='mparam.mtype' />
+                                       </span>
+                                       <span ng-if='mparam.is_vararg'>...</span>
+                               </span>
+                               <span ng-if='!first && !$last'>, </span>
+                       </span>
+                       <span>)</span>
+               </span>
+               <span ng-if='mentity.is_intro !== false && mentity.msignature.return_mtype'>
+                       <span>: </span>
+                       <entity-signature mentity='mentity.msignature.return_mtype' />
+               </span>
+       </span>
+       <span ng-if='mentity.is_intro !== false && mentity.static_mtype'>
+               <span>: </span>
+               <entity-signature mentity='mentity.static_mtype' />
+       </span>
+       <span ng-if='mentity.bound'>
+               <span>: </span>
+               <entity-signature mentity='mentity.bound' />
+       </span>
+</span>
diff --git a/share/nitweb/directives/group-block.html b/share/nitweb/directives/group-block.html
new file mode 100644 (file)
index 0000000..ad7f00e
--- /dev/null
@@ -0,0 +1,21 @@
+<div class='media'>
+       <div class='media-left text-center' ng-if='mentity.visibility' ng-class='{
+                       "text-success": mentity.visibility == "public",
+                       "text-warning": mentity.visibility == "protected",
+                       "text-danger": mentity.visibility == "private",
+       }'>
+               <span class="glyphicon glyphicon-tag"></span>
+       </div>
+       <div class='media-body'>
+               <h5 class='media-heading'>
+                       <entity-signature mentity='mentity'/>
+               </h5>
+               <span class='synopsis'>{{mentity.mdoc.synopsis}}</span>
+               <div ng-if='recursive && mentity.mgroups'>
+                       <group-block mentity-id='mgroup' ng-repeat='mgroup in mentity.mgroups' />
+               </div>
+               <div ng-if='recursive && mentity.mmodules'>
+                       <module-block mentity-id='mmodule' ng-repeat='mmodule in mentity.mmodules' />
+               </div>
+       </div>
+</div>
diff --git a/share/nitweb/index.html b/share/nitweb/index.html
new file mode 100644 (file)
index 0000000..2a4bdde
--- /dev/null
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang='en' ng-app='nitweb'>
+       <head>
+               <base href='/'>
+               <meta charset='utf-8'>
+               <meta http-equiv='X-UA-Compatible' content='IE=edge'>
+               <meta name='viewport' content='width=device-width, initial-scale=1'>
+               <title>ng-doc</title>
+
+               <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css'
+                       integrity='sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7'
+                       crossorigin='anonymous' rel='stylesheet'>
+
+               <link href='/stylesheets/nitweb_bootstrap.css' rel='stylesheet'>
+               <link href='/stylesheets/nitweb.css' rel='stylesheet'>
+       </head>
+       <body>
+               <nav class='navbar navbar-default navbar-fixed-top'>
+                       <div class='container-fluid'>
+                               <div class='col-xs-3 navbar-header'>
+                                       <a class='navbar-brand' ng-href='/'>Nitdoc</a>
+                               </div>
+                       </div>
+               </nav>
+               <div ng-view></div>
+
+               <script src='https://code.jquery.com/jquery-1.12.4.min.js'
+                       integrity='sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ='
+                       crossorigin='anonymous''></script>
+               <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js'
+                       integrity='sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS'
+                       crossorigin='anonymous'></script>
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.min.js'>
+               </script>
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-route.js'>
+               </script>
+               <script src='https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-sanitize.js'>
+               </script>
+
+               <script src='/javascripts/nitweb.js'></script>
+               <script src='/javascripts/model.js'></script>
+               <script src='/javascripts/entities.js'></script>
+       </body>
+</html>
diff --git a/share/nitweb/javascripts/entities.js b/share/nitweb/javascripts/entities.js
new file mode 100644 (file)
index 0000000..4fe181b
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       angular
+               .module('entities', ['model'])
+
+               .controller('EntityCtrl', ['Model', '$routeParams', '$scope', function(Model, $routeParams, $scope) {
+                       Model.loadEntity($routeParams.id,
+                               function(data) {
+                                       $scope.mentity = data;
+                               }, function(err) {
+                                       $scope.error = err;
+                               });
+               }])
+
+               .directive('entityLink', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       mentity: '='
+                               },
+                               templateUrl: '/directives/entity/link.html'
+                       };
+               })
+
+               .directive('entityDoc', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       mentity: '='
+                               },
+                               templateUrl: '/directives/entity/doc.html'
+                       };
+               })
+
+               .directive('entitySignature', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       mentity: '='
+                               },
+                               templateUrl: '/directives/entity/signature.html'
+                       };
+               })
+})();
diff --git a/share/nitweb/javascripts/model.js b/share/nitweb/javascripts/model.js
new file mode 100644 (file)
index 0000000..b5e8bbf
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       var apiUrl = '/api';
+
+       angular
+               .module('model', [])
+
+               .factory('Model', [ '$http', function($http) {
+                       return {
+                               loadEntity: function(id, cb, cbErr) {
+                                       $http.get(apiUrl + '/entity/' + id)
+                                               .success(cb)
+                                               .error(cbErr);
+                               }
+
+                       };
+               }])
+})();
diff --git a/share/nitweb/javascripts/nitweb.js b/share/nitweb/javascripts/nitweb.js
new file mode 100644 (file)
index 0000000..28291fb
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       angular.module('nitweb', ['ngRoute', 'ngSanitize', 'entities'])
+
+       .config(function($routeProvider, $locationProvider) {
+               $routeProvider
+                       .when('/', {
+                               templateUrl: 'views/index.html'
+                       })
+                       .when('/package/:id', {
+                               templateUrl: 'views/package.html',
+                               controller: 'EntityCtrl',
+                               controllerAs: 'entityCtrl'
+                       })
+                       .when('/group/:id', {
+                               templateUrl: 'views/group.html',
+                               controller: 'EntityCtrl',
+                               controllerAs: 'entityCtrl'
+                       })
+                       .when('/module/:id', {
+                               templateUrl: 'views/module.html',
+                               controller: 'EntityCtrl',
+                               controllerAs: 'entityCtrl'
+                       })
+                       .when('/class/:id', {
+                               templateUrl: 'views/class.html',
+                               controller: 'EntityCtrl',
+                               controllerAs: 'entityCtrl'
+                       })
+                       .when('/classdef/:id', {
+                               templateUrl: 'views/classdef.html',
+                               controller: 'EntityCtrl',
+                               controllerAs: 'entityCtrl'
+                       })
+                       .when('/property/:id', {
+                               templateUrl: 'views/property.html',
+                               controller: 'EntityCtrl',
+                               controllerAs: 'entityCtrl'
+                       })
+                       .when('/propdef/:id', {
+                               templateUrl: 'views/propdef.html',
+                               controller: 'EntityCtrl',
+                               controllerAs: 'entityCtrl'
+                       })
+                       .otherwise({
+                               redirectTo: '/'
+                       });
+               $locationProvider.html5Mode(true);
+       });
+})();
diff --git a/share/nitweb/stylesheets/nitweb.css b/share/nitweb/stylesheets/nitweb.css
new file mode 100644 (file)
index 0000000..2baad72
--- /dev/null
@@ -0,0 +1,124 @@
+/* Body */
+
+body {
+       background: #f2f2f2;
+       margin-top: 70px;
+       margin-bottom: 70px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+       color: #666;
+}
+
+a {
+       cursor: pointer;
+}
+
+.nitdoc h1, .nitdoc h2, .nitdoc h3, .nitdoc h4, .nitdoc h5, .nitdoc h6 {
+       color: #333;
+}
+
+.page-header {
+    margin-top: 0;
+    border: none;
+}
+
+/* cards */
+
+.card.active {
+       border: 1px solid #1E9431;
+}
+
+.card, .card-body { overflow: hidden; }
+
+.card-heading {
+    margin-top: 0;
+    margin-bottom: 5px;
+}
+
+.card {
+       background: #fff;
+       border: 1px solid #ccc;
+       margin-top: 10px;
+       box-shadow: 0 -1px 0 #e5e5e5,0 0 2px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.24);
+}
+
+.card-body {
+    padding: 15px;
+    width: 10000px;
+}
+
+.card-body {
+    display: table-cell;
+    vertical-align: top;
+}
+
+/* doc */
+
+.nitdoc .synopsys {
+       font-size: 2em;
+}
+
+.signature {
+       color: #666;
+       font-family: monospace;
+}
+
+.signature .name {
+       font-weight: bold;
+}
+
+.page-header .signature .name, .signature .signature .name {
+       font-weight: normal;
+}
+
+.signature .signature a {
+       color: #666;
+       font-family: monospace;
+}
+
+/* tabs */
+
+.nav-tabs li { cursor: pointer; }
+
+.nav>li.warning>a {
+    color: #fff;
+    background-color: #f0ad4e;
+}
+
+.nav>li.warning>a:focus, .nav>li.warning>a:hover {
+    background-color: #ff9c0f;
+}
+
+/*
+ * Code Highlighting
+ */
+
+.nitcode a { color: inherit; text-decoration: inherit; } /* hide links */
+.nitcode a:hover { text-decoration: underline; } /* underline links */
+.nitcode span[title]:hover { text-decoration: underline; } /* underline titles */
+/* lexical raw tokens. independent of usage or semantic: */
+.nitcode .nc_c { color: gray; font-style: italic; } /* comment */
+.nitcode .nc_d { color: #3D8127; font-style: italic; } /* documentation comments */
+.nitcode .nc_k { font-weight: bold; } /* keyword */
+.nitcode .nc_o {} /* operator */
+.nitcode .nc_i {} /* standard identifier */
+.nitcode .nc_t { color: #445588; font-weight: bold; } /* type/class identifier */
+.nitcode .nc_a { color: #445588; font-style: italic; } /* old style attribute identifier */
+.nitcode .nc_l { color: #009999; } /* char and number literal */
+.nitcode .nc_s { color: #8F1546; } /* string literal */
+/* syntactic token usage. added because of their position in the AST */
+.nitcode .nc_ast { color: blue; } /* assert label */
+.nitcode .nc_la { color: blue; } /* break/continue label */
+.nitcode .nc_m { color: #445588; } /* module name */
+/* syntactic groups */
+.nitcode .nc_def { font-weight: bold; color: blue; } /* name used in a definition */
+.nitcode .nc_def.nc_a { color: blue; } /* name used in a attribute definition */
+.nitcode .nc_def.nc_t { color: blue; } /* name used in a class or vt definition */
+.nitcode .nc_ss { color: #9E6BEB; } /* superstrings */
+.nitcode .nc_cdef {} /* A whole class definition */
+.nitcode .nc_pdef {} /* A whole property definition */
+/* semantic token usage */
+.nitcode .nc_v { font-style: italic; } /* local variable or parameter */
+.nitcode .nc_vt { font-style: italic; } /* virtual type or formal type */
+.nitcode .nc_error { border: 1px red solid;} /* not used */
diff --git a/share/nitweb/stylesheets/nitweb_bootstrap.css b/share/nitweb/stylesheets/nitweb_bootstrap.css
new file mode 100644 (file)
index 0000000..e82732b
--- /dev/null
@@ -0,0 +1,5769 @@
+/*! normalize.css v3.0.0 | MIT License | git.io/normalize */\r
+html {\r
+  font-family: sans-serif;\r
+  -ms-text-size-adjust: 100%;\r
+  -webkit-text-size-adjust: 100%;\r
+}\r
+body {\r
+  margin: 0;\r
+}\r
+article,\r
+aside,\r
+details,\r
+figcaption,\r
+figure,\r
+footer,\r
+header,\r
+hgroup,\r
+main,\r
+nav,\r
+section,\r
+summary {\r
+  display: block;\r
+}\r
+audio,\r
+canvas,\r
+progress,\r
+video {\r
+  display: inline-block;\r
+  vertical-align: baseline;\r
+}\r
+audio:not([controls]) {\r
+  display: none;\r
+  height: 0;\r
+}\r
+[hidden],\r
+template {\r
+  display: none;\r
+}\r
+a {\r
+  background: transparent;\r
+}\r
+a:active,\r
+a:hover {\r
+  outline: 0;\r
+}\r
+abbr[title] {\r
+  border-bottom: 1px dotted;\r
+}\r
+b,\r
+strong {\r
+  font-weight: bold;\r
+}\r
+dfn {\r
+  font-style: italic;\r
+}\r
+h1 {\r
+  font-size: 2em;\r
+  margin: 0.67em 0;\r
+}\r
+mark {\r
+  background: #ff0;\r
+  color: #000;\r
+}\r
+small {\r
+  font-size: 80%;\r
+}\r
+sub,\r
+sup {\r
+  font-size: 75%;\r
+  line-height: 0;\r
+  position: relative;\r
+  vertical-align: baseline;\r
+}\r
+sup {\r
+  top: -0.5em;\r
+}\r
+sub {\r
+  bottom: -0.25em;\r
+}\r
+img {\r
+  border: 0;\r
+}\r
+svg:not(:root) {\r
+  overflow: hidden;\r
+}\r
+figure {\r
+  margin: 1em 40px;\r
+}\r
+hr {\r
+  -moz-box-sizing: content-box;\r
+  box-sizing: content-box;\r
+  height: 0;\r
+}\r
+pre {\r
+  overflow: auto;\r
+}\r
+code,\r
+kbd,\r
+pre,\r
+samp {\r
+  font-family: monospace, monospace;\r
+  font-size: 1em;\r
+}\r
+button,\r
+input,\r
+optgroup,\r
+select,\r
+textarea {\r
+  color: inherit;\r
+  font: inherit;\r
+  margin: 0;\r
+}\r
+button {\r
+  overflow: visible;\r
+}\r
+button,\r
+select {\r
+  text-transform: none;\r
+}\r
+button,\r
+html input[type="button"],\r
+input[type="reset"],\r
+input[type="submit"] {\r
+  -webkit-appearance: button;\r
+  cursor: pointer;\r
+}\r
+button[disabled],\r
+html input[disabled] {\r
+  cursor: default;\r
+}\r
+button::-moz-focus-inner,\r
+input::-moz-focus-inner {\r
+  border: 0;\r
+  padding: 0;\r
+}\r
+input {\r
+  line-height: normal;\r
+}\r
+input[type="checkbox"],\r
+input[type="radio"] {\r
+  box-sizing: border-box;\r
+  padding: 0;\r
+}\r
+input[type="number"]::-webkit-inner-spin-button,\r
+input[type="number"]::-webkit-outer-spin-button {\r
+  height: auto;\r
+}\r
+input[type="search"] {\r
+  -webkit-appearance: textfield;\r
+  -moz-box-sizing: content-box;\r
+  -webkit-box-sizing: content-box;\r
+  box-sizing: content-box;\r
+}\r
+input[type="search"]::-webkit-search-cancel-button,\r
+input[type="search"]::-webkit-search-decoration {\r
+  -webkit-appearance: none;\r
+}\r
+fieldset {\r
+  border: 1px solid #c0c0c0;\r
+  margin: 0 2px;\r
+  padding: 0.35em 0.625em 0.75em;\r
+}\r
+legend {\r
+  border: 0;\r
+  padding: 0;\r
+}\r
+textarea {\r
+  overflow: auto;\r
+}\r
+optgroup {\r
+  font-weight: bold;\r
+}\r
+table {\r
+  border-collapse: collapse;\r
+  border-spacing: 0;\r
+}\r
+td,\r
+th {\r
+  padding: 0;\r
+}\r
+@media print {\r
+  * {\r
+    text-shadow: none !important;\r
+    color: #000 !important;\r
+    background: transparent !important;\r
+    box-shadow: none !important;\r
+  }\r
+  a,\r
+  a:visited {\r
+    text-decoration: underline;\r
+  }\r
+  a[href]:after {\r
+    content: " (" attr(href) ")";\r
+  }\r
+  abbr[title]:after {\r
+    content: " (" attr(title) ")";\r
+  }\r
+  a[href^="javascript:"]:after,\r
+  a[href^="#"]:after {\r
+    content: "";\r
+  }\r
+  pre,\r
+  blockquote {\r
+    border: 1px solid #999;\r
+    page-break-inside: avoid;\r
+  }\r
+  thead {\r
+    display: table-header-group;\r
+  }\r
+  tr,\r
+  img {\r
+    page-break-inside: avoid;\r
+  }\r
+  img {\r
+    max-width: 100% !important;\r
+  }\r
+  p,\r
+  h2,\r
+  h3 {\r
+    orphans: 3;\r
+    widows: 3;\r
+  }\r
+  h2,\r
+  h3 {\r
+    page-break-after: avoid;\r
+  }\r
+  select {\r
+    background: #fff !important;\r
+  }\r
+  .navbar {\r
+    display: none;\r
+  }\r
+  .table td,\r
+  .table th {\r
+    background-color: #fff !important;\r
+  }\r
+  .btn > .caret,\r
+  .dropup > .btn > .caret {\r
+    border-top-color: #000 !important;\r
+  }\r
+  .label {\r
+    border: 1px solid #000;\r
+  }\r
+  .table {\r
+    border-collapse: collapse !important;\r
+  }\r
+  .table-bordered th,\r
+  .table-bordered td {\r
+    border: 1px solid #ddd !important;\r
+  }\r
+}\r
+* {\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+}\r
+*:before,\r
+*:after {\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+}\r
+html {\r
+  font-size: 62.5%;\r
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\r
+}\r
+body {\r
+  font-family: sans-serif;\r
+  font-size: 14px;\r
+  line-height: 1.428571429;\r
+  color: #333333;\r
+  background-color: #f2f2f2;\r
+}\r
+input,\r
+button,\r
+select,\r
+textarea {\r
+  font-family: inherit;\r
+  font-size: inherit;\r
+  line-height: inherit;\r
+}\r
+a {\r
+  color: #0d8921;\r
+  text-decoration: none;\r
+}\r
+a:hover,\r
+a:focus {\r
+  color: #064310;\r
+  text-decoration: underline;\r
+}\r
+a:focus {\r
+  outline: thin dotted;\r
+  outline: 5px auto -webkit-focus-ring-color;\r
+  outline-offset: -2px;\r
+}\r
+figure {\r
+  margin: 0;\r
+}\r
+img {\r
+  vertical-align: middle;\r
+}\r
+.img-responsive,\r
+.thumbnail > img,\r
+.thumbnail a > img,\r
+.carousel-inner > .item > img,\r
+.carousel-inner > .item > a > img {\r
+  display: block;\r
+  max-width: 100%;\r
+  height: auto;\r
+}\r
+.img-rounded {\r
+  border-radius: 0px;\r
+}\r
+.img-thumbnail {\r
+  padding: 4px;\r
+  line-height: 1.428571429;\r
+  background-color: #f2f2f2;\r
+  border: 1px solid #dddddd;\r
+  border-radius: 0px;\r
+  -webkit-transition: all 0.2s ease-in-out;\r
+  transition: all 0.2s ease-in-out;\r
+  display: inline-block;\r
+  max-width: 100%;\r
+  height: auto;\r
+}\r
+.img-circle {\r
+  border-radius: 50%;\r
+}\r
+hr {\r
+  margin-top: 20px;\r
+  margin-bottom: 20px;\r
+  border: 0;\r
+  border-top: 1px solid #eeeeee;\r
+}\r
+.sr-only {\r
+  position: absolute;\r
+  width: 1px;\r
+  height: 1px;\r
+  margin: -1px;\r
+  padding: 0;\r
+  overflow: hidden;\r
+  clip: rect(0, 0, 0, 0);\r
+  border: 0;\r
+}\r
+h1,\r
+h2,\r
+h3,\r
+h4,\r
+h5,\r
+h6,\r
+.h1,\r
+.h2,\r
+.h3,\r
+.h4,\r
+.h5,\r
+.h6 {\r
+  font-family: sans-serif;\r
+  font-weight: 500;\r
+  line-height: 1.1;\r
+  color: inherit;\r
+}\r
+h1 small,\r
+h2 small,\r
+h3 small,\r
+h4 small,\r
+h5 small,\r
+h6 small,\r
+.h1 small,\r
+.h2 small,\r
+.h3 small,\r
+.h4 small,\r
+.h5 small,\r
+.h6 small,\r
+h1 .small,\r
+h2 .small,\r
+h3 .small,\r
+h4 .small,\r
+h5 .small,\r
+h6 .small,\r
+.h1 .small,\r
+.h2 .small,\r
+.h3 .small,\r
+.h4 .small,\r
+.h5 .small,\r
+.h6 .small {\r
+  font-weight: normal;\r
+  line-height: 1;\r
+  color: #999999;\r
+}\r
+h1,\r
+.h1,\r
+h2,\r
+.h2,\r
+h3,\r
+.h3 {\r
+  margin-top: 20px;\r
+  margin-bottom: 10px;\r
+}\r
+h1 small,\r
+.h1 small,\r
+h2 small,\r
+.h2 small,\r
+h3 small,\r
+.h3 small,\r
+h1 .small,\r
+.h1 .small,\r
+h2 .small,\r
+.h2 .small,\r
+h3 .small,\r
+.h3 .small {\r
+  font-size: 65%;\r
+}\r
+h4,\r
+.h4,\r
+h5,\r
+.h5,\r
+h6,\r
+.h6 {\r
+  margin-top: 10px;\r
+  margin-bottom: 10px;\r
+}\r
+h4 small,\r
+.h4 small,\r
+h5 small,\r
+.h5 small,\r
+h6 small,\r
+.h6 small,\r
+h4 .small,\r
+.h4 .small,\r
+h5 .small,\r
+.h5 .small,\r
+h6 .small,\r
+.h6 .small {\r
+  font-size: 75%;\r
+}\r
+h1,\r
+.h1 {\r
+  font-size: 36px;\r
+}\r
+h2,\r
+.h2 {\r
+  font-size: 30px;\r
+}\r
+h3,\r
+.h3 {\r
+  font-size: 23px;\r
+}\r
+h4,\r
+.h4 {\r
+  font-size: 17px;\r
+}\r
+h5,\r
+.h5 {\r
+  font-size: 14px;\r
+}\r
+h6,\r
+.h6 {\r
+  font-size: 11px;\r
+}\r
+p {\r
+  margin: 0 0 10px;\r
+}\r
+.lead {\r
+  margin-bottom: 20px;\r
+  font-size: 16px;\r
+  font-weight: 200;\r
+  line-height: 1.4;\r
+}\r
+@media (min-width: 768px) {\r
+  .lead {\r
+    font-size: 21px;\r
+  }\r
+}\r
+small,\r
+.small {\r
+  font-size: 85%;\r
+}\r
+cite {\r
+  font-style: normal;\r
+}\r
+.text-left {\r
+  text-align: left;\r
+}\r
+.text-right {\r
+  text-align: right;\r
+}\r
+.text-center {\r
+  text-align: center;\r
+}\r
+.text-justify {\r
+  text-align: justify;\r
+}\r
+.text-muted {\r
+  color: #999999;\r
+}\r
+.text-primary {\r
+  color: #0d8921;\r
+}\r
+a.text-primary:hover {\r
+  color: #095a16;\r
+}\r
+.text-success {\r
+  color: #5cb85c;\r
+}\r
+a.text-success:hover {\r
+  color: #449d44;\r
+}\r
+.text-info {\r
+  color: #5bc0de;\r
+}\r
+a.text-info:hover {\r
+  color: #31b0d5;\r
+}\r
+.text-warning {\r
+  color: #f0ad4e;\r
+}\r
+a.text-warning:hover {\r
+  color: #ec971f;\r
+}\r
+.text-danger {\r
+  color: #d9534f;\r
+}\r
+a.text-danger:hover {\r
+  color: #c9302c;\r
+}\r
+.bg-primary {\r
+  color: #fff;\r
+  background-color: #0d8921;\r
+}\r
+a.bg-primary:hover {\r
+  background-color: #095a16;\r
+}\r
+.bg-success {\r
+  background-color: #dff0d8;\r
+}\r
+a.bg-success:hover {\r
+  background-color: #c1e2b3;\r
+}\r
+.bg-info {\r
+  background-color: #d9edf7;\r
+}\r
+a.bg-info:hover {\r
+  background-color: #afd9ee;\r
+}\r
+.bg-warning {\r
+  background-color: #fcf8e3;\r
+}\r
+a.bg-warning:hover {\r
+  background-color: #f7ecb5;\r
+}\r
+.bg-danger {\r
+  background-color: #f2dede;\r
+}\r
+a.bg-danger:hover {\r
+  background-color: #e4b9b9;\r
+}\r
+.page-header {\r
+  padding-bottom: 9px;\r
+  margin: 40px 0 20px;\r
+  border-bottom: 1px solid #eeeeee;\r
+}\r
+ul,\r
+ol {\r
+  margin-top: 0;\r
+  margin-bottom: 10px;\r
+}\r
+ul ul,\r
+ol ul,\r
+ul ol,\r
+ol ol {\r
+  margin-bottom: 0;\r
+}\r
+.list-unstyled {\r
+  padding-left: 0;\r
+  list-style: none;\r
+}\r
+.list-inline {\r
+  padding-left: 0;\r
+  list-style: none;\r
+}\r
+.list-inline > li {\r
+  display: inline-block;\r
+  padding-left: 5px;\r
+  padding-right: 5px;\r
+}\r
+.list-inline > li:first-child {\r
+  padding-left: 0;\r
+}\r
+dl {\r
+  margin-top: 0;\r
+  margin-bottom: 20px;\r
+}\r
+dt,\r
+dd {\r
+  line-height: 1.428571429;\r
+}\r
+dt {\r
+  font-weight: bold;\r
+}\r
+dd {\r
+  margin-left: 0;\r
+}\r
+@media (min-width: 768px) {\r
+  .dl-horizontal dt {\r
+    float: left;\r
+    width: 160px;\r
+    clear: left;\r
+    text-align: right;\r
+    overflow: hidden;\r
+    text-overflow: ellipsis;\r
+    white-space: nowrap;\r
+  }\r
+  .dl-horizontal dd {\r
+    margin-left: 180px;\r
+  }\r
+}\r
+abbr[title],\r
+abbr[data-original-title] {\r
+  cursor: help;\r
+  border-bottom: 1px dotted #999999;\r
+}\r
+.initialism {\r
+  font-size: 90%;\r
+  text-transform: uppercase;\r
+}\r
+blockquote {\r
+  padding: 10px 20px;\r
+  margin: 0 0 20px;\r
+  font-size: 17.5px;\r
+  border-left: 5px solid #eeeeee;\r
+}\r
+blockquote p:last-child,\r
+blockquote ul:last-child,\r
+blockquote ol:last-child {\r
+  margin-bottom: 0;\r
+}\r
+blockquote footer,\r
+blockquote small,\r
+blockquote .small {\r
+  display: block;\r
+  font-size: 80%;\r
+  line-height: 1.428571429;\r
+  color: #999999;\r
+}\r
+blockquote footer:before,\r
+blockquote small:before,\r
+blockquote .small:before {\r
+  content: '\2014 \00A0';\r
+}\r
+.blockquote-reverse,\r
+blockquote.pull-right {\r
+  padding-right: 15px;\r
+  padding-left: 0;\r
+  border-right: 5px solid #eeeeee;\r
+  border-left: 0;\r
+  text-align: right;\r
+}\r
+.blockquote-reverse footer:before,\r
+blockquote.pull-right footer:before,\r
+.blockquote-reverse small:before,\r
+blockquote.pull-right small:before,\r
+.blockquote-reverse .small:before,\r
+blockquote.pull-right .small:before {\r
+  content: '';\r
+}\r
+.blockquote-reverse footer:after,\r
+blockquote.pull-right footer:after,\r
+.blockquote-reverse small:after,\r
+blockquote.pull-right small:after,\r
+.blockquote-reverse .small:after,\r
+blockquote.pull-right .small:after {\r
+  content: '\00A0 \2014';\r
+}\r
+blockquote:before,\r
+blockquote:after {\r
+  content: "";\r
+}\r
+address {\r
+  margin-bottom: 20px;\r
+  font-style: normal;\r
+  line-height: 1.428571429;\r
+}\r
+code,\r
+kbd,\r
+pre,\r
+samp {\r
+  font-family: monospace;\r
+}\r
+code {\r
+  padding: 2px 4px;\r
+  font-size: 90%;\r
+  color: #c7254e;\r
+  background-color: #f9f2f4;\r
+  white-space: nowrap;\r
+  border-radius: 0px;\r
+}\r
+kbd {\r
+  padding: 2px 4px;\r
+  font-size: 90%;\r
+  color: #ffffff;\r
+  background-color: #333333;\r
+  border-radius: 0px;\r
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\r
+}\r
+pre {\r
+  display: block;\r
+  padding: 9.5px;\r
+  margin: 0 0 10px;\r
+  font-size: 13px;\r
+  line-height: 1.428571429;\r
+  word-break: break-all;\r
+  word-wrap: break-word;\r
+  color: #333333;\r
+  background-color: #f5f5f5;\r
+  border: 1px solid #cccccc;\r
+  border-radius: 0px;\r
+}\r
+pre code {\r
+  padding: 0;\r
+  font-size: inherit;\r
+  color: inherit;\r
+  white-space: pre-wrap;\r
+  background-color: transparent;\r
+  border-radius: 0;\r
+}\r
+.pre-scrollable {\r
+  max-height: 340px;\r
+  overflow-y: scroll;\r
+}\r
+.container {\r
+  margin-right: auto;\r
+  margin-left: auto;\r
+  padding-left: 15px;\r
+  padding-right: 15px;\r
+}\r
+@media (min-width: 768px) {\r
+  .container {\r
+    width: 750px;\r
+  }\r
+}\r
+@media (min-width: 992px) {\r
+  .container {\r
+    width: 970px;\r
+  }\r
+}\r
+@media (min-width: 1200px) {\r
+  .container {\r
+    width: 1170px;\r
+  }\r
+}\r
+.container-fluid {\r
+  margin-right: auto;\r
+  margin-left: auto;\r
+  padding-left: 15px;\r
+  padding-right: 15px;\r
+}\r
+.row {\r
+  margin-left: -15px;\r
+  margin-right: -15px;\r
+}\r
+.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\r
+  position: relative;\r
+  min-height: 1px;\r
+  padding-left: 15px;\r
+  padding-right: 15px;\r
+}\r
+.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\r
+  float: left;\r
+}\r
+.col-xs-12 {\r
+  width: 100%;\r
+}\r
+.col-xs-11 {\r
+  width: 91.66666666666666%;\r
+}\r
+.col-xs-10 {\r
+  width: 83.33333333333334%;\r
+}\r
+.col-xs-9 {\r
+  width: 75%;\r
+}\r
+.col-xs-8 {\r
+  width: 66.66666666666666%;\r
+}\r
+.col-xs-7 {\r
+  width: 58.333333333333336%;\r
+}\r
+.col-xs-6 {\r
+  width: 50%;\r
+}\r
+.col-xs-5 {\r
+  width: 41.66666666666667%;\r
+}\r
+.col-xs-4 {\r
+  width: 33.33333333333333%;\r
+}\r
+.col-xs-3 {\r
+  width: 25%;\r
+}\r
+.col-xs-2 {\r
+  width: 16.666666666666664%;\r
+}\r
+.col-xs-1 {\r
+  width: 8.333333333333332%;\r
+}\r
+.col-xs-pull-12 {\r
+  right: 100%;\r
+}\r
+.col-xs-pull-11 {\r
+  right: 91.66666666666666%;\r
+}\r
+.col-xs-pull-10 {\r
+  right: 83.33333333333334%;\r
+}\r
+.col-xs-pull-9 {\r
+  right: 75%;\r
+}\r
+.col-xs-pull-8 {\r
+  right: 66.66666666666666%;\r
+}\r
+.col-xs-pull-7 {\r
+  right: 58.333333333333336%;\r
+}\r
+.col-xs-pull-6 {\r
+  right: 50%;\r
+}\r
+.col-xs-pull-5 {\r
+  right: 41.66666666666667%;\r
+}\r
+.col-xs-pull-4 {\r
+  right: 33.33333333333333%;\r
+}\r
+.col-xs-pull-3 {\r
+  right: 25%;\r
+}\r
+.col-xs-pull-2 {\r
+  right: 16.666666666666664%;\r
+}\r
+.col-xs-pull-1 {\r
+  right: 8.333333333333332%;\r
+}\r
+.col-xs-pull-0 {\r
+  right: 0%;\r
+}\r
+.col-xs-push-12 {\r
+  left: 100%;\r
+}\r
+.col-xs-push-11 {\r
+  left: 91.66666666666666%;\r
+}\r
+.col-xs-push-10 {\r
+  left: 83.33333333333334%;\r
+}\r
+.col-xs-push-9 {\r
+  left: 75%;\r
+}\r
+.col-xs-push-8 {\r
+  left: 66.66666666666666%;\r
+}\r
+.col-xs-push-7 {\r
+  left: 58.333333333333336%;\r
+}\r
+.col-xs-push-6 {\r
+  left: 50%;\r
+}\r
+.col-xs-push-5 {\r
+  left: 41.66666666666667%;\r
+}\r
+.col-xs-push-4 {\r
+  left: 33.33333333333333%;\r
+}\r
+.col-xs-push-3 {\r
+  left: 25%;\r
+}\r
+.col-xs-push-2 {\r
+  left: 16.666666666666664%;\r
+}\r
+.col-xs-push-1 {\r
+  left: 8.333333333333332%;\r
+}\r
+.col-xs-push-0 {\r
+  left: 0%;\r
+}\r
+.col-xs-offset-12 {\r
+  margin-left: 100%;\r
+}\r
+.col-xs-offset-11 {\r
+  margin-left: 91.66666666666666%;\r
+}\r
+.col-xs-offset-10 {\r
+  margin-left: 83.33333333333334%;\r
+}\r
+.col-xs-offset-9 {\r
+  margin-left: 75%;\r
+}\r
+.col-xs-offset-8 {\r
+  margin-left: 66.66666666666666%;\r
+}\r
+.col-xs-offset-7 {\r
+  margin-left: 58.333333333333336%;\r
+}\r
+.col-xs-offset-6 {\r
+  margin-left: 50%;\r
+}\r
+.col-xs-offset-5 {\r
+  margin-left: 41.66666666666667%;\r
+}\r
+.col-xs-offset-4 {\r
+  margin-left: 33.33333333333333%;\r
+}\r
+.col-xs-offset-3 {\r
+  margin-left: 25%;\r
+}\r
+.col-xs-offset-2 {\r
+  margin-left: 16.666666666666664%;\r
+}\r
+.col-xs-offset-1 {\r
+  margin-left: 8.333333333333332%;\r
+}\r
+.col-xs-offset-0 {\r
+  margin-left: 0%;\r
+}\r
+@media (min-width: 768px) {\r
+  .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\r
+    float: left;\r
+  }\r
+  .col-sm-12 {\r
+    width: 100%;\r
+  }\r
+  .col-sm-11 {\r
+    width: 91.66666666666666%;\r
+  }\r
+  .col-sm-10 {\r
+    width: 83.33333333333334%;\r
+  }\r
+  .col-sm-9 {\r
+    width: 75%;\r
+  }\r
+  .col-sm-8 {\r
+    width: 66.66666666666666%;\r
+  }\r
+  .col-sm-7 {\r
+    width: 58.333333333333336%;\r
+  }\r
+  .col-sm-6 {\r
+    width: 50%;\r
+  }\r
+  .col-sm-5 {\r
+    width: 41.66666666666667%;\r
+  }\r
+  .col-sm-4 {\r
+    width: 33.33333333333333%;\r
+  }\r
+  .col-sm-3 {\r
+    width: 25%;\r
+  }\r
+  .col-sm-2 {\r
+    width: 16.666666666666664%;\r
+  }\r
+  .col-sm-1 {\r
+    width: 8.333333333333332%;\r
+  }\r
+  .col-sm-pull-12 {\r
+    right: 100%;\r
+  }\r
+  .col-sm-pull-11 {\r
+    right: 91.66666666666666%;\r
+  }\r
+  .col-sm-pull-10 {\r
+    right: 83.33333333333334%;\r
+  }\r
+  .col-sm-pull-9 {\r
+    right: 75%;\r
+  }\r
+  .col-sm-pull-8 {\r
+    right: 66.66666666666666%;\r
+  }\r
+  .col-sm-pull-7 {\r
+    right: 58.333333333333336%;\r
+  }\r
+  .col-sm-pull-6 {\r
+    right: 50%;\r
+  }\r
+  .col-sm-pull-5 {\r
+    right: 41.66666666666667%;\r
+  }\r
+  .col-sm-pull-4 {\r
+    right: 33.33333333333333%;\r
+  }\r
+  .col-sm-pull-3 {\r
+    right: 25%;\r
+  }\r
+  .col-sm-pull-2 {\r
+    right: 16.666666666666664%;\r
+  }\r
+  .col-sm-pull-1 {\r
+    right: 8.333333333333332%;\r
+  }\r
+  .col-sm-pull-0 {\r
+    right: 0%;\r
+  }\r
+  .col-sm-push-12 {\r
+    left: 100%;\r
+  }\r
+  .col-sm-push-11 {\r
+    left: 91.66666666666666%;\r
+  }\r
+  .col-sm-push-10 {\r
+    left: 83.33333333333334%;\r
+  }\r
+  .col-sm-push-9 {\r
+    left: 75%;\r
+  }\r
+  .col-sm-push-8 {\r
+    left: 66.66666666666666%;\r
+  }\r
+  .col-sm-push-7 {\r
+    left: 58.333333333333336%;\r
+  }\r
+  .col-sm-push-6 {\r
+    left: 50%;\r
+  }\r
+  .col-sm-push-5 {\r
+    left: 41.66666666666667%;\r
+  }\r
+  .col-sm-push-4 {\r
+    left: 33.33333333333333%;\r
+  }\r
+  .col-sm-push-3 {\r
+    left: 25%;\r
+  }\r
+  .col-sm-push-2 {\r
+    left: 16.666666666666664%;\r
+  }\r
+  .col-sm-push-1 {\r
+    left: 8.333333333333332%;\r
+  }\r
+  .col-sm-push-0 {\r
+    left: 0%;\r
+  }\r
+  .col-sm-offset-12 {\r
+    margin-left: 100%;\r
+  }\r
+  .col-sm-offset-11 {\r
+    margin-left: 91.66666666666666%;\r
+  }\r
+  .col-sm-offset-10 {\r
+    margin-left: 83.33333333333334%;\r
+  }\r
+  .col-sm-offset-9 {\r
+    margin-left: 75%;\r
+  }\r
+  .col-sm-offset-8 {\r
+    margin-left: 66.66666666666666%;\r
+  }\r
+  .col-sm-offset-7 {\r
+    margin-left: 58.333333333333336%;\r
+  }\r
+  .col-sm-offset-6 {\r
+    margin-left: 50%;\r
+  }\r
+  .col-sm-offset-5 {\r
+    margin-left: 41.66666666666667%;\r
+  }\r
+  .col-sm-offset-4 {\r
+    margin-left: 33.33333333333333%;\r
+  }\r
+  .col-sm-offset-3 {\r
+    margin-left: 25%;\r
+  }\r
+  .col-sm-offset-2 {\r
+    margin-left: 16.666666666666664%;\r
+  }\r
+  .col-sm-offset-1 {\r
+    margin-left: 8.333333333333332%;\r
+  }\r
+  .col-sm-offset-0 {\r
+    margin-left: 0%;\r
+  }\r
+}\r
+@media (min-width: 992px) {\r
+  .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\r
+    float: left;\r
+  }\r
+  .col-md-12 {\r
+    width: 100%;\r
+  }\r
+  .col-md-11 {\r
+    width: 91.66666666666666%;\r
+  }\r
+  .col-md-10 {\r
+    width: 83.33333333333334%;\r
+  }\r
+  .col-md-9 {\r
+    width: 75%;\r
+  }\r
+  .col-md-8 {\r
+    width: 66.66666666666666%;\r
+  }\r
+  .col-md-7 {\r
+    width: 58.333333333333336%;\r
+  }\r
+  .col-md-6 {\r
+    width: 50%;\r
+  }\r
+  .col-md-5 {\r
+    width: 41.66666666666667%;\r
+  }\r
+  .col-md-4 {\r
+    width: 33.33333333333333%;\r
+  }\r
+  .col-md-3 {\r
+    width: 25%;\r
+  }\r
+  .col-md-2 {\r
+    width: 16.666666666666664%;\r
+  }\r
+  .col-md-1 {\r
+    width: 8.333333333333332%;\r
+  }\r
+  .col-md-pull-12 {\r
+    right: 100%;\r
+  }\r
+  .col-md-pull-11 {\r
+    right: 91.66666666666666%;\r
+  }\r
+  .col-md-pull-10 {\r
+    right: 83.33333333333334%;\r
+  }\r
+  .col-md-pull-9 {\r
+    right: 75%;\r
+  }\r
+  .col-md-pull-8 {\r
+    right: 66.66666666666666%;\r
+  }\r
+  .col-md-pull-7 {\r
+    right: 58.333333333333336%;\r
+  }\r
+  .col-md-pull-6 {\r
+    right: 50%;\r
+  }\r
+  .col-md-pull-5 {\r
+    right: 41.66666666666667%;\r
+  }\r
+  .col-md-pull-4 {\r
+    right: 33.33333333333333%;\r
+  }\r
+  .col-md-pull-3 {\r
+    right: 25%;\r
+  }\r
+  .col-md-pull-2 {\r
+    right: 16.666666666666664%;\r
+  }\r
+  .col-md-pull-1 {\r
+    right: 8.333333333333332%;\r
+  }\r
+  .col-md-pull-0 {\r
+    right: 0%;\r
+  }\r
+  .col-md-push-12 {\r
+    left: 100%;\r
+  }\r
+  .col-md-push-11 {\r
+    left: 91.66666666666666%;\r
+  }\r
+  .col-md-push-10 {\r
+    left: 83.33333333333334%;\r
+  }\r
+  .col-md-push-9 {\r
+    left: 75%;\r
+  }\r
+  .col-md-push-8 {\r
+    left: 66.66666666666666%;\r
+  }\r
+  .col-md-push-7 {\r
+    left: 58.333333333333336%;\r
+  }\r
+  .col-md-push-6 {\r
+    left: 50%;\r
+  }\r
+  .col-md-push-5 {\r
+    left: 41.66666666666667%;\r
+  }\r
+  .col-md-push-4 {\r
+    left: 33.33333333333333%;\r
+  }\r
+  .col-md-push-3 {\r
+    left: 25%;\r
+  }\r
+  .col-md-push-2 {\r
+    left: 16.666666666666664%;\r
+  }\r
+  .col-md-push-1 {\r
+    left: 8.333333333333332%;\r
+  }\r
+  .col-md-push-0 {\r
+    left: 0%;\r
+  }\r
+  .col-md-offset-12 {\r
+    margin-left: 100%;\r
+  }\r
+  .col-md-offset-11 {\r
+    margin-left: 91.66666666666666%;\r
+  }\r
+  .col-md-offset-10 {\r
+    margin-left: 83.33333333333334%;\r
+  }\r
+  .col-md-offset-9 {\r
+    margin-left: 75%;\r
+  }\r
+  .col-md-offset-8 {\r
+    margin-left: 66.66666666666666%;\r
+  }\r
+  .col-md-offset-7 {\r
+    margin-left: 58.333333333333336%;\r
+  }\r
+  .col-md-offset-6 {\r
+    margin-left: 50%;\r
+  }\r
+  .col-md-offset-5 {\r
+    margin-left: 41.66666666666667%;\r
+  }\r
+  .col-md-offset-4 {\r
+    margin-left: 33.33333333333333%;\r
+  }\r
+  .col-md-offset-3 {\r
+    margin-left: 25%;\r
+  }\r
+  .col-md-offset-2 {\r
+    margin-left: 16.666666666666664%;\r
+  }\r
+  .col-md-offset-1 {\r
+    margin-left: 8.333333333333332%;\r
+  }\r
+  .col-md-offset-0 {\r
+    margin-left: 0%;\r
+  }\r
+}\r
+@media (min-width: 1200px) {\r
+  .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\r
+    float: left;\r
+  }\r
+  .col-lg-12 {\r
+    width: 100%;\r
+  }\r
+  .col-lg-11 {\r
+    width: 91.66666666666666%;\r
+  }\r
+  .col-lg-10 {\r
+    width: 83.33333333333334%;\r
+  }\r
+  .col-lg-9 {\r
+    width: 75%;\r
+  }\r
+  .col-lg-8 {\r
+    width: 66.66666666666666%;\r
+  }\r
+  .col-lg-7 {\r
+    width: 58.333333333333336%;\r
+  }\r
+  .col-lg-6 {\r
+    width: 50%;\r
+  }\r
+  .col-lg-5 {\r
+    width: 41.66666666666667%;\r
+  }\r
+  .col-lg-4 {\r
+    width: 33.33333333333333%;\r
+  }\r
+  .col-lg-3 {\r
+    width: 25%;\r
+  }\r
+  .col-lg-2 {\r
+    width: 16.666666666666664%;\r
+  }\r
+  .col-lg-1 {\r
+    width: 8.333333333333332%;\r
+  }\r
+  .col-lg-pull-12 {\r
+    right: 100%;\r
+  }\r
+  .col-lg-pull-11 {\r
+    right: 91.66666666666666%;\r
+  }\r
+  .col-lg-pull-10 {\r
+    right: 83.33333333333334%;\r
+  }\r
+  .col-lg-pull-9 {\r
+    right: 75%;\r
+  }\r
+  .col-lg-pull-8 {\r
+    right: 66.66666666666666%;\r
+  }\r
+  .col-lg-pull-7 {\r
+    right: 58.333333333333336%;\r
+  }\r
+  .col-lg-pull-6 {\r
+    right: 50%;\r
+  }\r
+  .col-lg-pull-5 {\r
+    right: 41.66666666666667%;\r
+  }\r
+  .col-lg-pull-4 {\r
+    right: 33.33333333333333%;\r
+  }\r
+  .col-lg-pull-3 {\r
+    right: 25%;\r
+  }\r
+  .col-lg-pull-2 {\r
+    right: 16.666666666666664%;\r
+  }\r
+  .col-lg-pull-1 {\r
+    right: 8.333333333333332%;\r
+  }\r
+  .col-lg-pull-0 {\r
+    right: 0%;\r
+  }\r
+  .col-lg-push-12 {\r
+    left: 100%;\r
+  }\r
+  .col-lg-push-11 {\r
+    left: 91.66666666666666%;\r
+  }\r
+  .col-lg-push-10 {\r
+    left: 83.33333333333334%;\r
+  }\r
+  .col-lg-push-9 {\r
+    left: 75%;\r
+  }\r
+  .col-lg-push-8 {\r
+    left: 66.66666666666666%;\r
+  }\r
+  .col-lg-push-7 {\r
+    left: 58.333333333333336%;\r
+  }\r
+  .col-lg-push-6 {\r
+    left: 50%;\r
+  }\r
+  .col-lg-push-5 {\r
+    left: 41.66666666666667%;\r
+  }\r
+  .col-lg-push-4 {\r
+    left: 33.33333333333333%;\r
+  }\r
+  .col-lg-push-3 {\r
+    left: 25%;\r
+  }\r
+  .col-lg-push-2 {\r
+    left: 16.666666666666664%;\r
+  }\r
+  .col-lg-push-1 {\r
+    left: 8.333333333333332%;\r
+  }\r
+  .col-lg-push-0 {\r
+    left: 0%;\r
+  }\r
+  .col-lg-offset-12 {\r
+    margin-left: 100%;\r
+  }\r
+  .col-lg-offset-11 {\r
+    margin-left: 91.66666666666666%;\r
+  }\r
+  .col-lg-offset-10 {\r
+    margin-left: 83.33333333333334%;\r
+  }\r
+  .col-lg-offset-9 {\r
+    margin-left: 75%;\r
+  }\r
+  .col-lg-offset-8 {\r
+    margin-left: 66.66666666666666%;\r
+  }\r
+  .col-lg-offset-7 {\r
+    margin-left: 58.333333333333336%;\r
+  }\r
+  .col-lg-offset-6 {\r
+    margin-left: 50%;\r
+  }\r
+  .col-lg-offset-5 {\r
+    margin-left: 41.66666666666667%;\r
+  }\r
+  .col-lg-offset-4 {\r
+    margin-left: 33.33333333333333%;\r
+  }\r
+  .col-lg-offset-3 {\r
+    margin-left: 25%;\r
+  }\r
+  .col-lg-offset-2 {\r
+    margin-left: 16.666666666666664%;\r
+  }\r
+  .col-lg-offset-1 {\r
+    margin-left: 8.333333333333332%;\r
+  }\r
+  .col-lg-offset-0 {\r
+    margin-left: 0%;\r
+  }\r
+}\r
+table {\r
+  max-width: 100%;\r
+  background-color: transparent;\r
+}\r
+th {\r
+  text-align: left;\r
+}\r
+.table {\r
+  width: 100%;\r
+  margin-bottom: 20px;\r
+}\r
+.table > thead > tr > th,\r
+.table > tbody > tr > th,\r
+.table > tfoot > tr > th,\r
+.table > thead > tr > td,\r
+.table > tbody > tr > td,\r
+.table > tfoot > tr > td {\r
+  padding: 8px;\r
+  line-height: 1.428571429;\r
+  vertical-align: top;\r
+  border-top: 1px solid #dddddd;\r
+}\r
+.table > thead > tr > th {\r
+  vertical-align: bottom;\r
+  border-bottom: 2px solid #dddddd;\r
+}\r
+.table > caption + thead > tr:first-child > th,\r
+.table > colgroup + thead > tr:first-child > th,\r
+.table > thead:first-child > tr:first-child > th,\r
+.table > caption + thead > tr:first-child > td,\r
+.table > colgroup + thead > tr:first-child > td,\r
+.table > thead:first-child > tr:first-child > td {\r
+  border-top: 0;\r
+}\r
+.table > tbody + tbody {\r
+  border-top: 2px solid #dddddd;\r
+}\r
+.table .table {\r
+  background-color: #f2f2f2;\r
+}\r
+.table-condensed > thead > tr > th,\r
+.table-condensed > tbody > tr > th,\r
+.table-condensed > tfoot > tr > th,\r
+.table-condensed > thead > tr > td,\r
+.table-condensed > tbody > tr > td,\r
+.table-condensed > tfoot > tr > td {\r
+  padding: 5px;\r
+}\r
+.table-bordered {\r
+  border: 1px solid #dddddd;\r
+}\r
+.table-bordered > thead > tr > th,\r
+.table-bordered > tbody > tr > th,\r
+.table-bordered > tfoot > tr > th,\r
+.table-bordered > thead > tr > td,\r
+.table-bordered > tbody > tr > td,\r
+.table-bordered > tfoot > tr > td {\r
+  border: 1px solid #dddddd;\r
+}\r
+.table-bordered > thead > tr > th,\r
+.table-bordered > thead > tr > td {\r
+  border-bottom-width: 2px;\r
+}\r
+.table-striped > tbody > tr:nth-child(odd) > td,\r
+.table-striped > tbody > tr:nth-child(odd) > th {\r
+  background-color: #f9f9f9;\r
+}\r
+.table-hover > tbody > tr:hover > td,\r
+.table-hover > tbody > tr:hover > th {\r
+  background-color: #dbdbdb;\r
+}\r
+table col[class*="col-"] {\r
+  position: static;\r
+  float: none;\r
+  display: table-column;\r
+}\r
+table td[class*="col-"],\r
+table th[class*="col-"] {\r
+  position: static;\r
+  float: none;\r
+  display: table-cell;\r
+}\r
+.table > thead > tr > td.active,\r
+.table > tbody > tr > td.active,\r
+.table > tfoot > tr > td.active,\r
+.table > thead > tr > th.active,\r
+.table > tbody > tr > th.active,\r
+.table > tfoot > tr > th.active,\r
+.table > thead > tr.active > td,\r
+.table > tbody > tr.active > td,\r
+.table > tfoot > tr.active > td,\r
+.table > thead > tr.active > th,\r
+.table > tbody > tr.active > th,\r
+.table > tfoot > tr.active > th {\r
+  background-color: #dbdbdb;\r
+}\r
+.table-hover > tbody > tr > td.active:hover,\r
+.table-hover > tbody > tr > th.active:hover,\r
+.table-hover > tbody > tr.active:hover > td,\r
+.table-hover > tbody > tr.active:hover > th {\r
+  background-color: #cecece;\r
+}\r
+.table > thead > tr > td.success,\r
+.table > tbody > tr > td.success,\r
+.table > tfoot > tr > td.success,\r
+.table > thead > tr > th.success,\r
+.table > tbody > tr > th.success,\r
+.table > tfoot > tr > th.success,\r
+.table > thead > tr.success > td,\r
+.table > tbody > tr.success > td,\r
+.table > tfoot > tr.success > td,\r
+.table > thead > tr.success > th,\r
+.table > tbody > tr.success > th,\r
+.table > tfoot > tr.success > th {\r
+  background-color: #dff0d8;\r
+}\r
+.table-hover > tbody > tr > td.success:hover,\r
+.table-hover > tbody > tr > th.success:hover,\r
+.table-hover > tbody > tr.success:hover > td,\r
+.table-hover > tbody > tr.success:hover > th {\r
+  background-color: #d0e9c6;\r
+}\r
+.table > thead > tr > td.info,\r
+.table > tbody > tr > td.info,\r
+.table > tfoot > tr > td.info,\r
+.table > thead > tr > th.info,\r
+.table > tbody > tr > th.info,\r
+.table > tfoot > tr > th.info,\r
+.table > thead > tr.info > td,\r
+.table > tbody > tr.info > td,\r
+.table > tfoot > tr.info > td,\r
+.table > thead > tr.info > th,\r
+.table > tbody > tr.info > th,\r
+.table > tfoot > tr.info > th {\r
+  background-color: #d9edf7;\r
+}\r
+.table-hover > tbody > tr > td.info:hover,\r
+.table-hover > tbody > tr > th.info:hover,\r
+.table-hover > tbody > tr.info:hover > td,\r
+.table-hover > tbody > tr.info:hover > th {\r
+  background-color: #c4e3f3;\r
+}\r
+.table > thead > tr > td.warning,\r
+.table > tbody > tr > td.warning,\r
+.table > tfoot > tr > td.warning,\r
+.table > thead > tr > th.warning,\r
+.table > tbody > tr > th.warning,\r
+.table > tfoot > tr > th.warning,\r
+.table > thead > tr.warning > td,\r
+.table > tbody > tr.warning > td,\r
+.table > tfoot > tr.warning > td,\r
+.table > thead > tr.warning > th,\r
+.table > tbody > tr.warning > th,\r
+.table > tfoot > tr.warning > th {\r
+  background-color: #fcf8e3;\r
+}\r
+.table-hover > tbody > tr > td.warning:hover,\r
+.table-hover > tbody > tr > th.warning:hover,\r
+.table-hover > tbody > tr.warning:hover > td,\r
+.table-hover > tbody > tr.warning:hover > th {\r
+  background-color: #faf2cc;\r
+}\r
+.table > thead > tr > td.danger,\r
+.table > tbody > tr > td.danger,\r
+.table > tfoot > tr > td.danger,\r
+.table > thead > tr > th.danger,\r
+.table > tbody > tr > th.danger,\r
+.table > tfoot > tr > th.danger,\r
+.table > thead > tr.danger > td,\r
+.table > tbody > tr.danger > td,\r
+.table > tfoot > tr.danger > td,\r
+.table > thead > tr.danger > th,\r
+.table > tbody > tr.danger > th,\r
+.table > tfoot > tr.danger > th {\r
+  background-color: #f2dede;\r
+}\r
+.table-hover > tbody > tr > td.danger:hover,\r
+.table-hover > tbody > tr > th.danger:hover,\r
+.table-hover > tbody > tr.danger:hover > td,\r
+.table-hover > tbody > tr.danger:hover > th {\r
+  background-color: #ebcccc;\r
+}\r
+@media (max-width: 767px) {\r
+  .table-responsive {\r
+    width: 100%;\r
+    margin-bottom: 15px;\r
+    overflow-y: hidden;\r
+    overflow-x: scroll;\r
+    -ms-overflow-style: -ms-autohiding-scrollbar;\r
+    border: 1px solid #dddddd;\r
+    -webkit-overflow-scrolling: touch;\r
+  }\r
+  .table-responsive > .table {\r
+    margin-bottom: 0;\r
+  }\r
+  .table-responsive > .table > thead > tr > th,\r
+  .table-responsive > .table > tbody > tr > th,\r
+  .table-responsive > .table > tfoot > tr > th,\r
+  .table-responsive > .table > thead > tr > td,\r
+  .table-responsive > .table > tbody > tr > td,\r
+  .table-responsive > .table > tfoot > tr > td {\r
+    white-space: nowrap;\r
+  }\r
+  .table-responsive > .table-bordered {\r
+    border: 0;\r
+  }\r
+  .table-responsive > .table-bordered > thead > tr > th:first-child,\r
+  .table-responsive > .table-bordered > tbody > tr > th:first-child,\r
+  .table-responsive > .table-bordered > tfoot > tr > th:first-child,\r
+  .table-responsive > .table-bordered > thead > tr > td:first-child,\r
+  .table-responsive > .table-bordered > tbody > tr > td:first-child,\r
+  .table-responsive > .table-bordered > tfoot > tr > td:first-child {\r
+    border-left: 0;\r
+  }\r
+  .table-responsive > .table-bordered > thead > tr > th:last-child,\r
+  .table-responsive > .table-bordered > tbody > tr > th:last-child,\r
+  .table-responsive > .table-bordered > tfoot > tr > th:last-child,\r
+  .table-responsive > .table-bordered > thead > tr > td:last-child,\r
+  .table-responsive > .table-bordered > tbody > tr > td:last-child,\r
+  .table-responsive > .table-bordered > tfoot > tr > td:last-child {\r
+    border-right: 0;\r
+  }\r
+  .table-responsive > .table-bordered > tbody > tr:last-child > th,\r
+  .table-responsive > .table-bordered > tfoot > tr:last-child > th,\r
+  .table-responsive > .table-bordered > tbody > tr:last-child > td,\r
+  .table-responsive > .table-bordered > tfoot > tr:last-child > td {\r
+    border-bottom: 0;\r
+  }\r
+}\r
+fieldset {\r
+  padding: 0;\r
+  margin: 0;\r
+  border: 0;\r
+  min-width: 0;\r
+}\r
+legend {\r
+  display: block;\r
+  width: 100%;\r
+  padding: 0;\r
+  margin-bottom: 20px;\r
+  font-size: 21px;\r
+  line-height: inherit;\r
+  color: #333333;\r
+  border: 0;\r
+  border-bottom: 1px solid #e5e5e5;\r
+}\r
+label {\r
+  display: inline-block;\r
+  margin-bottom: 5px;\r
+  font-weight: bold;\r
+}\r
+input[type="search"] {\r
+  -webkit-box-sizing: border-box;\r
+  -moz-box-sizing: border-box;\r
+  box-sizing: border-box;\r
+}\r
+input[type="radio"],\r
+input[type="checkbox"] {\r
+  margin: 4px 0 0;\r
+  margin-top: 1px \9;\r
+  /* IE8-9 */\r
+\r
+  line-height: normal;\r
+}\r
+input[type="file"] {\r
+  display: block;\r
+}\r
+input[type="range"] {\r
+  display: block;\r
+  width: 100%;\r
+}\r
+select[multiple],\r
+select[size] {\r
+  height: auto;\r
+}\r
+input[type="file"]:focus,\r
+input[type="radio"]:focus,\r
+input[type="checkbox"]:focus {\r
+  outline: thin dotted;\r
+  outline: 5px auto -webkit-focus-ring-color;\r
+  outline-offset: -2px;\r
+}\r
+output {\r
+  display: block;\r
+  padding-top: 7px;\r
+  font-size: 14px;\r
+  line-height: 1.428571429;\r
+  color: #555555;\r
+}\r
+.form-control {\r
+  display: block;\r
+  width: 100%;\r
+  height: 34px;\r
+  padding: 6px 12px;\r
+  font-size: 14px;\r
+  line-height: 1.428571429;\r
+  color: #555555;\r
+  background-color: #ffffff;\r
+  background-image: none;\r
+  border: 1px solid #cccccc;\r
+  border-radius: 0px;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\r
+  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\r
+}\r
+.form-control:focus {\r
+  border-color: #66afe9;\r
+  outline: 0;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\r
+  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\r
+}\r
+.form-control::-moz-placeholder {\r
+  color: #999999;\r
+  opacity: 1;\r
+}\r
+.form-control:-ms-input-placeholder {\r
+  color: #999999;\r
+}\r
+.form-control::-webkit-input-placeholder {\r
+  color: #999999;\r
+}\r
+.form-control[disabled],\r
+.form-control[readonly],\r
+fieldset[disabled] .form-control {\r
+  cursor: not-allowed;\r
+  background-color: #eeeeee;\r
+  opacity: 1;\r
+}\r
+textarea.form-control {\r
+  height: auto;\r
+}\r
+input[type="search"] {\r
+  -webkit-appearance: none;\r
+}\r
+input[type="date"] {\r
+  line-height: 34px;\r
+}\r
+.form-group {\r
+  margin-bottom: 15px;\r
+}\r
+.radio,\r
+.checkbox {\r
+  display: block;\r
+  min-height: 20px;\r
+  margin-top: 10px;\r
+  margin-bottom: 10px;\r
+  padding-left: 20px;\r
+}\r
+.radio label,\r
+.checkbox label {\r
+  display: inline;\r
+  font-weight: normal;\r
+  cursor: pointer;\r
+}\r
+.radio input[type="radio"],\r
+.radio-inline input[type="radio"],\r
+.checkbox input[type="checkbox"],\r
+.checkbox-inline input[type="checkbox"] {\r
+  float: left;\r
+  margin-left: -20px;\r
+}\r
+.radio + .radio,\r
+.checkbox + .checkbox {\r
+  margin-top: -5px;\r
+}\r
+.radio-inline,\r
+.checkbox-inline {\r
+  display: inline-block;\r
+  padding-left: 20px;\r
+  margin-bottom: 0;\r
+  vertical-align: middle;\r
+  font-weight: normal;\r
+  cursor: pointer;\r
+}\r
+.radio-inline + .radio-inline,\r
+.checkbox-inline + .checkbox-inline {\r
+  margin-top: 0;\r
+  margin-left: 10px;\r
+}\r
+input[type="radio"][disabled],\r
+input[type="checkbox"][disabled],\r
+.radio[disabled],\r
+.radio-inline[disabled],\r
+.checkbox[disabled],\r
+.checkbox-inline[disabled],\r
+fieldset[disabled] input[type="radio"],\r
+fieldset[disabled] input[type="checkbox"],\r
+fieldset[disabled] .radio,\r
+fieldset[disabled] .radio-inline,\r
+fieldset[disabled] .checkbox,\r
+fieldset[disabled] .checkbox-inline {\r
+  cursor: not-allowed;\r
+}\r
+.input-sm {\r
+  height: 30px;\r
+  padding: 5px 10px;\r
+  font-size: 12px;\r
+  line-height: 1.5;\r
+  border-radius: 0px;\r
+}\r
+select.input-sm {\r
+  height: 30px;\r
+  line-height: 30px;\r
+}\r
+textarea.input-sm,\r
+select[multiple].input-sm {\r
+  height: auto;\r
+}\r
+.input-lg {\r
+  height: 45px;\r
+  padding: 10px 16px;\r
+  font-size: 18px;\r
+  line-height: 1.33;\r
+  border-radius: 0px;\r
+}\r
+select.input-lg {\r
+  height: 45px;\r
+  line-height: 45px;\r
+}\r
+textarea.input-lg,\r
+select[multiple].input-lg {\r
+  height: auto;\r
+}\r
+.has-feedback {\r
+  position: relative;\r
+}\r
+.has-feedback .form-control {\r
+  padding-right: 42.5px;\r
+}\r
+.has-feedback .form-control-feedback {\r
+  position: absolute;\r
+  top: 25px;\r
+  right: 0;\r
+  display: block;\r
+  width: 34px;\r
+  height: 34px;\r
+  line-height: 34px;\r
+  text-align: center;\r
+}\r
+.has-success .help-block,\r
+.has-success .control-label,\r
+.has-success .radio,\r
+.has-success .checkbox,\r
+.has-success .radio-inline,\r
+.has-success .checkbox-inline {\r
+  color: #5cb85c;\r
+}\r
+.has-success .form-control {\r
+  border-color: #5cb85c;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+}\r
+.has-success .form-control:focus {\r
+  border-color: #449d44;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #a3d7a3;\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #a3d7a3;\r
+}\r
+.has-success .input-group-addon {\r
+  color: #5cb85c;\r
+  border-color: #5cb85c;\r
+  background-color: #dff0d8;\r
+}\r
+.has-success .form-control-feedback {\r
+  color: #5cb85c;\r
+}\r
+.has-warning .help-block,\r
+.has-warning .control-label,\r
+.has-warning .radio,\r
+.has-warning .checkbox,\r
+.has-warning .radio-inline,\r
+.has-warning .checkbox-inline {\r
+  color: #f0ad4e;\r
+}\r
+.has-warning .form-control {\r
+  border-color: #f0ad4e;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+}\r
+.has-warning .form-control:focus {\r
+  border-color: #ec971f;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #f8d9ac;\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #f8d9ac;\r
+}\r
+.has-warning .input-group-addon {\r
+  color: #f0ad4e;\r
+  border-color: #f0ad4e;\r
+  background-color: #fcf8e3;\r
+}\r
+.has-warning .form-control-feedback {\r
+  color: #f0ad4e;\r
+}\r
+.has-error .help-block,\r
+.has-error .control-label,\r
+.has-error .radio,\r
+.has-error .checkbox,\r
+.has-error .radio-inline,\r
+.has-error .checkbox-inline {\r
+  color: #d9534f;\r
+}\r
+.has-error .form-control {\r
+  border-color: #d9534f;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\r
+}\r
+.has-error .form-control:focus {\r
+  border-color: #c9302c;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #eba5a3;\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #eba5a3;\r
+}\r
+.has-error .input-group-addon {\r
+  color: #d9534f;\r
+  border-color: #d9534f;\r
+  background-color: #f2dede;\r
+}\r
+.has-error .form-control-feedback {\r
+  color: #d9534f;\r
+}\r
+.form-control-static {\r
+  margin-bottom: 0;\r
+}\r
+.help-block {\r
+  display: block;\r
+  margin-top: 5px;\r
+  margin-bottom: 10px;\r
+  color: #737373;\r
+}\r
+@media (min-width: 768px) {\r
+  .form-inline .form-group {\r
+    display: inline-block;\r
+    margin-bottom: 0;\r
+    vertical-align: middle;\r
+  }\r
+  .form-inline .form-control {\r
+    display: inline-block;\r
+    width: auto;\r
+    vertical-align: middle;\r
+  }\r
+  .form-inline .input-group > .form-control {\r
+    width: 100%;\r
+  }\r
+  .form-inline .control-label {\r
+    margin-bottom: 0;\r
+    vertical-align: middle;\r
+  }\r
+  .form-inline .radio,\r
+  .form-inline .checkbox {\r
+    display: inline-block;\r
+    margin-top: 0;\r
+    margin-bottom: 0;\r
+    padding-left: 0;\r
+    vertical-align: middle;\r
+  }\r
+  .form-inline .radio input[type="radio"],\r
+  .form-inline .checkbox input[type="checkbox"] {\r
+    float: none;\r
+    margin-left: 0;\r
+  }\r
+  .form-inline .has-feedback .form-control-feedback {\r
+    top: 0;\r
+  }\r
+}\r
+.form-horizontal .control-label,\r
+.form-horizontal .radio,\r
+.form-horizontal .checkbox,\r
+.form-horizontal .radio-inline,\r
+.form-horizontal .checkbox-inline {\r
+  margin-top: 0;\r
+  margin-bottom: 0;\r
+  padding-top: 7px;\r
+}\r
+.form-horizontal .radio,\r
+.form-horizontal .checkbox {\r
+  min-height: 27px;\r
+}\r
+.form-horizontal .form-group {\r
+  margin-left: -15px;\r
+  margin-right: -15px;\r
+}\r
+.form-horizontal .form-control-static {\r
+  padding-top: 7px;\r
+}\r
+@media (min-width: 768px) {\r
+  .form-horizontal .control-label {\r
+    text-align: right;\r
+  }\r
+}\r
+.form-horizontal .has-feedback .form-control-feedback {\r
+  top: 0;\r
+  right: 15px;\r
+}\r
+.btn {\r
+  display: inline-block;\r
+  margin-bottom: 0;\r
+  font-weight: normal;\r
+  text-align: center;\r
+  vertical-align: middle;\r
+  cursor: pointer;\r
+  background-image: none;\r
+  border: 1px solid transparent;\r
+  white-space: nowrap;\r
+  padding: 6px 12px;\r
+  font-size: 14px;\r
+  line-height: 1.428571429;\r
+  border-radius: 0px;\r
+  -webkit-user-select: none;\r
+  -moz-user-select: none;\r
+  -ms-user-select: none;\r
+  -o-user-select: none;\r
+  user-select: none;\r
+}\r
+.btn:focus {\r
+  outline: thin dotted;\r
+  outline: 5px auto -webkit-focus-ring-color;\r
+  outline-offset: -2px;\r
+}\r
+.btn:hover,\r
+.btn:focus {\r
+  color: #333333;\r
+  text-decoration: none;\r
+}\r
+.btn:active,\r
+.btn.active {\r
+  outline: 0;\r
+  background-image: none;\r
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\r
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\r
+}\r
+.btn.disabled,\r
+.btn[disabled],\r
+fieldset[disabled] .btn {\r
+  cursor: not-allowed;\r
+  pointer-events: none;\r
+  opacity: 0.65;\r
+  filter: alpha(opacity=65);\r
+  -webkit-box-shadow: none;\r
+  box-shadow: none;\r
+}\r
+.btn-default {\r
+  color: #333333;\r
+  background-color: #ffffff;\r
+  border-color: #cccccc;\r
+}\r
+.btn-default:hover,\r
+.btn-default:focus,\r
+.btn-default:active,\r
+.btn-default.active,\r
+.open .dropdown-toggle.btn-default {\r
+  color: #333333;\r
+  background-color: #ebebeb;\r
+  border-color: #adadad;\r
+}\r
+.btn-default:active,\r
+.btn-default.active,\r
+.open .dropdown-toggle.btn-default {\r
+  background-image: none;\r
+}\r
+.btn-default.disabled,\r
+.btn-default[disabled],\r
+fieldset[disabled] .btn-default,\r
+.btn-default.disabled:hover,\r
+.btn-default[disabled]:hover,\r
+fieldset[disabled] .btn-default:hover,\r
+.btn-default.disabled:focus,\r
+.btn-default[disabled]:focus,\r
+fieldset[disabled] .btn-default:focus,\r
+.btn-default.disabled:active,\r
+.btn-default[disabled]:active,\r
+fieldset[disabled] .btn-default:active,\r
+.btn-default.disabled.active,\r
+.btn-default[disabled].active,\r
+fieldset[disabled] .btn-default.active {\r
+  background-color: #ffffff;\r
+  border-color: #cccccc;\r
+}\r
+.btn-default .badge {\r
+  color: #ffffff;\r
+  background-color: #333333;\r
+}\r
+.btn-primary {\r
+  color: #ffffff;\r
+  background-color: #0d8921;\r
+  border-color: #0b721b;\r
+}\r
+.btn-primary:hover,\r
+.btn-primary:focus,\r
+.btn-primary:active,\r
+.btn-primary.active,\r
+.open .dropdown-toggle.btn-primary {\r
+  color: #ffffff;\r
+  background-color: #096418;\r
+  border-color: #053a0e;\r
+}\r
+.btn-primary:active,\r
+.btn-primary.active,\r
+.open .dropdown-toggle.btn-primary {\r
+  background-image: none;\r
+}\r
+.btn-primary.disabled,\r
+.btn-primary[disabled],\r
+fieldset[disabled] .btn-primary,\r
+.btn-primary.disabled:hover,\r
+.btn-primary[disabled]:hover,\r
+fieldset[disabled] .btn-primary:hover,\r
+.btn-primary.disabled:focus,\r
+.btn-primary[disabled]:focus,\r
+fieldset[disabled] .btn-primary:focus,\r
+.btn-primary.disabled:active,\r
+.btn-primary[disabled]:active,\r
+fieldset[disabled] .btn-primary:active,\r
+.btn-primary.disabled.active,\r
+.btn-primary[disabled].active,\r
+fieldset[disabled] .btn-primary.active {\r
+  background-color: #0d8921;\r
+  border-color: #0b721b;\r
+}\r
+.btn-primary .badge {\r
+  color: #0d8921;\r
+  background-color: #ffffff;\r
+}\r
+.btn-success {\r
+  color: #ffffff;\r
+  background-color: #5cb85c;\r
+  border-color: #4cae4c;\r
+}\r
+.btn-success:hover,\r
+.btn-success:focus,\r
+.btn-success:active,\r
+.btn-success.active,\r
+.open .dropdown-toggle.btn-success {\r
+  color: #ffffff;\r
+  background-color: #47a447;\r
+  border-color: #398439;\r
+}\r
+.btn-success:active,\r
+.btn-success.active,\r
+.open .dropdown-toggle.btn-success {\r
+  background-image: none;\r
+}\r
+.btn-success.disabled,\r
+.btn-success[disabled],\r
+fieldset[disabled] .btn-success,\r
+.btn-success.disabled:hover,\r
+.btn-success[disabled]:hover,\r
+fieldset[disabled] .btn-success:hover,\r
+.btn-success.disabled:focus,\r
+.btn-success[disabled]:focus,\r
+fieldset[disabled] .btn-success:focus,\r
+.btn-success.disabled:active,\r
+.btn-success[disabled]:active,\r
+fieldset[disabled] .btn-success:active,\r
+.btn-success.disabled.active,\r
+.btn-success[disabled].active,\r
+fieldset[disabled] .btn-success.active {\r
+  background-color: #5cb85c;\r
+  border-color: #4cae4c;\r
+}\r
+.btn-success .badge {\r
+  color: #5cb85c;\r
+  background-color: #ffffff;\r
+}\r
+.btn-info {\r
+  color: #ffffff;\r
+  background-color: #5bc0de;\r
+  border-color: #46b8da;\r
+}\r
+.btn-info:hover,\r
+.btn-info:focus,\r
+.btn-info:active,\r
+.btn-info.active,\r
+.open .dropdown-toggle.btn-info {\r
+  color: #ffffff;\r
+  background-color: #39b3d7;\r
+  border-color: #269abc;\r
+}\r
+.btn-info:active,\r
+.btn-info.active,\r
+.open .dropdown-toggle.btn-info {\r
+  background-image: none;\r
+}\r
+.btn-info.disabled,\r
+.btn-info[disabled],\r
+fieldset[disabled] .btn-info,\r
+.btn-info.disabled:hover,\r
+.btn-info[disabled]:hover,\r
+fieldset[disabled] .btn-info:hover,\r
+.btn-info.disabled:focus,\r
+.btn-info[disabled]:focus,\r
+fieldset[disabled] .btn-info:focus,\r
+.btn-info.disabled:active,\r
+.btn-info[disabled]:active,\r
+fieldset[disabled] .btn-info:active,\r
+.btn-info.disabled.active,\r
+.btn-info[disabled].active,\r
+fieldset[disabled] .btn-info.active {\r
+  background-color: #5bc0de;\r
+  border-color: #46b8da;\r
+}\r
+.btn-info .badge {\r
+  color: #5bc0de;\r
+  background-color: #ffffff;\r
+}\r
+.btn-warning {\r
+  color: #ffffff;\r
+  background-color: #f0ad4e;\r
+  border-color: #eea236;\r
+}\r
+.btn-warning:hover,\r
+.btn-warning:focus,\r
+.btn-warning:active,\r
+.btn-warning.active,\r
+.open .dropdown-toggle.btn-warning {\r
+  color: #ffffff;\r
+  background-color: #ed9c28;\r
+  border-color: #d58512;\r
+}\r
+.btn-warning:active,\r
+.btn-warning.active,\r
+.open .dropdown-toggle.btn-warning {\r
+  background-image: none;\r
+}\r
+.btn-warning.disabled,\r
+.btn-warning[disabled],\r
+fieldset[disabled] .btn-warning,\r
+.btn-warning.disabled:hover,\r
+.btn-warning[disabled]:hover,\r
+fieldset[disabled] .btn-warning:hover,\r
+.btn-warning.disabled:focus,\r
+.btn-warning[disabled]:focus,\r
+fieldset[disabled] .btn-warning:focus,\r
+.btn-warning.disabled:active,\r
+.btn-warning[disabled]:active,\r
+fieldset[disabled] .btn-warning:active,\r
+.btn-warning.disabled.active,\r
+.btn-warning[disabled].active,\r
+fieldset[disabled] .btn-warning.active {\r
+  background-color: #f0ad4e;\r
+  border-color: #eea236;\r
+}\r
+.btn-warning .badge {\r
+  color: #f0ad4e;\r
+  background-color: #ffffff;\r
+}\r
+.btn-danger {\r
+  color: #ffffff;\r
+  background-color: #d9534f;\r
+  border-color: #d43f3a;\r
+}\r
+.btn-danger:hover,\r
+.btn-danger:focus,\r
+.btn-danger:active,\r
+.btn-danger.active,\r
+.open .dropdown-toggle.btn-danger {\r
+  color: #ffffff;\r
+  background-color: #d2322d;\r
+  border-color: #ac2925;\r
+}\r
+.btn-danger:active,\r
+.btn-danger.active,\r
+.open .dropdown-toggle.btn-danger {\r
+  background-image: none;\r
+}\r
+.btn-danger.disabled,\r
+.btn-danger[disabled],\r
+fieldset[disabled] .btn-danger,\r
+.btn-danger.disabled:hover,\r
+.btn-danger[disabled]:hover,\r
+fieldset[disabled] .btn-danger:hover,\r
+.btn-danger.disabled:focus,\r
+.btn-danger[disabled]:focus,\r
+fieldset[disabled] .btn-danger:focus,\r
+.btn-danger.disabled:active,\r
+.btn-danger[disabled]:active,\r
+fieldset[disabled] .btn-danger:active,\r
+.btn-danger.disabled.active,\r
+.btn-danger[disabled].active,\r
+fieldset[disabled] .btn-danger.active {\r
+  background-color: #d9534f;\r
+  border-color: #d43f3a;\r
+}\r
+.btn-danger .badge {\r
+  color: #d9534f;\r
+  background-color: #ffffff;\r
+}\r
+.btn-link {\r
+  color: #0d8921;\r
+  font-weight: normal;\r
+  cursor: pointer;\r
+  border-radius: 0;\r
+}\r
+.btn-link,\r
+.btn-link:active,\r
+.btn-link[disabled],\r
+fieldset[disabled] .btn-link {\r
+  background-color: transparent;\r
+  -webkit-box-shadow: none;\r
+  box-shadow: none;\r
+}\r
+.btn-link,\r
+.btn-link:hover,\r
+.btn-link:focus,\r
+.btn-link:active {\r
+  border-color: transparent;\r
+}\r
+.btn-link:hover,\r
+.btn-link:focus {\r
+  color: #064310;\r
+  text-decoration: underline;\r
+  background-color: transparent;\r
+}\r
+.btn-link[disabled]:hover,\r
+fieldset[disabled] .btn-link:hover,\r
+.btn-link[disabled]:focus,\r
+fieldset[disabled] .btn-link:focus {\r
+  color: #999999;\r
+  text-decoration: none;\r
+}\r
+.btn-lg,\r
+.btn-group-lg > .btn {\r
+  padding: 10px 16px;\r
+  font-size: 18px;\r
+  line-height: 1.33;\r
+  border-radius: 0px;\r
+}\r
+.btn-sm,\r
+.btn-group-sm > .btn {\r
+  padding: 5px 10px;\r
+  font-size: 12px;\r
+  line-height: 1.5;\r
+  border-radius: 0px;\r
+}\r
+.btn-xs,\r
+.btn-group-xs > .btn {\r
+  padding: 1px 5px;\r
+  font-size: 12px;\r
+  line-height: 1.5;\r
+  border-radius: 0px;\r
+}\r
+.btn-block {\r
+  display: block;\r
+  width: 100%;\r
+  padding-left: 0;\r
+  padding-right: 0;\r
+}\r
+.btn-block + .btn-block {\r
+  margin-top: 5px;\r
+}\r
+input[type="submit"].btn-block,\r
+input[type="reset"].btn-block,\r
+input[type="button"].btn-block {\r
+  width: 100%;\r
+}\r
+.fade {\r
+  opacity: 0;\r
+  -webkit-transition: opacity 0.15s linear;\r
+  transition: opacity 0.15s linear;\r
+}\r
+.fade.in {\r
+  opacity: 1;\r
+}\r
+.collapse {\r
+  display: none;\r
+}\r
+.collapse.in {\r
+  display: block;\r
+}\r
+.collapsing {\r
+  position: relative;\r
+  height: 0;\r
+  overflow: hidden;\r
+  -webkit-transition: height 0.35s ease;\r
+  transition: height 0.35s ease;\r
+}\r
+@font-face {\r
+  font-family: 'Glyphicons Halflings';\r
+  src: url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/fonts/glyphicons-halflings-regular.eot');\r
+  src: url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/fonts/glyphicons-halflings-regular.woff') format('woff'), url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\r
+}\r
+.glyphicon {\r
+  position: relative;\r
+  top: 1px;\r
+  display: inline-block;\r
+  font-family: 'Glyphicons Halflings';\r
+  font-style: normal;\r
+  font-weight: normal;\r
+  line-height: 1;\r
+  -webkit-font-smoothing: antialiased;\r
+  -moz-osx-font-smoothing: grayscale;\r
+}\r
+.glyphicon-asterisk:before {\r
+  content: "\2a";\r
+}\r
+.glyphicon-plus:before {\r
+  content: "\2b";\r
+}\r
+.glyphicon-euro:before {\r
+  content: "\20ac";\r
+}\r
+.glyphicon-minus:before {\r
+  content: "\2212";\r
+}\r
+.glyphicon-cloud:before {\r
+  content: "\2601";\r
+}\r
+.glyphicon-envelope:before {\r
+  content: "\2709";\r
+}\r
+.glyphicon-pencil:before {\r
+  content: "\270f";\r
+}\r
+.glyphicon-glass:before {\r
+  content: "\e001";\r
+}\r
+.glyphicon-music:before {\r
+  content: "\e002";\r
+}\r
+.glyphicon-search:before {\r
+  content: "\e003";\r
+}\r
+.glyphicon-heart:before {\r
+  content: "\e005";\r
+}\r
+.glyphicon-star:before {\r
+  content: "\e006";\r
+}\r
+.glyphicon-star-empty:before {\r
+  content: "\e007";\r
+}\r
+.glyphicon-user:before {\r
+  content: "\e008";\r
+}\r
+.glyphicon-film:before {\r
+  content: "\e009";\r
+}\r
+.glyphicon-th-large:before {\r
+  content: "\e010";\r
+}\r
+.glyphicon-th:before {\r
+  content: "\e011";\r
+}\r
+.glyphicon-th-list:before {\r
+  content: "\e012";\r
+}\r
+.glyphicon-ok:before {\r
+  content: "\e013";\r
+}\r
+.glyphicon-remove:before {\r
+  content: "\e014";\r
+}\r
+.glyphicon-zoom-in:before {\r
+  content: "\e015";\r
+}\r
+.glyphicon-zoom-out:before {\r
+  content: "\e016";\r
+}\r
+.glyphicon-off:before {\r
+  content: "\e017";\r
+}\r
+.glyphicon-signal:before {\r
+  content: "\e018";\r
+}\r
+.glyphicon-cog:before {\r
+  content: "\e019";\r
+}\r
+.glyphicon-trash:before {\r
+  content: "\e020";\r
+}\r
+.glyphicon-home:before {\r
+  content: "\e021";\r
+}\r
+.glyphicon-file:before {\r
+  content: "\e022";\r
+}\r
+.glyphicon-time:before {\r
+  content: "\e023";\r
+}\r
+.glyphicon-road:before {\r
+  content: "\e024";\r
+}\r
+.glyphicon-download-alt:before {\r
+  content: "\e025";\r
+}\r
+.glyphicon-download:before {\r
+  content: "\e026";\r
+}\r
+.glyphicon-upload:before {\r
+  content: "\e027";\r
+}\r
+.glyphicon-inbox:before {\r
+  content: "\e028";\r
+}\r
+.glyphicon-play-circle:before {\r
+  content: "\e029";\r
+}\r
+.glyphicon-repeat:before {\r
+  content: "\e030";\r
+}\r
+.glyphicon-refresh:before {\r
+  content: "\e031";\r
+}\r
+.glyphicon-list-alt:before {\r
+  content: "\e032";\r
+}\r
+.glyphicon-lock:before {\r
+  content: "\e033";\r
+}\r
+.glyphicon-flag:before {\r
+  content: "\e034";\r
+}\r
+.glyphicon-headphones:before {\r
+  content: "\e035";\r
+}\r
+.glyphicon-volume-off:before {\r
+  content: "\e036";\r
+}\r
+.glyphicon-volume-down:before {\r
+  content: "\e037";\r
+}\r
+.glyphicon-volume-up:before {\r
+  content: "\e038";\r
+}\r
+.glyphicon-qrcode:before {\r
+  content: "\e039";\r
+}\r
+.glyphicon-barcode:before {\r
+  content: "\e040";\r
+}\r
+.glyphicon-tag:before {\r
+  content: "\e041";\r
+}\r
+.glyphicon-tags:before {\r
+  content: "\e042";\r
+}\r
+.glyphicon-book:before {\r
+  content: "\e043";\r
+}\r
+.glyphicon-bookmark:before {\r
+  content: "\e044";\r
+}\r
+.glyphicon-print:before {\r
+  content: "\e045";\r
+}\r
+.glyphicon-camera:before {\r
+  content: "\e046";\r
+}\r
+.glyphicon-font:before {\r
+  content: "\e047";\r
+}\r
+.glyphicon-bold:before {\r
+  content: "\e048";\r
+}\r
+.glyphicon-italic:before {\r
+  content: "\e049";\r
+}\r
+.glyphicon-text-height:before {\r
+  content: "\e050";\r
+}\r
+.glyphicon-text-width:before {\r
+  content: "\e051";\r
+}\r
+.glyphicon-align-left:before {\r
+  content: "\e052";\r
+}\r
+.glyphicon-align-center:before {\r
+  content: "\e053";\r
+}\r
+.glyphicon-align-right:before {\r
+  content: "\e054";\r
+}\r
+.glyphicon-align-justify:before {\r
+  content: "\e055";\r
+}\r
+.glyphicon-list:before {\r
+  content: "\e056";\r
+}\r
+.glyphicon-indent-left:before {\r
+  content: "\e057";\r
+}\r
+.glyphicon-indent-right:before {\r
+  content: "\e058";\r
+}\r
+.glyphicon-facetime-video:before {\r
+  content: "\e059";\r
+}\r
+.glyphicon-picture:before {\r
+  content: "\e060";\r
+}\r
+.glyphicon-map-marker:before {\r
+  content: "\e062";\r
+}\r
+.glyphicon-adjust:before {\r
+  content: "\e063";\r
+}\r
+.glyphicon-tint:before {\r
+  content: "\e064";\r
+}\r
+.glyphicon-edit:before {\r
+  content: "\e065";\r
+}\r
+.glyphicon-share:before {\r
+  content: "\e066";\r
+}\r
+.glyphicon-check:before {\r
+  content: "\e067";\r
+}\r
+.glyphicon-move:before {\r
+  content: "\e068";\r
+}\r
+.glyphicon-step-backward:before {\r
+  content: "\e069";\r
+}\r
+.glyphicon-fast-backward:before {\r
+  content: "\e070";\r
+}\r
+.glyphicon-backward:before {\r
+  content: "\e071";\r
+}\r
+.glyphicon-play:before {\r
+  content: "\e072";\r
+}\r
+.glyphicon-pause:before {\r
+  content: "\e073";\r
+}\r
+.glyphicon-stop:before {\r
+  content: "\e074";\r
+}\r
+.glyphicon-forward:before {\r
+  content: "\e075";\r
+}\r
+.glyphicon-fast-forward:before {\r
+  content: "\e076";\r
+}\r
+.glyphicon-step-forward:before {\r
+  content: "\e077";\r
+}\r
+.glyphicon-eject:before {\r
+  content: "\e078";\r
+}\r
+.glyphicon-chevron-left:before {\r
+  content: "\e079";\r
+}\r
+.glyphicon-chevron-right:before {\r
+  content: "\e080";\r
+}\r
+.glyphicon-plus-sign:before {\r
+  content: "\e081";\r
+}\r
+.glyphicon-minus-sign:before {\r
+  content: "\e082";\r
+}\r
+.glyphicon-remove-sign:before {\r
+  content: "\e083";\r
+}\r
+.glyphicon-ok-sign:before {\r
+  content: "\e084";\r
+}\r
+.glyphicon-question-sign:before {\r
+  content: "\e085";\r
+}\r
+.glyphicon-info-sign:before {\r
+  content: "\e086";\r
+}\r
+.glyphicon-screenshot:before {\r
+  content: "\e087";\r
+}\r
+.glyphicon-remove-circle:before {\r
+  content: "\e088";\r
+}\r
+.glyphicon-ok-circle:before {\r
+  content: "\e089";\r
+}\r
+.glyphicon-ban-circle:before {\r
+  content: "\e090";\r
+}\r
+.glyphicon-arrow-left:before {\r
+  content: "\e091";\r
+}\r
+.glyphicon-arrow-right:before {\r
+  content: "\e092";\r
+}\r
+.glyphicon-arrow-up:before {\r
+  content: "\e093";\r
+}\r
+.glyphicon-arrow-down:before {\r
+  content: "\e094";\r
+}\r
+.glyphicon-share-alt:before {\r
+  content: "\e095";\r
+}\r
+.glyphicon-resize-full:before {\r
+  content: "\e096";\r
+}\r
+.glyphicon-resize-small:before {\r
+  content: "\e097";\r
+}\r
+.glyphicon-exclamation-sign:before {\r
+  content: "\e101";\r
+}\r
+.glyphicon-gift:before {\r
+  content: "\e102";\r
+}\r
+.glyphicon-leaf:before {\r
+  content: "\e103";\r
+}\r
+.glyphicon-fire:before {\r
+  content: "\e104";\r
+}\r
+.glyphicon-eye-open:before {\r
+  content: "\e105";\r
+}\r
+.glyphicon-eye-close:before {\r
+  content: "\e106";\r
+}\r
+.glyphicon-warning-sign:before {\r
+  content: "\e107";\r
+}\r
+.glyphicon-plane:before {\r
+  content: "\e108";\r
+}\r
+.glyphicon-calendar:before {\r
+  content: "\e109";\r
+}\r
+.glyphicon-random:before {\r
+  content: "\e110";\r
+}\r
+.glyphicon-comment:before {\r
+  content: "\e111";\r
+}\r
+.glyphicon-magnet:before {\r
+  content: "\e112";\r
+}\r
+.glyphicon-chevron-up:before {\r
+  content: "\e113";\r
+}\r
+.glyphicon-chevron-down:before {\r
+  content: "\e114";\r
+}\r
+.glyphicon-retweet:before {\r
+  content: "\e115";\r
+}\r
+.glyphicon-shopping-cart:before {\r
+  content: "\e116";\r
+}\r
+.glyphicon-folder-close:before {\r
+  content: "\e117";\r
+}\r
+.glyphicon-folder-open:before {\r
+  content: "\e118";\r
+}\r
+.glyphicon-resize-vertical:before {\r
+  content: "\e119";\r
+}\r
+.glyphicon-resize-horizontal:before {\r
+  content: "\e120";\r
+}\r
+.glyphicon-hdd:before {\r
+  content: "\e121";\r
+}\r
+.glyphicon-bullhorn:before {\r
+  content: "\e122";\r
+}\r
+.glyphicon-bell:before {\r
+  content: "\e123";\r
+}\r
+.glyphicon-certificate:before {\r
+  content: "\e124";\r
+}\r
+.glyphicon-thumbs-up:before {\r
+  content: "\e125";\r
+}\r
+.glyphicon-thumbs-down:before {\r
+  content: "\e126";\r
+}\r
+.glyphicon-hand-right:before {\r
+  content: "\e127";\r
+}\r
+.glyphicon-hand-left:before {\r
+  content: "\e128";\r
+}\r
+.glyphicon-hand-up:before {\r
+  content: "\e129";\r
+}\r
+.glyphicon-hand-down:before {\r
+  content: "\e130";\r
+}\r
+.glyphicon-circle-arrow-right:before {\r
+  content: "\e131";\r
+}\r
+.glyphicon-circle-arrow-left:before {\r
+  content: "\e132";\r
+}\r
+.glyphicon-circle-arrow-up:before {\r
+  content: "\e133";\r
+}\r
+.glyphicon-circle-arrow-down:before {\r
+  content: "\e134";\r
+}\r
+.glyphicon-globe:before {\r
+  content: "\e135";\r
+}\r
+.glyphicon-wrench:before {\r
+  content: "\e136";\r
+}\r
+.glyphicon-tasks:before {\r
+  content: "\e137";\r
+}\r
+.glyphicon-filter:before {\r
+  content: "\e138";\r
+}\r
+.glyphicon-briefcase:before {\r
+  content: "\e139";\r
+}\r
+.glyphicon-fullscreen:before {\r
+  content: "\e140";\r
+}\r
+.glyphicon-dashboard:before {\r
+  content: "\e141";\r
+}\r
+.glyphicon-paperclip:before {\r
+  content: "\e142";\r
+}\r
+.glyphicon-heart-empty:before {\r
+  content: "\e143";\r
+}\r
+.glyphicon-link:before {\r
+  content: "\e144";\r
+}\r
+.glyphicon-phone:before {\r
+  content: "\e145";\r
+}\r
+.glyphicon-pushpin:before {\r
+  content: "\e146";\r
+}\r
+.glyphicon-usd:before {\r
+  content: "\e148";\r
+}\r
+.glyphicon-gbp:before {\r
+  content: "\e149";\r
+}\r
+.glyphicon-sort:before {\r
+  content: "\e150";\r
+}\r
+.glyphicon-sort-by-alphabet:before {\r
+  content: "\e151";\r
+}\r
+.glyphicon-sort-by-alphabet-alt:before {\r
+  content: "\e152";\r
+}\r
+.glyphicon-sort-by-order:before {\r
+  content: "\e153";\r
+}\r
+.glyphicon-sort-by-order-alt:before {\r
+  content: "\e154";\r
+}\r
+.glyphicon-sort-by-attributes:before {\r
+  content: "\e155";\r
+}\r
+.glyphicon-sort-by-attributes-alt:before {\r
+  content: "\e156";\r
+}\r
+.glyphicon-unchecked:before {\r
+  content: "\e157";\r
+}\r
+.glyphicon-expand:before {\r
+  content: "\e158";\r
+}\r
+.glyphicon-collapse-down:before {\r
+  content: "\e159";\r
+}\r
+.glyphicon-collapse-up:before {\r
+  content: "\e160";\r
+}\r
+.glyphicon-log-in:before {\r
+  content: "\e161";\r
+}\r
+.glyphicon-flash:before {\r
+  content: "\e162";\r
+}\r
+.glyphicon-log-out:before {\r
+  content: "\e163";\r
+}\r
+.glyphicon-new-window:before {\r
+  content: "\e164";\r
+}\r
+.glyphicon-record:before {\r
+  content: "\e165";\r
+}\r
+.glyphicon-save:before {\r
+  content: "\e166";\r
+}\r
+.glyphicon-open:before {\r
+  content: "\e167";\r
+}\r
+.glyphicon-saved:before {\r
+  content: "\e168";\r
+}\r
+.glyphicon-import:before {\r
+  content: "\e169";\r
+}\r
+.glyphicon-export:before {\r
+  content: "\e170";\r
+}\r
+.glyphicon-send:before {\r
+  content: "\e171";\r
+}\r
+.glyphicon-floppy-disk:before {\r
+  content: "\e172";\r
+}\r
+.glyphicon-floppy-saved:before {\r
+  content: "\e173";\r
+}\r
+.glyphicon-floppy-remove:before {\r
+  content: "\e174";\r
+}\r
+.glyphicon-floppy-save:before {\r
+  content: "\e175";\r
+}\r
+.glyphicon-floppy-open:before {\r
+  content: "\e176";\r
+}\r
+.glyphicon-credit-card:before {\r
+  content: "\e177";\r
+}\r
+.glyphicon-transfer:before {\r
+  content: "\e178";\r
+}\r
+.glyphicon-cutlery:before {\r
+  content: "\e179";\r
+}\r
+.glyphicon-header:before {\r
+  content: "\e180";\r
+}\r
+.glyphicon-compressed:before {\r
+  content: "\e181";\r
+}\r
+.glyphicon-earphone:before {\r
+  content: "\e182";\r
+}\r
+.glyphicon-phone-alt:before {\r
+  content: "\e183";\r
+}\r
+.glyphicon-tower:before {\r
+  content: "\e184";\r
+}\r
+.glyphicon-stats:before {\r
+  content: "\e185";\r
+}\r
+.glyphicon-sd-video:before {\r
+  content: "\e186";\r
+}\r
+.glyphicon-hd-video:before {\r
+  content: "\e187";\r
+}\r
+.glyphicon-subtitles:before {\r
+  content: "\e188";\r
+}\r
+.glyphicon-sound-stereo:before {\r
+  content: "\e189";\r
+}\r
+.glyphicon-sound-dolby:before {\r
+  content: "\e190";\r
+}\r
+.glyphicon-sound-5-1:before {\r
+  content: "\e191";\r
+}\r
+.glyphicon-sound-6-1:before {\r
+  content: "\e192";\r
+}\r
+.glyphicon-sound-7-1:before {\r
+  content: "\e193";\r
+}\r
+.glyphicon-copyright-mark:before {\r
+  content: "\e194";\r
+}\r
+.glyphicon-registration-mark:before {\r
+  content: "\e195";\r
+}\r
+.glyphicon-cloud-download:before {\r
+  content: "\e197";\r
+}\r
+.glyphicon-cloud-upload:before {\r
+  content: "\e198";\r
+}\r
+.glyphicon-tree-conifer:before {\r
+  content: "\e199";\r
+}\r
+.glyphicon-tree-deciduous:before {\r
+  content: "\e200";\r
+}\r
+.caret {\r
+  display: inline-block;\r
+  width: 0;\r
+  height: 0;\r
+  margin-left: 2px;\r
+  vertical-align: middle;\r
+  border-top: 4px solid;\r
+  border-right: 4px solid transparent;\r
+  border-left: 4px solid transparent;\r
+}\r
+.dropdown {\r
+  position: relative;\r
+}\r
+.dropdown-toggle:focus {\r
+  outline: 0;\r
+}\r
+.dropdown-menu {\r
+  position: absolute;\r
+  top: 100%;\r
+  left: 0;\r
+  z-index: 1000;\r
+  display: none;\r
+  float: left;\r
+  min-width: 160px;\r
+  padding: 5px 0;\r
+  margin: 2px 0 0;\r
+  list-style: none;\r
+  font-size: 14px;\r
+  background-color: #ffffff;\r
+  border: 1px solid #cccccc;\r
+  border: 1px solid rgba(0, 0, 0, 0.15);\r
+  border-radius: 0px;\r
+  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\r
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\r
+  background-clip: padding-box;\r
+}\r
+.dropdown-menu.pull-right {\r
+  right: 0;\r
+  left: auto;\r
+}\r
+.dropdown-menu .divider {\r
+  height: 1px;\r
+  margin: 9px 0;\r
+  overflow: hidden;\r
+  background-color: #e5e5e5;\r
+}\r
+.dropdown-menu > li > a {\r
+  display: block;\r
+  padding: 3px 20px;\r
+  clear: both;\r
+  font-weight: normal;\r
+  line-height: 1.428571429;\r
+  color: #333333;\r
+  white-space: nowrap;\r
+}\r
+.dropdown-menu > li > a:hover,\r
+.dropdown-menu > li > a:focus {\r
+  text-decoration: none;\r
+  color: #ffffff;\r
+  background-color: #0d8921;\r
+}\r
+.dropdown-menu > .active > a,\r
+.dropdown-menu > .active > a:hover,\r
+.dropdown-menu > .active > a:focus {\r
+  color: #ffffff;\r
+  text-decoration: none;\r
+  outline: 0;\r
+  background-color: #0d8921;\r
+}\r
+.dropdown-menu > .disabled > a,\r
+.dropdown-menu > .disabled > a:hover,\r
+.dropdown-menu > .disabled > a:focus {\r
+  color: #999999;\r
+}\r
+.dropdown-menu > .disabled > a:hover,\r
+.dropdown-menu > .disabled > a:focus {\r
+  text-decoration: none;\r
+  background-color: transparent;\r
+  background-image: none;\r
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\r
+  cursor: not-allowed;\r
+}\r
+.open > .dropdown-menu {\r
+  display: block;\r
+}\r
+.open > a {\r
+  outline: 0;\r
+}\r
+.dropdown-menu-right {\r
+  left: auto;\r
+  right: 0;\r
+}\r
+.dropdown-menu-left {\r
+  left: 0;\r
+  right: auto;\r
+}\r
+.dropdown-header {\r
+  display: block;\r
+  padding: 3px 20px;\r
+  font-size: 12px;\r
+  line-height: 1.428571429;\r
+  color: #999999;\r
+}\r
+.dropdown-backdrop {\r
+  position: fixed;\r
+  left: 0;\r
+  right: 0;\r
+  bottom: 0;\r
+  top: 0;\r
+  z-index: 990;\r
+}\r
+.pull-right > .dropdown-menu {\r
+  right: 0;\r
+  left: auto;\r
+}\r
+.dropup .caret,\r
+.navbar-fixed-bottom .dropdown .caret {\r
+  border-top: 0;\r
+  border-bottom: 4px solid;\r
+  content: "";\r
+}\r
+.dropup .dropdown-menu,\r
+.navbar-fixed-bottom .dropdown .dropdown-menu {\r
+  top: auto;\r
+  bottom: 100%;\r
+  margin-bottom: 1px;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-right .dropdown-menu {\r
+    left: auto;\r
+    right: 0;\r
+  }\r
+  .navbar-right .dropdown-menu-left {\r
+    left: 0;\r
+    right: auto;\r
+  }\r
+}\r
+.btn-group,\r
+.btn-group-vertical {\r
+  position: relative;\r
+  display: inline-block;\r
+  vertical-align: middle;\r
+}\r
+.btn-group > .btn,\r
+.btn-group-vertical > .btn {\r
+  position: relative;\r
+  float: left;\r
+}\r
+.btn-group > .btn:hover,\r
+.btn-group-vertical > .btn:hover,\r
+.btn-group > .btn:focus,\r
+.btn-group-vertical > .btn:focus,\r
+.btn-group > .btn:active,\r
+.btn-group-vertical > .btn:active,\r
+.btn-group > .btn.active,\r
+.btn-group-vertical > .btn.active {\r
+  z-index: 2;\r
+}\r
+.btn-group > .btn:focus,\r
+.btn-group-vertical > .btn:focus {\r
+  outline: none;\r
+}\r
+.btn-group .btn + .btn,\r
+.btn-group .btn + .btn-group,\r
+.btn-group .btn-group + .btn,\r
+.btn-group .btn-group + .btn-group {\r
+  margin-left: -1px;\r
+}\r
+.btn-toolbar {\r
+  margin-left: -5px;\r
+}\r
+.btn-toolbar .btn-group,\r
+.btn-toolbar .input-group {\r
+  float: left;\r
+}\r
+.btn-toolbar > .btn,\r
+.btn-toolbar > .btn-group,\r
+.btn-toolbar > .input-group {\r
+  margin-left: 5px;\r
+}\r
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\r
+  border-radius: 0;\r
+}\r
+.btn-group > .btn:first-child {\r
+  margin-left: 0;\r
+}\r
+.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\r
+  border-bottom-right-radius: 0;\r
+  border-top-right-radius: 0;\r
+}\r
+.btn-group > .btn:last-child:not(:first-child),\r
+.btn-group > .dropdown-toggle:not(:first-child) {\r
+  border-bottom-left-radius: 0;\r
+  border-top-left-radius: 0;\r
+}\r
+.btn-group > .btn-group {\r
+  float: left;\r
+}\r
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\r
+  border-radius: 0;\r
+}\r
+.btn-group > .btn-group:first-child > .btn:last-child,\r
+.btn-group > .btn-group:first-child > .dropdown-toggle {\r
+  border-bottom-right-radius: 0;\r
+  border-top-right-radius: 0;\r
+}\r
+.btn-group > .btn-group:last-child > .btn:first-child {\r
+  border-bottom-left-radius: 0;\r
+  border-top-left-radius: 0;\r
+}\r
+.btn-group .dropdown-toggle:active,\r
+.btn-group.open .dropdown-toggle {\r
+  outline: 0;\r
+}\r
+.btn-group > .btn + .dropdown-toggle {\r
+  padding-left: 8px;\r
+  padding-right: 8px;\r
+}\r
+.btn-group > .btn-lg + .dropdown-toggle {\r
+  padding-left: 12px;\r
+  padding-right: 12px;\r
+}\r
+.btn-group.open .dropdown-toggle {\r
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\r
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\r
+}\r
+.btn-group.open .dropdown-toggle.btn-link {\r
+  -webkit-box-shadow: none;\r
+  box-shadow: none;\r
+}\r
+.btn .caret {\r
+  margin-left: 0;\r
+}\r
+.btn-lg .caret {\r
+  border-width: 5px 5px 0;\r
+  border-bottom-width: 0;\r
+}\r
+.dropup .btn-lg .caret {\r
+  border-width: 0 5px 5px;\r
+}\r
+.btn-group-vertical > .btn,\r
+.btn-group-vertical > .btn-group,\r
+.btn-group-vertical > .btn-group > .btn {\r
+  display: block;\r
+  float: none;\r
+  width: 100%;\r
+  max-width: 100%;\r
+}\r
+.btn-group-vertical > .btn-group > .btn {\r
+  float: none;\r
+}\r
+.btn-group-vertical > .btn + .btn,\r
+.btn-group-vertical > .btn + .btn-group,\r
+.btn-group-vertical > .btn-group + .btn,\r
+.btn-group-vertical > .btn-group + .btn-group {\r
+  margin-top: -1px;\r
+  margin-left: 0;\r
+}\r
+.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\r
+  border-radius: 0;\r
+}\r
+.btn-group-vertical > .btn:first-child:not(:last-child) {\r
+  border-top-right-radius: 0px;\r
+  border-bottom-right-radius: 0;\r
+  border-bottom-left-radius: 0;\r
+}\r
+.btn-group-vertical > .btn:last-child:not(:first-child) {\r
+  border-bottom-left-radius: 0px;\r
+  border-top-right-radius: 0;\r
+  border-top-left-radius: 0;\r
+}\r
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\r
+  border-radius: 0;\r
+}\r
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\r
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\r
+  border-bottom-right-radius: 0;\r
+  border-bottom-left-radius: 0;\r
+}\r
+.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\r
+  border-top-right-radius: 0;\r
+  border-top-left-radius: 0;\r
+}\r
+.btn-group-justified {\r
+  display: table;\r
+  width: 100%;\r
+  table-layout: fixed;\r
+  border-collapse: separate;\r
+}\r
+.btn-group-justified > .btn,\r
+.btn-group-justified > .btn-group {\r
+  float: none;\r
+  display: table-cell;\r
+  width: 1%;\r
+}\r
+.btn-group-justified > .btn-group .btn {\r
+  width: 100%;\r
+}\r
+[data-toggle="buttons"] > .btn > input[type="radio"],\r
+[data-toggle="buttons"] > .btn > input[type="checkbox"] {\r
+  display: none;\r
+}\r
+.input-group {\r
+  position: relative;\r
+  display: table;\r
+  border-collapse: separate;\r
+}\r
+.input-group[class*="col-"] {\r
+  float: none;\r
+  padding-left: 0;\r
+  padding-right: 0;\r
+}\r
+.input-group .form-control {\r
+  float: left;\r
+  width: 100%;\r
+  margin-bottom: 0;\r
+}\r
+.input-group-lg > .form-control,\r
+.input-group-lg > .input-group-addon,\r
+.input-group-lg > .input-group-btn > .btn {\r
+  height: 45px;\r
+  padding: 10px 16px;\r
+  font-size: 18px;\r
+  line-height: 1.33;\r
+  border-radius: 0px;\r
+}\r
+select.input-group-lg > .form-control,\r
+select.input-group-lg > .input-group-addon,\r
+select.input-group-lg > .input-group-btn > .btn {\r
+  height: 45px;\r
+  line-height: 45px;\r
+}\r
+textarea.input-group-lg > .form-control,\r
+textarea.input-group-lg > .input-group-addon,\r
+textarea.input-group-lg > .input-group-btn > .btn,\r
+select[multiple].input-group-lg > .form-control,\r
+select[multiple].input-group-lg > .input-group-addon,\r
+select[multiple].input-group-lg > .input-group-btn > .btn {\r
+  height: auto;\r
+}\r
+.input-group-sm > .form-control,\r
+.input-group-sm > .input-group-addon,\r
+.input-group-sm > .input-group-btn > .btn {\r
+  height: 30px;\r
+  padding: 5px 10px;\r
+  font-size: 12px;\r
+  line-height: 1.5;\r
+  border-radius: 0px;\r
+}\r
+select.input-group-sm > .form-control,\r
+select.input-group-sm > .input-group-addon,\r
+select.input-group-sm > .input-group-btn > .btn {\r
+  height: 30px;\r
+  line-height: 30px;\r
+}\r
+textarea.input-group-sm > .form-control,\r
+textarea.input-group-sm > .input-group-addon,\r
+textarea.input-group-sm > .input-group-btn > .btn,\r
+select[multiple].input-group-sm > .form-control,\r
+select[multiple].input-group-sm > .input-group-addon,\r
+select[multiple].input-group-sm > .input-group-btn > .btn {\r
+  height: auto;\r
+}\r
+.input-group-addon,\r
+.input-group-btn,\r
+.input-group .form-control {\r
+  display: table-cell;\r
+}\r
+.input-group-addon:not(:first-child):not(:last-child),\r
+.input-group-btn:not(:first-child):not(:last-child),\r
+.input-group .form-control:not(:first-child):not(:last-child) {\r
+  border-radius: 0;\r
+}\r
+.input-group-addon,\r
+.input-group-btn {\r
+  width: 1%;\r
+  white-space: nowrap;\r
+  vertical-align: middle;\r
+}\r
+.input-group-addon {\r
+  padding: 6px 12px;\r
+  font-size: 14px;\r
+  font-weight: normal;\r
+  line-height: 1;\r
+  color: #555555;\r
+  text-align: center;\r
+  background-color: #eeeeee;\r
+  border: 1px solid #cccccc;\r
+  border-radius: 0px;\r
+}\r
+.input-group-addon.input-sm {\r
+  padding: 5px 10px;\r
+  font-size: 12px;\r
+  border-radius: 0px;\r
+}\r
+.input-group-addon.input-lg {\r
+  padding: 10px 16px;\r
+  font-size: 18px;\r
+  border-radius: 0px;\r
+}\r
+.input-group-addon input[type="radio"],\r
+.input-group-addon input[type="checkbox"] {\r
+  margin-top: 0;\r
+}\r
+.input-group .form-control:first-child,\r
+.input-group-addon:first-child,\r
+.input-group-btn:first-child > .btn,\r
+.input-group-btn:first-child > .btn-group > .btn,\r
+.input-group-btn:first-child > .dropdown-toggle,\r
+.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\r
+.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\r
+  border-bottom-right-radius: 0;\r
+  border-top-right-radius: 0;\r
+}\r
+.input-group-addon:first-child {\r
+  border-right: 0;\r
+}\r
+.input-group .form-control:last-child,\r
+.input-group-addon:last-child,\r
+.input-group-btn:last-child > .btn,\r
+.input-group-btn:last-child > .btn-group > .btn,\r
+.input-group-btn:last-child > .dropdown-toggle,\r
+.input-group-btn:first-child > .btn:not(:first-child),\r
+.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\r
+  border-bottom-left-radius: 0;\r
+  border-top-left-radius: 0;\r
+}\r
+.input-group-addon:last-child {\r
+  border-left: 0;\r
+}\r
+.input-group-btn {\r
+  position: relative;\r
+  font-size: 0;\r
+  white-space: nowrap;\r
+}\r
+.input-group-btn > .btn {\r
+  position: relative;\r
+}\r
+.input-group-btn > .btn + .btn {\r
+  margin-left: -1px;\r
+}\r
+.input-group-btn > .btn:hover,\r
+.input-group-btn > .btn:focus,\r
+.input-group-btn > .btn:active {\r
+  z-index: 2;\r
+}\r
+.input-group-btn:first-child > .btn,\r
+.input-group-btn:first-child > .btn-group {\r
+  margin-right: -1px;\r
+}\r
+.input-group-btn:last-child > .btn,\r
+.input-group-btn:last-child > .btn-group {\r
+  margin-left: -1px;\r
+}\r
+.nav {\r
+  margin-bottom: 0;\r
+  padding-left: 0;\r
+  list-style: none;\r
+}\r
+.nav > li {\r
+  position: relative;\r
+  display: block;\r
+}\r
+.nav > li > a {\r
+  position: relative;\r
+  display: block;\r
+  padding: 10px 15px;\r
+}\r
+.nav > li > a:hover,\r
+.nav > li > a:focus {\r
+  text-decoration: none;\r
+  background-color: #dddddd;\r
+}\r
+.nav > li.disabled > a {\r
+  color: #999999;\r
+}\r
+.nav > li.disabled > a:hover,\r
+.nav > li.disabled > a:focus {\r
+  color: #999999;\r
+  text-decoration: none;\r
+  background-color: transparent;\r
+  cursor: not-allowed;\r
+}\r
+.nav .open > a,\r
+.nav .open > a:hover,\r
+.nav .open > a:focus {\r
+  background-color: #dddddd;\r
+  border-color: #0d8921;\r
+}\r
+.nav .nav-divider {\r
+  height: 1px;\r
+  margin: 9px 0;\r
+  overflow: hidden;\r
+  background-color: #e5e5e5;\r
+}\r
+.nav > li > a > img {\r
+  max-width: none;\r
+}\r
+.nav-tabs {\r
+  border-bottom: 1px solid #dddddd;\r
+}\r
+.nav-tabs > li {\r
+  float: left;\r
+  margin-bottom: -1px;\r
+}\r
+.nav-tabs > li > a {\r
+  margin-right: 2px;\r
+  line-height: 1.428571429;\r
+  border: 1px solid transparent;\r
+  border-radius: 0px 0px 0 0;\r
+}\r
+.nav-tabs > li > a:hover {\r
+  border-color: #eeeeee #eeeeee #dddddd;\r
+}\r
+.nav-tabs > li.active > a,\r
+.nav-tabs > li.active > a:hover,\r
+.nav-tabs > li.active > a:focus {\r
+  color: #555555;\r
+  background-color: #f2f2f2;\r
+  border: 1px solid #dddddd;\r
+  border-bottom-color: transparent;\r
+  cursor: default;\r
+}\r
+.nav-tabs.nav-justified {\r
+  width: 100%;\r
+  border-bottom: 0;\r
+}\r
+.nav-tabs.nav-justified > li {\r
+  float: none;\r
+}\r
+.nav-tabs.nav-justified > li > a {\r
+  text-align: center;\r
+  margin-bottom: 5px;\r
+}\r
+.nav-tabs.nav-justified > .dropdown .dropdown-menu {\r
+  top: auto;\r
+  left: auto;\r
+}\r
+@media (min-width: 768px) {\r
+  .nav-tabs.nav-justified > li {\r
+    display: table-cell;\r
+    width: 1%;\r
+  }\r
+  .nav-tabs.nav-justified > li > a {\r
+    margin-bottom: 0;\r
+  }\r
+}\r
+.nav-tabs.nav-justified > li > a {\r
+  margin-right: 0;\r
+  border-radius: 0px;\r
+}\r
+.nav-tabs.nav-justified > .active > a,\r
+.nav-tabs.nav-justified > .active > a:hover,\r
+.nav-tabs.nav-justified > .active > a:focus {\r
+  border: 1px solid #dddddd;\r
+}\r
+@media (min-width: 768px) {\r
+  .nav-tabs.nav-justified > li > a {\r
+    border-bottom: 1px solid #dddddd;\r
+    border-radius: 0px 0px 0 0;\r
+  }\r
+  .nav-tabs.nav-justified > .active > a,\r
+  .nav-tabs.nav-justified > .active > a:hover,\r
+  .nav-tabs.nav-justified > .active > a:focus {\r
+    border-bottom-color: #f2f2f2;\r
+  }\r
+}\r
+.nav-pills > li {\r
+  float: left;\r
+}\r
+.nav-pills > li > a {\r
+  border-radius: 0px;\r
+}\r
+.nav-pills > li + li {\r
+  margin-left: 2px;\r
+}\r
+.nav-pills > li.active > a,\r
+.nav-pills > li.active > a:hover,\r
+.nav-pills > li.active > a:focus {\r
+  color: #ffffff;\r
+  background-color: #0d8921;\r
+}\r
+.nav-stacked > li {\r
+  float: none;\r
+}\r
+.nav-stacked > li + li {\r
+  margin-top: 2px;\r
+  margin-left: 0;\r
+}\r
+.nav-justified {\r
+  width: 100%;\r
+}\r
+.nav-justified > li {\r
+  float: none;\r
+}\r
+.nav-justified > li > a {\r
+  text-align: center;\r
+  margin-bottom: 5px;\r
+}\r
+.nav-justified > .dropdown .dropdown-menu {\r
+  top: auto;\r
+  left: auto;\r
+}\r
+@media (min-width: 768px) {\r
+  .nav-justified > li {\r
+    display: table-cell;\r
+    width: 1%;\r
+  }\r
+  .nav-justified > li > a {\r
+    margin-bottom: 0;\r
+  }\r
+}\r
+.nav-tabs-justified {\r
+  border-bottom: 0;\r
+}\r
+.nav-tabs-justified > li > a {\r
+  margin-right: 0;\r
+  border-radius: 0px;\r
+}\r
+.nav-tabs-justified > .active > a,\r
+.nav-tabs-justified > .active > a:hover,\r
+.nav-tabs-justified > .active > a:focus {\r
+  border: 1px solid #dddddd;\r
+}\r
+@media (min-width: 768px) {\r
+  .nav-tabs-justified > li > a {\r
+    border-bottom: 1px solid #dddddd;\r
+    border-radius: 0px 0px 0 0;\r
+  }\r
+  .nav-tabs-justified > .active > a,\r
+  .nav-tabs-justified > .active > a:hover,\r
+  .nav-tabs-justified > .active > a:focus {\r
+    border-bottom-color: #f2f2f2;\r
+  }\r
+}\r
+.tab-content > .tab-pane {\r
+  display: none;\r
+}\r
+.tab-content > .active {\r
+  display: block;\r
+}\r
+.nav-tabs .dropdown-menu {\r
+  margin-top: -1px;\r
+  border-top-right-radius: 0;\r
+  border-top-left-radius: 0;\r
+}\r
+.navbar {\r
+  position: relative;\r
+  min-height: 50px;\r
+  margin-bottom: 20px;\r
+  border: 1px solid transparent;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar {\r
+    border-radius: 0px;\r
+  }\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-header {\r
+    float: left;\r
+  }\r
+}\r
+.navbar-collapse {\r
+  max-height: 340px;\r
+  overflow-x: visible;\r
+  padding-right: 15px;\r
+  padding-left: 15px;\r
+  border-top: 1px solid transparent;\r
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\r
+  -webkit-overflow-scrolling: touch;\r
+}\r
+.navbar-collapse.in {\r
+  overflow-y: auto;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-collapse {\r
+    width: auto;\r
+    border-top: 0;\r
+    box-shadow: none;\r
+  }\r
+  .navbar-collapse.collapse {\r
+    display: block !important;\r
+    height: auto !important;\r
+    padding-bottom: 0;\r
+    overflow: visible !important;\r
+  }\r
+  .navbar-collapse.in {\r
+    overflow-y: visible;\r
+  }\r
+  .navbar-fixed-top .navbar-collapse,\r
+  .navbar-static-top .navbar-collapse,\r
+  .navbar-fixed-bottom .navbar-collapse {\r
+    padding-left: 0;\r
+    padding-right: 0;\r
+  }\r
+}\r
+.container > .navbar-header,\r
+.container-fluid > .navbar-header,\r
+.container > .navbar-collapse,\r
+.container-fluid > .navbar-collapse {\r
+  margin-right: -15px;\r
+  margin-left: -15px;\r
+}\r
+@media (min-width: 768px) {\r
+  .container > .navbar-header,\r
+  .container-fluid > .navbar-header,\r
+  .container > .navbar-collapse,\r
+  .container-fluid > .navbar-collapse {\r
+    margin-right: 0;\r
+    margin-left: 0;\r
+  }\r
+}\r
+.navbar-static-top {\r
+  z-index: 1000;\r
+  border-width: 0 0 1px;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-static-top {\r
+    border-radius: 0;\r
+  }\r
+}\r
+.navbar-fixed-top,\r
+.navbar-fixed-bottom {\r
+  position: fixed;\r
+  right: 0;\r
+  left: 0;\r
+  z-index: 1030;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-fixed-top,\r
+  .navbar-fixed-bottom {\r
+    border-radius: 0;\r
+  }\r
+}\r
+.navbar-fixed-top {\r
+  top: 0;\r
+  border-width: 0 0 1px;\r
+}\r
+.navbar-fixed-bottom {\r
+  bottom: 0;\r
+  margin-bottom: 0;\r
+  border-width: 1px 0 0;\r
+}\r
+.navbar-brand {\r
+  float: left;\r
+  padding: 15px 15px;\r
+  font-size: 18px;\r
+  line-height: 20px;\r
+  height: 50px;\r
+}\r
+.navbar-brand:hover,\r
+.navbar-brand:focus {\r
+  text-decoration: none;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar > .container .navbar-brand,\r
+  .navbar > .container-fluid .navbar-brand {\r
+    margin-left: -15px;\r
+  }\r
+}\r
+.navbar-toggle {\r
+  position: relative;\r
+  float: right;\r
+  margin-right: 15px;\r
+  padding: 9px 10px;\r
+  margin-top: 8px;\r
+  margin-bottom: 8px;\r
+  background-color: transparent;\r
+  background-image: none;\r
+  border: 1px solid transparent;\r
+  border-radius: 0px;\r
+}\r
+.navbar-toggle:focus {\r
+  outline: none;\r
+}\r
+.navbar-toggle .icon-bar {\r
+  display: block;\r
+  width: 22px;\r
+  height: 2px;\r
+  border-radius: 1px;\r
+}\r
+.navbar-toggle .icon-bar + .icon-bar {\r
+  margin-top: 4px;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-toggle {\r
+    display: none;\r
+  }\r
+}\r
+.navbar-nav {\r
+  margin: 7.5px -15px;\r
+}\r
+.navbar-nav > li > a {\r
+  padding-top: 10px;\r
+  padding-bottom: 10px;\r
+  line-height: 20px;\r
+}\r
+@media (max-width: 767px) {\r
+  .navbar-nav .open .dropdown-menu {\r
+    position: static;\r
+    float: none;\r
+    width: auto;\r
+    margin-top: 0;\r
+    background-color: transparent;\r
+    border: 0;\r
+    box-shadow: none;\r
+  }\r
+  .navbar-nav .open .dropdown-menu > li > a,\r
+  .navbar-nav .open .dropdown-menu .dropdown-header {\r
+    padding: 5px 15px 5px 25px;\r
+  }\r
+  .navbar-nav .open .dropdown-menu > li > a {\r
+    line-height: 20px;\r
+  }\r
+  .navbar-nav .open .dropdown-menu > li > a:hover,\r
+  .navbar-nav .open .dropdown-menu > li > a:focus {\r
+    background-image: none;\r
+  }\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-nav {\r
+    float: left;\r
+    margin: 0;\r
+  }\r
+  .navbar-nav > li {\r
+    float: left;\r
+  }\r
+  .navbar-nav > li > a {\r
+    padding-top: 15px;\r
+    padding-bottom: 15px;\r
+  }\r
+  .navbar-nav.navbar-right:last-child {\r
+    margin-right: -15px;\r
+  }\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-left {\r
+    float: left !important;\r
+  }\r
+  .navbar-right {\r
+    float: right !important;\r
+  }\r
+}\r
+.navbar-form {\r
+  margin-left: -15px;\r
+  margin-right: -15px;\r
+  padding: 10px 15px;\r
+  border-top: 1px solid transparent;\r
+  border-bottom: 1px solid transparent;\r
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\r
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\r
+  margin-top: 8px;\r
+  margin-bottom: 8px;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-form .form-group {\r
+    display: inline-block;\r
+    margin-bottom: 0;\r
+    vertical-align: middle;\r
+  }\r
+  .navbar-form .form-control {\r
+    display: inline-block;\r
+    width: auto;\r
+    vertical-align: middle;\r
+  }\r
+  .navbar-form .input-group > .form-control {\r
+    width: 100%;\r
+  }\r
+  .navbar-form .control-label {\r
+    margin-bottom: 0;\r
+    vertical-align: middle;\r
+  }\r
+  .navbar-form .radio,\r
+  .navbar-form .checkbox {\r
+    display: inline-block;\r
+    margin-top: 0;\r
+    margin-bottom: 0;\r
+    padding-left: 0;\r
+    vertical-align: middle;\r
+  }\r
+  .navbar-form .radio input[type="radio"],\r
+  .navbar-form .checkbox input[type="checkbox"] {\r
+    float: none;\r
+    margin-left: 0;\r
+  }\r
+  .navbar-form .has-feedback .form-control-feedback {\r
+    top: 0;\r
+  }\r
+}\r
+@media (max-width: 767px) {\r
+  .navbar-form .form-group {\r
+    margin-bottom: 5px;\r
+  }\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-form {\r
+    width: auto;\r
+    border: 0;\r
+    margin-left: 0;\r
+    margin-right: 0;\r
+    padding-top: 0;\r
+    padding-bottom: 0;\r
+    -webkit-box-shadow: none;\r
+    box-shadow: none;\r
+  }\r
+  .navbar-form.navbar-right:last-child {\r
+    margin-right: -15px;\r
+  }\r
+}\r
+.navbar-nav > li > .dropdown-menu {\r
+  margin-top: 0;\r
+  border-top-right-radius: 0;\r
+  border-top-left-radius: 0;\r
+}\r
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\r
+  border-bottom-right-radius: 0;\r
+  border-bottom-left-radius: 0;\r
+}\r
+.navbar-btn {\r
+  margin-top: 8px;\r
+  margin-bottom: 8px;\r
+}\r
+.navbar-btn.btn-sm {\r
+  margin-top: 10px;\r
+  margin-bottom: 10px;\r
+}\r
+.navbar-btn.btn-xs {\r
+  margin-top: 14px;\r
+  margin-bottom: 14px;\r
+}\r
+.navbar-text {\r
+  margin-top: 15px;\r
+  margin-bottom: 15px;\r
+}\r
+@media (min-width: 768px) {\r
+  .navbar-text {\r
+    float: left;\r
+    margin-left: 15px;\r
+    margin-right: 15px;\r
+  }\r
+  .navbar-text.navbar-right:last-child {\r
+    margin-right: 0;\r
+  }\r
+}\r
+.navbar-default {\r
+  background-color: #0d8921;\r
+  border-color: none;\r
+}\r
+.navbar-default .navbar-brand {\r
+  color: #ffffff;\r
+}\r
+.navbar-default .navbar-brand:hover,\r
+.navbar-default .navbar-brand:focus {\r
+  color: #e6e6e6;\r
+  background-color: transparent;\r
+}\r
+.navbar-default .navbar-text {\r
+  color: #ffffff;\r
+}\r
+.navbar-default .navbar-nav > li > a {\r
+  color: #ffffff;\r
+}\r
+.navbar-default .navbar-nav > li > a:hover,\r
+.navbar-default .navbar-nav > li > a:focus {\r
+  color: #dddddd;\r
+  background-color: transparent;\r
+}\r
+.navbar-default .navbar-nav > .active > a,\r
+.navbar-default .navbar-nav > .active > a:hover,\r
+.navbar-default .navbar-nav > .active > a:focus {\r
+  color: #ffffff;\r
+  background-color: #0a6b1a;\r
+}\r
+.navbar-default .navbar-nav > .disabled > a,\r
+.navbar-default .navbar-nav > .disabled > a:hover,\r
+.navbar-default .navbar-nav > .disabled > a:focus {\r
+  color: #cccccc;\r
+  background-color: transparent;\r
+}\r
+.navbar-default .navbar-toggle {\r
+  border-color: #dddddd;\r
+}\r
+.navbar-default .navbar-toggle:hover,\r
+.navbar-default .navbar-toggle:focus {\r
+  background-color: #dddddd;\r
+}\r
+.navbar-default .navbar-toggle .icon-bar {\r
+  background-color: #ffffff;\r
+}\r
+.navbar-default .navbar-collapse,\r
+.navbar-default .navbar-form {\r
+  border-color: none;\r
+}\r
+.navbar-default .navbar-nav > .open > a,\r
+.navbar-default .navbar-nav > .open > a:hover,\r
+.navbar-default .navbar-nav > .open > a:focus {\r
+  background-color: #0a6b1a;\r
+  color: #ffffff;\r
+}\r
+@media (max-width: 767px) {\r
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a {\r
+    color: #ffffff;\r
+  }\r
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\r
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\r
+    color: #dddddd;\r
+    background-color: transparent;\r
+  }\r
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\r
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\r
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\r
+    color: #ffffff;\r
+    background-color: #0a6b1a;\r
+  }\r
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\r
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\r
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\r
+    color: #cccccc;\r
+    background-color: transparent;\r
+  }\r
+}\r
+.navbar-default .navbar-link {\r
+  color: #ffffff;\r
+}\r
+.navbar-default .navbar-link:hover {\r
+  color: #dddddd;\r
+}\r
+.navbar-inverse {\r
+  background-color: #222222;\r
+  border-color: #080808;\r
+}\r
+.navbar-inverse .navbar-brand {\r
+  color: #999999;\r
+}\r
+.navbar-inverse .navbar-brand:hover,\r
+.navbar-inverse .navbar-brand:focus {\r
+  color: #ffffff;\r
+  background-color: transparent;\r
+}\r
+.navbar-inverse .navbar-text {\r
+  color: #999999;\r
+}\r
+.navbar-inverse .navbar-nav > li > a {\r
+  color: #999999;\r
+}\r
+.navbar-inverse .navbar-nav > li > a:hover,\r
+.navbar-inverse .navbar-nav > li > a:focus {\r
+  color: #ffffff;\r
+  background-color: transparent;\r
+}\r
+.navbar-inverse .navbar-nav > .active > a,\r
+.navbar-inverse .navbar-nav > .active > a:hover,\r
+.navbar-inverse .navbar-nav > .active > a:focus {\r
+  color: #ffffff;\r
+  background-color: #080808;\r
+}\r
+.navbar-inverse .navbar-nav > .disabled > a,\r
+.navbar-inverse .navbar-nav > .disabled > a:hover,\r
+.navbar-inverse .navbar-nav > .disabled > a:focus {\r
+  color: #444444;\r
+  background-color: transparent;\r
+}\r
+.navbar-inverse .navbar-toggle {\r
+  border-color: #333333;\r
+}\r
+.navbar-inverse .navbar-toggle:hover,\r
+.navbar-inverse .navbar-toggle:focus {\r
+  background-color: #333333;\r
+}\r
+.navbar-inverse .navbar-toggle .icon-bar {\r
+  background-color: #ffffff;\r
+}\r
+.navbar-inverse .navbar-collapse,\r
+.navbar-inverse .navbar-form {\r
+  border-color: #101010;\r
+}\r
+.navbar-inverse .navbar-nav > .open > a,\r
+.navbar-inverse .navbar-nav > .open > a:hover,\r
+.navbar-inverse .navbar-nav > .open > a:focus {\r
+  background-color: #080808;\r
+  color: #ffffff;\r
+}\r
+@media (max-width: 767px) {\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\r
+    border-color: #080808;\r
+  }\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\r
+    background-color: #080808;\r
+  }\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\r
+    color: #999999;\r
+  }\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\r
+    color: #ffffff;\r
+    background-color: transparent;\r
+  }\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\r
+    color: #ffffff;\r
+    background-color: #080808;\r
+  }\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\r
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\r
+    color: #444444;\r
+    background-color: transparent;\r
+  }\r
+}\r
+.navbar-inverse .navbar-link {\r
+  color: #999999;\r
+}\r
+.navbar-inverse .navbar-link:hover {\r
+  color: #ffffff;\r
+}\r
+.breadcrumb {\r
+  padding: 8px 15px;\r
+  margin-bottom: 20px;\r
+  list-style: none;\r
+  background-color: #f5f5f5;\r
+  border-radius: 0px;\r
+}\r
+.breadcrumb > li {\r
+  display: inline-block;\r
+}\r
+.breadcrumb > li + li:before {\r
+  content: "\00a0";\r
+  padding: 0 5px;\r
+  color: #cccccc;\r
+}\r
+.breadcrumb > .active {\r
+  color: #999999;\r
+}\r
+.pagination {\r
+  display: inline-block;\r
+  padding-left: 0;\r
+  margin: 20px 0;\r
+  border-radius: 0px;\r
+}\r
+.pagination > li {\r
+  display: inline;\r
+}\r
+.pagination > li > a,\r
+.pagination > li > span {\r
+  position: relative;\r
+  float: left;\r
+  padding: 6px 12px;\r
+  line-height: 1.428571429;\r
+  text-decoration: none;\r
+  color: #0d8921;\r
+  background-color: #ffffff;\r
+  border: 1px solid #dddddd;\r
+  margin-left: -1px;\r
+}\r
+.pagination > li:first-child > a,\r
+.pagination > li:first-child > span {\r
+  margin-left: 0;\r
+  border-bottom-left-radius: 0px;\r
+  border-top-left-radius: 0px;\r
+}\r
+.pagination > li:last-child > a,\r
+.pagination > li:last-child > span {\r
+  border-bottom-right-radius: 0px;\r
+  border-top-right-radius: 0px;\r
+}\r
+.pagination > li > a:hover,\r
+.pagination > li > span:hover,\r
+.pagination > li > a:focus,\r
+.pagination > li > span:focus {\r
+  color: #064310;\r
+  background-color: #eeeeee;\r
+  border-color: #dddddd;\r
+}\r
+.pagination > .active > a,\r
+.pagination > .active > span,\r
+.pagination > .active > a:hover,\r
+.pagination > .active > span:hover,\r
+.pagination > .active > a:focus,\r
+.pagination > .active > span:focus {\r
+  z-index: 2;\r
+  color: #ffffff;\r
+  background-color: #0d8921;\r
+  border-color: #0d8921;\r
+  cursor: default;\r
+}\r
+.pagination > .disabled > span,\r
+.pagination > .disabled > span:hover,\r
+.pagination > .disabled > span:focus,\r
+.pagination > .disabled > a,\r
+.pagination > .disabled > a:hover,\r
+.pagination > .disabled > a:focus {\r
+  color: #999999;\r
+  background-color: #ffffff;\r
+  border-color: #dddddd;\r
+  cursor: not-allowed;\r
+}\r
+.pagination-lg > li > a,\r
+.pagination-lg > li > span {\r
+  padding: 10px 16px;\r
+  font-size: 18px;\r
+}\r
+.pagination-lg > li:first-child > a,\r
+.pagination-lg > li:first-child > span {\r
+  border-bottom-left-radius: 0px;\r
+  border-top-left-radius: 0px;\r
+}\r
+.pagination-lg > li:last-child > a,\r
+.pagination-lg > li:last-child > span {\r
+  border-bottom-right-radius: 0px;\r
+  border-top-right-radius: 0px;\r
+}\r
+.pagination-sm > li > a,\r
+.pagination-sm > li > span {\r
+  padding: 5px 10px;\r
+  font-size: 12px;\r
+}\r
+.pagination-sm > li:first-child > a,\r
+.pagination-sm > li:first-child > span {\r
+  border-bottom-left-radius: 0px;\r
+  border-top-left-radius: 0px;\r
+}\r
+.pagination-sm > li:last-child > a,\r
+.pagination-sm > li:last-child > span {\r
+  border-bottom-right-radius: 0px;\r
+  border-top-right-radius: 0px;\r
+}\r
+.pager {\r
+  padding-left: 0;\r
+  margin: 20px 0;\r
+  list-style: none;\r
+  text-align: center;\r
+}\r
+.pager li {\r
+  display: inline;\r
+}\r
+.pager li > a,\r
+.pager li > span {\r
+  display: inline-block;\r
+  padding: 5px 14px;\r
+  background-color: #ffffff;\r
+  border: 1px solid #dddddd;\r
+  border-radius: 15px;\r
+}\r
+.pager li > a:hover,\r
+.pager li > a:focus {\r
+  text-decoration: none;\r
+  background-color: #eeeeee;\r
+}\r
+.pager .next > a,\r
+.pager .next > span {\r
+  float: right;\r
+}\r
+.pager .previous > a,\r
+.pager .previous > span {\r
+  float: left;\r
+}\r
+.pager .disabled > a,\r
+.pager .disabled > a:hover,\r
+.pager .disabled > a:focus,\r
+.pager .disabled > span {\r
+  color: #999999;\r
+  background-color: #ffffff;\r
+  cursor: not-allowed;\r
+}\r
+.label {\r
+  display: inline;\r
+  padding: .2em .6em .3em;\r
+  font-size: 75%;\r
+  font-weight: bold;\r
+  line-height: 1;\r
+  color: #ffffff;\r
+  text-align: center;\r
+  white-space: nowrap;\r
+  vertical-align: baseline;\r
+  border-radius: .25em;\r
+}\r
+.label[href]:hover,\r
+.label[href]:focus {\r
+  color: #ffffff;\r
+  text-decoration: none;\r
+  cursor: pointer;\r
+}\r
+.label:empty {\r
+  display: none;\r
+}\r
+.btn .label {\r
+  position: relative;\r
+  top: -1px;\r
+}\r
+.label-default {\r
+  background-color: #999999;\r
+}\r
+.label-default[href]:hover,\r
+.label-default[href]:focus {\r
+  background-color: #808080;\r
+}\r
+.label-primary {\r
+  background-color: #0d8921;\r
+}\r
+.label-primary[href]:hover,\r
+.label-primary[href]:focus {\r
+  background-color: #095a16;\r
+}\r
+.label-success {\r
+  background-color: #5cb85c;\r
+}\r
+.label-success[href]:hover,\r
+.label-success[href]:focus {\r
+  background-color: #449d44;\r
+}\r
+.label-info {\r
+  background-color: #5bc0de;\r
+}\r
+.label-info[href]:hover,\r
+.label-info[href]:focus {\r
+  background-color: #31b0d5;\r
+}\r
+.label-warning {\r
+  background-color: #f0ad4e;\r
+}\r
+.label-warning[href]:hover,\r
+.label-warning[href]:focus {\r
+  background-color: #ec971f;\r
+}\r
+.label-danger {\r
+  background-color: #d9534f;\r
+}\r
+.label-danger[href]:hover,\r
+.label-danger[href]:focus {\r
+  background-color: #c9302c;\r
+}\r
+.badge {\r
+  display: inline-block;\r
+  min-width: 10px;\r
+  padding: 3px 7px;\r
+  font-size: 12px;\r
+  font-weight: bold;\r
+  color: : #fff;\r
+  line-height: 1;\r
+  vertical-align: baseline;\r
+  white-space: nowrap;\r
+  text-align: center;\r
+  background-color: #999999;\r
+  border-radius: 10px;\r
+}\r
+.badge:empty {\r
+  display: none;\r
+}\r
+.btn .badge {\r
+  position: relative;\r
+  top: -1px;\r
+}\r
+.btn-xs .badge {\r
+  top: 0;\r
+  padding: 1px 5px;\r
+}\r
+a.badge:hover,\r
+a.badge:focus {\r
+  color: #ffffff;\r
+  text-decoration: none;\r
+  cursor: pointer;\r
+}\r
+a.list-group-item.active > .badge,\r
+.nav-pills > .active > a > .badge {\r
+  color: #0d8921;\r
+  background-color: #ffffff;\r
+}\r
+.nav-pills > li > a > .badge {\r
+  margin-left: 3px;\r
+}\r
+.jumbotron {\r
+  padding: 30px;\r
+  margin-bottom: 30px;\r
+  color: inherit;\r
+  background-color: #ffffff;\r
+}\r
+.jumbotron h1,\r
+.jumbotron .h1 {\r
+  color: inherit;\r
+}\r
+.jumbotron p {\r
+  margin-bottom: 15px;\r
+  font-size: 21px;\r
+  font-weight: 200;\r
+}\r
+.container .jumbotron {\r
+  border-radius: 0px;\r
+}\r
+.jumbotron .container {\r
+  max-width: 100%;\r
+}\r
+@media screen and (min-width: 768px) {\r
+  .jumbotron {\r
+    padding-top: 48px;\r
+    padding-bottom: 48px;\r
+  }\r
+  .container .jumbotron {\r
+    padding-left: 60px;\r
+    padding-right: 60px;\r
+  }\r
+  .jumbotron h1,\r
+  .jumbotron .h1 {\r
+    font-size: 63px;\r
+  }\r
+}\r
+.thumbnail {\r
+  display: block;\r
+  padding: 4px;\r
+  margin-bottom: 20px;\r
+  line-height: 1.428571429;\r
+  background-color: #f2f2f2;\r
+  border: 1px solid #dddddd;\r
+  border-radius: 0px;\r
+  -webkit-transition: all 0.2s ease-in-out;\r
+  transition: all 0.2s ease-in-out;\r
+}\r
+.thumbnail > img,\r
+.thumbnail a > img {\r
+  margin-left: auto;\r
+  margin-right: auto;\r
+}\r
+a.thumbnail:hover,\r
+a.thumbnail:focus,\r
+a.thumbnail.active {\r
+  border-color: #0d8921;\r
+}\r
+.thumbnail .caption {\r
+  padding: 9px;\r
+  color: #333333;\r
+}\r
+.alert {\r
+  padding: 15px;\r
+  margin-bottom: 20px;\r
+  border: 1px solid transparent;\r
+  border-radius: 0px;\r
+}\r
+.alert h4 {\r
+  margin-top: 0;\r
+  color: inherit;\r
+}\r
+.alert .alert-link {\r
+  font-weight: bold;\r
+}\r
+.alert > p,\r
+.alert > ul {\r
+  margin-bottom: 0;\r
+}\r
+.alert > p + p {\r
+  margin-top: 5px;\r
+}\r
+.alert-dismissable {\r
+  padding-right: 35px;\r
+}\r
+.alert-dismissable .close {\r
+  position: relative;\r
+  top: -2px;\r
+  right: -21px;\r
+  color: inherit;\r
+}\r
+.alert-success {\r
+  background-color: #dff0d8;\r
+  border-color: #d6e9c6;\r
+  color: #5cb85c;\r
+}\r
+.alert-success hr {\r
+  border-top-color: #c9e2b3;\r
+}\r
+.alert-success .alert-link {\r
+  color: #449d44;\r
+}\r
+.alert-info {\r
+  background-color: #d9edf7;\r
+  border-color: #bce8f1;\r
+  color: #5bc0de;\r
+}\r
+.alert-info hr {\r
+  border-top-color: #a6e1ec;\r
+}\r
+.alert-info .alert-link {\r
+  color: #31b0d5;\r
+}\r
+.alert-warning {\r
+  background-color: #fcf8e3;\r
+  border-color: #fbeed5;\r
+  color: #f0ad4e;\r
+}\r
+.alert-warning hr {\r
+  border-top-color: #f8e5be;\r
+}\r
+.alert-warning .alert-link {\r
+  color: #ec971f;\r
+}\r
+.alert-danger {\r
+  background-color: #f2dede;\r
+  border-color: #eed3d7;\r
+  color: #d9534f;\r
+}\r
+.alert-danger hr {\r
+  border-top-color: #e6c1c7;\r
+}\r
+.alert-danger .alert-link {\r
+  color: #c9302c;\r
+}\r
+@-webkit-keyframes progress-bar-stripes {\r
+  from {\r
+    background-position: 40px 0;\r
+  }\r
+  to {\r
+    background-position: 0 0;\r
+  }\r
+}\r
+@keyframes progress-bar-stripes {\r
+  from {\r
+    background-position: 40px 0;\r
+  }\r
+  to {\r
+    background-position: 0 0;\r
+  }\r
+}\r
+.progress {\r
+  overflow: hidden;\r
+  height: 20px;\r
+  margin-bottom: 20px;\r
+  background-color: #f5f5f5;\r
+  border-radius: 0px;\r
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\r
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\r
+}\r
+.progress-bar {\r
+  float: left;\r
+  width: 0%;\r
+  height: 100%;\r
+  font-size: 12px;\r
+  line-height: 20px;\r
+  color: #ffffff;\r
+  text-align: center;\r
+  background-color: #0d8921;\r
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\r
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\r
+  -webkit-transition: width 0.6s ease;\r
+  transition: width 0.6s ease;\r
+}\r
+.progress-striped .progress-bar {\r
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+  background-size: 40px 40px;\r
+}\r
+.progress.active .progress-bar {\r
+  -webkit-animation: progress-bar-stripes 2s linear infinite;\r
+  animation: progress-bar-stripes 2s linear infinite;\r
+}\r
+.progress-bar-success {\r
+  background-color: #5cb85c;\r
+}\r
+.progress-striped .progress-bar-success {\r
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+}\r
+.progress-bar-info {\r
+  background-color: #5bc0de;\r
+}\r
+.progress-striped .progress-bar-info {\r
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+}\r
+.progress-bar-warning {\r
+  background-color: #f0ad4e;\r
+}\r
+.progress-striped .progress-bar-warning {\r
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+}\r
+.progress-bar-danger {\r
+  background-color: #d9534f;\r
+}\r
+.progress-striped .progress-bar-danger {\r
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\r
+}\r
+.media,\r
+.media-body {\r
+  overflow: hidden;\r
+  zoom: 1;\r
+}\r
+.media,\r
+.media .media {\r
+  margin-top: 15px;\r
+}\r
+.media:first-child {\r
+  margin-top: 0;\r
+}\r
+.media-object {\r
+  display: block;\r
+}\r
+.media-heading {\r
+  margin: 0 0 5px;\r
+}\r
+.media > .pull-left {\r
+  margin-right: 10px;\r
+}\r
+.media > .pull-right {\r
+  margin-left: 10px;\r
+}\r
+.media-list {\r
+  padding-left: 0;\r
+  list-style: none;\r
+}\r
+.list-group {\r
+  margin-bottom: 20px;\r
+  padding-left: 0;\r
+}\r
+.list-group-item {\r
+  position: relative;\r
+  display: block;\r
+  padding: 10px 15px;\r
+  margin-bottom: -1px;\r
+  background-color: #ffffff;\r
+  border: 1px solid #dddddd;\r
+}\r
+.list-group-item:first-child {\r
+  border-top-right-radius: 0px;\r
+  border-top-left-radius: 0px;\r
+}\r
+.list-group-item:last-child {\r
+  margin-bottom: 0;\r
+  border-bottom-right-radius: 0px;\r
+  border-bottom-left-radius: 0px;\r
+}\r
+.list-group-item > .badge {\r
+  float: right;\r
+}\r
+.list-group-item > .badge + .badge {\r
+  margin-right: 5px;\r
+}\r
+a.list-group-item {\r
+  color: #555555;\r
+}\r
+a.list-group-item .list-group-item-heading {\r
+  color: #333333;\r
+}\r
+a.list-group-item:hover,\r
+a.list-group-item:focus {\r
+  text-decoration: none;\r
+  background-color: #f5f5f5;\r
+}\r
+a.list-group-item.active,\r
+a.list-group-item.active:hover,\r
+a.list-group-item.active:focus {\r
+  z-index: 2;\r
+  color: #ffffff;\r
+  background-color: #0d8921;\r
+  border-color: #0d8921;\r
+}\r
+a.list-group-item.active .list-group-item-heading,\r
+a.list-group-item.active:hover .list-group-item-heading,\r
+a.list-group-item.active:focus .list-group-item-heading {\r
+  color: inherit;\r
+}\r
+a.list-group-item.active .list-group-item-text,\r
+a.list-group-item.active:hover .list-group-item-text,\r
+a.list-group-item.active:focus .list-group-item-text {\r
+  color: #cccccc;\r
+}\r
+.list-group-item-success {\r
+  color: #5cb85c;\r
+  background-color: #dff0d8;\r
+}\r
+a.list-group-item-success {\r
+  color: #5cb85c;\r
+}\r
+a.list-group-item-success .list-group-item-heading {\r
+  color: inherit;\r
+}\r
+a.list-group-item-success:hover,\r
+a.list-group-item-success:focus {\r
+  color: #5cb85c;\r
+  background-color: #d0e9c6;\r
+}\r
+a.list-group-item-success.active,\r
+a.list-group-item-success.active:hover,\r
+a.list-group-item-success.active:focus {\r
+  color: #fff;\r
+  background-color: #5cb85c;\r
+  border-color: #5cb85c;\r
+}\r
+.list-group-item-info {\r
+  color: #5bc0de;\r
+  background-color: #d9edf7;\r
+}\r
+a.list-group-item-info {\r
+  color: #5bc0de;\r
+}\r
+a.list-group-item-info .list-group-item-heading {\r
+  color: inherit;\r
+}\r
+a.list-group-item-info:hover,\r
+a.list-group-item-info:focus {\r
+  color: #5bc0de;\r
+  background-color: #c4e3f3;\r
+}\r
+a.list-group-item-info.active,\r
+a.list-group-item-info.active:hover,\r
+a.list-group-item-info.active:focus {\r
+  color: #fff;\r
+  background-color: #5bc0de;\r
+  border-color: #5bc0de;\r
+}\r
+.list-group-item-warning {\r
+  color: #f0ad4e;\r
+  background-color: #fcf8e3;\r
+}\r
+a.list-group-item-warning {\r
+  color: #f0ad4e;\r
+}\r
+a.list-group-item-warning .list-group-item-heading {\r
+  color: inherit;\r
+}\r
+a.list-group-item-warning:hover,\r
+a.list-group-item-warning:focus {\r
+  color: #f0ad4e;\r
+  background-color: #faf2cc;\r
+}\r
+a.list-group-item-warning.active,\r
+a.list-group-item-warning.active:hover,\r
+a.list-group-item-warning.active:focus {\r
+  color: #fff;\r
+  background-color: #f0ad4e;\r
+  border-color: #f0ad4e;\r
+}\r
+.list-group-item-danger {\r
+  color: #d9534f;\r
+  background-color: #f2dede;\r
+}\r
+a.list-group-item-danger {\r
+  color: #d9534f;\r
+}\r
+a.list-group-item-danger .list-group-item-heading {\r
+  color: inherit;\r
+}\r
+a.list-group-item-danger:hover,\r
+a.list-group-item-danger:focus {\r
+  color: #d9534f;\r
+  background-color: #ebcccc;\r
+}\r
+a.list-group-item-danger.active,\r
+a.list-group-item-danger.active:hover,\r
+a.list-group-item-danger.active:focus {\r
+  color: #fff;\r
+  background-color: #d9534f;\r
+  border-color: #d9534f;\r
+}\r
+.list-group-item-heading {\r
+  margin-top: 0;\r
+  margin-bottom: 5px;\r
+}\r
+.list-group-item-text {\r
+  margin-bottom: 0;\r
+  line-height: 1.3;\r
+}\r
+.panel {\r
+  margin-bottom: 20px;\r
+  background-color: #ffffff;\r
+  border: 1px solid transparent;\r
+  border-radius: 0px;\r
+  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\r
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\r
+}\r
+.panel-body {\r
+  padding: 15px;\r
+}\r
+.panel-heading {\r
+  padding: 10px 15px;\r
+  border-bottom: 1px solid transparent;\r
+  border-top-right-radius: -1px;\r
+  border-top-left-radius: -1px;\r
+}\r
+.panel-heading > .dropdown .dropdown-toggle {\r
+  color: inherit;\r
+}\r
+.panel-title {\r
+  margin-top: 0;\r
+  margin-bottom: 0;\r
+  font-size: 16px;\r
+  color: inherit;\r
+}\r
+.panel-title > a {\r
+  color: inherit;\r
+}\r
+.panel-footer {\r
+  padding: 10px 15px;\r
+  background-color: #f5f5f5;\r
+  border-top: 1px solid #dddddd;\r
+  border-bottom-right-radius: -1px;\r
+  border-bottom-left-radius: -1px;\r
+}\r
+.panel > .list-group {\r
+  margin-bottom: 0;\r
+}\r
+.panel > .list-group .list-group-item {\r
+  border-width: 1px 0;\r
+  border-radius: 0;\r
+}\r
+.panel > .list-group .list-group-item:first-child {\r
+  border-top: 0;\r
+}\r
+.panel > .list-group .list-group-item:last-child {\r
+  border-bottom: 0;\r
+}\r
+.panel > .list-group:first-child .list-group-item:first-child {\r
+  border-top-right-radius: -1px;\r
+  border-top-left-radius: -1px;\r
+}\r
+.panel > .list-group:last-child .list-group-item:last-child {\r
+  border-bottom-right-radius: -1px;\r
+  border-bottom-left-radius: -1px;\r
+}\r
+.panel-heading + .list-group .list-group-item:first-child {\r
+  border-top-width: 0;\r
+}\r
+.panel > .table,\r
+.panel > .table-responsive > .table {\r
+  margin-bottom: 0;\r
+}\r
+.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\r
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\r
+.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\r
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\r
+.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\r
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\r
+.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\r
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\r
+  border-top-left-radius: -1px;\r
+}\r
+.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\r
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\r
+.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\r
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\r
+.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\r
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\r
+.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\r
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\r
+  border-top-right-radius: -1px;\r
+}\r
+.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\r
+.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\r
+.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\r
+.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\r
+  border-bottom-left-radius: -1px;\r
+}\r
+.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\r
+.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\r
+.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\r
+.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\r
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\r
+  border-bottom-right-radius: -1px;\r
+}\r
+.panel > .panel-body + .table,\r
+.panel > .panel-body + .table-responsive {\r
+  border-top: 1px solid #dddddd;\r
+}\r
+.panel > .table > tbody:first-child > tr:first-child th,\r
+.panel > .table > tbody:first-child > tr:first-child td {\r
+  border-top: 0;\r
+}\r
+.panel > .table-bordered,\r
+.panel > .table-responsive > .table-bordered {\r
+  border: 0;\r
+}\r
+.panel > .table-bordered > thead > tr > th:first-child,\r
+.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\r
+.panel > .table-bordered > tbody > tr > th:first-child,\r
+.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\r
+.panel > .table-bordered > tfoot > tr > th:first-child,\r
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\r
+.panel > .table-bordered > thead > tr > td:first-child,\r
+.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\r
+.panel > .table-bordered > tbody > tr > td:first-child,\r
+.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\r
+.panel > .table-bordered > tfoot > tr > td:first-child,\r
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\r
+  border-left: 0;\r
+}\r
+.panel > .table-bordered > thead > tr > th:last-child,\r
+.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\r
+.panel > .table-bordered > tbody > tr > th:last-child,\r
+.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\r
+.panel > .table-bordered > tfoot > tr > th:last-child,\r
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\r
+.panel > .table-bordered > thead > tr > td:last-child,\r
+.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\r
+.panel > .table-bordered > tbody > tr > td:last-child,\r
+.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\r
+.panel > .table-bordered > tfoot > tr > td:last-child,\r
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\r
+  border-right: 0;\r
+}\r
+.panel > .table-bordered > thead > tr:first-child > td,\r
+.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\r
+.panel > .table-bordered > tbody > tr:first-child > td,\r
+.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\r
+.panel > .table-bordered > thead > tr:first-child > th,\r
+.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\r
+.panel > .table-bordered > tbody > tr:first-child > th,\r
+.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\r
+  border-bottom: 0;\r
+}\r
+.panel > .table-bordered > tbody > tr:last-child > td,\r
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\r
+.panel > .table-bordered > tfoot > tr:last-child > td,\r
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\r
+.panel > .table-bordered > tbody > tr:last-child > th,\r
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\r
+.panel > .table-bordered > tfoot > tr:last-child > th,\r
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\r
+  border-bottom: 0;\r
+}\r
+.panel > .table-responsive {\r
+  border: 0;\r
+  margin-bottom: 0;\r
+}\r
+.panel-group {\r
+  margin-bottom: 20px;\r
+}\r
+.panel-group .panel {\r
+  margin-bottom: 0;\r
+  border-radius: 0px;\r
+  overflow: hidden;\r
+}\r
+.panel-group .panel + .panel {\r
+  margin-top: 5px;\r
+}\r
+.panel-group .panel-heading {\r
+  border-bottom: 0;\r
+}\r
+.panel-group .panel-heading + .panel-collapse .panel-body {\r
+  border-top: 1px solid #dddddd;\r
+}\r
+.panel-group .panel-footer {\r
+  border-top: 0;\r
+}\r
+.panel-group .panel-footer + .panel-collapse .panel-body {\r
+  border-bottom: 1px solid #dddddd;\r
+}\r
+.panel-default {\r
+  border-color: #dddddd;\r
+}\r
+.panel-default > .panel-heading {\r
+  color: #333333;\r
+  background-color: #f5f5f5;\r
+  border-color: #dddddd;\r
+}\r
+.panel-default > .panel-heading + .panel-collapse .panel-body {\r
+  border-top-color: #dddddd;\r
+}\r
+.panel-default > .panel-footer + .panel-collapse .panel-body {\r
+  border-bottom-color: #dddddd;\r
+}\r
+.panel-primary {\r
+  border-color: #0d8921;\r
+}\r
+.panel-primary > .panel-heading {\r
+  color: #ffffff;\r
+  background-color: #0d8921;\r
+  border-color: #0d8921;\r
+}\r
+.panel-primary > .panel-heading + .panel-collapse .panel-body {\r
+  border-top-color: #0d8921;\r
+}\r
+.panel-primary > .panel-footer + .panel-collapse .panel-body {\r
+  border-bottom-color: #0d8921;\r
+}\r
+.panel-success {\r
+  border-color: #d6e9c6;\r
+}\r
+.panel-success > .panel-heading {\r
+  color: #5cb85c;\r
+  background-color: #dff0d8;\r
+  border-color: #d6e9c6;\r
+}\r
+.panel-success > .panel-heading + .panel-collapse .panel-body {\r
+  border-top-color: #d6e9c6;\r
+}\r
+.panel-success > .panel-footer + .panel-collapse .panel-body {\r
+  border-bottom-color: #d6e9c6;\r
+}\r
+.panel-info {\r
+  border-color: #bce8f1;\r
+}\r
+.panel-info > .panel-heading {\r
+  color: #5bc0de;\r
+  background-color: #d9edf7;\r
+  border-color: #bce8f1;\r
+}\r
+.panel-info > .panel-heading + .panel-collapse .panel-body {\r
+  border-top-color: #bce8f1;\r
+}\r
+.panel-info > .panel-footer + .panel-collapse .panel-body {\r
+  border-bottom-color: #bce8f1;\r
+}\r
+.panel-warning {\r
+  border-color: #fbeed5;\r
+}\r
+.panel-warning > .panel-heading {\r
+  color: #f0ad4e;\r
+  background-color: #fcf8e3;\r
+  border-color: #fbeed5;\r
+}\r
+.panel-warning > .panel-heading + .panel-collapse .panel-body {\r
+  border-top-color: #fbeed5;\r
+}\r
+.panel-warning > .panel-footer + .panel-collapse .panel-body {\r
+  border-bottom-color: #fbeed5;\r
+}\r
+.panel-danger {\r
+  border-color: #eed3d7;\r
+}\r
+.panel-danger > .panel-heading {\r
+  color: #d9534f;\r
+  background-color: #f2dede;\r
+  border-color: #eed3d7;\r
+}\r
+.panel-danger > .panel-heading + .panel-collapse .panel-body {\r
+  border-top-color: #eed3d7;\r
+}\r
+.panel-danger > .panel-footer + .panel-collapse .panel-body {\r
+  border-bottom-color: #eed3d7;\r
+}\r
+.well {\r
+  min-height: 20px;\r
+  padding: 19px;\r
+  margin-bottom: 20px;\r
+  background-color: #f5f5f5;\r
+  border: 1px solid #e3e3e3;\r
+  border-radius: 0px;\r
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\r
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\r
+}\r
+.well blockquote {\r
+  border-color: #ddd;\r
+  border-color: rgba(0, 0, 0, 0.15);\r
+}\r
+.well-lg {\r
+  padding: 24px;\r
+  border-radius: 0px;\r
+}\r
+.well-sm {\r
+  padding: 9px;\r
+  border-radius: 0px;\r
+}\r
+.close {\r
+  float: right;\r
+  font-size: 21px;\r
+  font-weight: bold;\r
+  line-height: 1;\r
+  color: #000000;\r
+  text-shadow: 0 1px 0 #ffffff;\r
+  opacity: 0.2;\r
+  filter: alpha(opacity=20);\r
+}\r
+.close:hover,\r
+.close:focus {\r
+  color: #000000;\r
+  text-decoration: none;\r
+  cursor: pointer;\r
+  opacity: 0.5;\r
+  filter: alpha(opacity=50);\r
+}\r
+button.close {\r
+  padding: 0;\r
+  cursor: pointer;\r
+  background: transparent;\r
+  border: 0;\r
+  -webkit-appearance: none;\r
+}\r
+.modal-open {\r
+  overflow: hidden;\r
+}\r
+.modal {\r
+  display: none;\r
+  overflow: auto;\r
+  overflow-y: scroll;\r
+  position: fixed;\r
+  top: 0;\r
+  right: 0;\r
+  bottom: 0;\r
+  left: 0;\r
+  z-index: 1050;\r
+  -webkit-overflow-scrolling: touch;\r
+  outline: 0;\r
+}\r
+.modal.fade .modal-dialog {\r
+  -webkit-transform: translate(0, -25%);\r
+  -ms-transform: translate(0, -25%);\r
+  transform: translate(0, -25%);\r
+  -webkit-transition: -webkit-transform 0.3s ease-out;\r
+  -moz-transition: -moz-transform 0.3s ease-out;\r
+  -o-transition: -o-transform 0.3s ease-out;\r
+  transition: transform 0.3s ease-out;\r
+}\r
+.modal.in .modal-dialog {\r
+  -webkit-transform: translate(0, 0);\r
+  -ms-transform: translate(0, 0);\r
+  transform: translate(0, 0);\r
+}\r
+.modal-dialog {\r
+  position: relative;\r
+  width: auto;\r
+  margin: 10px;\r
+}\r
+.modal-content {\r
+  position: relative;\r
+  background-color: #ffffff;\r
+  border: 1px solid #999999;\r
+  border: 1px solid rgba(0, 0, 0, 0.2);\r
+  border-radius: 0px;\r
+  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\r
+  box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\r
+  background-clip: padding-box;\r
+  outline: none;\r
+}\r
+.modal-backdrop {\r
+  position: fixed;\r
+  top: 0;\r
+  right: 0;\r
+  bottom: 0;\r
+  left: 0;\r
+  z-index: 1040;\r
+  background-color: #000000;\r
+}\r
+.modal-backdrop.fade {\r
+  opacity: 0;\r
+  filter: alpha(opacity=0);\r
+}\r
+.modal-backdrop.in {\r
+  opacity: 0.5;\r
+  filter: alpha(opacity=50);\r
+}\r
+.modal-header {\r
+  padding: 15px;\r
+  border-bottom: 1px solid #e5e5e5;\r
+  min-height: 16.428571429px;\r
+}\r
+.modal-header .close {\r
+  margin-top: -2px;\r
+}\r
+.modal-title {\r
+  margin: 0;\r
+  line-height: 1.428571429;\r
+}\r
+.modal-body {\r
+  position: relative;\r
+  padding: 20px;\r
+}\r
+.modal-footer {\r
+  margin-top: 15px;\r
+  padding: 19px 20px 20px;\r
+  text-align: right;\r
+  border-top: 1px solid #e5e5e5;\r
+}\r
+.modal-footer .btn + .btn {\r
+  margin-left: 5px;\r
+  margin-bottom: 0;\r
+}\r
+.modal-footer .btn-group .btn + .btn {\r
+  margin-left: -1px;\r
+}\r
+.modal-footer .btn-block + .btn-block {\r
+  margin-left: 0;\r
+}\r
+@media (min-width: 768px) {\r
+  .modal-dialog {\r
+    width: 600px;\r
+    margin: 30px auto;\r
+  }\r
+  .modal-content {\r
+    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\r
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\r
+  }\r
+  .modal-sm {\r
+    width: 300px;\r
+  }\r
+}\r
+@media (min-width: 992px) {\r
+  .modal-lg {\r
+    width: 900px;\r
+  }\r
+}\r
+.tooltip {\r
+  position: absolute;\r
+  z-index: 1030;\r
+  display: block;\r
+  visibility: visible;\r
+  font-size: 12px;\r
+  line-height: 1.4;\r
+  opacity: 0;\r
+  filter: alpha(opacity=0);\r
+}\r
+.tooltip.in {\r
+  opacity: 0.9;\r
+  filter: alpha(opacity=90);\r
+}\r
+.tooltip.top {\r
+  margin-top: -3px;\r
+  padding: 5px 0;\r
+}\r
+.tooltip.right {\r
+  margin-left: 3px;\r
+  padding: 0 5px;\r
+}\r
+.tooltip.bottom {\r
+  margin-top: 3px;\r
+  padding: 5px 0;\r
+}\r
+.tooltip.left {\r
+  margin-left: -3px;\r
+  padding: 0 5px;\r
+}\r
+.tooltip-inner {\r
+  max-width: 200px;\r
+  padding: 3px 8px;\r
+  color: #ffffff;\r
+  text-align: center;\r
+  text-decoration: none;\r
+  background-color: #000000;\r
+  border-radius: 0px;\r
+}\r
+.tooltip-arrow {\r
+  position: absolute;\r
+  width: 0;\r
+  height: 0;\r
+  border-color: transparent;\r
+  border-style: solid;\r
+}\r
+.tooltip.top .tooltip-arrow {\r
+  bottom: 0;\r
+  left: 50%;\r
+  margin-left: -5px;\r
+  border-width: 5px 5px 0;\r
+  border-top-color: #000000;\r
+}\r
+.tooltip.top-left .tooltip-arrow {\r
+  bottom: 0;\r
+  left: 5px;\r
+  border-width: 5px 5px 0;\r
+  border-top-color: #000000;\r
+}\r
+.tooltip.top-right .tooltip-arrow {\r
+  bottom: 0;\r
+  right: 5px;\r
+  border-width: 5px 5px 0;\r
+  border-top-color: #000000;\r
+}\r
+.tooltip.right .tooltip-arrow {\r
+  top: 50%;\r
+  left: 0;\r
+  margin-top: -5px;\r
+  border-width: 5px 5px 5px 0;\r
+  border-right-color: #000000;\r
+}\r
+.tooltip.left .tooltip-arrow {\r
+  top: 50%;\r
+  right: 0;\r
+  margin-top: -5px;\r
+  border-width: 5px 0 5px 5px;\r
+  border-left-color: #000000;\r
+}\r
+.tooltip.bottom .tooltip-arrow {\r
+  top: 0;\r
+  left: 50%;\r
+  margin-left: -5px;\r
+  border-width: 0 5px 5px;\r
+  border-bottom-color: #000000;\r
+}\r
+.tooltip.bottom-left .tooltip-arrow {\r
+  top: 0;\r
+  left: 5px;\r
+  border-width: 0 5px 5px;\r
+  border-bottom-color: #000000;\r
+}\r
+.tooltip.bottom-right .tooltip-arrow {\r
+  top: 0;\r
+  right: 5px;\r
+  border-width: 0 5px 5px;\r
+  border-bottom-color: #000000;\r
+}\r
+.popover {\r
+  position: absolute;\r
+  top: 0;\r
+  left: 0;\r
+  z-index: 1010;\r
+  display: none;\r
+  max-width: 276px;\r
+  padding: 1px;\r
+  text-align: left;\r
+  background-color: #ffffff;\r
+  background-clip: padding-box;\r
+  border: 1px solid #cccccc;\r
+  border: 1px solid rgba(0, 0, 0, 0.2);\r
+  border-radius: 0px;\r
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\r
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\r
+  white-space: normal;\r
+}\r
+.popover.top {\r
+  margin-top: -10px;\r
+}\r
+.popover.right {\r
+  margin-left: 10px;\r
+}\r
+.popover.bottom {\r
+  margin-top: 10px;\r
+}\r
+.popover.left {\r
+  margin-left: -10px;\r
+}\r
+.popover-title {\r
+  margin: 0;\r
+  padding: 8px 14px;\r
+  font-size: 14px;\r
+  font-weight: normal;\r
+  line-height: 18px;\r
+  background-color: #f7f7f7;\r
+  border-bottom: 1px solid #ebebeb;\r
+  border-radius: 5px 5px 0 0;\r
+}\r
+.popover-content {\r
+  padding: 9px 14px;\r
+}\r
+.popover > .arrow,\r
+.popover > .arrow:after {\r
+  position: absolute;\r
+  display: block;\r
+  width: 0;\r
+  height: 0;\r
+  border-color: transparent;\r
+  border-style: solid;\r
+}\r
+.popover > .arrow {\r
+  border-width: 11px;\r
+}\r
+.popover > .arrow:after {\r
+  border-width: 10px;\r
+  content: "";\r
+}\r
+.popover.top > .arrow {\r
+  left: 50%;\r
+  margin-left: -11px;\r
+  border-bottom-width: 0;\r
+  border-top-color: #999999;\r
+  border-top-color: rgba(0, 0, 0, 0.25);\r
+  bottom: -11px;\r
+}\r
+.popover.top > .arrow:after {\r
+  content: " ";\r
+  bottom: 1px;\r
+  margin-left: -10px;\r
+  border-bottom-width: 0;\r
+  border-top-color: #ffffff;\r
+}\r
+.popover.right > .arrow {\r
+  top: 50%;\r
+  left: -11px;\r
+  margin-top: -11px;\r
+  border-left-width: 0;\r
+  border-right-color: #999999;\r
+  border-right-color: rgba(0, 0, 0, 0.25);\r
+}\r
+.popover.right > .arrow:after {\r
+  content: " ";\r
+  left: 1px;\r
+  bottom: -10px;\r
+  border-left-width: 0;\r
+  border-right-color: #ffffff;\r
+}\r
+.popover.bottom > .arrow {\r
+  left: 50%;\r
+  margin-left: -11px;\r
+  border-top-width: 0;\r
+  border-bottom-color: #999999;\r
+  border-bottom-color: rgba(0, 0, 0, 0.25);\r
+  top: -11px;\r
+}\r
+.popover.bottom > .arrow:after {\r
+  content: " ";\r
+  top: 1px;\r
+  margin-left: -10px;\r
+  border-top-width: 0;\r
+  border-bottom-color: #ffffff;\r
+}\r
+.popover.left > .arrow {\r
+  top: 50%;\r
+  right: -11px;\r
+  margin-top: -11px;\r
+  border-right-width: 0;\r
+  border-left-color: #999999;\r
+  border-left-color: rgba(0, 0, 0, 0.25);\r
+}\r
+.popover.left > .arrow:after {\r
+  content: " ";\r
+  right: 1px;\r
+  border-right-width: 0;\r
+  border-left-color: #ffffff;\r
+  bottom: -10px;\r
+}\r
+.carousel {\r
+  position: relative;\r
+}\r
+.carousel-inner {\r
+  position: relative;\r
+  overflow: hidden;\r
+  width: 100%;\r
+}\r
+.carousel-inner > .item {\r
+  display: none;\r
+  position: relative;\r
+  -webkit-transition: 0.6s ease-in-out left;\r
+  transition: 0.6s ease-in-out left;\r
+}\r
+.carousel-inner > .item > img,\r
+.carousel-inner > .item > a > img {\r
+  line-height: 1;\r
+}\r
+.carousel-inner > .active,\r
+.carousel-inner > .next,\r
+.carousel-inner > .prev {\r
+  display: block;\r
+}\r
+.carousel-inner > .active {\r
+  left: 0;\r
+}\r
+.carousel-inner > .next,\r
+.carousel-inner > .prev {\r
+  position: absolute;\r
+  top: 0;\r
+  width: 100%;\r
+}\r
+.carousel-inner > .next {\r
+  left: 100%;\r
+}\r
+.carousel-inner > .prev {\r
+  left: -100%;\r
+}\r
+.carousel-inner > .next.left,\r
+.carousel-inner > .prev.right {\r
+  left: 0;\r
+}\r
+.carousel-inner > .active.left {\r
+  left: -100%;\r
+}\r
+.carousel-inner > .active.right {\r
+  left: 100%;\r
+}\r
+.carousel-control {\r
+  position: absolute;\r
+  top: 0;\r
+  left: 0;\r
+  bottom: 0;\r
+  width: 15%;\r
+  opacity: 0.5;\r
+  filter: alpha(opacity=50);\r
+  font-size: 20px;\r
+  color: #ffffff;\r
+  text-align: center;\r
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\r
+}\r
+.carousel-control.left {\r
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0%), color-stop(rgba(0, 0, 0, 0.0001) 100%));\r
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\r
+  background-repeat: repeat-x;\r
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\r
+}\r
+.carousel-control.right {\r
+  left: auto;\r
+  right: 0;\r
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0%), color-stop(rgba(0, 0, 0, 0.5) 100%));\r
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\r
+  background-repeat: repeat-x;\r
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\r
+}\r
+.carousel-control:hover,\r
+.carousel-control:focus {\r
+  outline: none;\r
+  color: #ffffff;\r
+  text-decoration: none;\r
+  opacity: 0.9;\r
+  filter: alpha(opacity=90);\r
+}\r
+.carousel-control .icon-prev,\r
+.carousel-control .icon-next,\r
+.carousel-control .glyphicon-chevron-left,\r
+.carousel-control .glyphicon-chevron-right {\r
+  position: absolute;\r
+  top: 50%;\r
+  z-index: 5;\r
+  display: inline-block;\r
+}\r
+.carousel-control .icon-prev,\r
+.carousel-control .glyphicon-chevron-left {\r
+  left: 50%;\r
+}\r
+.carousel-control .icon-next,\r
+.carousel-control .glyphicon-chevron-right {\r
+  right: 50%;\r
+}\r
+.carousel-control .icon-prev,\r
+.carousel-control .icon-next {\r
+  width: 20px;\r
+  height: 20px;\r
+  margin-top: -10px;\r
+  margin-left: -10px;\r
+  font-family: serif;\r
+}\r
+.carousel-control .icon-prev:before {\r
+  content: '\2039';\r
+}\r
+.carousel-control .icon-next:before {\r
+  content: '\203a';\r
+}\r
+.carousel-indicators {\r
+  position: absolute;\r
+  bottom: 10px;\r
+  left: 50%;\r
+  z-index: 15;\r
+  width: 60%;\r
+  margin-left: -30%;\r
+  padding-left: 0;\r
+  list-style: none;\r
+  text-align: center;\r
+}\r
+.carousel-indicators li {\r
+  display: inline-block;\r
+  width: 10px;\r
+  height: 10px;\r
+  margin: 1px;\r
+  text-indent: -999px;\r
+  border: 1px solid #ffffff;\r
+  border-radius: 10px;\r
+  cursor: pointer;\r
+  background-color: #000 \9;\r
+  background-color: rgba(0, 0, 0, 0);\r
+}\r
+.carousel-indicators .active {\r
+  margin: 0;\r
+  width: 12px;\r
+  height: 12px;\r
+  background-color: #ffffff;\r
+}\r
+.carousel-caption {\r
+  position: absolute;\r
+  left: 15%;\r
+  right: 15%;\r
+  bottom: 20px;\r
+  z-index: 10;\r
+  padding-top: 20px;\r
+  padding-bottom: 20px;\r
+  color: #ffffff;\r
+  text-align: center;\r
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\r
+}\r
+.carousel-caption .btn {\r
+  text-shadow: none;\r
+}\r
+@media screen and (min-width: 768px) {\r
+  .carousel-control .glyphicons-chevron-left,\r
+  .carousel-control .glyphicons-chevron-right,\r
+  .carousel-control .icon-prev,\r
+  .carousel-control .icon-next {\r
+    width: 30px;\r
+    height: 30px;\r
+    margin-top: -15px;\r
+    margin-left: -15px;\r
+    font-size: 30px;\r
+  }\r
+  .carousel-caption {\r
+    left: 20%;\r
+    right: 20%;\r
+    padding-bottom: 30px;\r
+  }\r
+  .carousel-indicators {\r
+    bottom: 20px;\r
+  }\r
+}\r
+.clearfix:before,\r
+.clearfix:after,\r
+.container:before,\r
+.container:after,\r
+.container-fluid:before,\r
+.container-fluid:after,\r
+.row:before,\r
+.row:after,\r
+.form-horizontal .form-group:before,\r
+.form-horizontal .form-group:after,\r
+.btn-toolbar:before,\r
+.btn-toolbar:after,\r
+.btn-group-vertical > .btn-group:before,\r
+.btn-group-vertical > .btn-group:after,\r
+.nav:before,\r
+.nav:after,\r
+.navbar:before,\r
+.navbar:after,\r
+.navbar-header:before,\r
+.navbar-header:after,\r
+.navbar-collapse:before,\r
+.navbar-collapse:after,\r
+.pager:before,\r
+.pager:after,\r
+.panel-body:before,\r
+.panel-body:after,\r
+.modal-footer:before,\r
+.modal-footer:after {\r
+  content: " ";\r
+  display: table;\r
+}\r
+.clearfix:after,\r
+.container:after,\r
+.container-fluid:after,\r
+.row:after,\r
+.form-horizontal .form-group:after,\r
+.btn-toolbar:after,\r
+.btn-group-vertical > .btn-group:after,\r
+.nav:after,\r
+.navbar:after,\r
+.navbar-header:after,\r
+.navbar-collapse:after,\r
+.pager:after,\r
+.panel-body:after,\r
+.modal-footer:after {\r
+  clear: both;\r
+}\r
+.center-block {\r
+  display: block;\r
+  margin-left: auto;\r
+  margin-right: auto;\r
+}\r
+.pull-right {\r
+  float: right !important;\r
+}\r
+.pull-left {\r
+  float: left !important;\r
+}\r
+.hide {\r
+  display: none !important;\r
+}\r
+.show {\r
+  display: block !important;\r
+}\r
+.invisible {\r
+  visibility: hidden;\r
+}\r
+.text-hide {\r
+  font: 0/0 a;\r
+  color: transparent;\r
+  text-shadow: none;\r
+  background-color: transparent;\r
+  border: 0;\r
+}\r
+.hidden {\r
+  display: none !important;\r
+  visibility: hidden !important;\r
+}\r
+.affix {\r
+  position: fixed;\r
+}\r
+@-ms-viewport {\r
+  width: device-width;\r
+}\r
+.visible-xs,\r
+.visible-sm,\r
+.visible-md,\r
+.visible-lg {\r
+  display: none !important;\r
+}\r
+@media (max-width: 767px) {\r
+  .visible-xs {\r
+    display: block !important;\r
+  }\r
+  table.visible-xs {\r
+    display: table;\r
+  }\r
+  tr.visible-xs {\r
+    display: table-row !important;\r
+  }\r
+  th.visible-xs,\r
+  td.visible-xs {\r
+    display: table-cell !important;\r
+  }\r
+}\r
+@media (min-width: 768px) and (max-width: 991px) {\r
+  .visible-sm {\r
+    display: block !important;\r
+  }\r
+  table.visible-sm {\r
+    display: table;\r
+  }\r
+  tr.visible-sm {\r
+    display: table-row !important;\r
+  }\r
+  th.visible-sm,\r
+  td.visible-sm {\r
+    display: table-cell !important;\r
+  }\r
+}\r
+@media (min-width: 992px) and (max-width: 1199px) {\r
+  .visible-md {\r
+    display: block !important;\r
+  }\r
+  table.visible-md {\r
+    display: table;\r
+  }\r
+  tr.visible-md {\r
+    display: table-row !important;\r
+  }\r
+  th.visible-md,\r
+  td.visible-md {\r
+    display: table-cell !important;\r
+  }\r
+}\r
+@media (min-width: 1200px) {\r
+  .visible-lg {\r
+    display: block !important;\r
+  }\r
+  table.visible-lg {\r
+    display: table;\r
+  }\r
+  tr.visible-lg {\r
+    display: table-row !important;\r
+  }\r
+  th.visible-lg,\r
+  td.visible-lg {\r
+    display: table-cell !important;\r
+  }\r
+}\r
+@media (max-width: 767px) {\r
+  .hidden-xs {\r
+    display: none !important;\r
+  }\r
+}\r
+@media (min-width: 768px) and (max-width: 991px) {\r
+  .hidden-sm {\r
+    display: none !important;\r
+  }\r
+}\r
+@media (min-width: 992px) and (max-width: 1199px) {\r
+  .hidden-md {\r
+    display: none !important;\r
+  }\r
+}\r
+@media (min-width: 1200px) {\r
+  .hidden-lg {\r
+    display: none !important;\r
+  }\r
+}\r
+.visible-print {\r
+  display: none !important;\r
+}\r
+@media print {\r
+  .visible-print {\r
+    display: block !important;\r
+  }\r
+  table.visible-print {\r
+    display: table;\r
+  }\r
+  tr.visible-print {\r
+    display: table-row !important;\r
+  }\r
+  th.visible-print,\r
+  td.visible-print {\r
+    display: table-cell !important;\r
+  }\r
+}\r
+@media print {\r
+  .hidden-print {\r
+    display: none !important;\r
+  }\r
+}\r
+   \r
diff --git a/share/nitweb/views/class.html b/share/nitweb/views/class.html
new file mode 100644 (file)
index 0000000..ac7c432
--- /dev/null
@@ -0,0 +1,20 @@
+<div class='container-fluid'>
+       <div class='page-header'>
+               <h2><entity-signature mentity='mentity.intro'/></h2>
+               <entity-link mentity='mentity.mpackage' /> :: {{mentity.name}}
+       </div>
+
+       <ul class='nav nav-tabs'>
+               <li class='active'>
+                       <a data-toggle='tab' data-target='#doc'>
+                               <span class='glyphicon glyphicon-book'/> Doc
+                       </a>
+               </li>
+       </ul>
+
+       <div class='tab-content'>
+               <div class='tab-pane fade in active' id='doc'>
+                       <entity-doc mentity='mentity.intro'/>
+               </div>
+       </div>
+</div>
diff --git a/share/nitweb/views/classdef.html b/share/nitweb/views/classdef.html
new file mode 100644 (file)
index 0000000..50768ec
--- /dev/null
@@ -0,0 +1,16 @@
+<div class='container-fluid'>
+       <div class='page-header'>
+               <h2><entity-signature mentity='mentity'/></h2>
+               <entity-link mentity='mentity.mpackage' />
+               :: <entity-link mentity='mentity.mmodule' />
+               :: {{mentity.name}}
+       </div>
+
+       <ul class='nav nav-tabs' role='tablist'>
+               <li class='warning'>
+                       <a ng-href='{{mentity.mclass.web_url}}'>
+                               <span class='glyphicon glyphicon-chevron-left'/> Go to class
+                       </a>
+               </li>
+       </ul>
+</div>
diff --git a/share/nitweb/views/group.html b/share/nitweb/views/group.html
new file mode 100644 (file)
index 0000000..ef07e48
--- /dev/null
@@ -0,0 +1,21 @@
+<div class='container-fluid'>
+               <div class='page-header'>
+                       <h2><entity-signature mentity='mentity' /></h2>
+                       <entity-link mentity='mentity.mpackage' /> :: {{mentity.name}}
+               </div>
+
+               <ul class='nav nav-tabs'>
+                       <li class='active'>
+                               <a data-toggle='tab' data-target='#doc'>
+                                       <span class='glyphicon glyphicon-book'/> Doc
+                               </a>
+                       </li>
+               </ul>
+
+               <div class='tab-content'>
+                       <div class='tab-pane fade in active' id='doc'>
+                               <entity-doc mentity='mentity'/>
+                       </div>
+               </div>
+       </div>
+</div>
diff --git a/share/nitweb/views/index.html b/share/nitweb/views/index.html
new file mode 100644 (file)
index 0000000..9f9b93d
--- /dev/null
@@ -0,0 +1 @@
+<h1>Hello nitweb!</h1>
diff --git a/share/nitweb/views/module.html b/share/nitweb/views/module.html
new file mode 100644 (file)
index 0000000..23f6d52
--- /dev/null
@@ -0,0 +1,20 @@
+<div class='container-fluid'>
+       <div class='page-header'>
+               <h2><entity-signature mentity='mentity'/></h2>
+               <entity-link mentity='mentity.mpackage' /> :: {{mentity.name}}
+       </div>
+
+       <ul class='nav nav-tabs'>
+               <li class='active'>
+                       <a data-toggle='tab' data-target='#doc'>
+                               <span class='glyphicon glyphicon-book'/> Doc
+                       </a>
+               </li>
+       </ul>
+
+       <div class='tab-content'>
+               <div role='tabpanel' class='tab-pane fade in active' id='doc'>
+                       <entity-doc mentity='mentity'/>
+               </div>
+       </div>
+</div>
diff --git a/share/nitweb/views/package.html b/share/nitweb/views/package.html
new file mode 100644 (file)
index 0000000..4abcbc7
--- /dev/null
@@ -0,0 +1,19 @@
+<div class='container-fluid'>
+       <div class='page-header'>
+               <h2><entity-signature mentity='mentity'/></h2>
+       </div>
+
+       <ul class='nav nav-tabs' role='tablist'>
+               <li role='presentation' class='active'>
+                       <a data-toggle='tab' role='tab' data-target='#doc' aria-controls="doc">
+                               <span class='glyphicon glyphicon-book'/> Doc
+                       </a>
+               </li>
+       </ul>
+
+       <div class='tab-content'>
+               <div role='tabpanel' class='tab-pane fade in active' id='doc'>
+                       <entity-doc mentity='mentity'/>
+               </div>
+       </div>
+</div>
diff --git a/share/nitweb/views/propdef.html b/share/nitweb/views/propdef.html
new file mode 100644 (file)
index 0000000..2692c9c
--- /dev/null
@@ -0,0 +1,18 @@
+<div class='container-fluid'>
+
+       <div class='page-header'>
+               <h2><entity-signature mentity='mentity'/></h2>
+               <entity-link mentity='mentity.mpackage' />
+               :: <entity-link mentity='mentity.mmodule' />
+               :: <entity-link mentity='mentity.mclassdef' />
+               :: {{mentity.name}}
+       </div>
+
+       <ul class='nav nav-tabs'>
+               <li class='warning'>
+                       <a href='{{mentity.mproperty.web_url}}'>
+                               <span class='glyphicon glyphicon-chevron-left'/> Go to property
+                       </a>
+               </li>
+       </ul>
+</div>
diff --git a/share/nitweb/views/property.html b/share/nitweb/views/property.html
new file mode 100644 (file)
index 0000000..d5e7352
--- /dev/null
@@ -0,0 +1,23 @@
+<div class='container-fluid'>
+
+       <div class='page-header'>
+               <h2><entity-signature mentity='mentity.intro'/></h2>
+               <entity-link mentity='mentity.mpackage' />
+               :: <entity-link mentity='mentity.intro_mclassdef' />
+               :: {{mentity.name}}
+       </div>
+
+       <ul class='nav nav-tabs'>
+               <li class='active'>
+                       <a data-toggle='tab' data-target='#doc'>
+                               <span class='glyphicon glyphicon-book'/> Doc
+                       </a>
+               </li>
+       </ul>
+
+       <div class='tab-content'>
+               <div role='tabpanel' class='tab-pane fade in active' id='doc'>
+                       <entity-doc mentity='mentity.intro'/>
+               </div>
+       </div>
+</div>
index 3ebc01e..92510bc 100644 (file)
@@ -229,6 +229,18 @@ class Catalog
        # Number of line of code by package
        var loc = new Counter[MPackage]
 
+       # Number of errors
+       var errors = new Counter[MPackage]
+
+       # Number of warnings and advices
+       var warnings = new Counter[MPackage]
+
+       # Number of warnings per 1000 lines of code (w/kloc)
+       var warnings_per_kloc = new Counter[MPackage]
+
+       # Documentation score (between 0 and 100)
+       var documentation_score = new Counter[MPackage]
+
        # Number of commits by package
        var commits = new Counter[MPackage]
 
@@ -245,8 +257,13 @@ class Catalog
        do
                var p = persons.get_or_null(person)
                if p == null then
-                       p = new Person.parse(person)
-                       persons[person] = p
+                       var new_p = new Person.parse(person)
+                       # Maybe, we already have this person in fact?
+                       p = persons.get_or_null(new_p.to_s)
+                       if p == null then
+                               p = new_p
+                               persons[p.to_s] = p
+                       end
                end
                var projs = contrib2proj[p]
                if not projs.has(mpackage) then
@@ -338,9 +355,27 @@ class Catalog
                var mclasses = 0
                var mmethods = 0
                var loc = 0
+               var errors = 0
+               var warnings = 0
+               # The documentation value of each entity is ad hoc.
+               var entity_score = 0.0
+               var doc_score = 0.0
                for g in mpackage.mgroups do
                        mmodules += g.mmodules.length
+                       var gs = 1.0
+                       entity_score += gs
+                       if g.mdoc != null then doc_score += gs
                        for m in g.mmodules do
+                               var source = m.location.file
+                               if source != null then
+                                       for msg in source.messages do
+                                               if msg.level == 2 then
+                                                       errors += 1
+                                               else
+                                                       warnings += 1
+                                               end
+                                       end
+                               end
                                var am = modelbuilder.mmodule2node(m)
                                if am != null then
                                        var file = am.location.file
@@ -348,9 +383,23 @@ class Catalog
                                                loc += file.line_starts.length - 1
                                        end
                                end
+                               var ms = gs
+                               if m.is_test_suite then ms /= 100.0
+                               entity_score += ms
+                               if m.mdoc != null then doc_score += ms else ms /= 10.0
                                for cd in m.mclassdefs do
+                                       var cs = ms * 0.2
+                                       if not cd.is_intro then cs /= 100.0
+                                       if not cd.mclass.visibility <= private_visibility then cs /= 100.0
+                                       entity_score += cs
+                                       if cd.mdoc != null then doc_score += cs
                                        mclasses += 1
                                        for pd in cd.mpropdefs do
+                                               var ps = ms * 0.1
+                                               if not pd.is_intro then ps /= 100.0
+                                               if not pd.mproperty.visibility <= private_visibility then ps /= 100.0
+                                               entity_score += ps
+                                               if pd.mdoc != null then doc_score += ps
                                                if not pd isa MMethodDef then continue
                                                mmethods += 1
                                        end
@@ -361,11 +410,19 @@ class Catalog
                self.mclasses[mpackage] = mclasses
                self.mmethods[mpackage] = mmethods
                self.loc[mpackage] = loc
+               self.errors[mpackage] = errors
+               self.warnings[mpackage] = warnings
+               if loc > 0 then
+                       self.warnings_per_kloc[mpackage] = warnings * 1000 / loc
+               end
+               var documentation_score =  (100.0 * doc_score / entity_score).to_i
+               self.documentation_score[mpackage] = documentation_score
 
                #score += mmodules.score
                score += mclasses.score
                score += mmethods.score
                score += loc.score
+               score += documentation_score.score
 
                self.score[mpackage] = score.to_i
        end
index c2ed988..a8d86b6 100644 (file)
@@ -31,9 +31,6 @@ redef class ToolContext
        var opt_source = new OptionString("Format to link source code (%f for filename, " +
                "%l for first line, %L for last line)", "--source")
 
-       # Directory where the CSS and JS is stored.
-       var opt_sharedir = new OptionString("Directory containing nitdoc assets", "--sharedir")
-
        # Use a shareurl instead of copy shared files.
        #
        # This is usefull if you don't want to store the Nitdoc templates with your
@@ -77,7 +74,7 @@ redef class ToolContext
                super
 
                option_context.add_option(
-                       opt_source, opt_sharedir, opt_shareurl, opt_custom_title,
+                       opt_source, opt_share_dir, opt_shareurl, opt_custom_title,
                        opt_custom_footer, opt_custom_intro, opt_custom_brand,
                        opt_github_upstream, opt_github_base_sha1, opt_github_gitdir,
                        opt_piwik_tracker, opt_piwik_site_id,
@@ -120,15 +117,7 @@ class RenderHTMLPhase
                var output_dir = ctx.output_dir
                if not output_dir.file_exists then output_dir.mkdir
                # locate share dir
-               var sharedir = ctx.opt_sharedir.value
-               if sharedir == null then
-                       var dir = ctx.nit_dir
-                       sharedir = dir/"share/nitdoc"
-                       if not sharedir.file_exists then
-                               print "Error: cannot locate nitdoc share files. Uses --sharedir or envvar NIT_DIR"
-                               abort
-                       end
-               end
+               var sharedir = ctx.share_dir / "nitdoc"
                # copy shared files
                if ctx.opt_shareurl.value == null then
                        sys.system("cp -r -- {sharedir.to_s.escape_to_sh}/* {output_dir.to_s.escape_to_sh}/")
index 4207a01..4e4a5b3 100644 (file)
@@ -128,7 +128,12 @@ redef class AModule
                srcs.add_all mmodule.ffi_files
 
                # Compiler options specific to this module
-               var ldflags = mmodule.ldflags[""].join(" ")
+               var ldflags_array = mmodule.ldflags[""]
+               if ldflags_array.has("-lrt") and system("sh -c 'uname -s 2>/dev/null || echo not' | grep Darwin >/dev/null") == 0 then
+                       # Remove -lrt on OS X
+                       ldflags_array.remove "-lrt"
+               end
+               var ldflags = ldflags_array.join(" ")
 
                # Protect pkg-config
                var pkgconfigs = mmodule.pkgconfigs
index 6df4d61..967d335 100644 (file)
@@ -381,9 +381,9 @@ redef class ModelBuilder
 
                if mgroup == null then
                        # singleton package
-                       var mpackage = new MPackage(pn, model)
-                       mgroup = new MGroup(pn, mpackage, null) # same name for the root group
-                       mgroup.filepath = path
+                       var loc = new Location.opaque_file(path)
+                       var mpackage = new MPackage(pn, model, loc)
+                       mgroup = new MGroup(pn, loc, mpackage, null) # same name for the root group
                        mpackage.root = mgroup
                        toolcontext.info("found singleton package `{pn}` at {path}", 2)
 
@@ -395,10 +395,8 @@ redef class ModelBuilder
                        end
                end
 
-               var src = new SourceFile.from_string(path, "")
-               var loc = new Location(src, 0, 0, 0, 0)
+               var loc = new Location.opaque_file(path)
                var res = new MModule(model, mgroup, pn, loc)
-               res.filepath = path
 
                identified_modules_by_path[rp] = res
                identified_modules_by_path[path] = res
@@ -483,17 +481,18 @@ redef class ModelBuilder
                        end
                end
 
+               var loc = new Location.opaque_file(dirpath)
                var mgroup
                if parent == null then
                        # no parent, thus new package
                        if ini != null then pn = ini["package.name"] or else pn
-                       var mpackage = new MPackage(pn, model)
-                       mgroup = new MGroup(pn, mpackage, null) # same name for the root group
+                       var mpackage = new MPackage(pn, model, loc)
+                       mgroup = new MGroup(pn, loc, mpackage, null) # same name for the root group
                        mpackage.root = mgroup
                        toolcontext.info("found package `{mpackage}` at {dirpath}", 2)
                        mpackage.ini = ini
                else
-                       mgroup = new MGroup(pn, parent.mpackage, parent)
+                       mgroup = new MGroup(pn, loc, parent.mpackage, parent)
                        toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
                end
 
@@ -507,7 +506,6 @@ redef class ModelBuilder
                        mdoc.original_mentity = mgroup
                end
 
-               mgroup.filepath = dirpath
                mgroups[rdp] = mgroup
                return mgroup
        end
@@ -612,6 +610,11 @@ redef class ModelBuilder
                var keep = new Array[String]
                var res = new Array[String]
                for a in args do
+                       var stat = a.to_path.stat
+                       if stat != null and stat.is_dir then
+                               res.add a
+                               continue
+                       end
                        var l = identify_module(a)
                        if l == null then
                                keep.add a
index 0c5b55d..99083ad 100644 (file)
@@ -140,6 +140,15 @@ class Location
                end
        end
 
+       # Initialize a location corresponding to an opaque file.
+       #
+       # The path is used as is and is not open nor read.
+       init opaque_file(path: String)
+       do
+               var source = new SourceFile.from_string(path, "")
+               init(source, 0, 0, 0, 0)
+       end
+
        # The index in the start character in the source
        fun pstart: Int do return file.line_starts[line_start-1] + column_start-1
 
index 8927f8c..1232352 100644 (file)
@@ -17,7 +17,6 @@
 # modules and module hierarchies in the metamodel
 module mmodule
 
-import location
 import mpackage
 private import more_collections
 
@@ -82,7 +81,13 @@ class MModule
        var mgroup: nullable MGroup
 
        # The path of the module source, if any
-       var filepath: nullable String = null is writable
+       #
+       # safe alias to `location.file.filepath`
+       fun filepath: nullable String do
+               var res = self.location.file
+               if res == null then return null
+               return res.filename
+       end
 
        # The package of the module if any
        # Safe alias for `mgroup.mpackage`
@@ -95,8 +100,7 @@ class MModule
        # The short name of the module
        redef var name: String
 
-       # The origin of the definition
-       var location: Location is writable
+       redef var location: Location is writable
 
        # Alias for `name`
        redef fun to_s do return self.name
index 6c8798b..e7c3b15 100644 (file)
@@ -30,6 +30,15 @@ import mdoc
 import ordered_tree
 private import more_collections
 
+redef class MEntity
+       # The visibility of the MEntity.
+       #
+       # MPackages, MGroups and MModules are always public.
+       # The visibility of `MClass` and `MProperty` is defined by the keyword used.
+       # `MClassDef` and `MPropDef` return the visibility of `MClass` and `MProperty`.
+       fun visibility: MVisibility do return public_visibility
+end
+
 redef class Model
        # All known classes
        var mclasses = new Array[MClass]
@@ -285,8 +294,9 @@ redef class MModule
                        if name == "Bool" and self.model.get_mclasses_by_name("Object") != null then
                                # Bool is injected because it is needed by engine to code the result
                                # of the implicit casts.
-                               var c = new MClass(self, name, null, enum_kind, public_visibility)
-                               var cladef = new MClassDef(self, c.mclass_type, new Location(null, 0,0,0,0))
+                               var loc = model.no_location
+                               var c = new MClass(self, name, loc, null, enum_kind, public_visibility)
+                               var cladef = new MClassDef(self, c.mclass_type, loc)
                                cladef.set_supertypes([object_type])
                                cladef.add_in_hierarchy
                                return c
@@ -386,6 +396,8 @@ class MClass
        # In Nit, the name of a class cannot evolve in refinements
        redef var name
 
+       redef var location
+
        # The canonical name of the class
        #
        # It is the name of the class prefixed by the full_name of the `intro_mmodule`
@@ -462,7 +474,7 @@ class MClass
 
        # The visibility of the class
        # In Nit, the visibility of a class cannot evolve in refinements
-       var visibility: MVisibility
+       redef var visibility
 
        init
        do
@@ -552,6 +564,8 @@ class MClass
 
        # Is `self` and abstract class?
        var is_abstract: Bool is lazy do return kind == abstract_kind
+
+       redef fun mdoc_or_fallback do return intro.mdoc_or_fallback
 end
 
 
@@ -588,8 +602,9 @@ class MClassDef
        # ENSURE: `bound_mtype.mclass == self.mclass`
        var bound_mtype: MClassType
 
-       # The origin of the definition
-       var location: Location
+       redef var location: Location
+
+       redef fun visibility do return mclass.visibility
 
        # Internal name combining the module and the class
        # Example: "mymodule$MyClass"
@@ -1165,6 +1180,8 @@ class MClassType
 
        redef fun model do return self.mclass.intro_mmodule.model
 
+       redef fun location do return mclass.location
+
        # TODO: private init because strongly bounded to its mclass. see `mclass.mclass_type`
 
        # The formal arguments of the type
@@ -1370,6 +1387,8 @@ class MVirtualType
        # Its the definitions of this property that determine the bound or the virtual type.
        var mproperty: MVirtualTypeProp
 
+       redef fun location do return mproperty.location
+
        redef fun model do return self.mproperty.intro_mclassdef.mmodule.model
 
        redef fun lookup_bound(mmodule: MModule, resolved_receiver: MType): MType
@@ -1500,6 +1519,8 @@ class MParameterType
 
        redef fun model do return self.mclass.intro_mmodule.model
 
+       redef fun location do return mclass.location
+
        # The position of the parameter (0 for the first parameter)
        # FIXME: is `position` a better name?
        var rank: Int
@@ -1625,6 +1646,8 @@ abstract class MProxyType
        # The base type
        var mtype: MType
 
+       redef fun location do return mtype.location
+
        redef fun model do return self.mtype.model
        redef fun need_anchor do return mtype.need_anchor
        redef fun as_nullable do return mtype.as_nullable
@@ -1951,6 +1974,10 @@ abstract class MProperty
        # The (short) name of the property
        redef var name
 
+       redef var location
+
+       redef fun mdoc_or_fallback do return intro.mdoc_or_fallback
+
        # The canonical name of the property.
        #
        # It is currently the short-`name` prefixed by the short-name of the class and the full-name of the module.
@@ -1976,7 +2003,7 @@ abstract class MProperty
        end
 
        # The visibility of the property
-       var visibility: MVisibility
+       redef var visibility
 
        # Is the property usable as an initializer?
        var is_autoinit = false is writable
@@ -2241,8 +2268,9 @@ abstract class MPropDef
        # The associated global property
        var mproperty: MPROPERTY
 
-       # The origin of the definition
-       var location: Location
+       redef var location: Location
+
+       redef fun visibility do return mproperty.visibility
 
        init
        do
index 5cfe9b2..0c136b8 100644 (file)
@@ -16,6 +16,7 @@
 
 # The abstract concept of model and related common things
 module model_base
+import location
 
 # The container class of a Nit object-oriented model.
 # A model knows modules, classes and properties and can retrieve them.
@@ -23,6 +24,11 @@ class Model
        super MEntity
 
        redef fun model do return self
+
+       # Place-holder object that means no-location
+       #
+       # See `MEntity::location`
+       var no_location = new Location(null, 0, 0, 0, 0)
 end
 
 # A named and possibly documented entity in the model.
@@ -65,6 +71,16 @@ abstract class MEntity
        # indirect use should be restricted (e.g. to name a web-page)
        fun c_name: String is abstract
 
+       # The origin of the definition.
+       #
+       # Most model entities are defined in a specific place in the source base.
+       #
+       # Because most model entities have one,
+       # it is simpler for the client to have a non-nullable return value.
+       # For entities that lack a location, mock-up special locations are used instead.
+       # By default it is `model.no_location`.
+       fun location: Location do return model.no_location
+
        # A Model Entity has a direct link to its model
        fun model: Model is abstract
 
index 65c1c62..79970e5 100644 (file)
@@ -31,8 +31,38 @@ module model_collect
 
 import model_views
 
+redef class MEntity
+
+       # Collect modifier keywords like `redef`, `private` etc.
+       fun collect_modifiers: Array[String] do
+               return new Array[String]
+       end
+end
+
+redef class MPackage
+       redef fun collect_modifiers do
+               var res = super
+               res.add "package"
+               return res
+       end
+end
+
+redef class MGroup
+       redef fun collect_modifiers do
+               var res = super
+               res.add "group"
+               return res
+       end
+end
+
 redef class MModule
 
+       redef fun collect_modifiers do
+               var res = super
+               res.add "module"
+               return res
+       end
+
        # Collect all transitive imports.
        fun collect_ancestors(view: ModelView): Set[MModule] do
                var res = new HashSet[MModule]
@@ -82,8 +112,6 @@ redef class MModule
                var mmodules = new HashSet[MModule]
                mmodules.add self
                mmodules.add_all collect_ancestors(view)
-               mmodules.add_all collect_parents(view)
-               mmodules.add_all collect_children(view)
                mmodules.add_all collect_descendants(view)
                return view.mmodules_poset(mmodules)
        end
@@ -133,6 +161,8 @@ end
 
 redef class MClass
 
+       redef fun collect_modifiers do return intro.collect_modifiers
+
        # Collect direct parents of `self` with `visibility >= to min_visibility`.
        fun collect_parents(view: ModelView): Set[MClass] do
                var res = new HashSet[MClass]
@@ -188,6 +218,15 @@ redef class MClass
                return res
        end
 
+       # Build a class hierarchy poset for `self` based on its ancestors and descendants.
+       fun hierarchy_poset(mainmodule: MModule, view: ModelView): POSet[MClass] do
+               var mclasses = new HashSet[MClass]
+               mclasses.add self
+               mclasses.add_all collect_ancestors(view)
+               mclasses.add_all collect_descendants(view)
+               return view.mclasses_poset(mainmodule, mclasses)
+       end
+
        # Collect all mproperties introduced in 'self' with `visibility >= min_visibility`.
        fun collect_intro_mproperties(view: ModelView): Set[MProperty] do
                var set = new HashSet[MProperty]
@@ -404,9 +443,8 @@ redef class MClassDef
                return res
        end
 
-       # Collect modifiers like redef, private etc.
-       fun collect_modifiers: Array[String] do
-               var res = new Array[String]
+       redef fun collect_modifiers do
+               var res = super
                if not is_intro then
                        res.add "redef"
                else
@@ -417,10 +455,13 @@ redef class MClassDef
        end
 end
 
+redef class MProperty
+       redef fun collect_modifiers do return intro.collect_modifiers
+end
+
 redef class MPropDef
-       # Collect modifiers like redef, private, abstract, intern, fun etc.
-       fun collect_modifiers: Array[String] do
-               var res = new Array[String]
+       redef fun collect_modifiers do
+               var res = super
                if not is_intro then
                        res.add "redef"
                else
@@ -440,6 +481,8 @@ redef class MPropDef
                        else
                                res.add "fun"
                        end
+               else if mprop isa MAttributeDef then
+                       res.add "var"
                end
                return res
        end
diff --git a/src/model/model_json.nit b/src/model/model_json.nit
new file mode 100644 (file)
index 0000000..658e73d
--- /dev/null
@@ -0,0 +1,296 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Make model entities Jsonable.
+#
+# To avoid cycles, every reference from a MEntity to another is replaced by a
+# MEntityRef.
+#
+# How subobjects are retrieved using the MEntityRef is the responsability of the
+# client. Json objects can be returned as this or inflated with concrete objet
+# rather than the refs.
+#
+# TODO consider serialization module?
+module model_json
+
+import model::model_collect
+import json
+import loader
+
+# A reference to another mentity.
+class MEntityRef
+       super MEntity
+
+       # MEntity to link to.
+       var mentity: MEntity
+
+       # Return `self` as a Json Object.
+       #
+       # By default, MEntity references contain only the `full_name` of the Mentity.
+       # You should redefine this method in your client to implement a different behavior.
+       redef fun json do
+               var obj = new JsonObject
+               obj["full_name"] = mentity.full_name
+               return obj
+       end
+end
+
+redef class MEntity
+       super Jsonable
+
+       # Return `self` as a JsonObject.
+       #
+       # By default, every reference to another MEntity is replaced by a pointer
+       # to the MEntity::json_id.
+       fun json: JsonObject do
+               var obj = new JsonObject
+               obj["name"] = name
+               obj["class_name"] = class_name
+               obj["full_name"] = full_name
+               obj["mdoc"] = mdoc_or_fallback
+               obj["visibility"] = visibility
+               obj["location"] = location
+               var modifiers = new JsonArray
+               for modifier in collect_modifiers do
+                       modifiers.add modifier
+               end
+               obj["modifiers"] = modifiers
+               return obj
+       end
+
+       redef fun to_json do return json.to_json
+end
+
+redef class MDoc
+       super Jsonable
+
+       # Return `self` as a JsonObject.
+       fun json: JsonObject do
+               var obj = new JsonObject
+               obj["content"] = content.join("\n")
+               obj["location"] = location
+               return obj
+       end
+
+       redef fun to_json do return json.to_json
+end
+
+redef class Location
+       super Jsonable
+
+       # Return `self` as a JsonObject.
+       fun json: JsonObject do
+               var obj = new JsonObject
+               obj["column_end"] = column_end
+               obj["column_start"] = column_start
+               obj["line_end"] = line_end
+               obj["line_start"] = line_start
+               var file = self.file
+               if file != null then
+                       obj["file"] = file.filename
+               end
+               return obj
+       end
+
+       redef fun to_json do return json.to_json
+end
+
+redef class MVisibility
+       super Jsonable
+
+       redef fun to_json do return to_s.to_json
+end
+
+redef class MPackage
+
+       redef fun json do
+               var obj = super
+               if ini != null then
+                       obj["ini"] = new JsonObject.from(ini.as(not null).to_map)
+               end
+               obj["root"] = to_mentity_ref(root)
+               obj["mgroups"] = to_mentity_refs(mgroups)
+               return obj
+       end
+end
+
+redef class MGroup
+       redef fun json do
+               var obj = super
+               obj["is_root"] = is_root
+               obj["mpackage"] = to_mentity_ref(mpackage)
+               obj["default_mmodule"] = to_mentity_ref(default_mmodule)
+               obj["parent"] = to_mentity_ref(parent)
+               obj["mmodules"] = to_mentity_refs(mmodules)
+               obj["mgroups"] = to_mentity_refs(in_nesting.direct_smallers)
+               return obj
+       end
+end
+
+redef class MModule
+       redef fun json do
+               var obj = super
+               obj["mpackage"] = to_mentity_ref(mpackage)
+               obj["mgroup"] = to_mentity_ref(mgroup)
+               obj["intro_mclasses"] = to_mentity_refs(intro_mclasses)
+               obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+               return obj
+       end
+end
+
+redef class MClass
+       redef fun json do
+               var obj = super
+               var arr = new JsonArray
+               for mparameter in mparameters do arr.add mparameter
+               obj["mparameters"] = arr
+               obj["intro"] = to_mentity_ref(intro)
+               obj["intro_mmodule"] = to_mentity_ref(intro_mmodule)
+               obj["mpackage"] = to_mentity_ref(intro_mmodule.mpackage)
+               obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+               return obj
+       end
+end
+
+redef class MClassDef
+       redef fun json do
+               var obj = super
+               obj["is_intro"] = is_intro
+               var arr = new JsonArray
+               for mparameter in mclass.mparameters do arr.add mparameter
+               obj["mparameters"] = arr
+               obj["mmodule"] = to_mentity_ref(mmodule)
+               obj["mclass"] = to_mentity_ref(mclass)
+               obj["mpropdefs"] = to_mentity_refs(mpropdefs)
+               obj["intro_mproperties"] = to_mentity_refs(intro_mproperties)
+               return obj
+       end
+end
+
+redef class MProperty
+       redef fun json do
+               var obj = super
+               obj["intro"] = to_mentity_ref(intro)
+               obj["intro_mclassdef"] = to_mentity_ref(intro_mclassdef)
+               obj["mpropdefs"] = to_mentity_refs(mpropdefs)
+               return obj
+       end
+end
+
+redef class MMethod
+       redef fun json do
+               var obj = super
+               obj["is_init"] = is_init
+               obj["msignature"] = intro.msignature
+               return obj
+       end
+end
+
+redef class MAttribute
+       redef fun json do
+               var obj = super
+               obj["static_mtype"] = to_mentity_ref(intro.static_mtype)
+               return obj
+       end
+end
+
+redef class MVirtualTypeProp
+       redef fun json do
+               var obj = super
+               obj["mvirtualtype"] = to_mentity_ref(mvirtualtype)
+               obj["bound"] = to_mentity_ref(intro.bound)
+               return obj
+       end
+end
+
+redef class MPropDef
+       redef fun json do
+               var obj = super
+               obj["is_intro"] = is_intro
+               obj["mclassdef"] = to_mentity_ref(mclassdef)
+               obj["mproperty"] = to_mentity_ref(mproperty)
+               return obj
+       end
+end
+
+redef class MMethodDef
+       redef fun json do
+               var obj = super
+               obj["msignature"] = msignature
+               return obj
+       end
+end
+
+redef class MAttributeDef
+       redef fun json do
+               var obj = super
+               obj["static_mtype"] = to_mentity_ref(static_mtype)
+               return obj
+       end
+end
+
+redef class MVirtualTypeDef
+       redef fun json do
+               var obj = super
+               obj["bound"] = to_mentity_ref(bound)
+               obj["is_fixed"] = is_fixed
+               return obj
+       end
+end
+
+redef class MSignature
+       redef fun json do
+               var obj = new JsonObject
+               obj["arity"] = arity
+               var arr = new JsonArray
+               for mparam in mparameters do arr.add mparam
+               obj["mparams"] = arr
+               obj["return_mtype"] = to_mentity_ref(return_mtype)
+               obj["vararg_rank"] = vararg_rank
+               return obj
+       end
+end
+
+redef class MParameterType
+       redef fun json do
+               var obj = new JsonObject
+               obj["name"] = name
+               obj["rank"] = rank
+               obj["mtype"] = to_mentity_ref(mclass.intro.bound_mtype.arguments[rank])
+               return obj
+       end
+end
+
+redef class MParameter
+       redef fun json do
+               var obj = new JsonObject
+               obj["is_vararg"] = is_vararg
+               obj["name"] = name
+               obj["mtype"] = to_mentity_ref(mtype)
+               return obj
+       end
+end
+
+# Create a ref to a `mentity`.
+fun to_mentity_ref(mentity: nullable MEntity): nullable MEntityRef do
+       if mentity == null then return null
+       return new MEntityRef(mentity)
+end
+
+# Return a collection of `mentities` as a JsonArray of MEntityRefs.
+fun to_mentity_refs(mentities: Collection[MEntity]): JsonArray do
+       var array = new JsonArray
+       for mentity in mentities do array.add to_mentity_ref(mentity)
+       return array
+end
index f130f09..566a5bd 100644 (file)
@@ -132,6 +132,14 @@ class ModelView
                return res
        end
 
+       # Searches the MEntity that matches `full_name`.
+       fun mentity_by_full_name(full_name: String): nullable MEntity do
+               for mentity in mentities do
+                       if mentity.full_name == full_name then return mentity
+               end
+               return null
+       end
+
        # Looks up a MEntity by its full `namespace`.
        #
        # Usefull when `mentities_by_name` returns conflicts.
index da881a0..59d3e61 100644 (file)
@@ -144,7 +144,10 @@ redef class MEntity
        # See the specific implementation in the subclasses.
        fun visit_all(v: ModelVisitor) do end
 
-       private fun accept_visibility(min_visibility: nullable MVisibility): Bool do return true
+       private fun accept_visibility(min_visibility: nullable MVisibility): Bool do
+               if min_visibility == null then return true
+               return visibility >= min_visibility
+       end
 end
 
 redef class Model
@@ -183,13 +186,6 @@ redef class MModule
        end
 end
 
-redef class MClass
-       redef fun accept_visibility(min_visibility) do
-               if min_visibility == null then return true
-               return visibility >= min_visibility
-       end
-end
-
 redef class MClassDef
        # Visit all the classes and class definitions of the module.
        #
@@ -202,23 +198,4 @@ redef class MClassDef
                        v.enter_visit(x)
                end
        end
-
-       redef fun accept_visibility(min_visibility) do
-               if min_visibility == null then return true
-               return mclass.visibility >= min_visibility
-       end
-end
-
-redef class MProperty
-       redef fun accept_visibility(min_visibility) do
-               if min_visibility == null then return true
-               return visibility >= min_visibility
-       end
-end
-
-redef class MPropDef
-       redef fun accept_visibility(min_visibility) do
-               if min_visibility == null then return true
-               return mproperty.visibility >= min_visibility
-       end
 end
index 1771938..8309b5a 100644 (file)
@@ -34,6 +34,8 @@ class MPackage
        # The model of the package
        redef var model: Model
 
+       redef var location
+
        # The root of the group tree
        var root: nullable MGroup = null is writable
 
@@ -66,6 +68,8 @@ class MGroup
        # empty name for a default group in a single-module package
        redef var name: String
 
+       redef var location
+
        # The enclosing package
        var mpackage: MPackage
 
@@ -94,7 +98,14 @@ class MGroup
        fun is_root: Bool do return mpackage.root == self
 
        # The filepath (usually a directory) of the group, if any
-       var filepath: nullable String = null is writable
+       #
+       # safe alias to `location.file.filename`
+       fun filepath: nullable String do
+               var res
+               res = self.location.file
+               if res == null then return null
+               return res.filename
+       end
 
        init
        do
index e020b50..f5ed779 100644 (file)
@@ -122,7 +122,7 @@ redef class ModelBuilder
                                end
                        end
 
-                       mclass = new MClass(mmodule, name, names, mkind, mvisibility)
+                       mclass = new MClass(mmodule, name, nclassdef.location, names, mkind, mvisibility)
                        #print "new class {mclass}"
                else if nclassdef isa AStdClassdef and nmodule.mclass2nclassdef.has_key(mclass) then
                        error(nclassdef, "Error: a class `{name}` is already defined at line {nmodule.mclass2nclassdef[mclass].location.line_start}.")
index 141ed81..246e813 100644 (file)
@@ -145,7 +145,7 @@ redef class ModelBuilder
                # Look for the init in Object, or create it
                if mclassdef.mclass.name == "Object" and the_root_init_mmethod == null then
                        # Create the implicit root-init method
-                       var mprop = new MMethod(mclassdef, "init", mclassdef.mclass.visibility)
+                       var mprop = new MMethod(mclassdef, "init", nclassdef.location, mclassdef.mclass.visibility)
                        mprop.is_root_init = true
                        var mpropdef = new MMethodDef(mclassdef, mprop, nclassdef.location)
                        var mparameters = new Array[MParameter]
@@ -822,7 +822,7 @@ redef class AMethPropdef
                end
                if mprop == null then
                        var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility)
-                       mprop = new MMethod(mclassdef, name, mvisibility)
+                       mprop = new MMethod(mclassdef, name, self.location, mvisibility)
                        if look_like_a_root_init and modelbuilder.the_root_init_mmethod == null then
                                modelbuilder.the_root_init_mmethod = mprop
                                mprop.is_root_init = true
@@ -836,9 +836,7 @@ redef class AMethPropdef
                                return
                        end
                else
-                       if mprop.is_broken then
-                               return
-                       end
+                       if mprop.is_broken then return
                        if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, not self isa AMainMethPropdef, mprop) then return
                        check_redef_property_visibility(modelbuilder, self.n_visibility, mprop)
                end
@@ -1186,7 +1184,7 @@ redef class AAttrPropdef
                                modelbuilder.error(self, "Error: attempt to define attribute `{name}` in the {mclass.kind} `{mclass}`.")
                        end
 
-                       var mprop = new MAttribute(mclassdef, "_" + name, private_visibility)
+                       var mprop = new MAttribute(mclassdef, "_" + name, self.location, private_visibility)
                        var mpropdef = new MAttributeDef(mclassdef, mprop, self.location)
                        self.mpropdef = mpropdef
                        modelbuilder.mpropdef2npropdef[mpropdef] = self
@@ -1196,9 +1194,13 @@ redef class AAttrPropdef
                var mreadprop = modelbuilder.try_get_mproperty_by_name(nid2, mclassdef, readname).as(nullable MMethod)
                if mreadprop == null then
                        var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility)
-                       mreadprop = new MMethod(mclassdef, readname, mvisibility)
-                       if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, false, mreadprop) then return
+                       mreadprop = new MMethod(mclassdef, readname, self.location, mvisibility)
+                       if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, false, mreadprop) then
+                               mreadprop.is_broken = true
+                               return
+                       end
                else
+                       if mreadprop.is_broken then return
                        if not self.check_redef_keyword(modelbuilder, mclassdef, n_kwredef, true, mreadprop) then return
                        check_redef_property_visibility(modelbuilder, self.n_visibility, mreadprop)
                end
@@ -1248,7 +1250,7 @@ redef class AAttrPropdef
                                return
                        end
                        is_lazy = true
-                       var mlazyprop = new MAttribute(mclassdef, "lazy _" + name, none_visibility)
+                       var mlazyprop = new MAttribute(mclassdef, "lazy _" + name, self.location, none_visibility)
                        mlazyprop.is_fictive = true
                        var mlazypropdef = new MAttributeDef(mclassdef, mlazyprop, self.location)
                        mlazypropdef.is_fictive = true
@@ -1295,10 +1297,14 @@ redef class AAttrPropdef
                                # By default, use protected visibility at most
                                if mvisibility > protected_visibility then mvisibility = protected_visibility
                        end
-                       mwriteprop = new MMethod(mclassdef, writename, mvisibility)
-                       if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef, false, mwriteprop) then return
+                       mwriteprop = new MMethod(mclassdef, writename, self.location, mvisibility)
+                       if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef, false, mwriteprop) then
+                               mwriteprop.is_broken = true
+                               return
+                       end
                        mwriteprop.deprecation = mreadprop.deprecation
                else
+                       if mwriteprop.is_broken then return
                        if not self.check_redef_keyword(modelbuilder, mclassdef, nwkwredef or else n_kwredef, true, mwriteprop) then return
                        if atwritable != null then
                                check_redef_property_visibility(modelbuilder, atwritable.n_visibility, mwriteprop)
@@ -1602,12 +1608,13 @@ redef class ATypePropdef
                var mprop = modelbuilder.try_get_mproperty_by_name(self.n_qid, mclassdef, name)
                if mprop == null then
                        var mvisibility = new_property_visibility(modelbuilder, mclassdef, self.n_visibility)
-                       mprop = new MVirtualTypeProp(mclassdef, name, mvisibility)
+                       mprop = new MVirtualTypeProp(mclassdef, name, self.location, mvisibility)
                        for c in name.chars do if c >= 'a' and c<= 'z' then
                                modelbuilder.warning(n_qid, "bad-type-name", "Warning: lowercase in the virtual type `{name}`.")
                                break
                        end
                else
+                       if mprop.is_broken then return
                        assert mprop isa MVirtualTypeProp
                        check_redef_property_visibility(modelbuilder, self.n_visibility, mprop)
                end
index c110468..1e3fb5d 100644 (file)
@@ -365,9 +365,14 @@ class NeoModel
                node.labels.add "MEntity"
                node.labels.add model_name
                node["name"] = mentity.name
-               if mentity.mdoc != null then
-                       node["mdoc"] = new JsonArray.from(mentity.mdoc.content)
-                       node["mdoc_location"] = mentity.mdoc.location.to_s
+               if not mentity isa MSignature then
+                       #FIXME: MSignature is a MEntity, but has no model :/
+                       node["location"] = mentity.location.to_s
+               end
+               var mdoc = mentity.mdoc
+               if mdoc != null then
+                       node["mdoc"] = new JsonArray.from(mdoc.content)
+                       node["mdoc_location"] = mdoc.location.to_s
                end
                return node
        end
@@ -391,7 +396,8 @@ class NeoModel
                if m isa MPackage then return m
 
                assert node.labels.has("MPackage")
-               var mpackage = new MPackage(node["name"].to_s, model)
+               var location = to_location(node["location"].to_s)
+               var mpackage = new MPackage(node["name"].to_s, model, location)
                mentities[node.id.as(Int)] = mpackage
                set_doc(node, mpackage)
                mpackage.root = to_mgroup(model, node.out_nodes("ROOT").first)
@@ -424,13 +430,14 @@ class NeoModel
                if m isa MGroup then return m
 
                assert node.labels.has("MGroup")
+               var location = to_location(node["location"].to_s)
                var mpackage = to_mpackage(model, node.out_nodes("PROJECT").first)
                var parent: nullable MGroup = null
                var out = node.out_nodes("PARENT")
                if not out.is_empty then
                        parent = to_mgroup(model, out.first)
                end
-               var mgroup = new MGroup(node["name"].to_s, mpackage, parent)
+               var mgroup = new MGroup(node["name"].to_s, location, mpackage, parent)
                mentities[node.id.as(Int)] = mgroup
                set_doc(node, mgroup)
                return mgroup
@@ -440,7 +447,6 @@ class NeoModel
        private fun mmodule_node(mmodule: MModule): NeoNode do
                var node = make_node(mmodule)
                node.labels.add "MModule"
-               node["location"] = mmodule.location.to_s
                for parent in mmodule.in_importation.direct_greaters do
                        node.out_edges.add(new NeoEdge(node, "IMPORTS", to_node(parent)))
                end
@@ -504,6 +510,7 @@ class NeoModel
                assert node.labels.has("MClass")
                var mmodule = to_mmodule(model, node.in_nodes("INTRODUCES").first)
                var name = node["name"].to_s
+               var location = to_location(node["location"].to_s)
                var kind = to_kind(node["kind"].to_s)
                var visibility = to_visibility(node["visibility"].to_s)
                var parameter_names = new Array[String]
@@ -512,7 +519,7 @@ class NeoModel
                                parameter_names.add e.to_s
                        end
                end
-               var mclass = new MClass(mmodule, name, parameter_names, kind, visibility)
+               var mclass = new MClass(mmodule, name, location, parameter_names, kind, visibility)
                mentities[node.id.as(Int)] = mclass
                set_doc(node, mclass)
                return mclass
@@ -522,7 +529,6 @@ class NeoModel
        private fun mclassdef_node(mclassdef: MClassDef): NeoNode do
                var node = make_node(mclassdef)
                node.labels.add "MClassDef"
-               node["location"] = mclassdef.location.to_s
                node.out_edges.add(new NeoEdge(node, "BOUNDTYPE", to_node(mclassdef.bound_mtype)))
                node.out_edges.add(new NeoEdge(node, "MCLASS", to_node(mclassdef.mclass)))
                for mproperty in mclassdef.intro_mproperties do
@@ -590,18 +596,19 @@ class NeoModel
                assert node.labels.has("MProperty")
                var intro_mclassdef = to_mclassdef(model, node.out_nodes("INTRO_CLASSDEF").first)
                var name = node["name"].to_s
+               var location = to_location(node["location"].to_s)
                var visibility = to_visibility(node["visibility"].to_s)
                var mprop: nullable MProperty = null
                if node.labels.has("MMethod") then
-                       mprop = new MMethod(intro_mclassdef, name, visibility)
+                       mprop = new MMethod(intro_mclassdef, name, location, visibility)
                        mprop.is_init = node["is_init"].as(Bool)
                else if node.labels.has("MAttribute") then
-                       mprop = new MAttribute(intro_mclassdef, name, visibility)
+                       mprop = new MAttribute(intro_mclassdef, name, location, visibility)
                else if node.labels.has("MVirtualTypeProp") then
-                       mprop = new MVirtualTypeProp(intro_mclassdef, name, visibility)
+                       mprop = new MVirtualTypeProp(intro_mclassdef, name, location, visibility)
                else if node.labels.has("MInnerClass") then
                        var inner = to_mclass(model, node.out_nodes("NESTS").first)
-                       mprop = new MInnerClass(intro_mclassdef, name, visibility, inner)
+                       mprop = new MInnerClass(intro_mclassdef, name, location, visibility, inner)
                end
                if mprop == null then
                        print "not yet implemented to_mproperty for {node.labels.join(",")}"
@@ -616,7 +623,6 @@ class NeoModel
        private fun mpropdef_node(mpropdef: MPropDef): NeoNode do
                var node = make_node(mpropdef)
                node.labels.add "MPropDef"
-               node["location"] = mpropdef.location.to_s
                node.out_edges.add(new NeoEdge(node, "DEFINES", to_node(mpropdef.mproperty)))
                if mpropdef isa MMethodDef then
                        node.labels.add "MMethodDef"
index b3dcd32..5c478f0 100644 (file)
@@ -315,6 +315,15 @@ redef class Catalog
                end
                res.add "</ul>\n"
 
+               res.add "<h3>Quality</h3>\n<ul class=\"box\">\n"
+               var errors = errors[mpackage]
+               if errors > 0 then
+                       res.add "<li>{errors} errors</li>\n"
+               end
+               res.add "<li>{warnings[mpackage]} warnings ({warnings_per_kloc[mpackage]}/kloc)</li>\n"
+               res.add "<li>{documentation_score[mpackage]}% documented</li>\n"
+               res.add "</ul>\n"
+
                res.add "<h3>Tags</h3>\n"
                var ts2 = new Array[String]
                for t in mpackage.tags do
@@ -474,6 +483,10 @@ redef class Catalog
                res.add "<th data-field=\"met\" data-sortable=\"true\">methods</th>\n"
                res.add "<th data-field=\"loc\" data-sortable=\"true\">lines</th>\n"
                res.add "<th data-field=\"score\" data-sortable=\"true\">score</th>\n"
+               res.add "<th data-field=\"errors\" data-sortable=\"true\">errors</th>\n"
+               res.add "<th data-field=\"warnings\" data-sortable=\"true\">warnings</th>\n"
+               res.add "<th data-field=\"warnings_per_kloc\" data-sortable=\"true\">w/kloc</th>\n"
+               res.add "<th data-field=\"doc\" data-sortable=\"true\">doc</th>\n"
                res.add "</tr></thead>"
                for p in mpackages do
                        res.add "<tr>"
@@ -493,6 +506,10 @@ redef class Catalog
                        res.add "<td>{mmethods[p]}</td>"
                        res.add "<td>{loc[p]}</td>"
                        res.add "<td>{score[p]}</td>"
+                       res.add "<td>{errors[p]}</td>"
+                       res.add "<td>{warnings[p]}</td>"
+                       res.add "<td>{warnings_per_kloc[p]}</td>"
+                       res.add "<td>{documentation_score[p]}</td>"
                        res.add "</tr>\n"
                end
                res.add "</table>\n"
index 165ea13..176ec5f 100644 (file)
@@ -41,7 +41,7 @@ var args = toolcontext.option_context.rest
 var mmodules = modelbuilder.parse_full(args)
 modelbuilder.run_phases
 
-if opt_full.value then mmodules = model.mmodules
+if opt_full.value then mmodules = modelbuilder.parsed_modules
 
 var dir = opt_dir.value
 if dir != null then
index 522591d..8d79e80 100644 (file)
@@ -20,7 +20,7 @@ import testing
 
 var toolcontext = new ToolContext
 
-toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show)
+toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show, toolcontext.opt_nitc)
 toolcontext.tooldescription = "Usage: nitunit [OPTION]... <file.nit>...\nExecutes the unit tests from Nit source files."
 
 toolcontext.process_options(args)
@@ -63,6 +63,14 @@ if toolcontext.opt_gen_unit.value then
        exit(0)
 end
 
+"NIT_TESTING".setenv("true")
+"NIT_TESTING_ID".setenv(pid.to_s)
+"SRAND".setenv("0")
+
+var test_dir = toolcontext.test_dir
+test_dir.mkdir
+"# This file prevents the Nit modules of the directory to be part of the package".write_to_file(test_dir / "packages.ini")
+
 var page = new HTMLTag("testsuites")
 
 if toolcontext.opt_full.value then mmodules = model.mmodules
@@ -90,19 +98,62 @@ end
 var file = toolcontext.opt_output.value
 if file == null then file = "nitunit.xml"
 page.write_to_file(file)
-# print docunits results
-print "DocUnits:"
-if modelbuilder.unit_entities == 0 then
-       print "No doc units found"
-else if modelbuilder.failed_entities == 0 and not toolcontext.opt_noact.value then
-       print "DocUnits Success"
+
+# Print results
+printn "Docunits: Entities: {modelbuilder.total_entities}; Documented ones: {modelbuilder.doc_entities}; With nitunits: {modelbuilder.unit_entities}"
+if modelbuilder.unit_entities == 0 or toolcontext.opt_noact.value then
+       print ""
+else
+       printn "; Failures: "
+       var cpt = modelbuilder.failed_entities
+       if toolcontext.opt_no_color.value then
+               print cpt
+       else if cpt == 0 then
+               print "0".green.bold
+       else
+               print cpt.to_s.red.bold
+       end
 end
-print "Entities: {modelbuilder.total_entities}; Documented ones: {modelbuilder.doc_entities}; With nitunits: {modelbuilder.unit_entities}; Failures: {modelbuilder.failed_entities}"
-# print testsuites results
-print "\nTestSuites:"
-if modelbuilder.total_tests == 0 then
-       print "No test cases found"
-else if modelbuilder.failed_tests == 0 and not toolcontext.opt_noact.value then
-       print "TestSuites Success"
+printn "Test suites: Classes: {modelbuilder.total_classes}; Test Cases: {modelbuilder.total_tests}"
+if modelbuilder.total_tests == 0 or toolcontext.opt_noact.value then
+       print ""
+else
+       printn "; Failures: "
+       var cpt = modelbuilder.failed_tests
+       if toolcontext.opt_no_color.value then
+               print cpt
+       else if cpt == 0 then
+               print "0".green.bold
+       else
+               print cpt.to_s.red.bold
+       end
 end
-print "Class suites: {modelbuilder.total_classes}; Test Cases: {modelbuilder.total_tests}; Failures: {modelbuilder.failed_tests}"
+
+var total = modelbuilder.unit_entities + modelbuilder.total_tests
+var fail = modelbuilder.failed_entities + modelbuilder.failed_tests
+if toolcontext.opt_noact.value then
+       # nothing
+else if total == 0 then
+       var head = "[NOTHING]"
+       if not toolcontext.opt_no_color.value then
+               head = head.yellow
+       end
+       print "{head} No unit tests to execute."
+else if fail == 0 then
+       var head = "[SUCCESS]"
+       if not toolcontext.opt_no_color.value then
+               head = head.green.bold
+       end
+       print "{head} All {total} tests passed."
+else
+       var head = "[FAILURE]"
+       if not toolcontext.opt_no_color.value then
+               head = head.red.bold
+       end
+       print "{head} {fail}/{total} tests failed."
+
+       print "`{test_dir}` is not removed for investigation."
+       exit 1
+end
+
+test_dir.rmdir
index b924ec2..93d15aa 100644 (file)
@@ -47,15 +47,13 @@ private class NitwebPhase
                var host = toolcontext.opt_host.value or else "localhost"
                var port = toolcontext.opt_port.value
 
-               var srv = new NitServer(host, port.to_i)
-               srv.routes.add new Route("/random", new RandomAction(srv, model))
-               srv.routes.add new Route("/doc/:namespace", new DocAction(srv, model, modelbuilder))
-               srv.routes.add new Route("/code/:namespace", new CodeAction(srv, model, modelbuilder))
-               srv.routes.add new Route("/search/:namespace", new SearchAction(srv, model))
-               srv.routes.add new Route("/uml/:namespace", new UMLDiagramAction(srv, model, mainmodule))
-               srv.routes.add new Route("/", new TreeAction(srv, model))
-
-               srv.listen
+               var app = new App
+
+               app.use("/api", new APIRouter(model, modelbuilder, mainmodule))
+               app.use("/doc/:namespace", new DocAction(model, mainmodule, modelbuilder))
+               app.use("/*", new StaticHandler(toolcontext.share_dir / "nitweb", "index.html"))
+
+               app.listen(host, port.to_i)
        end
 end
 
index 64ceecf..73ebcd0 100644 (file)
@@ -1,6 +1,6 @@
 Parser and AST for the Nit language
 
-The parser ans the AST are mostly used by all tools.
+The parser and the AST are mostly used by all tools.
 
 The `parser` is the tool that transform source-files into abstract syntax trees (AST) (see `parser_nodes`)
 While the AST is a higher abstraction than blob of text, the AST is still limited and represents only *What the programmer says*.
index 62fbbb0..1d60f15 100644 (file)
@@ -88,8 +88,6 @@ redef class AAnnotation
                        return ""
                else
                        for arg in args do
-                               var format_error = "Syntax Eror: `{name}` expects its arguments to be of type Int or a call to `git_revision`."
-
                                var value
                                value = arg.as_int
                                if value != null then
@@ -107,7 +105,13 @@ redef class AAnnotation
                                        # Get Git short revision
                                        var proc = new ProcessReader("git", "rev-parse", "--short", "HEAD")
                                        proc.wait
-                                       assert proc.status == 0
+                                       if proc.status != 0 then
+                                               # Fallback if this is not a git repository or git bins are missing
+                                               version_fields.add "0"
+                                               modelbuilder.warning(self, "git_revision", "Warning: `git_revision` used outside of a git repository or git binaries not available")
+                                               continue
+                                       end
+
                                        var lines = proc.read_all
                                        var revision = lines.split("\n").first
 
@@ -122,6 +126,7 @@ redef class AAnnotation
                                        continue
                                end
 
+                               var format_error = "Syntax Error: `{name}` expects its arguments to be of type Int or a call to `git_revision`."
                                modelbuilder.error(self, format_error)
                                return ""
                        end
index c072314..88e2b7e 100644 (file)
@@ -255,6 +255,8 @@ class RapidTypeAnalysis
                                add_cast(paramtype)
                        end
 
+                       if mmethoddef.is_abstract then continue
+
                        var npropdef = modelbuilder.mpropdef2node(mmethoddef)
 
                        if npropdef isa AClassdef then
@@ -314,7 +316,9 @@ class RapidTypeAnalysis
                                if not check_depth(rt) then continue
                                #print "{ot}/{t} -> {rt}"
                                live_types.add(rt)
-                               todo_types.add(rt)
+                               # unshift means a deep-first visit.
+                               # So that the `check_depth` limit is reached sooner.
+                               todo_types.unshift(rt)
                        end
                end
                #print "MType {live_types.length}: {live_types.join(", ")}"
index 69329f9..6c4de01 100644 (file)
@@ -637,8 +637,7 @@ end
 class CallSite
        super MEntity
 
-       # The associated location of the callsite
-       var location: Location
+       redef var location: Location
 
        # The static type of the receiver (possibly unresolved)
        var recv: MType
index b46af5c..d7cc947 100644 (file)
@@ -117,6 +117,6 @@ do
                var c = ""
                var d = e.mdoc_or_fallback
                if d != null and d.content.not_empty then c = d.content.first
-               print "{n}\t{e.class_name}\t{c}"
+               print "{n}\t{e.class_name}\t{e.location}\t{c}"
        end
 end
index 62cfb08..ddc167f 100644 (file)
@@ -17,6 +17,8 @@ module testing_base
 
 import modelize
 private import parser_util
+import html
+import console
 
 redef class ToolContext
        # opt --full
@@ -27,11 +29,256 @@ redef class ToolContext
        var opt_dir = new OptionString("Working directory (default is '.nitunit')", "--dir")
        # opt --no-act
        var opt_noact = new OptionBool("Does not compile and run tests", "--no-act")
+       # opt --nitc
+       var opt_nitc = new OptionString("nitc compiler to use", "--nitc")
 
        # Working directory for testing.
        fun test_dir: String do
                var dir = opt_dir.value
-               if dir == null then return ".nitunit"
+               if dir == null then return "nitunit.out"
                return dir
        end
+
+       # Search the `nitc` compiler to use
+       #
+       # If not `nitc` is suitable, then prints an error and quit.
+       fun find_nitc: String
+       do
+               var nitc = opt_nitc.value
+               if nitc != null then
+                       if not nitc.file_exists then
+                               fatal_error(null, "error: cannot find `{nitc}` given by --nitc.")
+                               abort
+                       end
+                       return nitc
+               end
+
+               nitc = "NITC".environ
+               if nitc != "" then
+                       if not nitc.file_exists then
+                               fatal_error(null, "error: cannot find `{nitc}` given by NITC.")
+                               abort
+                       end
+                       return nitc
+               end
+
+               var nit_dir = nit_dir
+               nitc = nit_dir/"bin/nitc"
+               if not nitc.file_exists then
+                       fatal_error(null, "Error: cannot find nitc. Set envvar NIT_DIR or NITC or use the --nitc option.")
+                       abort
+               end
+               return nitc
+       end
+
+       # Execute a system command in a more safe context than `Sys::system`.
+       fun safe_exec(command: String): Int
+       do
+               info(command, 2)
+               var real_command = """
+bash -c "
+ulimit -f {{{ulimit_file}}} 2> /dev/null
+ulimit -t {{{ulimit_usertime}}} 2> /dev/null
+{{{command}}}
+"
+"""
+               return system(real_command)
+       end
+
+       # The maximum size (in KB) of files written by a command executed trough `safe_exec`
+       #
+       # Default: 64MB
+       var ulimit_file = 65536 is writable
+
+       # The maximum amount of cpu time (in seconds) for a command executed trough `safe_exec`
+       #
+       # Default: 10 CPU minute
+       var ulimit_usertime = 600 is writable
+
+       # Show a single-line status to use as a progression.
+       #
+       # Note that the line starts with `'\r'` and is not ended by a `'\n'`.
+       # So it is expected that:
+       # * no other output is printed between two calls
+       # * the last `show_unit_status` is followed by a new-line
+       fun show_unit_status(name: String, tests: SequenceRead[UnitTest], more_message: nullable String)
+       do
+               var esc = 27.code_point.to_s
+               var line = "\r{esc}[K* {name} ["
+               var done = tests.length
+               for t in tests do
+                       if not t.is_done then
+                               line += " "
+                               done -= 1
+                       else if t.error == null then
+                               line += ".".green.bold
+                       else
+                               line += "X".red.bold
+                       end
+               end
+
+               if opt_no_color.value then
+                       if done == 0 then
+                               print "* {name} ({tests.length} tests)"
+                       end
+                       return
+               end
+
+               line += "] {done}/{tests.length}"
+               if more_message != null then
+                       line += " " + more_message
+               end
+               printn "{line}"
+       end
+
+       # Shoe the full description of the test-case.
+       #
+       # The output honors `--no-color`.
+       #
+       # `more message`, if any, is added after the error message.
+       fun show_unit(test: UnitTest, more_message: nullable String) do
+               print test.to_screen(more_message, not opt_no_color.value)
+       end
+end
+
+# A unit test is an elementary test discovered, run and reported by nitunit.
+#
+# This class factorizes `DocUnit` and `TestCase`.
+abstract class UnitTest
+       # The name of the unit to show in messages
+       fun full_name: String is abstract
+
+       # The location of the unit test to show in messages.
+       fun location: Location is abstract
+
+       # Flag that indicates if the unit test was compiled/run.
+       var is_done: Bool = false is writable
+
+       # Error message occurred during test-case execution (or compilation).
+       #
+       # e.g.: `Runtime Error`
+       var error: nullable String = null is writable
+
+       # Was the test case executed at least once?
+       #
+       # This will indicate the status of the test (failture or error)
+       var was_exec = false is writable
+
+       # The raw output of the execution (or compilation)
+       #
+       # It merges the standard output and error output
+       var raw_output: nullable String = null is writable
+
+       # The location where the error occurred, if it makes sense.
+       var error_location: nullable Location = null is writable
+
+       # A colorful `[OK]` or `[KO]`.
+       fun status_tag(color: nullable Bool): String do
+               color = color or else true
+               if not is_done then
+                       return "[  ]"
+               else if error != null then
+                       var res = "[KO]"
+                       if color then res = res.red.bold
+                       return res
+               else
+                       var res = "[OK]"
+                       if color then res = res.green.bold
+                       return res
+               end
+       end
+
+       # The full (color) description of the test-case.
+       #
+       # `more message`, if any, is added after the error message.
+       fun to_screen(more_message: nullable String, color: nullable Bool): String do
+               color = color or else true
+               var res
+               var error = self.error
+               if error != null then
+                       if more_message != null then error += " " + more_message
+                       var loc = error_location or else location
+                       if color then
+                               res = "{status_tag(color)} {full_name}\n     {loc.to_s.yellow}: {error}\n{loc.colored_line("1;31")}"
+                       else
+                               res = "{status_tag(color)} {full_name}\n     {loc}: {error}"
+                       end
+                       var output = self.raw_output
+                       if output != null then
+                               res += "\n     Output\n\t{output.chomp.replace("\n", "\n\t")}\n"
+                       end
+               else
+                       res = "{status_tag(color)} {full_name}"
+                       if more_message != null then res += more_message
+               end
+               return res
+       end
+
+       # Return a `<testcase>` XML node in format compatible with Jenkins unit tests.
+       fun to_xml: HTMLTag do
+               var tc = new HTMLTag("testcase")
+               tc.attr("classname", xml_classname)
+               tc.attr("name", xml_name)
+               var error = self.error
+               if error != null then
+                       if was_exec then
+                               tc.open("error").append(error)
+                       else
+                               tc.open("failure").append(error)
+                       end
+               end
+               var output = self.raw_output
+               if output != null then
+                       tc.open("system-err").append(output.trunc(8192).filter_nonprintable)
+               end
+               return tc
+       end
+
+       # The `classname` attribute of the XML format.
+       #
+       # NOTE: jenkins expects a '.' in the classname attr
+       #
+       # See to_xml
+       fun xml_classname: String is abstract
+
+       # The `name` attribute of the XML format.
+       #
+       # See to_xml
+       fun xml_name: String is abstract
+end
+
+redef class String
+       # If needed, truncate `self` at `max_length` characters and append an informative `message`.
+       #
+       # ~~~
+       # assert "hello".trunc(10) == "hello"
+       # assert "hello".trunc(2) == "he[truncated. Full size is 5]"
+       # assert "hello".trunc(2, "...") == "he..."
+       # ~~~
+       fun trunc(max_length: Int, message: nullable String): String
+       do
+               if length <= max_length then return self
+               if message == null then message = "[truncated. Full size is {length}]"
+               return substring(0, max_length) + message
+       end
+
+       # Use a special notation for whitespace characters that are not `'\n'` (LFD) or `' '` (space).
+       #
+       # ~~~
+       # assert "hello".filter_nonprintable == "hello"
+       # assert "\r\n\t".filter_nonprintable == "^13\n^9"
+       # ~~~
+       fun filter_nonprintable: String
+       do
+               var buf = new Buffer
+               for c in self do
+                       var cp = c.code_point
+                       if cp < 32 and c != '\n' then
+                               buf.append "^{cp}"
+                       else
+                               buf.add c
+                       end
+               end
+               return buf.to_s
+       end
 end
index 2c0e456..a2fa067 100644 (file)
@@ -36,11 +36,8 @@ class NitUnitExecutor
        # The XML node associated to the module
        var testsuite: HTMLTag
 
-       # All blocks of code from a same `ADoc`
-       var blocks = new Array[Buffer]
-
-       # All failures from a same `ADoc`
-       var failures = new Array[String]
+       # The name of the suite
+       var name: String
 
        # Markdown processor used to parse markdown comments and extract code.
        var mdproc = new MarkdownProcessor
@@ -55,47 +52,59 @@ class NitUnitExecutor
        # used to generate distinct names
        var cpt = 0
 
+       # The last docunit extracted from a mdoc.
+       #
+       # Is used because a new code-block might just be added to it.
+       var last_docunit: nullable DocUnit = null
+
+       var xml_classname: String is noautoinit
+
+       var xml_name: String is noautoinit
+
        # The entry point for a new `ndoc` node
        # Fill `docunits` with new discovered unit of tests.
-       #
-       # `tc` (testcase) is the pre-filled XML node
-       fun extract(mdoc: MDoc, tc: HTMLTag)
+       fun extract(mdoc: MDoc, xml_classname, xml_name: String)
        do
-               blocks.clear
-               failures.clear
+               last_docunit = null
+               self.xml_classname = xml_classname
+               self.xml_name = xml_name
 
                self.mdoc = mdoc
 
                # Populate `blocks` from the markdown decorator
                mdproc.process(mdoc.content.join("\n"))
-
-               toolcontext.check_errors
-
-               if not failures.is_empty then
-                       for msg in failures do
-                               var ne = new HTMLTag("failure")
-                               ne.attr("message", msg)
-                               tc.add ne
-                               toolcontext.modelbuilder.unit_entities += 1
-                               toolcontext.modelbuilder.failed_entities += 1
-                       end
-                       if blocks.is_empty then testsuite.add(tc)
-               end
-
-               if blocks.is_empty then return
-               for block in blocks do
-                       docunits.add new DocUnit(mdoc, tc, block.write_to_string)
-               end
        end
 
        # All extracted docunits
        var docunits = new Array[DocUnit]
 
+       fun show_status(more_message: nullable String)
+       do
+               toolcontext.show_unit_status(name, docunits, more_message)
+       end
+
+       fun mark_done(du: DocUnit)
+       do
+               du.is_done = true
+               show_status(du.full_name + " " + du.status_tag)
+       end
+
        # Execute all the docunits
        fun run_tests
        do
+               if docunits.is_empty then
+                       return
+               end
+
                var simple_du = new Array[DocUnit]
+               show_status
                for du in docunits do
+                       # Skip existing errors
+                       if du.error != null then
+                               mark_done(du)
+                               continue
+                       end
+
                        var ast = toolcontext.parse_something(du.block)
                        if ast isa AExpr then
                                simple_du.add du
@@ -105,6 +114,17 @@ class NitUnitExecutor
                end
 
                test_simple_docunits(simple_du)
+
+               show_status
+               print ""
+
+               for du in docunits do
+                       toolcontext.show_unit(du)
+               end
+
+               for du in docunits do
+                       testsuite.add du.to_xml
+               end
        end
 
        # Executes multiples doc-units in a shared program.
@@ -128,7 +148,7 @@ class NitUnitExecutor
 
                        i += 1
                        f.write("fun run_{i} do\n")
-                       f.write("# {du.testcase.attrs["name"]}\n")
+                       f.write("# {du.full_name}\n")
                        f.write(du.block)
                        f.write("end\n")
                end
@@ -153,34 +173,19 @@ class NitUnitExecutor
 
                i = 0
                for du in dus do
-                       var tc = du.testcase
-                       toolcontext.modelbuilder.unit_entities += 1
                        i += 1
-                       toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1)
-                       var res2 = sys.system("{file.to_program_name}.bin {i} >>'{file}.out1' 2>&1 </dev/null")
-
-                       var msg
-                       f = new FileReader.open("{file}.out1")
-                       var n2
-                       n2 = new HTMLTag("system-err")
-                       tc.add n2
-                       msg = f.read_all
-                       f.close
+                       toolcontext.info("Execute doc-unit {du.full_name} in {file} {i}", 1)
+                       var res2 = toolcontext.safe_exec("{file.to_program_name}.bin {i} >'{file}.out1' 2>&1 </dev/null")
+                       du.was_exec = true
 
-                       n2 = new HTMLTag("system-out")
-                       tc.add n2
-                       n2.append(du.block)
+                       var content = "{file}.out1".to_path.read_all
+                       du.raw_output = content
 
                        if res2 != 0 then
-                               var ne = new HTMLTag("error")
-                               ne.attr("message", msg)
-                               tc.add ne
-                               toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                               du.error = "Runtime error in {file} with argument {i}"
                                toolcontext.modelbuilder.failed_entities += 1
                        end
-                       toolcontext.check_errors
-
-                       testsuite.add(tc)
+                       mark_done(du)
                end
        end
 
@@ -188,13 +193,10 @@ class NitUnitExecutor
        # Used for docunits larger than a single block of code (with modules, classes, functions etc.)
        fun test_single_docunit(du: DocUnit)
        do
-               var tc = du.testcase
-               toolcontext.modelbuilder.unit_entities += 1
-
                cpt += 1
                var file = "{prefix}-{cpt}.nit"
 
-               toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
+               toolcontext.info("Execute doc-unit {du.full_name} in {file}", 1)
 
                var f
                f = create_unitfile(file)
@@ -206,38 +208,21 @@ class NitUnitExecutor
                var res = compile_unitfile(file)
                var res2 = 0
                if res == 0 then
-                       res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
+                       res2 = toolcontext.safe_exec("{file.to_program_name}.bin >'{file}.out1' 2>&1 </dev/null")
+                       du.was_exec = true
                end
 
-               var msg
-               f = new FileReader.open("{file}.out1")
-               var n2
-               n2 = new HTMLTag("system-err")
-               tc.add n2
-               msg = f.read_all
-               f.close
-
-               n2 = new HTMLTag("system-out")
-               tc.add n2
-               n2.append(du.block)
-
+               var content = "{file}.out1".to_path.read_all
+               du.raw_output = content
 
                if res != 0 then
-                       var ne = new HTMLTag("failure")
-                       ne.attr("message", msg)
-                       tc.add ne
-                       toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       du.error = "Compilation error in {file}"
                        toolcontext.modelbuilder.failed_entities += 1
                else if res2 != 0 then
-                       var ne = new HTMLTag("error")
-                       ne.attr("message", msg)
-                       tc.add ne
-                       toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       du.error = "Runtime error in {file}"
                        toolcontext.modelbuilder.failed_entities += 1
                end
-               toolcontext.check_errors
-
-               testsuite.add(tc)
+               mark_done(du)
        end
 
        # Create and fill the header of a unit file `file`.
@@ -268,18 +253,13 @@ class NitUnitExecutor
        # Can terminate the program if the compiler is not found
        private fun compile_unitfile(file: String): Int
        do
-               var nit_dir = toolcontext.nit_dir
-               var nitc = nit_dir/"bin/nitc"
-               if not nitc.file_exists then
-                       toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.")
-                       toolcontext.check_errors
-               end
+               var nitc = toolcontext.find_nitc
                var opts = new Array[String]
                if mmodule != null then
                        opts.add "-I {mmodule.filepath.dirname}"
                end
                var cmd = "{nitc} --ignore-visibility --no-color '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
-               var res = sys.system(cmd)
+               var res = toolcontext.safe_exec(cmd)
                return res
        end
 end
@@ -297,39 +277,161 @@ private class NitunitDecorator
                # Try to parse code blocks
                var ast = executor.toolcontext.parse_something(code)
 
+               var mdoc = executor.mdoc
+               assert mdoc != null
+
                # Skip pure comments
                if ast isa TComment then return
 
+               # The location is computed according to the starts of the mdoc and the block
+               # Note, the following assumes that all the comments of the mdoc are correctly aligned.
+               var loc = block.block.location
+               var line_offset = loc.line_start + mdoc.location.line_start - 2
+               var column_offset = loc.column_start + mdoc.location.column_start
+               # Hack to handle precise location in blocks
+               # TODO remove when markdown is more reliable
+               if block isa BlockFence then
+                       # Skip the starting fence
+                       line_offset += 1
+               else
+                       # Account a standard 4 space indentation
+                       column_offset += 4
+               end
+
                # We want executable code
                if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
-                       var message = ""
-                       if ast isa AError then message = " At {ast.location}: {ast.message}."
-                       executor.toolcontext.warning(executor.mdoc.location, "invalid-block", "Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).{message}")
-                       executor.failures.add("{executor.mdoc.location}: Invalid block of code.{message}")
+                       var message
+                       var l = ast.location
+                       # Get real location of the node (or error)
+                       var location = new Location(mdoc.location.file,
+                               l.line_start + line_offset,
+                               l.line_end + line_offset,
+                               l.column_start + column_offset,
+                               l.column_end + column_offset)
+                       if ast isa AError then
+                               message = ast.message
+                       else
+                               message = "Error: Invalid Nit code."
+                       end
+
+                       var du = new_docunit
+                       du.block += code
+                       du.error_location = location
+                       du.error = message
+                       executor.toolcontext.modelbuilder.failed_entities += 1
                        return
                end
 
                # Create a first block
                # Or create a new block for modules that are more than a main part
-               if executor.blocks.is_empty or ast isa AModule then
-                       executor.blocks.add(new Buffer)
+               var last_docunit = executor.last_docunit
+               if last_docunit == null or ast isa AModule then
+                       last_docunit = new_docunit
+                       executor.last_docunit = last_docunit
                end
 
                # Add it to the file
-               executor.blocks.last.append code
+               last_docunit.block += code
+
+               # In order to retrieve precise positions,
+               # the real position of each line of the raw_content is stored.
+               # See `DocUnit::real_location`
+               line_offset -= loc.line_start - 1
+               for i in [loc.line_start..loc.line_end] do
+                       last_docunit.lines.add i + line_offset
+                       last_docunit.columns.add column_offset
+               end
+       end
+
+       # Return and register a new empty docunit
+       fun new_docunit: DocUnit
+       do
+               var mdoc = executor.mdoc
+               assert mdoc != null
+
+               var next_number = 0
+               var name = executor.xml_name
+               if executor.docunits.not_empty and executor.docunits.last.mdoc == mdoc then
+                       next_number = executor.docunits.last.number + 1
+                       name += "+" + next_number.to_s
+               end
+
+               var res = new DocUnit(mdoc, next_number, "", executor.xml_classname, name)
+               executor.docunits.add res
+               executor.toolcontext.modelbuilder.unit_entities += 1
+               return res
        end
 end
 
-# A unit-test to run
+# A unit-test extracted from some documentation.
+#
+# A docunit is extracted from the code-blocks of mdocs.
+# Each mdoc can contains more than one docunit, and a single docunit can be made of more that a single code-block.
 class DocUnit
+       super UnitTest
+
        # The doc that contains self
        var mdoc: MDoc
 
-       # The XML node that contains the information about the execution
-       var testcase: HTMLTag
+       # The numbering of self in mdoc (starting with 0)
+       var number: Int
+
+       redef fun full_name do
+               var mentity = mdoc.original_mentity
+               if mentity != null then
+                       return mentity.full_name
+               else
+                       return xml_classname + "." + xml_name
+               end
+       end
 
-       # The text of the code to execute
+       # The text of the code to execute.
+       #
+       # This is the verbatim content on one, or more, code-blocks from `mdoc`
        var block: String
+
+       # For each line in `block`, the associated line in the mdoc
+       #
+       # Is used to give precise locations
+       var lines = new Array[Int]
+
+       # For each line in `block`, the associated column in the mdoc
+       #
+       # Is used to give precise locations
+       var columns = new Array[Int]
+
+       # The location of the whole docunit.
+       #
+       # If `self` is made of multiple code-blocks, then the location
+       # starts at the first code-books and finish at the last one, thus includes anything between.
+       redef var location is lazy do
+               return new Location(mdoc.location.file, lines.first, lines.last+1, columns.first+1, 0)
+       end
+
+       # Compute the real location of a node on the `ast` based on `mdoc.location`
+       #
+       # The result is basically: ast_location + markdown location of the piece + mdoc.location
+       #
+       # The fun is that a single docunit can be made of various pieces of code blocks.
+       fun real_location(ast_location: Location): Location
+       do
+               var mdoc = self.mdoc
+               var res = new Location(mdoc.location.file, lines[ast_location.line_start-1],
+                       lines[ast_location.line_end-1],
+                       columns[ast_location.line_start-1] + ast_location.column_start,
+                       columns[ast_location.line_end-1] + ast_location.column_end)
+               return res
+       end
+
+       redef fun to_xml
+       do
+               var res = super
+               res.open("system-out").append(block)
+               return res
+       end
+
+       redef var xml_classname
+       redef var xml_name
 end
 
 redef class ModelBuilder
@@ -368,9 +470,7 @@ redef class ModelBuilder
 
                var prefix = toolcontext.test_dir
                prefix = prefix.join_path(mmodule.to_s)
-               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
-
-               var tc
+               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of module {mmodule.full_name}")
 
                do
                        total_entities += 1
@@ -379,11 +479,8 @@ redef class ModelBuilder
                        var ndoc = nmoduledecl.n_doc
                        if ndoc == null then break label x
                        doc_entities += 1
-                       tc = new HTMLTag("testcase")
                        # NOTE: jenkins expects a '.' in the classname attr
-                       tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
-                       tc.attr("name", "<module>")
-                       d2m.extract(ndoc.to_mdoc, tc)
+                       d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + ".<module>", "<module>")
                end label x
                for nclassdef in nmodule.n_classdefs do
                        var mclassdef = nclassdef.mclassdef
@@ -393,10 +490,7 @@ redef class ModelBuilder
                                var ndoc = nclassdef.n_doc
                                if ndoc != null then
                                        doc_entities += 1
-                                       tc = new HTMLTag("testcase")
-                                       tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
-                                       tc.attr("name", "<class>")
-                                       d2m.extract(ndoc.to_mdoc, tc)
+                                       d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, "<class>")
                                end
                        end
                        for npropdef in nclassdef.n_propdefs do
@@ -406,10 +500,7 @@ redef class ModelBuilder
                                var ndoc = npropdef.n_doc
                                if ndoc != null then
                                        doc_entities += 1
-                                       tc = new HTMLTag("testcase")
-                                       tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
-                                       tc.attr("name", mpropdef.mproperty.full_name)
-                                       d2m.extract(ndoc.to_mdoc, tc)
+                                       d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, mpropdef.mproperty.full_name)
                                end
                        end
                end
@@ -433,20 +524,15 @@ redef class ModelBuilder
 
                var prefix = toolcontext.test_dir
                prefix = prefix.join_path(mgroup.to_s)
-               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
-
-               var tc
+               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of group {mgroup.full_name}")
 
                total_entities += 1
                var mdoc = mgroup.mdoc
                if mdoc == null then return ts
 
                doc_entities += 1
-               tc = new HTMLTag("testcase")
                # NOTE: jenkins expects a '.' in the classname attr
-               tc.attr("classname", "nitunit." + mgroup.full_name)
-               tc.attr("name", "<group>")
-               d2m.extract(mdoc, tc)
+               d2m.extract(mdoc, "nitunit." + mgroup.full_name, "<group>")
 
                d2m.run_tests
 
@@ -464,19 +550,13 @@ redef class ModelBuilder
                ts.attr("package", file)
 
                var prefix = toolcontext.test_dir / "file"
-               var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
-
-               var tc
+               var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts, "Docunits of file {file}")
 
                total_entities += 1
                doc_entities += 1
 
-               tc = new HTMLTag("testcase")
                # NOTE: jenkins expects a '.' in the classname attr
-               tc.attr("classname", "nitunit.<file>")
-               tc.attr("name", file)
-
-               d2m.extract(mdoc, tc)
+               d2m.extract(mdoc, "nitunit.<file>", file)
                d2m.run_tests
 
                return ts
index 0de6e0a..f755d25 100644 (file)
@@ -132,8 +132,14 @@ class TestSuite
        # Test to be executed after the whole test suite.
        var after_module: nullable TestCase = null
 
+       fun show_status(more_message: nullable String)
+       do
+               toolcontext.show_unit_status("Test-suite of module " + mmodule.full_name, test_cases, more_message)
+       end
+
        # Execute the test suite
        fun run do
+               show_status
                if not toolcontext.test_dir.file_exists then
                        toolcontext.test_dir.mkdir
                end
@@ -142,9 +148,19 @@ class TestSuite
                toolcontext.info("Execute test-suite {mmodule.name}", 1)
                var before_module = self.before_module
                if not before_module == null then before_module.run
-               for case in test_cases do case.run
+               for case in test_cases do
+                       case.run
+                       show_status(case.full_name + " " + case.status_tag)
+               end
+
+               show_status
+               print ""
+
                var after_module = self.after_module
                if not after_module == null then after_module.run
+               for case in test_cases do
+                       toolcontext.show_unit(case)
+               end
        end
 
        # Write the test unit for `self` in a nit compilable file.
@@ -183,12 +199,7 @@ class TestSuite
        # Compile all `test_cases` cases in one file.
        fun compile do
                # find nitc
-               var nit_dir = toolcontext.nit_dir
-               var nitc = nit_dir/"bin/nitc"
-               if not nitc.file_exists then
-                       toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.")
-                       toolcontext.check_errors
-               end
+               var nitc = toolcontext.find_nitc
                # compile test suite
                var file = test_file
                var module_file = mmodule.location.file
@@ -199,7 +210,7 @@ class TestSuite
                end
                var include_dir = module_file.filename.dirname
                var cmd = "{nitc} --no-color '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 </dev/null"
-               var res = sys.system(cmd)
+               var res = toolcontext.safe_exec(cmd)
                var f = new FileReader.open("{file}.out")
                var msg = f.read_all
                f.close
@@ -219,6 +230,7 @@ end
 
 # A test case is a unit test considering only a `MMethodDef`.
 class TestCase
+       super UnitTest
 
        # Test suite wich `self` belongs to.
        var test_suite: TestSuite
@@ -226,6 +238,10 @@ class TestCase
        # Test method to be compiled and tested.
        var test_method: MMethodDef
 
+       redef fun full_name do return test_method.full_name
+
+       redef fun location do return test_method.location
+
        # `ToolContext` to use to display messages and find `nitc` bin.
        var toolcontext: ToolContext
 
@@ -253,47 +269,41 @@ class TestCase
                var method_name = test_method.name
                var test_file = test_suite.test_file
                var res_name = "{test_file}_{method_name.escape_to_c}"
-               var res = sys.system("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
-               var f = new FileReader.open("{res_name}.out1")
-               var msg = f.read_all
-               f.close
+               var res = toolcontext.safe_exec("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
+               self.raw_output = "{res_name}.out1".to_path.read_all
                # set test case result
-               var loc = test_method.location
                if res != 0 then
-                       error = msg
-                       toolcontext.warning(loc, "failure",
-                          "ERROR: {method_name} (in file {test_file}.nit): {msg}")
+                       error = "Runtime Error in file {test_file}.nit"
                        toolcontext.modelbuilder.failed_tests += 1
+               else
+                       # no error, check with res file, if any.
+                       var mmodule = test_method.mclassdef.mmodule
+                       var file = mmodule.filepath
+                       if file != null then
+                               var sav = file.dirname / mmodule.name + ".sav" / test_method.name + ".res"
+                               if sav.file_exists then
+                                       toolcontext.info("Diff output with {sav}", 1)
+                                       res = toolcontext.safe_exec("diff -u --label 'expected:{sav}' --label 'got:{res_name}.out1' '{sav}' '{res_name}.out1' > '{res_name}.diff' 2>&1 </dev/null")
+                                       if res != 0 then
+                                               self.raw_output = "Diff\n" + "{res_name}.diff".to_path.read_all
+                                               error = "Difference with expected output: diff -u {sav} {res_name}.out1"
+                                               toolcontext.modelbuilder.failed_tests += 1
+                                       end
+                               else
+                                       toolcontext.info("No diff: {sav} not found", 2)
+                               end
+                       end
                end
-               toolcontext.check_errors
+               is_done = true
        end
 
-       # Error occured during test-case execution.
-       var error: nullable String = null
-
-       # Was the test case executed at least one?
-       var was_exec = false
-
-       # Return the `TestCase` in XML format compatible with Jenkins.
-       fun to_xml: HTMLTag do
+       redef fun xml_classname do
                var mclassdef = test_method.mclassdef
-               var tc = new HTMLTag("testcase")
-               # NOTE: jenkins expects a '.' in the classname attr
-               tc.attr("classname", "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name)
-               tc.attr("name", test_method.mproperty.full_name)
-               if was_exec then
-                       tc.add  new HTMLTag("system-err")
-                       var n = new HTMLTag("system-out")
-                       n.append "out"
-                       tc.add n
-                       var error = self.error
-                       if error != null then
-                               n = new HTMLTag("error")
-                               n.attr("message", error.to_s)
-                               tc.add n
-                       end
-               end
-               return tc
+               return "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name
+       end
+
+       redef fun xml_name do
+               return test_method.mproperty.full_name
        end
 end
 
index 850781e..de15aed 100644 (file)
@@ -58,6 +58,13 @@ class Message
        # * enclose identifiers, keywords and pieces of code with back-quotes.
        var text: String
 
+       # The severity level
+       #
+       # * 0 is advices (see `ToolContext::advice`)
+       # * 1 is warnings (see `ToolContext::warning`)
+       # * 2 is errors (see `ToolContext::error`)
+       var level: Int
+
        # Comparisons are made on message locations.
        redef fun <(other: OTHER): Bool do
                if location == null then return true
@@ -123,9 +130,16 @@ redef class Location
                        messages = ms
                end
                ms.add m
+               var s = file
+               if s != null then s.messages.add m
        end
 end
 
+redef class SourceFile
+       # Errors and warnings associated to the whole source.
+       var messages = new Array[Message]
+end
+
 # Global context for tools
 class ToolContext
        # Number of errors
@@ -225,7 +239,7 @@ class ToolContext
        # Return the message (to add information)
        fun error(l: nullable Location, s: String): Message
        do
-               var m = new Message(l,null,s)
+               var m = new Message(l, null, s, 2)
                if messages.has(m) then return m
                if l != null then l.add_message m
                messages.add m
@@ -257,12 +271,12 @@ class ToolContext
        # Return the message (to add information) or null if the warning is disabled
        fun warning(l: nullable Location, tag: String, text: String): nullable Message
        do
-               if opt_warning.value.has("no-{tag}") then return null
-               if not opt_warning.value.has(tag) and opt_warn.value == 0 then return null
                if is_warning_blacklisted(l, tag) then return null
-               var m = new Message(l, tag, text)
+               var m = new Message(l, tag, text, 1)
                if messages.has(m) then return null
                if l != null then l.add_message m
+               if opt_warning.value.has("no-{tag}") then return null
+               if not opt_warning.value.has(tag) and opt_warn.value == 0 then return null
                messages.add m
                warning_count = warning_count + 1
                if opt_stop_on_first_error.value then check_errors
@@ -286,12 +300,12 @@ class ToolContext
        # Return the message (to add information) or null if the warning is disabled
        fun advice(l: nullable Location, tag: String, text: String): nullable Message
        do
-               if opt_warning.value.has("no-{tag}") then return null
-               if not opt_warning.value.has(tag) and opt_warn.value <= 1 then return null
                if is_warning_blacklisted(l, tag) then return null
-               var m = new Message(l, tag, text)
+               var m = new Message(l, tag, text, 0)
                if messages.has(m) then return null
                if l != null then l.add_message m
+               if opt_warning.value.has("no-{tag}") then return null
+               if not opt_warning.value.has(tag) and opt_warn.value <= 1 then return null
                messages.add m
                warning_count = warning_count + 1
                if opt_stop_on_first_error.value then check_errors
@@ -360,6 +374,9 @@ class ToolContext
        # Option --nit-dir
        var opt_nit_dir = new OptionString("Base directory of the Nit installation", "--nit-dir")
 
+       # Option --share-dir
+       var opt_share_dir = new OptionString("Directory containing tools assets", "--share-dir")
+
        # Option --help
        var opt_help = new OptionBool("Show Help (This screen)", "-h", "-?", "--help")
 
@@ -528,6 +545,20 @@ The Nit language documentation and the source code of its tools and libraries ma
        # The identified root directory of the Nit package
        var nit_dir: String is noinit
 
+       # Shared files directory.
+       #
+       # Most often `nit/share/`.
+       var share_dir: String is lazy do
+               var sharedir = opt_share_dir.value
+               if sharedir == null then
+                       sharedir = nit_dir / "share"
+                       if not sharedir.file_exists then
+                               fatal_error(null, "Fatal Error: cannot locate shared files directory in {sharedir}. Uses --share-dir to define it's location.")
+                       end
+               end
+               return sharedir
+       end
+
        private fun compute_nit_dir: String
        do
                # the option has precedence
diff --git a/src/web/model_api.nit b/src/web/model_api.nit
new file mode 100644 (file)
index 0000000..85a2360
--- /dev/null
@@ -0,0 +1,251 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module model_api
+
+import web_base
+import highlight
+import uml
+
+# Specific handler for nitweb API.
+abstract class APIHandler
+       super ModelHandler
+
+       # The JSON API does not filter anything by default.
+       #
+       # So we can cache the model view.
+       var view: ModelView is lazy do
+               var view = new ModelView(model)
+               view.min_visibility = private_visibility
+               view.include_fictive = true
+               view.include_empty_doc = true
+               view.include_attribute = true
+               view.include_test_suite = true
+               return view
+       end
+
+       # Try to load the mentity from uri with `/:id`.
+       #
+       # Send 400 if `:id` is null.
+       # Send 404 if no entity is found.
+       # Return null in both cases.
+       fun mentity_from_uri(req: HttpRequest, res: HttpResponse): nullable MEntity do
+               var id = req.param("id")
+               if id == null then
+                       res.error 400
+                       return null
+               end
+               var mentity = find_mentity(view, id)
+               if mentity == null then
+                       res.error 404
+               end
+               return mentity
+       end
+end
+
+# Group all api handlers in one router.
+class APIRouter
+       super Router
+
+       # Model to pass to handlers.
+       var model: Model
+
+       # ModelBuilder to pass to handlers.
+       var modelbuilder: ModelBuilder
+
+       # Mainmodule to pass to handlers.
+       var mainmodule: MModule
+
+       init do
+               use("/list", new APIList(model, mainmodule))
+               use("/search", new APISearch(model, mainmodule))
+               use("/random", new APIRandom(model, mainmodule))
+               use("/entity/:id", new APIEntity(model, mainmodule))
+               use("/code/:id", new APIEntityCode(model, mainmodule, modelbuilder))
+               use("/uml/:id", new APIEntityUML(model, mainmodule))
+       end
+end
+
+# Search mentities from a query string.
+#
+# Example: `GET /search?q=Arr`
+class APISearch
+       super APIHandler
+
+       redef fun get(req, res) do
+               var q = req.string_arg("q")
+               if q == null then
+                       res.error 400
+                       return
+               end
+               var arr = new JsonArray
+               for mentity in view.mentities do
+                       if mentity.name.has_prefix(q) then arr.add mentity
+               end
+               res.json arr
+       end
+end
+
+# List all mentities.
+#
+# MEntities can be filtered on their kind using the `k` parameter.
+# Allowed kinds are `package`, `group`, `module`, `class`, `classdef`, `property`, `propdef`.
+#
+# List size can be limited with the `n` parameter.
+#
+# Example: `GET /list?k=module?n=10`
+class APIList
+       super APIHandler
+
+       # List mentities depending on the `k` kind parameter.
+       fun list_mentities(req: HttpRequest): Array[MEntity] do
+               var k = req.string_arg("k")
+               var mentities = new Array[MEntity]
+               if k == "package" then
+                       for mentity in view.mpackages do mentities.add mentity
+               else if k == "group" then
+                       for mentity in view.mgroups do mentities.add mentity
+               else if k == "module" then
+                       for mentity in view.mmodules do mentities.add mentity
+               else if k == "class" then
+                       for mentity in view.mclasses do mentities.add mentity
+               else if k == "classdef" then
+                       for mentity in view.mclassdefs do mentities.add mentity
+               else if k == "property" then
+                       for mentity in view.mproperties do mentities.add mentity
+               else if k == "propdef" then
+                       for mentity in view.mpropdefs do mentities.add mentity
+               else
+                       for mentity in view.mentities do mentities.add mentity
+               end
+               return mentities
+       end
+
+       # Limit mentities depending on the `n` parameter.
+       fun limit_mentities(req: HttpRequest, mentities: Array[MEntity]): Array[MEntity] do
+               var n = req.int_arg("n")
+               if n != null then
+                       return mentities.sub(0, n)
+               end
+               return mentities
+       end
+
+       redef fun get(req, res) do
+               var mentities = list_mentities(req)
+               mentities = limit_mentities(req, mentities)
+               var arr = new JsonArray
+               for mentity in mentities do arr.add mentity
+               res.json arr
+       end
+end
+
+# Return a random list of MEntities.
+#
+# Example: `GET /random?n=10&k=module`
+class APIRandom
+       super APIList
+
+       # Randomize mentities order.
+       fun randomize_mentities(req: HttpRequest, mentities: Array[MEntity]): Array[MEntity] do
+               var res = mentities.to_a
+               res.shuffle
+               return res
+       end
+
+       redef fun get(req, res) do
+               var mentities = list_mentities(req)
+               mentities = limit_mentities(req, mentities)
+               mentities = randomize_mentities(req, mentities)
+               var arr = new JsonArray
+               for mentity in mentities do arr.add mentity
+               res.json arr
+       end
+end
+
+# Return the JSON representation of a MEntity.
+#
+# Example: `GET /entity/core::Array`
+class APIEntity
+       super APIHandler
+
+       redef fun get(req, res) do
+               var mentity = mentity_from_uri(req, res)
+               if mentity == null then return
+               res.json mentity.api_json(self)
+       end
+end
+
+
+# Return a UML representation of MEntity.
+#
+# Example: `GET /entity/core::Array/uml`
+class APIEntityUML
+       super APIHandler
+
+       redef fun get(req, res) do
+               var mentity = mentity_from_uri(req, res)
+               var dot
+               if mentity isa MClassDef then mentity = mentity.mclass
+               if mentity isa MClass then
+                       var uml = new UMLModel(view, mainmodule)
+                       dot = uml.generate_class_uml.write_to_string
+               else if mentity isa MModule then
+                       var uml = new UMLModel(view, mentity)
+                       dot = uml.generate_package_uml.write_to_string
+               else
+                       res.error 404
+                       return
+               end
+               res.send render_svg(dot)
+       end
+
+       # Render a `dot` string as a svg image.
+       fun render_svg(dot: String): String do
+               var proc = new ProcessDuplex("dot", "-Tsvg")
+               var svg = proc.write_and_read(dot)
+               proc.close
+               proc.wait
+               return svg
+       end
+end
+
+# Return the source code of MEntity.
+#
+# Example: `GET /entity/core::Array/code`
+class APIEntityCode
+       super APIHandler
+
+       # Modelbuilder used to access sources.
+       var modelbuilder: ModelBuilder
+
+       redef fun get(req, res) do
+               var mentity = mentity_from_uri(req, res)
+               if mentity == null then return
+               var source = render_source(mentity)
+               if source == null then
+                       res.error 404
+                       return
+               end
+               res.send source
+       end
+
+       # Highlight `mentity` source code.
+       private fun render_source(mentity: MEntity): nullable HTMLTag do
+               var node = modelbuilder.mentity2node(mentity)
+               if node == null then return null
+               var hl = new HighlightVisitor
+               hl.enter_visit node
+               return hl.html
+       end
+end
index 2b49b9e..0310a9c 100644 (file)
@@ -32,12 +32,12 @@ redef class MEntity
        # * MPropdef: `foo(e)`
        var html_name: String is lazy do return name.html_escape
 
-       # MEntity namespace escaped for html.
-       fun html_raw_namespace: String is abstract
+       # Returns the MEntity full_name escaped for html.
+       var html_full_name: String is lazy do return full_name.html_escape
 
        # Link to MEntity in the web server.
        # TODO this should be parameterizable... but how?
-       fun html_link: Link do return new Link("/doc/{html_raw_namespace}", html_name)
+       fun html_link: Link do return new Link("/doc/{full_name}", html_name)
 
        # Returns the list of keyword used in `self` declaration.
        fun html_modifiers: Array[String] is abstract
@@ -107,22 +107,12 @@ redef class MEntity
 end
 
 redef class MPackage
-       redef fun html_raw_namespace do return html_name
-
        redef var html_modifiers = ["package"]
        redef fun html_namespace do return html_link
        redef var css_classes = ["public"]
 end
 
 redef class MGroup
-       redef fun html_raw_namespace do
-               var parent = self.parent
-               if parent != null then
-                       return "{parent.html_raw_namespace}::{html_name}"
-               end
-               return "{mpackage.html_raw_namespace}::{html_name}"
-       end
-
        redef var html_modifiers = ["group"]
 
        # Depends if `self` is root or not.
@@ -161,17 +151,6 @@ redef class MModule
                return tpl
        end
 
-       redef fun html_raw_namespace do
-               var mpackage = self.mpackage
-               var mgroup = self.mgroup
-               if mgroup != null then
-                       return "{mgroup.html_raw_namespace}::{html_name}"
-               else if mpackage != null then
-                       return "{mpackage.html_raw_namespace}::{html_name}"
-               end
-               return html_name
-       end
-
        redef var css_classes = ["public"]
 end
 
@@ -211,8 +190,6 @@ redef class MClass
                return tpl
        end
 
-       redef fun html_raw_namespace do return intro.html_raw_namespace
-
        # Returns `intro.html_short_signature`.
        fun html_short_signature: Template do return intro.html_short_signature
 
@@ -224,8 +201,6 @@ redef class MClass
 end
 
 redef class MClassDef
-       redef fun html_raw_namespace do return "{mmodule.html_raw_namespace}::{html_name}"
-
        redef fun mdoc_or_fallback do return mdoc or else mclass.mdoc_or_fallback
 
        # Depends if `self` is an intro or not.
@@ -342,8 +317,6 @@ redef class MProperty
                return tpl
        end
 
-       redef fun html_raw_namespace do return intro.html_raw_namespace
-
        # Returns `intro.html_short_signature`.
        fun html_short_signature: Template do return intro.html_short_signature
 
@@ -354,7 +327,6 @@ redef class MProperty
 end
 
 redef class MPropDef
-       redef fun html_raw_namespace do return "{mclassdef.html_raw_namespace}::{html_name}"
        redef fun mdoc_or_fallback do return mdoc or else mproperty.mdoc_or_fallback
 
        # Depends if `self` is an intro or not.
@@ -601,12 +573,10 @@ end
 redef class MParameterType
        redef fun html_short_signature do return html_link
        redef fun html_signature do return html_link
-       redef fun html_raw_namespace do return html_name
 end
 
 redef class MVirtualType
        redef fun html_signature do return html_link
-       redef fun html_raw_namespace do return html_name
 end
 
 redef class MSignature
index 6f581f4..c6a12f0 100644 (file)
@@ -16,3 +16,4 @@
 module web
 
 import web_actions
+import model_api
index f4c4584..cb193fd 100644 (file)
@@ -20,163 +20,31 @@ import uml
 
 # Display the tree of all loaded mentities.
 class TreeAction
-       super ModelAction
+       super ModelHandler
 
-       redef fun answer(request, url) do
-               var model = init_model_view(request)
+       redef fun get(req, res) do
+               var model = init_model_view(req)
                var view = new HtmlHomePage(model.to_tree)
-               return render_view(view)
-       end
-end
-
-# Display the list of mentities matching `namespace`.
-class SearchAction
-       super ModelAction
-
-       # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
-               end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if request.is_json_asked then
-                       var json = new JsonArray
-                       for mentity in mentities do
-                               json.add mentity.to_json
-                       end
-                       return render_json(json)
-               end
-               var view = new HtmlResultPage(namespace, mentities)
-               return render_view(view)
-       end
-end
-
-# Display a MEntity source code.
-class CodeAction
-       super ModelAction
-
-       # Modelbuilder used to access sources.
-       var modelbuilder: ModelBuilder
-
-       # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
-               end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if mentities.is_empty then
-                       return render_error(404, "No mentity matching this namespace.")
-               end
-               var view = new HtmlSourcePage(modelbuilder, mentities.first)
-               return render_view(view)
+               res.send_view(view)
        end
 end
 
 # Display the doc of a MEntity.
 class DocAction
-       super ModelAction
+       super ModelHandler
 
        # Modelbuilder used to access sources.
        var modelbuilder: ModelBuilder
 
-       # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
-               end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if mentities.is_empty then
-                       return render_error(404, "No mentity matching this namespace.")
-               end
-               var view = new HtmlDocPage(modelbuilder, mentities.first)
-               return render_view(view)
-       end
-end
-
-# Return an UML diagram for `namespace`.
-class UMLDiagramAction
-       super ModelAction
-
-       # Mainmodule used for hierarchy flattening.
-       var mainmodule: MModule
-
-       redef fun answer(request, url) do
-               var namespace = request.param("namespace")
-               if namespace == null or namespace.is_empty then
-                       return render_error(400, "Missing :namespace.")
-               end
-               var model = init_model_view(request)
-               var mentities = model.mentities_by_namespace(namespace)
-               if mentities.is_empty then
-                       return render_error(404, "No mentity matching this namespace.")
-               end
-               var mentity = mentities.first
-               if mentity isa MClassDef then mentity = mentity.mclass
-
-               var dot
-               if mentity isa MClass then
-                       var uml = new UMLModel(model, mainmodule)
-                       dot = uml.generate_class_uml.write_to_string
-               else if mentity isa MModule then
-                       var uml = new UMLModel(model, mentity)
-                       dot = uml.generate_package_uml.write_to_string
-               else
-                       return render_error(404, "No diagram matching this namespace.")
-               end
-               var view = new HtmlDotPage(dot, mentity.html_name)
-               return render_view(view)
-       end
-end
-
-# Return a random list of MEntities.
-class RandomAction
-       super ModelAction
-
-       # TODO handle more than full namespaces.
-       redef fun answer(request, url) do
-               var n = request.int_arg("n") or else 10
-               var k = request.string_arg("k") or else "modules"
-               var model = init_model_view(request)
-               var mentities: Array[MEntity]
-               if k == "modules" then
-                       mentities = model.mmodules.to_a
-               else if k == "classdefs" then
-                       mentities = model.mclassdefs.to_a
-               else
-                       mentities = model.mpropdefs.to_a
-               end
-               mentities.shuffle
-               mentities = mentities.sub(0, n)
-               if request.is_json_asked then
-                       var json = new JsonArray
-                       for mentity in mentities do
-                               json.add mentity.to_json
-                       end
-                       return render_json(json)
-               end
-               var view = new HtmlResultPage("random", mentities)
-               return render_view(view)
-       end
-end
-
-redef class MEntity
-
-       # Return `self` as a JsonObject.
-       fun to_json: JsonObject do
-               var obj = new JsonObject
-               obj["name"] = html_name
-               obj["namespace"] = html_raw_namespace
-               var mdoc = self.mdoc
-               if mdoc != null then
-                       obj["synopsis"] = mdoc.content.first.html_escape
-                       obj["mdoc"] = mdoc.content.join("\n").html_escape
+       redef fun get(req, res) do
+               var namespace = req.param("namespace")
+               var model = init_model_view(req)
+               var mentity = find_mentity(model, namespace)
+               if mentity == null then
+                       res.error(404)
+                       return
                end
-               return obj
+               var view = new HtmlDocPage(modelbuilder, mentity)
+               res.send_view(view)
        end
 end
index ecf639e..ec9bf75 100644 (file)
 module web_base
 
 import model::model_views
-import nitcorn
-import json
+import model::model_json
+import doc_down
+import popcorn
 
-# Nitcorn server runned by `nitweb`.
-#
-# Usage:
-#
-# ~~~nitish
-# var srv = new NitServer("localhost", 3000)
-# srv.routes.add new Route("/", new MyAction)
-# src.listen
-# ~~~
-class NitServer
+# Specific nitcorn Action that uses a Model
+class ModelHandler
+       super Handler
 
-       # Host to bind.
-       var host: String
+       # Model to use.
+       var model: Model
 
-       # Port to use.
-       var port: Int
+       # MModule used to flatten model.
+       var mainmodule: MModule
 
-       # Routes knwon by the server.
-       var routes = new Array[Route]
+       # Find the MEntity ` with `full_name`.
+       fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do
+               if full_name == null then return null
+               return model.mentity_by_full_name(full_name.from_percent_encoding)
+       end
 
-       # Start listen on `host:port`.
-       fun listen do
-               var iface = "{host}:{port}"
-               print "Launching server on http://{iface}/"
+       # Init the model view from the `req` uri parameters.
+       fun init_model_view(req: HttpRequest): ModelView do
+               var view = new ModelView(model)
+               var show_private = req.bool_arg("private") or else false
+               if not show_private then view.min_visibility = protected_visibility
 
-               var vh = new VirtualHost(iface)
-               for route in routes do vh.routes.add route
+               view.include_fictive = req.bool_arg("fictive") or else false
+               view.include_empty_doc = req.bool_arg("empty-doc") or else true
+               view.include_test_suite = req.bool_arg("test-suite") or else false
+               view.include_attribute = req.bool_arg("attributes") or else true
 
-               var fac = new HttpFactory.and_libevent
-               fac.config.virtual_hosts.add vh
-               fac.run
+               return view
        end
 end
 
-# Specific nitcorn Action for nitweb.
-class NitAction
-       super Action
+# A NitView is rendered by an action.
+interface NitView
+       # Renders this view and returns something that can be written to a HTTP response.
+       fun render: Writable is abstract
+end
+
+redef class HttpResponse
+       # Render a NitView as response.
+       fun send_view(view: NitView, status: nullable Int) do send(view.render, status)
+end
+
+redef class MEntity
+
+       # URL to `self` within the web interface.
+       fun web_url: String is abstract
 
-       # Link to the NitServer that runs this action.
-       var srv: NitServer
+       # URL to `self` within the JSON api.
+       fun api_url: String do return "/api/entity/" / full_name
 
-       # Build a custom http response for errors.
-       fun render_error(code: Int, message: String): HttpResponse do
-               var response = new HttpResponse(code)
-               var tpl = new Template
-               tpl.add "<h1>Error {code}</h1>"
-               tpl.add "<pre><code>{message.html_escape}</code></pre>"
-               response.body = tpl.write_to_string
-               return response
+       redef fun json do
+               var obj = super
+               obj["web_url"] = web_url
+               obj["api_url"] = api_url
+               return obj
        end
 
-       # Render a view as a HttpResponse 200.
-       fun render_view(view: NitView): HttpResponse do
-               var response = new HttpResponse(200)
-               response.body = view.render(srv).write_to_string
-               return response
+       # Get the full json repesentation of `self` with MEntityRefs resolved.
+       fun api_json(handler: ModelHandler): JsonObject do return json
+end
+
+redef class MEntityRef
+       redef fun json do
+               var obj = super
+               obj["web_url"] = mentity.web_url
+               obj["api_url"] = mentity.api_url
+               obj["name"] = mentity.name
+               obj["mdoc"] = mentity.mdoc_or_fallback
+               obj["visibility"] = mentity.visibility
+               obj["location"] = mentity.location
+               var modifiers = new JsonArray
+               for modifier in mentity.collect_modifiers do
+                       modifiers.add modifier
+               end
+               obj["modifiers"] = modifiers
+               return obj
        end
+end
 
-       # Return a HttpResponse containing `json`.
-       fun render_json(json: Jsonable): HttpResponse do
-               var response = new HttpResponse(200)
-               response.body = json.to_json
-               return response
+redef class MDoc
+
+       # Add doc down processing
+       redef fun json do
+               var obj = super
+               obj["synopsis"] = synopsis
+               obj["documentation"] = documentation
+               obj["comment"] = comment
+               obj["html_synopsis"] = html_synopsis.write_to_string
+               obj["html_documentation"] = html_documentation.write_to_string
+               obj["html_comment"] = html_comment.write_to_string
+               return obj
        end
 end
 
-# Specific nitcorn Action that uses a Model
-class ModelAction
-       super NitAction
+redef class MPackage
+       redef var web_url = "/package/{full_name}" is lazy
+end
 
-       # Model to use.
-       var model: Model
+redef class MGroup
+       redef var web_url = "/group/{full_name}" is lazy
+end
 
-       # Init the model view from the `req` uri parameters.
-       fun init_model_view(req: HttpRequest): ModelView do
-               var view = new ModelView(model)
+redef class MModule
+       redef var web_url = "/module/{full_name}" is lazy
 
-               var show_private = req.bool_arg("private") or else false
-               if not show_private then view.min_visibility = protected_visibility
+       redef fun api_json(handler) do
+               var obj = super
+               obj["intro_mclassdefs"] = to_mentity_refs(collect_intro_mclassdefs(private_view))
+               obj["redef_mclassdefs"] = to_mentity_refs(collect_redef_mclassdefs(private_view))
+               obj["imports"] = to_mentity_refs(in_importation.direct_greaters)
+               return obj
+       end
+end
 
-               view.include_fictive = req.bool_arg("fictive") or else false
-               view.include_empty_doc = req.bool_arg("empty-doc") or else true
-               view.include_test_suite = req.bool_arg("test-suite") or else false
-               view.include_attribute = req.bool_arg("attributes") or else true
+redef class MClass
+       redef var web_url = "/class/{full_name}" is lazy
+
+       redef fun api_json(handler) do
+               var obj = super
+               obj["all_mproperties"] = to_mentity_refs(collect_accessible_mproperties(private_view))
+               obj["intro_mproperties"] = to_mentity_refs(collect_intro_mproperties(private_view))
+               obj["redef_mproperties"] = to_mentity_refs(collect_redef_mproperties(private_view))
+               var poset = hierarchy_poset(handler.mainmodule, private_view)
+               obj["parents"] = to_mentity_refs(poset[self].direct_greaters)
+               return obj
+       end
+end
 
-               return view
+redef class MClassDef
+       redef var web_url = "/classdef/{full_name}" is lazy
+
+       redef fun json do
+               var obj = super
+               obj["intro"] = to_mentity_ref(mclass.intro)
+               obj["mpackage"] = to_mentity_ref(mmodule.mpackage)
+               return obj
+       end
+
+       redef fun api_json(handler) do
+               var obj = super
+               obj["intro_mpropdefs"] = to_mentity_refs(collect_intro_mpropdefs(private_view))
+               obj["redef_mpropdefs"] = to_mentity_refs(collect_redef_mpropdefs(private_view))
+               return obj
        end
 end
 
-# A NitView is rendered by an action.
-interface NitView
-       # Renders this view and returns something that can be written to a HTTP response.
-       fun render(srv: NitServer): Writable is abstract
+redef class MProperty
+       redef var web_url = "/property/{full_name}" is lazy
+
+       redef fun json do
+               var obj = super
+               obj["intro_mclass"] = to_mentity_ref(intro_mclassdef.mclass)
+               obj["mpackage"] = to_mentity_ref(intro_mclassdef.mmodule.mpackage)
+               return obj
+       end
+end
+
+redef class MPropDef
+       redef var web_url = "/propdef/{full_name}" is lazy
+
+       redef fun json do
+               var obj = super
+               obj["intro"] = to_mentity_ref(mproperty.intro)
+               obj["intro_mclassdef"] = to_mentity_ref(mproperty.intro.mclassdef)
+               obj["mmodule"] = to_mentity_ref(mclassdef.mmodule)
+               obj["mgroup"] = to_mentity_ref(mclassdef.mmodule.mgroup)
+               obj["mpackage"] = to_mentity_ref(mclassdef.mmodule.mpackage)
+               return obj
+       end
+end
+
+redef class MClassType
+       redef var web_url = mclass.web_url is lazy
+end
+
+redef class MNullableType
+       redef var web_url = mtype.web_url is lazy
+end
+
+redef class MParameterType
+       redef var web_url = mclass.web_url is lazy
 end
 
-redef class HttpRequest
-       # Does the client asked for a json formatted response?
-       #
-       # Checks the URL get parameter `?json=true`.
-       fun is_json_asked: Bool do return bool_arg("json") or else false
+redef class MVirtualType
+       redef var web_url = mproperty.web_url is lazy
 end
index 950c839..d8f8c9f 100644 (file)
@@ -27,7 +27,7 @@ class HtmlHomePage
        # Loaded model to display.
        var tree: MEntityTree
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, "Loaded model")
                tpl.add tree.html_list
@@ -35,34 +35,6 @@ class HtmlHomePage
        end
 end
 
-# Display a search results list.
-class HtmlResultPage
-       super NitView
-
-       # Initial query.
-       var query: String
-
-       # Result set
-       var results: Array[MEntity]
-
-       redef fun render(srv) do
-               var tpl = new Template
-               tpl.add new Header(1, "Results for {query}")
-               if results.is_empty then
-                       tpl.add "<p>No result for {query}.<p>"
-                       return tpl
-               end
-               var list = new UnorderedList
-               for mentity in results do
-                       var link = mentity.html_link
-                       link.text = mentity.html_raw_namespace
-                       list.add_li new ListItem(link)
-               end
-               tpl.add list
-               return tpl
-       end
-end
-
 # Display the source for each mentities
 class HtmlSourcePage
        super NitView
@@ -76,7 +48,7 @@ class HtmlSourcePage
        # HiglightVisitor used to hilight the source code
        var hl = new HighlightVisitor
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, "Source Code")
                tpl.add render_source
@@ -103,7 +75,7 @@ end
 class HtmlDocPage
        super HtmlSourcePage
 
-       redef fun render(srv) do
+       redef fun render do
                var tpl = new Template
                tpl.add new Header(1, mentity.html_name)
                tpl.add "<p>"
@@ -119,29 +91,3 @@ class HtmlDocPage
                return tpl
        end
 end
-
-# Display the source for each mentities
-class HtmlDotPage
-       super NitView
-
-       # Dot to process.
-       var dot: Text
-
-       # Page title.
-       var title: String
-
-       redef fun render(srv) do
-               var tpl = new Template
-               tpl.add new Header(1, title)
-               tpl.add render_dot
-               return tpl
-       end
-
-       private fun render_dot: String do
-               var proc = new ProcessDuplex("dot", "-Tsvg", "-Tcmapx")
-               var svg = proc.write_and_read(dot)
-               proc.close
-               proc.wait
-               return svg
-       end
-end
index b70b71d..6a693ff 100644 (file)
@@ -6,3 +6,4 @@ neo
 mpi
 emscripten
 ui_test
+readline
diff --git a/tests/base_gen_infinite2.nit b/tests/base_gen_infinite2.nit
new file mode 100644 (file)
index 0000000..05c4fb6
--- /dev/null
@@ -0,0 +1,32 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import end
+
+interface Object
+       type SELF: Object
+       fun output_class_name is intern
+end
+
+class A[E]
+       new do return new B[E]
+end
+
+class B[E]
+       super A[E]
+
+       fun foo: A[E] do return new A[SELF]
+end
+
+(new B[Object]).foo.output_class_name
diff --git a/tests/base_redef.nit b/tests/base_redef.nit
new file mode 100644 (file)
index 0000000..d42bfec
--- /dev/null
@@ -0,0 +1,65 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import core::kernel
+
+class A
+       fun f do 1.output
+       fun f2: Int do return 2
+       fun f2=(i:Int) do i.output
+       fun f3=(i:Int) do i.output
+       var v = 4
+       type T: A
+       fun t(t: T): T do return t
+end
+
+class B
+       super A#alt1#
+       redef fun f do 10.output
+       redef var f2 = 20
+       var f3 = 30 is redef writable
+       redef var v = 40
+       redef type T: B
+end
+
+class C
+       super B#alt2#
+       redef fun f do 100.output
+       redef var f2 = 200
+       redef var f3 = 300
+       redef var v = 400
+       redef type T: C
+
+end
+
+var a = new A
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
+
+a = new B
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
+
+a = new C
+a.f
+a.f2 = -2
+a.f2.output
+a.f3 = -3
+a.t(a).f
index 30c792b..70e648a 100755 (executable)
@@ -1,5 +1,5 @@
 #!/bin/sh
-printf "%s\n" "$@" \
+ls -1 -- "%s\n" "$@" \
        ../src/nit*.nit \
        ../src/test_*.nit \
        ../src/examples/*.nit \
@@ -9,11 +9,11 @@ printf "%s\n" "$@" \
        ../examples/*/src/*_android.nit \
        ../examples/*/src/*_linux.nit \
        ../examples/*/src/*_null.nit \
-       ../examples/nitcorn/src/*.nit \
        ../lib/*/examples/*.nit \
        ../lib/*/examples/*/*.nit \
        ../contrib/friendz/src/solver_cmd.nit \
        ../contrib/neo_doxygen/src/tests/neo_doxygen_*.nit \
        ../contrib/pep8analysis/src/pep8analysis.nit \
        ../contrib/nitiwiki/src/nitiwiki.nit \
-       *.nit
+       *.nit \
+       | grep -v ../lib/popcorn/examples/
index 571f594..4634f40 100644 (file)
@@ -40,3 +40,4 @@ test_ffi_c_lots_of_refs
 test_rubix_cube
 test_rubix_visual
 test_csv
+repeating_key_xor_solve
index 390709a..2116656 100644 (file)
@@ -1,7 +1,7 @@
 test_nitunit.nit --no-color -o $WRITE
 test_nitunit.nit --gen-suite --only-show
 test_nitunit.nit --gen-suite --only-show --private
-test_nitunit2.nit -o $WRITE
+test_nitunit2.nit --no-color -o $WRITE
 test_doc2.nit --no-color -o $WRITE
 test_nitunit3 --no-color -o $WRITE
 test_nitunit_md.md --no-color -o $WRITE
index 47cb8a0..a68b4cc 100644 (file)
@@ -40,3 +40,4 @@ test_ffi_c_lots_of_refs
 test_rubix_visual
 test_rubix_cube
 test_csv
+repeating_key_xor_solve
diff --git a/tests/repeating_key_xor_solve.args b/tests/repeating_key_xor_solve.args
new file mode 100644 (file)
index 0000000..2f6c9ff
--- /dev/null
@@ -0,0 +1 @@
+../lib/crapto/examples/repeating_key_xor_cipher.txt
diff --git a/tests/sav/base_gen_infinite2.res b/tests/sav/base_gen_infinite2.res
new file mode 100644 (file)
index 0000000..e38f38e
--- /dev/null
@@ -0,0 +1 @@
+B[B[Object]]
diff --git a/tests/sav/base_redef.res b/tests/sav/base_redef.res
new file mode 100644 (file)
index 0000000..3b792d6
--- /dev/null
@@ -0,0 +1,11 @@
+1
+-2
+2
+-3
+1
+10
+-2
+10
+100
+-2
+100
diff --git a/tests/sav/base_redef_alt1.res b/tests/sav/base_redef_alt1.res
new file mode 100644 (file)
index 0000000..50e360c
--- /dev/null
@@ -0,0 +1,5 @@
+alt/base_redef_alt1.nit:29,12: Error: no property `B::f` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:30,12--13: Error: no property `B::f2` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:31,6--7: Error: no property `B::f3=` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:32,12: Error: no property `B::v` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt1.nit:33,2--16: Error: no property `B::T` is inherited. Remove the `redef` keyword to define a new property.
diff --git a/tests/sav/base_redef_alt2.res b/tests/sav/base_redef_alt2.res
new file mode 100644 (file)
index 0000000..1c263c4
--- /dev/null
@@ -0,0 +1,5 @@
+alt/base_redef_alt2.nit:38,12: Error: no property `C::f` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:39,12--13: Error: no property `C::f2` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:40,12--13: Error: no property `C::f3` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:41,12: Error: no property `C::v` is inherited. Remove the `redef` keyword to define a new property.
+alt/base_redef_alt2.nit:42,2--16: Error: no property `C::T` is inherited. Remove the `redef` keyword to define a new property.
index 143e589..f97fee4 100644 (file)
 <li><a href="https:&#47;&#47;github.com&#47;nitlang&#47;nit&#47;tree&#47;master&#47;tests&#47;test_prog">https:&#47;&#47;github.com&#47;nitlang&#47;nit&#47;tree&#47;master&#47;tests&#47;test_prog</a></li>
 <li><tt>https:&#47;&#47;github.com&#47;nitlang&#47;nit.git</tt></li>
 </ul>
+<h3>Quality</h3>
+<ul class="box">
+<li>28 warnings (63/kloc)</li>
+<li>95% documented</li>
+</ul>
 <h3>Tags</h3>
 <a href="../index.html#tag_test">test</a>, <a href="../index.html#tag_game">game</a><h3>Requirements</h3>
 none<h3>Clients</h3>
diff --git a/tests/sav/nitce/base_gen_infinite2.res b/tests/sav/nitce/base_gen_infinite2.res
new file mode 100644 (file)
index 0000000..223b783
--- /dev/null
@@ -0,0 +1 @@
+B
diff --git a/tests/sav/nitcg/fixme/base_gen_infinite2.res b/tests/sav/nitcg/fixme/base_gen_infinite2.res
new file mode 100644 (file)
index 0000000..2f63d29
--- /dev/null
@@ -0,0 +1 @@
+Fatal Error: limitation in the rapidtype analysis engine: a type depth of 256 is too important, the problematic type is `A[A[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[Sys]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]`.
diff --git a/tests/sav/nitcs/fixme/base_gen_infinite2.res b/tests/sav/nitcs/fixme/base_gen_infinite2.res
new file mode 100644 (file)
index 0000000..2f63d29
--- /dev/null
@@ -0,0 +1 @@
+Fatal Error: limitation in the rapidtype analysis engine: a type depth of 256 is too important, the problematic type is `A[A[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[Sys]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]`.
diff --git a/tests/sav/nitcsg/fixme/base_gen_infinite2.res b/tests/sav/nitcsg/fixme/base_gen_infinite2.res
new file mode 100644 (file)
index 0000000..2f63d29
--- /dev/null
@@ -0,0 +1 @@
+Fatal Error: limitation in the rapidtype analysis engine: a type depth of 256 is too important, the problematic type is `A[A[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[B[Sys]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]`.
index 20d487e..fdae4a5 100644 (file)
@@ -33,7 +33,7 @@
 </span></span><span class="line" id="L33">
 </span><span class="nc_cdef foldable" id="base_simple3$B"><span class="line" id="L34"><span class="nc_k">class</span> <span class="nc_t">B</span>
 </span><span class="nc_pdef foldable" id="base_simple3$B$_val"><a id="base_simple3$B$val"></a><a id="base_simple3$B$val="></a><span class="line" id="L35">     <span class="nc_k">var</span> <span class="nc_def nc_i">val</span><span>:</span> <span class="nc_t">Int</span>
-</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36">     <span class="nc_k">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
+</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36">     <span class="nc_k popupable" style="border-bottom: solid 2px red" title="Messages" data-content="&lt;div&gt;&lt;div class=&#34;dropdown&#34;&gt; &lt;a data-toggle=&#34;dropdown&#34; href=&#34;#&#34;&gt;&lt;b&gt;1 message(s)&lt;&#47;b&gt; &lt;span class=&#34;caret&#34;&gt;&lt;&#47;span&gt;&lt;&#47;a&gt;&lt;ul class=&#34;dropdown-menu&#34; role=&#34;menu&#34; aria-labelledby=&#34;dLabel&#34;&gt;&lt;li&gt;Warning: init with signature in base_simple3$B&lt;&#47;li&gt;&lt;&#47;ul&gt;&lt;&#47;div&gt;&lt;&#47;div&gt;" data-toggle="popover">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
 </span><span class="line" id="L37">    <span class="nc_k">do</span>
 </span><span class="line" id="L38">            <span class="nc_l">7</span><span>.</span><span class="nc_i">output</span>
 </span><span class="line" id="L39">            <span class="nc_k">self</span><span>.</span><span class="nc_i">val</span> <span>=</span> <span class="nc_v nc_i">v</span>
index 58aa2a8..a45c8d4 100644 (file)
@@ -1,18 +1,36 @@
-test_nitunit.nit:20,1--22,0: ERROR: nitunit.test_nitunit::test_nitunit.test_nitunit::X.<class> (in .nitunit/test_nitunit-2.nit): Runtime error: Assert failed (.nitunit/test_nitunit-2.nit:5)
+* Docunits of module test_nitunit::test_nitunit (4 tests)
 
-test_nitunit.nit:23,2--25,0: FAILURE: nitunit.test_nitunit::test_nitunit.test_nitunit::X.test_nitunit::X::foo (in .nitunit/test_nitunit-3.nit): .nitunit/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
+[OK] test_nitunit::test_nitunit
+[KO] test_nitunit$X
+     test_nitunit.nit:21,7--22,0: Runtime error in nitunit.out/test_nitunit-2.nit
+     Output
+       Runtime error: Assert failed (nitunit.out/test_nitunit-2.nit:5)
 
-test_test_nitunit.nit:36,2--40,4: ERROR: test_foo1 (in file .nitunit/gen_test_test_nitunit.nit): Runtime error: Assert failed (test_test_nitunit.nit:39)
+[KO] test_nitunit$X$foo
+     test_nitunit.nit:24,8--25,0: Compilation error in nitunit.out/test_nitunit-3.nit
+     Output
+       nitunit.out/test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
 
-DocUnits:
-Entities: 27; Documented ones: 3; With nitunits: 3; Failures: 2
+[KO] test_nitunit$X$foo1
+     test_nitunit.nit:28,15: Syntax Error: unexpected operator '!'.
+* Test-suite of module test_test_nitunit::test_test_nitunit (3 tests)
 
-TestSuites:
-Class suites: 1; Test Cases: 3; Failures: 1
+[OK] test_test_nitunit$TestX$test_foo
+[KO] test_test_nitunit$TestX$test_foo1
+     test_test_nitunit.nit:36,2--40,4: Runtime Error in file nitunit.out/gen_test_test_nitunit.nit
+     Output
+       Runtime error: Assert failed (test_test_nitunit.nit:39)
+
+[OK] test_test_nitunit$TestX$test_foo2
+Docunits: Entities: 27; Documented ones: 4; With nitunits: 4; Failures: 3
+Test suites: Classes: 1; Test Cases: 3; Failures: 1
+[FAILURE] 4/7 tests failed.
+`nitunit.out` is not removed for investigation.
 <testsuites><testsuite package="test_nitunit::test_nitunit"><testcase classname="nitunit.test_nitunit::test_nitunit.&lt;module&gt;" name="&lt;module&gt;"><system-err></system-err><system-out>assert true
-</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="&lt;class&gt;"><system-err></system-err><system-out>assert false
-</system-out><error message="Runtime error: Assert failed (.nitunit&#47;test_nitunit-2.nit:5)
-"></error></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo"><system-err></system-err><system-out>assert undefined_identifier
-</system-out><failure message=".nitunit&#47;test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
-"></failure></testcase></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo"><system-err></system-err><system-out>out</system-out></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><system-err></system-err><system-out>out</system-out><error message="Runtime error: Assert failed (test_test_nitunit.nit:39)
-"></error></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo2"><system-err></system-err><system-out>out</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="&lt;class&gt;"><error>Runtime error in nitunit.out&#47;test_nitunit-2.nit</error><system-err>Runtime error: Assert failed (nitunit.out&#47;test_nitunit-2.nit:5)
+</system-err><system-out>assert false
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo"><failure>Compilation error in nitunit.out&#47;test_nitunit-3.nit</failure><system-err>nitunit.out&#47;test_nitunit-3.nit:5,8--27: Error: method or variable `undefined_identifier` unknown in `Sys`.
+</system-err><system-out>assert undefined_identifier
+</system-out></testcase><testcase classname="nitunit.test_nitunit::test_nitunit.test_nitunit::X" name="test_nitunit::X::foo1"><failure>Syntax Error: unexpected operator &#39;!&#39;.</failure><system-out>assert !@#$%^&amp;*()
+</system-out></testcase></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo"><system-err></system-err></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo1"><error>Runtime Error in file nitunit.out&#47;gen_test_test_nitunit.nit</error><system-err>Runtime error: Assert failed (test_test_nitunit.nit:39)
+</system-err></testcase><testcase classname="nitunit.test_test_nitunit::test_test_nitunit.test_test_nitunit::TestX" name="test_test_nitunit::TestX::test_foo2"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
index 4f4ab53..a1f4cb1 100644 (file)
@@ -1,10 +1,11 @@
-DocUnits:
-DocUnits Success
-Entities: 4; Documented ones: 3; With nitunits: 3; Failures: 0
+* Docunits of module test_nitunit2::test_nitunit2 (3 tests)
 
-TestSuites:
-No test cases found
-Class suites: 0; Test Cases: 0; Failures: 0
+[OK] test_nitunit2::test_nitunit2$core::Sys$foo1
+[OK] test_nitunit2::test_nitunit2$core::Sys$bar2
+[OK] test_nitunit2::test_nitunit2$core::Sys$foo3
+Docunits: Entities: 4; Documented ones: 3; With nitunits: 3; Failures: 0
+Test suites: Classes: 0; Test Cases: 0
+[SUCCESS] All 3 tests passed.
 <testsuites><testsuite package="test_nitunit2::test_nitunit2"><testcase classname="nitunit.test_nitunit2::test_nitunit2.core::Sys" name="test_nitunit2::test_nitunit2::Sys::foo1"><system-err></system-err><system-out>if true then
 
    assert true
index dd4f11a..3e25819 100644 (file)
@@ -1,10 +1,11 @@
-DocUnits:
-DocUnits Success
-Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 0
+* Docunits of module test_doc2::test_doc2 (3 tests)
 
-TestSuites:
-No test cases found
-Class suites: 0; Test Cases: 0; Failures: 0
+[OK] test_doc2::test_doc2$core::Sys$foo1
+[OK] test_doc2::test_doc2$core::Sys$foo2
+[OK] test_doc2::test_doc2$core::Sys$foo3
+Docunits: Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 0
+Test suites: Classes: 0; Test Cases: 0
+[SUCCESS] All 3 tests passed.
 <testsuites><testsuite package="test_doc2::test_doc2"><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo1"><system-err></system-err><system-out>assert true # tested
 </system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo2"><system-err></system-err><system-out>assert true # tested
 </system-out></testcase><testcase classname="nitunit.test_doc2::test_doc2.core::Sys" name="test_doc2::test_doc2::Sys::foo3"><system-err></system-err><system-out>assert true # tested
index b0a65a1..2b931af 100644 (file)
@@ -1,14 +1,22 @@
-test_nitunit3/README.md:1,0--13,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,2--4: Syntax Error: unexpected malformed character '\]..
-test_nitunit3/README.md:1,0--13,0: ERROR: nitunit.test_nitunit3>.<group> (in .nitunit/test_nitunit3-0.nit): Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
+* Docunits of group test_nitunit3> (2 tests)
 
-DocUnits:
-Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2
+[KO] test_nitunit3>
+     test_nitunit3/README.md:4,2--15,0: Runtime error in nitunit.out/test_nitunit3-0.nit with argument 1
+     Output
+       Runtime error: Assert failed (nitunit.out/test_nitunit3-0.nit:7)
 
-TestSuites:
-No test cases found
-Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_nitunit3&gt;"><testcase classname="nitunit.test_nitunit3&gt;" name="&lt;group&gt;"><failure message="test_nitunit3&#47;README.md:1,0--13,0: Invalid block of code. At 1,2--4: Syntax Error: unexpected malformed character &#39;\].."></failure><system-err></system-err><system-out>assert false
+[KO] test_nitunit3>
+     test_nitunit3/README.md:7,3--5: Syntax Error: unexpected malformed character '\].
+* Docunits of module test_nitunit3::test_nitunit3 (1 tests)
+
+[OK] test_nitunit3::test_nitunit3
+Docunits: Entities: 2; Documented ones: 2; With nitunits: 3; Failures: 2
+Test suites: Classes: 0; Test Cases: 0
+[FAILURE] 2/3 tests failed.
+`nitunit.out` is not removed for investigation.
+<testsuites><testsuite package="test_nitunit3&gt;"><testcase classname="nitunit.test_nitunit3&gt;" name="&lt;group&gt;"><error>Runtime error in nitunit.out&#47;test_nitunit3-0.nit with argument 1</error><system-err>Runtime error: Assert failed (nitunit.out&#47;test_nitunit3-0.nit:7)
+</system-err><system-out>assert false
 assert true
-</system-out><error message="Runtime error: Assert failed (.nitunit&#47;test_nitunit3-0.nit:7)
-"></error></testcase></testsuite><testsuite package="test_nitunit3::test_nitunit3"><testcase classname="nitunit.test_nitunit3::test_nitunit3.&lt;module&gt;" name="&lt;module&gt;"><system-err></system-err><system-out>assert true
+</system-out></testcase><testcase classname="nitunit.test_nitunit3&gt;" name="&lt;group&gt;+1"><failure>Syntax Error: unexpected malformed character &#39;\].</failure><system-out>;&#39;\][]
+</system-out></testcase></testsuite><testsuite package="test_nitunit3::test_nitunit3"><testcase classname="nitunit.test_nitunit3::test_nitunit3.&lt;module&gt;" name="&lt;module&gt;"><system-err></system-err><system-out>assert true
 </system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index fc20a76..4550bb5 100644 (file)
@@ -1,13 +1,16 @@
-test_nitunit_md.md:1,0--15,0: ERROR: nitunit.<file>.test_nitunit_md.md:1,0--15,0 (in .nitunit/file-0.nit): Runtime error: Assert failed (.nitunit/file-0.nit:8)
+* Docunits of file test_nitunit_md.md:1,0--15,0 (1 tests)
 
-DocUnits:
-Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1
+[KO] nitunit.<file>.test_nitunit_md.md:1,0--15,0
+     test_nitunit_md.md:4,2--16,0: Runtime error in nitunit.out/file-0.nit with argument 1
+     Output
+       Runtime error: Assert failed (nitunit.out/file-0.nit:8)
 
-TestSuites:
-No test cases found
-Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_nitunit_md.md:1,0--15,0"><testcase classname="nitunit.&lt;file&gt;" name="test_nitunit_md.md:1,0--15,0"><system-err></system-err><system-out>var a = 1
+Docunits: Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1
+Test suites: Classes: 0; Test Cases: 0
+[FAILURE] 1/1 tests failed.
+`nitunit.out` is not removed for investigation.
+<testsuites><testsuite package="test_nitunit_md.md:1,0--15,0"><testcase classname="nitunit.&lt;file&gt;" name="test_nitunit_md.md:1,0--15,0"><error>Runtime error in nitunit.out&#47;file-0.nit with argument 1</error><system-err>Runtime error: Assert failed (nitunit.out&#47;file-0.nit:8)
+</system-err><system-out>var a = 1
 assert 1 == 1
 assert false
-</system-out><error message="Runtime error: Assert failed (.nitunit&#47;file-0.nit:8)
-"></error></testcase></testsuite></testsuites>
\ No newline at end of file
+</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
index 7e2dc83..65fd9b3 100644 (file)
@@ -1,10 +1,16 @@
-test_doc3.nit:15,1--18,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,3--9: Syntax Error: unexpected identifier 'garbage'..
-test_doc3.nit:20,1--25,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,2--8: Syntax Error: unexpected identifier 'garbage'..
-test_doc3.nit:27,1--32,0: Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,2--8: Syntax Error: unexpected identifier 'garbage'..
-DocUnits:
-Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 3
+* Docunits of module test_doc3::test_doc3 (3 tests)
 
-TestSuites:
-No test cases found
-Class suites: 0; Test Cases: 0; Failures: 0
-<testsuites><testsuite package="test_doc3::test_doc3"><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo1"><failure message="test_doc3.nit:15,1--18,0: Invalid block of code. At 1,3--9: Syntax Error: unexpected identifier &#39;garbage&#39;.."></failure></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo2"><failure message="test_doc3.nit:20,1--25,0: Invalid block of code. At 1,2--8: Syntax Error: unexpected identifier &#39;garbage&#39;.."></failure></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo3"><failure message="test_doc3.nit:27,1--32,0: Invalid block of code. At 1,2--8: Syntax Error: unexpected identifier &#39;garbage&#39;.."></failure></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
+[KO] test_doc3::test_doc3$core::Sys$foo1
+     test_doc3.nit:17,9--15: Syntax Error: unexpected identifier 'garbage'.
+[KO] test_doc3::test_doc3$core::Sys$foo2
+     test_doc3.nit:23,4--10: Syntax Error: unexpected identifier 'garbage'.
+[KO] test_doc3::test_doc3$core::Sys$foo3
+     test_doc3.nit:30,4--10: Syntax Error: unexpected identifier 'garbage'.
+Docunits: Entities: 6; Documented ones: 5; With nitunits: 3; Failures: 3
+Test suites: Classes: 0; Test Cases: 0
+[FAILURE] 3/3 tests failed.
+`nitunit.out` is not removed for investigation.
+<testsuites><testsuite package="test_doc3::test_doc3"><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo1"><failure>Syntax Error: unexpected identifier &#39;garbage&#39;.</failure><system-out> *garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo2"><failure>Syntax Error: unexpected identifier &#39;garbage&#39;.</failure><system-out>*garbage*
+</system-out></testcase><testcase classname="nitunit.test_doc3::test_doc3.core::Sys" name="test_doc3::test_doc3::Sys::foo3"><failure>Syntax Error: unexpected identifier &#39;garbage&#39;.</failure><system-out>*garbage*
+</system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
index 42cbaa9..7aa0887 100644 (file)
@@ -1,16 +1,43 @@
-test_nitunit4/test_nitunit4.nit:22,2--25,4: ERROR: test_foo (in file .nitunit/gen_test_nitunit4.nit): Before Test
-Tested method
-After Test
-Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31)
+* Test-suite of module test_nitunit4::test_nitunit4 (3 tests)
+
+[KO] test_nitunit4$TestTestSuite$test_foo
+     test_nitunit4/test_nitunit4.nit:22,2--26,4: Runtime Error in file nitunit.out/gen_test_nitunit4.nit
+     Output
+       Before Test
+       Tested method
+       After Test
+       Runtime error: Assert failed (test_nitunit4/test_nitunit4_base.nit:31)
 
-DocUnits:
-No doc units found
-Entities: 10; Documented ones: 0; With nitunits: 0; Failures: 0
+[OK] test_nitunit4$TestTestSuite$test_bar
+[KO] test_nitunit4$TestTestSuite$test_baz
+     test_nitunit4/test_nitunit4.nit:32,2--34,4: Difference with expected output: diff -u test_nitunit4/test_nitunit4.sav/test_baz.res nitunit.out/gen_test_nitunit4_test_baz.out1
+     Output
+       Diff
+       --- expected:test_nitunit4/test_nitunit4.sav/test_baz.res
+       +++ got:nitunit.out/gen_test_nitunit4_test_baz.out1
+       @@ -1 +1,3 @@
+       -Bad result file
+       +Before Test
+       +Tested method
+       +After Test
 
-TestSuites:
-Class suites: 1; Test Cases: 1; Failures: 1
-<testsuites><testsuite package="test_nitunit4&gt;"></testsuite><testsuite package="test_nitunit4::test_nitunit4"></testsuite><testsuite></testsuite><testsuite package="test_nitunit4::nitunit4"></testsuite><testsuite package="test_nitunit4"><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_foo"><system-err></system-err><system-out>out</system-out><error message="Before Test
+Docunits: Entities: 12; Documented ones: 0; With nitunits: 0
+Test suites: Classes: 1; Test Cases: 3; Failures: 2
+[FAILURE] 2/3 tests failed.
+`nitunit.out` is not removed for investigation.
+<testsuites><testsuite package="test_nitunit4&gt;"></testsuite><testsuite package="test_nitunit4::nitunit4"></testsuite><testsuite package="test_nitunit4"><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_foo"><error>Runtime Error in file nitunit.out&#47;gen_test_nitunit4.nit</error><system-err>Before Test
 Tested method
 After Test
 Runtime error: Assert failed (test_nitunit4&#47;test_nitunit4_base.nit:31)
-"></error></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4_base"></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
+</system-err></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_bar"><system-err>Before Test
+Tested method
+After Test
+</system-err></testcase><testcase classname="nitunit.test_nitunit4::test_nitunit4.test_nitunit4::TestTestSuite" name="test_nitunit4::TestTestSuite::test_baz"><error>Difference with expected output: diff -u test_nitunit4&#47;test_nitunit4.sav&#47;test_baz.res nitunit.out&#47;gen_test_nitunit4_test_baz.out1</error><system-err>Diff
+--- expected:test_nitunit4&#47;test_nitunit4.sav&#47;test_baz.res
++++ got:nitunit.out&#47;gen_test_nitunit4_test_baz.out1
+@@ -1 +1,3 @@
+-Bad result file
++Before Test
++Tested method
++After Test
+</system-err></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4"></testsuite><testsuite></testsuite><testsuite package="test_nitunit4::test_nitunit4_base"></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
diff --git a/tests/sav/repeating_key_xor_solve.res b/tests/sav/repeating_key_xor_solve.res
new file mode 100644 (file)
index 0000000..8a61309
--- /dev/null
@@ -0,0 +1 @@
+Usage: repeating_key_xor_solve <cipher_file>
diff --git a/tests/sav/repeating_key_xor_solve_args1.res b/tests/sav/repeating_key_xor_solve_args1.res
new file mode 100644 (file)
index 0000000..f81264e
--- /dev/null
@@ -0,0 +1,80 @@
+I'm back and I'm ringin' the bell 
+A rockin' on the mike while the fly girls yell 
+In ecstasy in the back of me 
+Well that's my DJ Deshay cuttin' all them Z's 
+Hittin' hard and the girlies goin' crazy 
+Vanilla's on the mike, man I'm not lazy. 
+
+I'm lettin' my drug kick in 
+It controls my mouth and I begin 
+To just let it flow, let my concepts go 
+My posse's to the side yellin', Go Vanilla Go! 
+
+Smooth 'cause that's the way I will be 
+And if you don't give a damn, then 
+Why you starin' at me 
+So get off 'cause I control the stage 
+There's no dissin' allowed 
+I'm in my own phase 
+The girlies sa y they love me and that is ok 
+And I can dance better than any kid n' play 
+
+Stage 2 -- Yea the one ya' wanna listen to 
+It's off my head so let the beat play through 
+So I can funk it up and make it sound good 
+1-2-3 Yo -- Knock on some wood 
+For good luck, I like my rhymes atrocious 
+Supercalafragilisticexpialidocious 
+I'm an effect and that you can bet 
+I can take a fly girl and make her wet. 
+
+I'm like Samson -- Samson to Delilah 
+There's no denyin', You can try to hang 
+But you'll keep tryin' to get my style 
+Over and over, practice makes perfect 
+But not if you're a loafer. 
+
+You'll get nowhere, no place, no time, no girls 
+Soon -- Oh my God, homebody, you probably eat 
+Spaghetti with a spoon! Come on and say it! 
+
+VIP. Vanilla Ice yep, yep, I'm comin' hard like a rhino 
+Intoxicating so you stagger like a wino 
+So punks stop trying and girl stop cryin' 
+Vanilla Ice is sellin' and you people are buyin' 
+'Cause why the freaks are jockin' like Crazy Glue 
+Movin' and groovin' trying to sing along 
+All through the ghetto groovin' this here song 
+Now you're amazed by the VIP posse. 
+
+Steppin' so hard like a German Nazi 
+Startled by the bases hittin' ground 
+There's no trippin' on mine, I'm just gettin' down 
+Sparkamatic, I'm hangin' tight like a fanatic 
+You trapped me once and I thought that 
+You might have it 
+So step down and lend me your ear 
+'89 in my time! You, '90 is my year. 
+
+You're weakenin' fast, YO! and I can tell it 
+Your body's gettin' hot, so, so I can smell it 
+So don't be mad and don't be sad 
+'Cause the lyrics belong to ICE, You can call me Dad 
+You're pitchin' a fit, so step back and endure 
+Let the witch doctor, Ice, do the dance to cure 
+So come up close and don't be square 
+You wanna battle me -- Anytime, anywhere 
+
+You thought that I was weak, Boy, you're dead wrong 
+So come on, everybody and sing this song 
+
+Say -- Play that funky music Say, go white boy, go white boy go 
+play that funky music Go white boy, go white boy, go 
+Lay down and boogie and play that funky music till you die. 
+
+Play that funky music Come on, Come on, let me hear 
+Play that funky music white boy you say it, say it 
+Play that funky music A little louder now 
+Play that funky music, white boy Come on, Come on, Come on 
+Play that funky music 
+
diff --git a/tests/sav/test_copy_to_native.res b/tests/sav/test_copy_to_native.res
new file mode 100644 (file)
index 0000000..764d170
--- /dev/null
@@ -0,0 +1 @@
+gâštr
diff --git a/tests/sav/test_copy_to_native_alt1.res b/tests/sav/test_copy_to_native_alt1.res
new file mode 100644 (file)
index 0000000..785cb5d
--- /dev/null
@@ -0,0 +1 @@
+gâštr%D
diff --git a/tests/sav/test_copy_to_native_alt2.res b/tests/sav/test_copy_to_native_alt2.res
new file mode 100644 (file)
index 0000000..ec88d9a
--- /dev/null
@@ -0,0 +1 @@
+gâštré
index fe9ca03..449fa7f 100644 (file)
@@ -53,7 +53,7 @@
 <h1>base_simple3$B$val=</h1>
 <pre><code><span class="nitcode"><span class="nc_pdef foldable" id="base_simple3$B$_val"><a id="base_simple3$B$val"></a><a id="base_simple3$B$val="></a><span class="line" id="L35">   <span class="nc_k">var</span> <span class="nc_def nc_i">val</span><span>:</span> <span class="nc_t">Int</span></span></span></span></code></pre>
 <h1>base_simple3$B$init</h1>
-<pre><code><span class="nitcode"><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36">  <span class="nc_k">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
+<pre><code><span class="nitcode"><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36">  <span class="nc_k popupable" style="border-bottom: solid 2px red" title="Messages" data-content="&lt;div&gt;&lt;div class=&#34;dropdown&#34;&gt; &lt;a data-toggle=&#34;dropdown&#34; href=&#34;#&#34;&gt;&lt;b&gt;1 message(s)&lt;&#47;b&gt; &lt;span class=&#34;caret&#34;&gt;&lt;&#47;span&gt;&lt;&#47;a&gt;&lt;ul class=&#34;dropdown-menu&#34; role=&#34;menu&#34; aria-labelledby=&#34;dLabel&#34;&gt;&lt;li&gt;Warning: init with signature in base_simple3$B&lt;&#47;li&gt;&lt;&#47;ul&gt;&lt;&#47;div&gt;&lt;&#47;div&gt;" data-toggle="popover">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
 </span><span class="line" id="L37">    <span class="nc_k">do</span>
 </span><span class="line" id="L38">            <span class="nc_l">7</span><span>.</span><span class="nc_i">output</span>
 </span><span class="line" id="L39">            <span class="nc_k">self</span><span>.</span><span class="nc_i">val</span> <span>=</span> <span class="nc_v nc_i">v</span>
 </span></span><span class="line" id="L33">
 </span><span class="nc_cdef foldable" id="base_simple3$B"><span class="line" id="L34"><span class="nc_k">class</span> <span class="nc_t">B</span>
 </span><span class="nc_pdef foldable" id="base_simple3$B$_val"><a id="base_simple3$B$val"></a><a id="base_simple3$B$val="></a><span class="line" id="L35">     <span class="nc_k">var</span> <span class="nc_def nc_i">val</span><span>:</span> <span class="nc_t">Int</span>
-</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36">     <span class="nc_k">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
+</span></span><span class="nc_pdef foldable" id="base_simple3$B$init"><span class="line" id="L36">     <span class="nc_k popupable" style="border-bottom: solid 2px red" title="Messages" data-content="&lt;div&gt;&lt;div class=&#34;dropdown&#34;&gt; &lt;a data-toggle=&#34;dropdown&#34; href=&#34;#&#34;&gt;&lt;b&gt;1 message(s)&lt;&#47;b&gt; &lt;span class=&#34;caret&#34;&gt;&lt;&#47;span&gt;&lt;&#47;a&gt;&lt;ul class=&#34;dropdown-menu&#34; role=&#34;menu&#34; aria-labelledby=&#34;dLabel&#34;&gt;&lt;li&gt;Warning: init with signature in base_simple3$B&lt;&#47;li&gt;&lt;&#47;ul&gt;&lt;&#47;div&gt;&lt;&#47;div&gt;" data-toggle="popover">init</span><span>(</span><span class="nc_v nc_i">v</span><span>:</span> <span class="nc_t">Int</span><span>)</span>
 </span><span class="line" id="L37">    <span class="nc_k">do</span>
 </span><span class="line" id="L38">            <span class="nc_l">7</span><span>.</span><span class="nc_i">output</span>
 </span><span class="line" id="L39">            <span class="nc_k">self</span><span>.</span><span class="nc_i">val</span> <span>=</span> <span class="nc_v nc_i">v</span>
index eb9da24..71dc9e7 100644 (file)
@@ -27,6 +27,6 @@
 # Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>
 
 # JSON: not valid json
-# Errors: 'Error Parsing JSON: [1:1-1:2] Unexpected character 'n'; is acceptable instead: value'
+# Errors: 'Parsing error at line 1, position 1: Error: bad JSON entity'
 # Nit: null
 
index c19ddce..fb0ab9b 100644 (file)
@@ -110,58 +110,58 @@ Names:
   base_simple3: 12 (1.06%)
 
 # All entities
-base_simple3   MPackage        
-base_simple3>  MGroup  
-base_simple3::base_simple3     MModule 
-base_simple3::Object   MClass  
-base_simple3$Object    MClassDef       
-base_simple3::Object::init     MMethod 
-base_simple3$Object$init       MMethodDef      
-base_simple3::Bool     MClass  
-base_simple3$Bool      MClassDef       
-base_simple3::Int      MClass  
-base_simple3$Int       MClassDef       
-base_simple3::Int::output      MMethod 
-base_simple3$Int$output        MMethodDef      
-base_simple3::A        MClass  
-base_simple3$A MClassDef       
-base_simple3$A$Object::init    MMethodDef      
-base_simple3::A::run   MMethod 
-base_simple3$A$run     MMethodDef      
-base_simple3::B        MClass  
-base_simple3$B MClassDef       
-base_simple3::base_simple3::B::_val    MAttribute      
-base_simple3$B$_val    MAttributeDef   
-base_simple3::B::val   MMethod 
-base_simple3$B$val     MMethodDef      
-base_simple3::B::val=  MMethod 
-base_simple3$B$val=    MMethodDef      
-base_simple3::B::init  MMethod 
-base_simple3$B$init    MMethodDef      
-base_simple3::B::run   MMethod 
-base_simple3$B$run     MMethodDef      
-base_simple3::C        MClass  
-base_simple3$C MClassDef       
-base_simple3::base_simple3::C::_val1   MAttribute      
-base_simple3$C$_val1   MAttributeDef   
-base_simple3::C::val1  MMethod 
-base_simple3$C$val1    MMethodDef      
-base_simple3::C::val1= MMethod 
-base_simple3$C$val1=   MMethodDef      
-base_simple3::base_simple3::C::_val2   MAttribute      
-base_simple3$C$_val2   MAttributeDef   
-base_simple3::C::val2  MMethod 
-base_simple3$C$val2    MMethodDef      
-base_simple3::C::val2= MMethod 
-base_simple3$C$val2=   MMethodDef      
-base_simple3$C$Object::init    MMethodDef      
-base_simple3::Sys      MClass  
-base_simple3$Sys       MClassDef       
-base_simple3::Sys::foo MMethod 
-base_simple3$Sys$foo   MMethodDef      
-base_simple3::Sys::bar MMethod 
-base_simple3$Sys$bar   MMethodDef      
-base_simple3::Sys::baz MMethod 
-base_simple3$Sys$baz   MMethodDef      
-base_simple3::Sys::main        MMethod 
-base_simple3$Sys$main  MMethodDef      
+base_simple3   MPackage        base_simple3.nit        
+base_simple3>  MGroup  base_simple3.nit        
+base_simple3::base_simple3     MModule base_simple3.nit:17,1--66,13    
+base_simple3::Object   MClass  base_simple3.nit:19,1--20,3     
+base_simple3$Object    MClassDef       base_simple3.nit:19,1--20,3     
+base_simple3::Object::init     MMethod base_simple3.nit:19,1--20,3     
+base_simple3$Object$init       MMethodDef      base_simple3.nit:19,1--20,3     
+base_simple3::Bool     MClass  base_simple3.nit:22,1--23,3     
+base_simple3$Bool      MClassDef       base_simple3.nit:22,1--23,3     
+base_simple3::Int      MClass  base_simple3.nit:25,1--27,3     
+base_simple3$Int       MClassDef       base_simple3.nit:25,1--27,3     
+base_simple3::Int::output      MMethod base_simple3.nit:26,2--21       
+base_simple3$Int$output        MMethodDef      base_simple3.nit:26,2--21       
+base_simple3::A        MClass  base_simple3.nit:29,1--32,3     
+base_simple3$A MClassDef       base_simple3.nit:29,1--32,3     
+base_simple3$A$Object::init    MMethodDef      base_simple3.nit:30,2--17       
+base_simple3::A::run   MMethod base_simple3.nit:31,2--20       
+base_simple3$A$run     MMethodDef      base_simple3.nit:31,2--20       
+base_simple3::B        MClass  base_simple3.nit:34,1--42,3     
+base_simple3$B MClassDef       base_simple3.nit:34,1--42,3     
+base_simple3::base_simple3::B::_val    MAttribute      base_simple3.nit:35,2--13       
+base_simple3$B$_val    MAttributeDef   base_simple3.nit:35,2--13       
+base_simple3::B::val   MMethod base_simple3.nit:35,2--13       
+base_simple3$B$val     MMethodDef      base_simple3.nit:35,2--13       
+base_simple3::B::val=  MMethod base_simple3.nit:35,2--13       
+base_simple3$B$val=    MMethodDef      base_simple3.nit:35,2--13       
+base_simple3::B::init  MMethod base_simple3.nit:36,2--40,4     
+base_simple3$B$init    MMethodDef      base_simple3.nit:36,2--40,4     
+base_simple3::B::run   MMethod base_simple3.nit:41,2--22       
+base_simple3$B$run     MMethodDef      base_simple3.nit:41,2--22       
+base_simple3::C        MClass  base_simple3.nit:44,1--47,3     
+base_simple3$C MClassDef       base_simple3.nit:44,1--47,3     
+base_simple3::base_simple3::C::_val1   MAttribute      base_simple3.nit:45,2--14       
+base_simple3$C$_val1   MAttributeDef   base_simple3.nit:45,2--14       
+base_simple3::C::val1  MMethod base_simple3.nit:45,2--14       
+base_simple3$C$val1    MMethodDef      base_simple3.nit:45,2--14       
+base_simple3::C::val1= MMethod base_simple3.nit:45,2--14       
+base_simple3$C$val1=   MMethodDef      base_simple3.nit:45,2--14       
+base_simple3::base_simple3::C::_val2   MAttribute      base_simple3.nit:46,2--19       
+base_simple3$C$_val2   MAttributeDef   base_simple3.nit:46,2--19       
+base_simple3::C::val2  MMethod base_simple3.nit:46,2--19       
+base_simple3$C$val2    MMethodDef      base_simple3.nit:46,2--19       
+base_simple3::C::val2= MMethod base_simple3.nit:46,2--19       
+base_simple3$C$val2=   MMethodDef      base_simple3.nit:46,2--19       
+base_simple3$C$Object::init    MMethodDef      base_simple3.nit:44,1--47,3     
+base_simple3::Sys      MClass  base_simple3.nit:49,1--19       
+base_simple3$Sys       MClassDef       base_simple3.nit:49,1--19       
+base_simple3::Sys::foo MMethod base_simple3.nit:49,1--19       
+base_simple3$Sys$foo   MMethodDef      base_simple3.nit:49,1--19       
+base_simple3::Sys::bar MMethod base_simple3.nit:50,1--27       
+base_simple3$Sys$bar   MMethodDef      base_simple3.nit:50,1--27       
+base_simple3::Sys::baz MMethod base_simple3.nit:51,1--24       
+base_simple3$Sys$baz   MMethodDef      base_simple3.nit:51,1--24       
+base_simple3::Sys::main        MMethod base_simple3.nit:53,1--66,13    
+base_simple3$Sys$main  MMethodDef      base_simple3.nit:53,1--66,13    
index 2dd778a..445de88 100644 (file)
@@ -106,101 +106,101 @@ Names:
   names: 5 (0.28%)
 
 # All entities
-names  MPackage        Group of modules used to test various full_name configurations and conflicts.
-names> MGroup  Group of modules used to test various full_name configurations and conflicts.
-names::n3      MModule The bottom module
-names::n3$A1   MClassDef       a refinement of a subclass in a submodule
-names::n3$A1$A::a      MMethodDef      a refinement (3 distinct modules)
-names::n3$A1$::n0::P::p        MMethodDef      a refinement (3 distinct modules)
-names::n3$::n1::P1     MClassDef       a refinement of a subclass in a submodule
-names::n3$::n1::P1$A::a        MMethodDef      a refinement (3 distinct modules)
-names::n3$::n1::P1$::n0::P::p  MMethodDef      a refinement (3 distinct modules)
-names::n0      MModule Root module
-names::Object  MClass  
-names$Object   MClassDef       Root interface
-names::Object::init    MMethod 
-names$Object$init      MMethodDef      
-names::A       MClass  
-names$A        MClassDef       A public class
-names::A::a    MMethod 
-names$A$a      MMethodDef      A public method in a public class
-names::n0::A::z        MMethod 
-names$A$z      MMethodDef      A private method in a public class
-names::A0      MClass  
-names$A0       MClassDef       A public subclass in the same module
-names$A0$A::a  MMethodDef      Redefinition it the same module of a public method
-names$A0$::n0::A::z    MMethodDef      Redefinition it the same module of a private method
-names$A0$::n0::P::p    MMethodDef      Redefinition it the same module of a private method
-names::n0::P   MClass  
-names::n0$P    MClassDef       A private class
-names::n0::P::p        MMethod 
-names::n0$P$p  MMethodDef      A private method in a private class
-names::n0::P0  MClass  
-names::n0$P0   MClassDef       A private subclass introduced in the same module
-names::n0$P0$A::a      MMethodDef      Redefinition it the same module of a public method
-names::n0$P0$::n0::A::z        MMethodDef      Redefinition it the same module of a private method
-names::n0$P0$::n0::P::p        MMethodDef      Redefinition it the same module of a private method
-names::n1      MModule Second module
-names::n1$A    MClassDef       A refinement of a class
-names::n1$A$a  MMethodDef      A refinement in the same class
-names::n1$A$z  MMethodDef      A refinement in the same class
-names::n1::A::b        MMethod 
-names::n1$A$b  MMethodDef      A public method introduced in a refinement
-names::n1$A0   MClassDef       A refinement of a subclass
-names::n1$A0$A::a      MMethodDef      A refinement+redefinition
-names::n1$A0$::n0::A::z        MMethodDef      A refinement+redefinition
-names::n1$A0$::n0::P::p        MMethodDef      A refinement+redefinition
-names::A1      MClass  
-names$A1       MClassDef       A subclass introduced in a submodule
-names$A1$A::a  MMethodDef      A redefinition in a subclass from a different module
-names$A1$::n0::A::z    MMethodDef      A redefinition in a subclass from a different module
-names$A1$::n0::P::p    MMethodDef      A redefinition in a subclass from a different module
-names::n1$::n0::P      MClassDef       A refinement of a class
-names::n1$::n0::P$p    MMethodDef      A refinement in the same class
-names::n1$::n0::P0     MClassDef       A refinement of a subclass
-names::n1$::n0::P0$A::a        MMethodDef      A refinement+redefinition
-names::n1$::n0::P0$::n0::A::z  MMethodDef      A refinement+redefinition
-names::n1$::n0::P0$::n0::P::p  MMethodDef      A refinement+redefinition
-names::n1::P1  MClass  
-names::n1$P1   MClassDef       A private subclass introduced in a different module
-names::n1$P1$A::a      MMethodDef      A redefinition in a subclass from a different module
-names::n1$P1$::n0::A::z        MMethodDef      A redefinition in a subclass from a different module
-names::n1$P1$::n0::P::p        MMethodDef      A redefinition in a subclass from a different module
-names::n2      MModule A alternative second module, used to make name conflicts
-names::n2$A    MClassDef       A refinement of a class
-names::n2::A::b        MMethod 
-names::n2$A$b  MMethodDef      Name conflict? A second public method
-names::n2::A::z        MMethod 
-names::n2$A$z  MMethodDef      Name conflict? A second private method
-names::n2::P   MClass  
-names::n2$P    MClassDef       Name conflict? A second private class
-names::n2::P::p        MMethod 
-names::n2$P$p  MMethodDef      Name conflict? A private method in an homonym class.
-names1 MPackage        An alternative second module in a distinct package
-names1>        MGroup  An alternative second module in a distinct package
-names1::names1 MModule An alternative second module in a distinct package
-names1::names1$names::A        MClassDef       A refinement of a class
-names1::names1$names::A$a      MMethodDef      A refinement in the same class
-names1::names1$names::A$z      MMethodDef      A refinement in the same class
-names1::names1::A::b   MMethod 
-names1::names1$names::A$b      MMethodDef      A public method introduced in a refinement
-names1::names1$names::A0       MClassDef       A refinement of a subclass
-names1::names1$names::A0$names::A::a   MMethodDef      A refinement+redefinition
-names1::names1$names::A0$names::n0::A::z       MMethodDef      A refinement+redefinition
-names1::names1$names::A0$names::n0::P::p       MMethodDef      A refinement+redefinition
-names1::A1     MClass  
-names1$A1      MClassDef       A subclass introduced in a submodule
-names1$A1$names::A::a  MMethodDef      A redefinition in a subclass from a different module
-names1$A1$names::n0::A::z      MMethodDef      A redefinition in a subclass from a different module
-names1$A1$names::n0::P::p      MMethodDef      A redefinition in a subclass from a different module
-names1::names1$names::n0::P    MClassDef       A refinement of a class
-names1::names1$names::n0::P$p  MMethodDef      A refinement in the same class
-names1::names1$names::n0::P0   MClassDef       A refinement of a subclass
-names1::names1$names::n0::P0$names::A::a       MMethodDef      A refinement+redefinition
-names1::names1$names::n0::P0$names::n0::A::z   MMethodDef      A refinement+redefinition
-names1::names1$names::n0::P0$names::n0::P::p   MMethodDef      A refinement+redefinition
-names1::names1::P1     MClass  
-names1::names1$P1      MClassDef       A private subclass introduced in a different module
-names1::names1$P1$names::A::a  MMethodDef      A redefinition in a subclass from a different module
-names1::names1$P1$names::n0::A::z      MMethodDef      A redefinition in a subclass from a different module
-names1::names1$P1$names::n0::P::p      MMethodDef      A redefinition in a subclass from a different module
+names  MPackage        names   Group of modules used to test various full_name configurations and conflicts.
+names> MGroup  names   Group of modules used to test various full_name configurations and conflicts.
+names::n3      MModule names/n3.nit:15,1--35,3 The bottom module
+names::n3$A1   MClassDef       names/n3.nit:21,1--27,3 a refinement of a subclass in a submodule
+names::n3$A1$A::a      MMethodDef      names/n3.nit:23,2--24,19        a refinement (3 distinct modules)
+names::n3$A1$::n0::P::p        MMethodDef      names/n3.nit:25,2--26,19        a refinement (3 distinct modules)
+names::n3$::n1::P1     MClassDef       names/n3.nit:29,1--35,3 a refinement of a subclass in a submodule
+names::n3$::n1::P1$A::a        MMethodDef      names/n3.nit:31,2--32,19        a refinement (3 distinct modules)
+names::n3$::n1::P1$::n0::P::p  MMethodDef      names/n3.nit:33,2--34,19        a refinement (3 distinct modules)
+names::n0      MModule names/n0.nit:15,1--67,3 Root module
+names::Object  MClass  names/n0.nit:20,1--22,3 Root interface
+names$Object   MClassDef       names/n0.nit:20,1--22,3 Root interface
+names::Object::init    MMethod names/n0.nit:20,1--22,3 
+names$Object$init      MMethodDef      names/n0.nit:20,1--22,3 
+names::A       MClass  names/n0.nit:24,1--31,3 A public class
+names$A        MClassDef       names/n0.nit:24,1--31,3 A public class
+names::A::a    MMethod names/n0.nit:26,2--27,13        A public method in a public class
+names$A$a      MMethodDef      names/n0.nit:26,2--27,13        A public method in a public class
+names::n0::A::z        MMethod names/n0.nit:29,2--30,21        A private method in a public class
+names$A$z      MMethodDef      names/n0.nit:29,2--30,21        A private method in a public class
+names::A0      MClass  names/n0.nit:33,1--46,3 A public subclass in the same module
+names$A0       MClassDef       names/n0.nit:33,1--46,3 A public subclass in the same module
+names$A0$A::a  MMethodDef      names/n0.nit:38,2--39,19        Redefinition it the same module of a public method
+names$A0$::n0::A::z    MMethodDef      names/n0.nit:41,2--42,19        Redefinition it the same module of a private method
+names$A0$::n0::P::p    MMethodDef      names/n0.nit:44,2--45,19        Redefinition it the same module of a private method
+names::n0::P   MClass  names/n0.nit:48,1--52,3 A private class
+names::n0$P    MClassDef       names/n0.nit:48,1--52,3 A private class
+names::n0::P::p        MMethod names/n0.nit:50,2--51,13        A private method in a private class
+names::n0$P$p  MMethodDef      names/n0.nit:50,2--51,13        A private method in a private class
+names::n0::P0  MClass  names/n0.nit:54,1--67,3 A private subclass introduced in the same module
+names::n0$P0   MClassDef       names/n0.nit:54,1--67,3 A private subclass introduced in the same module
+names::n0$P0$A::a      MMethodDef      names/n0.nit:59,2--60,19        Redefinition it the same module of a public method
+names::n0$P0$::n0::A::z        MMethodDef      names/n0.nit:62,2--63,19        Redefinition it the same module of a private method
+names::n0$P0$::n0::P::p        MMethodDef      names/n0.nit:65,2--66,19        Redefinition it the same module of a private method
+names::n1      MModule names/n1.nit:15,1--90,3 Second module
+names::n1$A    MClassDef       names/n1.nit:20,1--30,3 A refinement of a class
+names::n1$A$a  MMethodDef      names/n1.nit:22,2--23,19        A refinement in the same class
+names::n1$A$z  MMethodDef      names/n1.nit:25,2--26,19        A refinement in the same class
+names::n1::A::b        MMethod names/n1.nit:28,2--29,13        A public method introduced in a refinement
+names::n1$A$b  MMethodDef      names/n1.nit:28,2--29,13        A public method introduced in a refinement
+names::n1$A0   MClassDef       names/n1.nit:32,1--42,3 A refinement of a subclass
+names::n1$A0$A::a      MMethodDef      names/n1.nit:34,2--35,19        A refinement+redefinition
+names::n1$A0$::n0::A::z        MMethodDef      names/n1.nit:37,2--38,19        A refinement+redefinition
+names::n1$A0$::n0::P::p        MMethodDef      names/n1.nit:40,2--41,19        A refinement+redefinition
+names::A1      MClass  names/n1.nit:44,1--57,3 A subclass introduced in a submodule
+names$A1       MClassDef       names/n1.nit:44,1--57,3 A subclass introduced in a submodule
+names$A1$A::a  MMethodDef      names/n1.nit:49,2--50,19        A redefinition in a subclass from a different module
+names$A1$::n0::A::z    MMethodDef      names/n1.nit:52,2--53,19        A redefinition in a subclass from a different module
+names$A1$::n0::P::p    MMethodDef      names/n1.nit:55,2--56,19        A redefinition in a subclass from a different module
+names::n1$::n0::P      MClassDef       names/n1.nit:59,1--63,3 A refinement of a class
+names::n1$::n0::P$p    MMethodDef      names/n1.nit:61,2--62,19        A refinement in the same class
+names::n1$::n0::P0     MClassDef       names/n1.nit:65,1--75,3 A refinement of a subclass
+names::n1$::n0::P0$A::a        MMethodDef      names/n1.nit:67,2--68,19        A refinement+redefinition
+names::n1$::n0::P0$::n0::A::z  MMethodDef      names/n1.nit:70,2--71,19        A refinement+redefinition
+names::n1$::n0::P0$::n0::P::p  MMethodDef      names/n1.nit:73,2--74,19        A refinement+redefinition
+names::n1::P1  MClass  names/n1.nit:77,1--90,3 A private subclass introduced in a different module
+names::n1$P1   MClassDef       names/n1.nit:77,1--90,3 A private subclass introduced in a different module
+names::n1$P1$A::a      MMethodDef      names/n1.nit:82,2--83,19        A redefinition in a subclass from a different module
+names::n1$P1$::n0::A::z        MMethodDef      names/n1.nit:85,2--86,19        A redefinition in a subclass from a different module
+names::n1$P1$::n0::P::p        MMethodDef      names/n1.nit:88,2--89,19        A redefinition in a subclass from a different module
+names::n2      MModule names/n2.nit:15,1--33,3 A alternative second module, used to make name conflicts
+names::n2$A    MClassDef       names/n2.nit:20,1--27,3 A refinement of a class
+names::n2::A::b        MMethod names/n2.nit:22,2--23,13        Name conflict? A second public method
+names::n2$A$b  MMethodDef      names/n2.nit:22,2--23,13        Name conflict? A second public method
+names::n2::A::z        MMethod names/n2.nit:25,2--26,13        Name conflict? A second private method
+names::n2$A$z  MMethodDef      names/n2.nit:25,2--26,13        Name conflict? A second private method
+names::n2::P   MClass  names/n2.nit:29,1--33,3 Name conflict? A second private class
+names::n2$P    MClassDef       names/n2.nit:29,1--33,3 Name conflict? A second private class
+names::n2::P::p        MMethod names/n2.nit:31,2--32,13        Name conflict? A private method in an homonym class.
+names::n2$P$p  MMethodDef      names/n2.nit:31,2--32,13        Name conflict? A private method in an homonym class.
+names1 MPackage        names1.nit      An alternative second module in a distinct package
+names1>        MGroup  names1.nit      An alternative second module in a distinct package
+names1::names1 MModule names1.nit:15,1--90,3   An alternative second module in a distinct package
+names1::names1$names::A        MClassDef       names1.nit:20,1--30,3   A refinement of a class
+names1::names1$names::A$a      MMethodDef      names1.nit:22,2--23,19  A refinement in the same class
+names1::names1$names::A$z      MMethodDef      names1.nit:25,2--26,19  A refinement in the same class
+names1::names1::A::b   MMethod names1.nit:28,2--29,13  A public method introduced in a refinement
+names1::names1$names::A$b      MMethodDef      names1.nit:28,2--29,13  A public method introduced in a refinement
+names1::names1$names::A0       MClassDef       names1.nit:32,1--42,3   A refinement of a subclass
+names1::names1$names::A0$names::A::a   MMethodDef      names1.nit:34,2--35,19  A refinement+redefinition
+names1::names1$names::A0$names::n0::A::z       MMethodDef      names1.nit:37,2--38,19  A refinement+redefinition
+names1::names1$names::A0$names::n0::P::p       MMethodDef      names1.nit:40,2--41,19  A refinement+redefinition
+names1::A1     MClass  names1.nit:44,1--57,3   A subclass introduced in a submodule
+names1$A1      MClassDef       names1.nit:44,1--57,3   A subclass introduced in a submodule
+names1$A1$names::A::a  MMethodDef      names1.nit:49,2--50,19  A redefinition in a subclass from a different module
+names1$A1$names::n0::A::z      MMethodDef      names1.nit:52,2--53,19  A redefinition in a subclass from a different module
+names1$A1$names::n0::P::p      MMethodDef      names1.nit:55,2--56,19  A redefinition in a subclass from a different module
+names1::names1$names::n0::P    MClassDef       names1.nit:59,1--63,3   A refinement of a class
+names1::names1$names::n0::P$p  MMethodDef      names1.nit:61,2--62,19  A refinement in the same class
+names1::names1$names::n0::P0   MClassDef       names1.nit:65,1--75,3   A refinement of a subclass
+names1::names1$names::n0::P0$names::A::a       MMethodDef      names1.nit:67,2--68,19  A refinement+redefinition
+names1::names1$names::n0::P0$names::n0::A::z   MMethodDef      names1.nit:70,2--71,19  A refinement+redefinition
+names1::names1$names::n0::P0$names::n0::P::p   MMethodDef      names1.nit:73,2--74,19  A refinement+redefinition
+names1::names1::P1     MClass  names1.nit:77,1--90,3   A private subclass introduced in a different module
+names1::names1$P1      MClassDef       names1.nit:77,1--90,3   A private subclass introduced in a different module
+names1::names1$P1$names::A::a  MMethodDef      names1.nit:82,2--83,19  A redefinition in a subclass from a different module
+names1::names1$P1$names::n0::A::z      MMethodDef      names1.nit:85,2--86,19  A redefinition in a subclass from a different module
+names1::names1$P1$names::n0::P::p      MMethodDef      names1.nit:88,2--89,19  A redefinition in a subclass from a different module
diff --git a/tests/sav/test_nativestring_fill_from.res b/tests/sav/test_nativestring_fill_from.res
new file mode 100644 (file)
index 0000000..2c26c70
--- /dev/null
@@ -0,0 +1 @@
+S&éstr
diff --git a/tests/sav/test_nativestring_fill_from_alt1.res b/tests/sav/test_nativestring_fill_from_alt1.res
new file mode 100644 (file)
index 0000000..0667158
--- /dev/null
@@ -0,0 +1 @@
+S&éstrS&éstr
diff --git a/tests/sav/test_nativestring_fill_from_alt2.res b/tests/sav/test_nativestring_fill_from_alt2.res
new file mode 100644 (file)
index 0000000..2c26c70
--- /dev/null
@@ -0,0 +1 @@
+S&éstr
diff --git a/tests/sav/test_nativestring_fill_from_alt3.res b/tests/sav/test_nativestring_fill_from_alt3.res
new file mode 100644 (file)
index 0000000..f28809f
--- /dev/null
@@ -0,0 +1 @@
+&éstr
index 9e5ceab..38c4890 100644 (file)
@@ -1,4 +1,4 @@
-Runtime error: Cast failed. Expected `E`, got `Bool` (../lib/core/collection/array.nit:989)
+Runtime error: Cast failed. Expected `E`, got `Bool` (../lib/core/collection/array.nit:991)
 NativeString
 0x4e
 Nit
diff --git a/tests/sav/test_postgres_native.res b/tests/sav/test_postgres_native.res
new file mode 100644 (file)
index 0000000..0c511fe
--- /dev/null
@@ -0,0 +1,3 @@
+aname   class   sex   
+Whale   mammal   1   
+Snake   reptile   0   
diff --git a/tests/sav/test_readline.res b/tests/sav/test_readline.res
new file mode 100644 (file)
index 0000000..666312a
--- /dev/null
@@ -0,0 +1,9 @@
+prompt>line 1
+line 1
+prompt>line 2
+line 2
+prompt>line 2bis
+line 2bis
+prompt>line 3 \b \bine\b \b\b \b\b \b3\b \b
+line 3
+prompt>
\ No newline at end of file
diff --git a/tests/sav/test_realtime.res b/tests/sav/test_realtime.res
deleted file mode 100644 (file)
index 65b7078..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-sleeping 1s
-true
-true
-sleeping 5000ns
-true
-true
-true
index f40c047..54cb7f8 100644 (file)
@@ -43,3 +43,6 @@ test.has_suffix("test") => true
 test.has_suffix("bt") => false
 test.has_suffix("bat") => false
 test.has_suffix("foot") => false
+........
+
+........
diff --git a/tests/test_copy_to_native.nit b/tests/test_copy_to_native.nit
new file mode 100644 (file)
index 0000000..380c02a
--- /dev/null
@@ -0,0 +1,28 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import core
+#alt1 intrude import core::text::ropes
+#alt2 intrude import core::text::ropes
+
+var ons = new NativeString(9)
+var base_str = "%Dégâštr"
+
+var str: String = base_str
+#alt1 str = new Concat(base_str, base_str)
+#alt2 str = new Concat(base_str, base_str.substring_from(2))
+
+var copy_len = (str.bytelen - 4).min(9)
+str.copy_to_native(ons, copy_len, 4, 0)
+print ons.to_s_with_length(copy_len)
index 21728c1..a9f3e61 100644 (file)
@@ -21,12 +21,11 @@ var clock = new Clock
 var p = new Process("sleep", "10")
 
 # Send some signals
-p.signal(sigalarm)
+p.signal sigalarm
 p.kill
 
 # Wait for it to die
 p.wait
-var lapse = clock.lapse
 
 # Let's be generous here, as long as it does not take 5 secs out of 10 it's OK
-print lapse.sec < 5
+print clock.lapse < 5.0
diff --git a/tests/test_nativestring_fill_from.nit b/tests/test_nativestring_fill_from.nit
new file mode 100644 (file)
index 0000000..1b9e491
--- /dev/null
@@ -0,0 +1,27 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#alt1 intrude import core::text::ropes
+import core
+
+var src_s = "S&éstr"
+var cpstr: Text = src_s
+#alt1 cpstr = new Concat(src_s, src_s)
+#alt2 cpstr = new FlatBuffer.from(src_s)
+#alt3 cpstr = cpstr.substring(1, 5)
+
+var ns = new NativeString(cpstr.bytelen)
+ns.fill_from(cpstr)
+
+print ns.to_s_with_length(cpstr.bytelen)
index e4088a2..5f2ca1c 100644 (file)
@@ -17,8 +17,8 @@ import pthreads
 
 redef class Sys
        var iface: String is lazy do
-               srand
-               return "localhost:{10000+20000.rand}"
+               var testid = "NIT_TESTING_ID".environ.to_i
+               return "localhost:{10000+testid}"
        end
 
        var fs_path: String = getcwd / "../lib/nitcorn/examples/www/hello_world/dir" is lazy
index b583197..d260cd1 100644 (file)
@@ -24,6 +24,8 @@ class X
        #     assert undefined_identifier
        fun foo do end
 
+       # a 'failure' unit test (does not parse)
+       #     assert !@#$%^&*()
        fun foo1(a, b: Int) do end
 
        private fun foo2: Bool do return true
index db734a6..cd13aca 100644 (file)
@@ -22,5 +22,14 @@ class TestTestSuite
        fun test_foo do
                print "Tested method"
                assert before
+               before = false
+       end
+
+       fun test_bar do
+               print "Tested method"
+       end
+
+       fun test_baz do
+               print "Tested method"
        end
 end
diff --git a/tests/test_nitunit4/test_nitunit4.sav/test_bar.res b/tests/test_nitunit4/test_nitunit4.sav/test_bar.res
new file mode 100644 (file)
index 0000000..f6d97cf
--- /dev/null
@@ -0,0 +1,3 @@
+Before Test
+Tested method
+After Test
diff --git a/tests/test_nitunit4/test_nitunit4.sav/test_baz.res b/tests/test_nitunit4/test_nitunit4.sav/test_baz.res
new file mode 100644 (file)
index 0000000..0e89bcd
--- /dev/null
@@ -0,0 +1 @@
+Bad result file
index 3e7a1e7..af21e73 100644 (file)
@@ -28,6 +28,6 @@ class SuperTestSuite
 
        redef fun after_test do
                print "After Test"
-               assert false
+               assert before
        end
 end
diff --git a/tests/test_postgres_native.nit b/tests/test_postgres_native.nit
new file mode 100644 (file)
index 0000000..ff3be6f
--- /dev/null
@@ -0,0 +1,73 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module test_postgres_native
+
+import postgresql::native_postgres
+
+var db = new NativePostgres.connectdb("dbname=postgres")
+assert postgres_open: db.status.is_ok else print_error db.error
+
+var result = db.exec("CREATE TABLE IF NOT EXISTS animals (aname TEXT PRIMARY KEY, class TEXT NOT NULL, sex INTEGER)")
+assert postgres_create_table: result.status.is_ok else print_error db.error
+
+result = db.exec("INSERT INTO animals VALUES('Whale', 'mammal', 1)")
+assert postgres_insert_1: result.status.is_ok else print_error db.error
+
+result = db.exec("INSERT INTO animals VALUES('Snake', 'reptile', 0)")
+assert postgres_insert_2: result.status.is_ok else print_error db.error
+
+result = db.exec("SELECT * FROM animals")
+assert postgres_select: result.status.is_ok else print_error db.error
+
+assert postgres_ntuples: result.ntuples == 2 else print_error db.error
+assert postgres_nfields: result.nfields == 3 else print_error db.error
+assert postgres_fname: result.fname(0) == "aname" else print_error db.error
+assert postgres_isnull: result.is_null(0,0) == false else print_error db.error
+assert postgres_value: result.value(0,0) == "Whale" else print_error db.error
+
+var cols: Int = result.nfields
+var rows: Int = result.ntuples
+var fields: String = ""
+for c in [0..cols[ do fields += result.fname(c) + "   "
+print fields
+for i in [0..rows[ do
+  fields = ""
+  for j in [0..cols[ do fields +=  result.value(i, j) + "   "
+  print fields
+end
+
+result = db.exec("DELETE FROM animals WHERE aname = 'Lioness'")
+assert postgres_delete_1: result.status.is_ok else print_error db.error
+
+result = db.exec("DELETE FROM animals WHERE aname = 'Snake'")
+assert postgres_delete_2: result.status.is_ok else print_error db.error
+
+result = db.prepare("PREPARED_INSERT", "INSERT INTO animals(aname, class, sex) VALUES ($1, $2, $3)", 3)
+assert postgres_prepare: result.status.is_ok else print_error db.error
+
+result = db.exec("DELETE FROM animals WHERE aname = 'Frog'")
+assert postgres_delete_3: result.status.is_ok else print_error db.error
+
+var values = ["Frog", "Anphibian", "1"]
+var lengths = [values[0].length, values[1].length, values[2].length]
+var formats = [0,0,0]
+result = db.exec_prepared("PREPARED_INSERT", 3, values, lengths, formats,0)
+assert postgres_exec_prepared: result.status.is_ok else print_error db.error
+
+result = db.exec("DROP TABLE animals")
+assert postgres_drop_table: result.status.is_ok else print_error db.error
+db.finish
diff --git a/tests/test_postgres_nity.nit b/tests/test_postgres_nity.nit
new file mode 100644 (file)
index 0000000..9565335
--- /dev/null
@@ -0,0 +1,49 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Guilherme Mansur <guilhermerpmansur@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module test_postgres_nity
+
+import postgresql::postgres
+
+var db = new Postgres.open("dbname=postgres")
+assert open_db: not db.is_closed else print db.error
+
+assert create_table: db.create_table("IF NOT EXISTS users (uname TEXT PRIMARY KEY, pass TEXT NOT NULL, activated INTEGER, perc FLOAT)") else
+  print db.error
+end
+
+assert insert1: db.insert("INTO users VALUES('Bob', 'zzz', 1, 77.7)") else
+  print db.error
+end
+
+assert insert2: db.insert("INTO users VALUES('Guilherme', 'xxx', 1, 88)") else
+  print db.error
+end
+
+var result = db.raw_execute("SELECT * FROM users")
+
+assert raw_exec: result.is_ok else print db.error
+
+assert postgres_nfields: result.nfields == 4 else print_error db.error
+assert postgres_fname: result.fname(0) == "uname" else print_error db.error
+assert postgres_isnull: result.is_null(0,0) == false else print_error db.error
+assert postgres_value: result.value(0,0) == "Bob" else print_error db.error
+
+assert drop_table: db.execute("DROP TABLE users") else print db.error
+
+db.finish
+
+assert db.is_closed else print db.error
diff --git a/tests/test_readline.inputs b/tests/test_readline.inputs
new file mode 100644 (file)
index 0000000..59f6fe7
--- /dev/null
@@ -0,0 +1,4 @@
+line 1
+line 2
+line 2bis
+line 3 \bine\b\b\b3\b
diff --git a/tests/test_readline.nit b/tests/test_readline.nit
new file mode 100644 (file)
index 0000000..c254b24
--- /dev/null
@@ -0,0 +1,21 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import readline
+
+loop
+       var line = readline("prompt>", true)
+       if line == null then break
+       print line
+end
index 0573a88..1e16375 100644 (file)
@@ -29,18 +29,18 @@ var select_req = "SELECT * FROM users"
 var db = new NativeSqlite3.open(filename.to_cstring)
 assert sqlite_open: db.error.is_ok
 
-db.exec(create_req)
+db.exec(create_req.to_cstring)
 assert sqlite_create_table: db.error.is_ok
 
-db.exec(insert_req_1)
+db.exec(insert_req_1.to_cstring)
 assert sqlite_insert_1: db.error.is_ok
 
-db.exec(insert_req_2)
+db.exec(insert_req_2.to_cstring)
 assert sqlite_insert_2: db.error.is_ok
 
-var stmt = db.prepare(select_req)
+var stmt = db.prepare(select_req.to_cstring)
 assert sqlite_select: db.error.is_ok
-if stmt == null then
+if stmt.address_is_null then
        print "Prepared failed got: {db.error.to_s}"
        abort
 end
@@ -56,8 +56,8 @@ db.close
 db = new NativeSqlite3.open(filename.to_cstring)
 assert sqlite_reopen: db.error.is_ok
 
-stmt = db.prepare(select_req)
+stmt = db.prepare(select_req.to_cstring)
 assert sqlite_reselect: db.error.is_ok
-assert stmt != null
+assert not stmt.address_is_null
 stmt.step
 assert sqlite_column_0_0_reopened: stmt.column_text(0).to_s == "Bob"
index c2393aa..ed7d73d 100644 (file)
@@ -75,3 +75,6 @@ print("test.has_suffix(\"bt\") => {test.has_suffix("bt")}")
 print("test.has_suffix(\"bat\") => {test.has_suffix("bat")}")
 print("test.has_suffix(\"foot\") => {test.has_suffix("foot")}")
 
+print "........"
+print "/".substring(7, 1)
+print "........"
index 84ba34a..d11c60a 100755 (executable)
@@ -21,6 +21,8 @@
 export LANG=C
 export LC_ALL=C
 export NIT_TESTING=true
+# Use the pid as a collision prevention
+export NIT_TESTING_ID=$$
 export NIT_SRAND=0
 
 unset NIT_DIR