Merge: niti: teach the interpreter to read from the standard input
authorJean Privat <jean@pryen.org>
Fri, 3 Jun 2016 17:17:00 +0000 (13:17 -0400)
committerJean Privat <jean@pryen.org>
Fri, 3 Jun 2016 17:17:00 +0000 (13:17 -0400)
With `-` the program is read from stdin.

~~~
$ echo H4sIAN7lUFcAAysoyswrUVDySM3JyVfiAgCjfiaWDgAAAA== | base64 -d | gunzip | nit -
Hello
~~~

Pull-Request: #2159
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>

27 files changed:
contrib/benitlux/src/client/base.nit
contrib/benitlux/src/client/features/translations.nit
contrib/benitlux/src/client/views/user_views.nit
contrib/benitlux/src/server/benitlux_social.nit
contrib/nitiwiki/src/wiki_base.nit
lib/app/README.md
lib/app/http_request.nit
lib/app/package.ini
lib/core/text/abstract_text.nit
share/nitweb/directives/entity/card.html [new file with mode: 0644]
share/nitweb/directives/entity/list.html [new file with mode: 0644]
share/nitweb/directives/entity/location.html [new file with mode: 0644]
share/nitweb/directives/entity/tag.html [new file with mode: 0644]
share/nitweb/directives/ui-filter-button-vis.html [new file with mode: 0644]
share/nitweb/directives/ui-filter-field.html [new file with mode: 0644]
share/nitweb/directives/ui-filter-form.html [new file with mode: 0644]
share/nitweb/directives/ui-filter-group-vis.html [new file with mode: 0644]
share/nitweb/index.html
share/nitweb/javascripts/entities.js
share/nitweb/javascripts/model.js
share/nitweb/javascripts/ui.js [new file with mode: 0644]
share/nitweb/stylesheets/nitweb.css
share/nitweb/views/class.html
share/nitweb/views/group.html
share/nitweb/views/module.html
share/nitweb/views/package.html
src/web/model_api.nit

index bcc1efd..438a125 100644 (file)
@@ -117,14 +117,17 @@ class BenitluxHttpRequest
                        app.user = null
                        return true
                else if res isa BenitluxError then
-                       app.feedback((res.user_message or else res.message).t)
+                       feedback((res.user_message or else res.message).t)
                        return true
                else if res isa Error then
-                       app.feedback res.message.t
+                       feedback res.message.t
                        return true
                end
                return false
        end
+
+       # Show feedback pertinent to the user, defaults to a platform specific popup
+       fun feedback(text: String) do app.feedback text
 end
 
 # Async request with services to act on the windows of the app
index 02deb61..be42f57 100644 (file)
@@ -63,13 +63,23 @@ do
        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["Repeat password"] = "Répéter le mot de passe"
        map["Email"] = "Courriel"
        map["Login"] = "Se connecter"
+       map["Loging in..."] = "Authentification..."
        map["Logout"] = "Se déconnecter"
        map["Signup"] = "Créer un compte"
+       map["Signing up..."] = "Création du compte..."
+
+       map["Passwords must be composed of at least 6 characters."] = "Le mot de passe doit avoir au moins 6 charactères."
+       map["Fill the following fields to sign up."] = "Remplissez les champs suivants pour créer un compte."
+
+       map["Passwords do not match."] = "Les mots de passe ne correspondent pas."
+       map["Invalid username."] = "Nom d'utilisateur invalide."
+       map["Invalid password."] = "Mot de passe invalide."
+       map["Username already in use."] = "Le nom d'utilisateur est déjà réservé."
+       map["Invalid username and password combination."] = "La combinaison de nom et mot de passe n'est pas reconnue."
 
        # Social views
        map["Follow"] = "Suivre"
index e45fcb5..b69b1db 100644 (file)
@@ -86,38 +86,50 @@ end
 class SignupWindow
        super Window
 
-       # Main window layout
-       var layout = new ListLayout(parent=self)
+       private var list = new ListLayout(parent=self)
+       private var lbl_feedback = new Label(parent=list, text="Welcome")
 
-       private var lbl_welcome = new Label(parent=layout, text="Welcome")
+       private var layout_login = new VerticalLayout(parent=list)
+
+       # ---
+       # First the login options
 
        # Name
-       private var name_line = new HorizontalLayout(parent=layout)
+       private var name_line = new HorizontalLayout(parent=layout_login)
        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)
+       # Password
+       private var pass_line = new HorizontalLayout(parent=layout_login)
        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,
+       private var lbl_pass_desc = new Label(parent=layout_login, size = 0.5,
                text="Passwords must be composed of at least 6 characters.".t)
 
-       private var but_login = new Button(parent=layout, text="Login".t)
+       private var but_login = new Button(parent=layout_login, text="Login".t)
+
+       # ---
+       # Then, the signup options
+
+       private var layout_register = new VerticalLayout(parent=list)
+
+       private var lbl_signup_desc = new Label(parent=layout_register, size = 0.5,
+               text="Fill the following fields to sign up.".t)
+
+       # Repeat password
+       private var pass_line2 = new HorizontalLayout(parent=layout_register)
+       private var lbl_pass2 = new Label(parent=pass_line2, text="Repeat password".t)
+       private var txt_pass2 = new TextInput(parent=pass_line2, is_password=true)
 
        # Email
-       private var email_line = new HorizontalLayout(parent=layout)
+       private var email_line = new HorizontalLayout(parent=layout_register)
        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="")
+       private var but_signup = new Button(parent=layout_register, text="Signup".t)
 
        init
        do
-               lbl_pass_desc.size = 0.5
-
                for c in [but_login, but_signup] do
                        c.observers.add self
                end
@@ -133,25 +145,32 @@ class SignupWindow
 
                                var name = txt_name.text
                                if name == null or not name.name_is_ok then
-                                       feedback "Invalid name".t
+                                       feedback "Invalid username.".t
                                        return
                                end
 
                                var pass = txt_pass.text
                                if pass == null or not pass.pass_is_ok then
-                                       feedback "Invalid password".t
+                                       feedback "Invalid password.".t
                                        return
                                end
 
                                if sender == but_login then
+                                       feedback "Logging in...".t
                                        (new LoginOrSignupAction(self, "rest/login?name={name}&pass={pass.pass_hash}")).start
                                else if sender == but_signup then
+                                       if pass != txt_pass2.text then
+                                               feedback "Passwords do not match.".t
+                                               return
+                                       end
+
                                        var email = txt_email.text
                                        if email == null or email.is_empty then
                                                feedback "Invalid email".t
                                                return
                                        end
 
+                                       feedback "Signing up...".t
                                        (new LoginOrSignupAction(self, "rest/signup?name={name}&pass={pass.pass_hash}&email={email}")).start
                                end
                        end
@@ -189,27 +208,6 @@ class LoginOrSignupAction
 
                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
+       redef fun feedback(text) do window.feedback text
 end
index bdd83b4..7764c75 100644 (file)
@@ -182,7 +182,7 @@ GROUP BY beer0, beer1""") else
                # Check if already in user
                var stmt = select("ROWID FROM users WHERE lower({user.to_sql_string}) = lower(name)")
                assert stmt != null else print_error "Select 'sign_up' failed with: {error or else "?"}"
-               if not stmt.iterator.to_a.is_empty then return "Username already in use"
+               if not stmt.iterator.to_a.is_empty then return "Username already in use."
 
                # Check email use
                stmt = select("ROWID FROM users WHERE lower({email.to_sql_string}) = lower(email)")
index 6fb5e05..d3adc26 100644 (file)
@@ -244,7 +244,7 @@ class Nitiwiki
        # Used to translate ids in beautiful page names.
        fun pretty_name(name: String): String do
                name = name.replace("_", " ")
-               name = name.capitalized
+               name = name.capitalized(keep_upper=true)
                return name
        end
 end
index c5caa4b..0d2d24c 100644 (file)
@@ -5,13 +5,14 @@ The framework provides services to manage common needs of modern mobile applicat
 * Life-cycle
 * User interface
 * Persistence
+* Async HTTP requests
 * Package metadata
 * Compilation and packaging
 
 The features offered by _app.nit_ are common to all platforms, but
 may not be available on all devices.
 
-## Application Life-Cycle
+# Application Life-Cycle
 
 The _app.nit_ application life-cycle is compatible with all target platforms.
 It relies on the following sequence of events, represented here by their callback method name:
@@ -44,7 +45,7 @@ The `App` instance is the first to be notified of these events.
 Other UI elements, from the `ui` submodule, are notified of the same events using a simple depth first visit.
 So all UI elements can react separately to live-cycle events.
 
-## User Interface
+# User Interface
 
 The `app::ui` module defines an abstract API to build a portable graphical application.
 The API is composed of interactive `Control`s, visible `View`s and an active `Window`.
@@ -65,21 +66,20 @@ So there is two ways  to customize the behavior on a given event:
 
 * Add an observer to a `Button` instance, and implement `on_event` in the observer.
 
-### Usage Example
+## Usage Example
 
 The calculator example (at `../../examples/calculator/src/calculator.nit`) is a concrete,
 simple and complete use of the _app.nit_ portable UI.
 
-### Platform-specific UI
+## Platform-specific UI
 
 You can go beyond the portable UI API of _app.nit_ by using the natives services of a platform.
 
 The suggested approach is to use platform specific modules to customize the application on a precise platform.
-This module redefine `Window::on_start` to call the native language of the platform and setup a native UI.
+See the calculator example for an adaptation of the UI on Android,
+the interesting module is in this repository at ../../examples/calculator/src/android_calculator.nit
 
-_TODO complete description and add concrete examples_
-
-## Persistent State with data\_store
+# Persistent State with data\_store
 
 _app.nit_ offers the submodule `app::data_store` to easily save the application state and user preferences.
 The service is accessible by the method `App::data_store`. The `DataStore` itself defines 2 methods:
@@ -90,7 +90,7 @@ Pass `null` to clear the value associated to a key.
 * `DataStore::[]` returns the object associated to a `String` key.
 It returns `null` if nothing is associated to the key.
 
-### Usage Example
+## Usage Example
 
 ~~~
 import app::data_store
@@ -123,7 +123,14 @@ redef class App
 end
 ~~~
 
-## Metadata annotations
+# Async HTTP request
+
+The module `app::http_request` provides services to execute asynchronous HTTP request.
+The class `AsyncHttpRequest` hides the complex parallel logic and
+lets the user implement methods acting only on the UI thread.
+See the documentation of `AsyncHttpRequest` for more information.
+
+# Metadata annotations
 
 The _app.nit_ framework defines three annotations to customize the application package.
 
@@ -142,7 +149,7 @@ The _app.nit_ framework defines three annotations to customize the application p
   The special function `git_revision` will use the prefix of the hash of the latest git commit.
   By default, the version is 0.1.
 
-### Usage Example
+## Usage Example
 
 ~~~
 module my_module is
@@ -152,33 +159,38 @@ module my_module is
 end
 ~~~
 
-## Compiling and Packaging an Application
+# Compiling and Packaging an Application
 
 The Nit compiler detects the target platform from the importations and generates the appropriate application format and package.
 
 Applications using only the portable services of _app.nit_ require some special care at compilation.
 Such an application, let's say `calculator.nit`, does not depend on a specific platform and use the portable UI.
-The target platform must be specifed to the compiler for it to produce the correct application package.
+The target platform must be specified to the compiler for it to produce the correct application package.
 There is two main ways to achieve this goal:
 
-* The the mixin option (`-m path`) loads an additionnal module before compiling.
+* The mixin option (`-m module`) imports an additional module before compiling.
   It can be used to load platform specific implementations of the _app.nit_ portable UI.
 
   ~~~
   # GNU/Linux version, using GTK
-  nitc calculator.nit -m NIT_DIR/lib/linux/ui.nit
+  nitc calculator.nit -m linux
 
   # Android version
-  nitc calculator.nit -m NIT_DIR/lib/android/ui/
+  nitc calculator.nit -m android
+
+  # iOS version
+  nitc calculator.nit -m ios
   ~~~
 
 * A common alternative for larger projects is to use platform specific modules.
-  Continuing with the `calculator.nit` example, it can be accompagnied by the module `calculator_linux.nit`.
-  This module imports both `calculator` and `linux::ui`, and can also use other GNU/Linux specific code.
+  Continuing with the calculator example, it is adapted for Android by the module `android_calculator.nit`.
+  This module imports both `calculator` and `android`, it can then use Android specific code.
 
   ~~~
-  module calculator_linux
+  module android_calculator
 
   import calculator
-  import linux::ui
+  import android
+
+  # ...
   ~~~
index 9a5feca..694ba33 100644 (file)
@@ -28,14 +28,21 @@ redef class App
        fun run_on_ui_thread(task: Task) is abstract
 end
 
-# Thread executing an HTTP request and deserializing JSON asynchronously
+# Thread executing an HTTP request asynchronously
 #
-# This class defines four methods acting on the main/UI thread,
-# they should be implemented as needed:
-# * before
-# * on_load
-# * on_fail
-# * after
+# The request is sent to `rest_server_uri / rest_action`.
+#
+# If `deserialize_json`, the default behavior, the response is deserialized from JSON
+#
+# If `delay > 0.0`, sending the reqest is delayed by the given `delay` in seconds.
+# It can be used to delay resending a request on error.
+#
+# Four callback methods act on the main/UI thread,
+# they should be implemented as needed in subclasses:
+# * `before`
+# * `on_load`
+# * `on_fail`
+# * `after`
 class AsyncHttpRequest
        super Thread
 
index d5c48c1..88a75fb 100644 (file)
@@ -1,6 +1,6 @@
 [package]
 name=app
-tags=lib
+tags=lib,mobile
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 [upstream]
index ed04967..8fefb24 100644 (file)
@@ -1360,30 +1360,19 @@ abstract class String
        # Letters that follow a letter are lowercased
        # Letters that follow a non-letter are upcased.
        #
+       # If `keep_upper = true`, already uppercase letters are not lowercased.
+       #
        # SEE : `Char::is_letter` for the definition of letter.
        #
        #     assert "jAVASCRIPT".capitalized == "Javascript"
        #     assert "i am root".capitalized == "I Am Root"
        #     assert "ab_c -ab0c ab\nc".capitalized == "Ab_C -Ab0C Ab\nC"
-       fun capitalized: SELFTYPE do
+       #     assert "preserve my ACRONYMS".capitalized(keep_upper=true) == "Preserve My ACRONYMS"
+       fun capitalized(keep_upper: nullable Bool): SELFTYPE do
                if length == 0 then return self
 
                var buf = new Buffer.with_cap(length)
-
-               var curr = chars[0].to_upper
-               var prev = curr
-               buf[0] = curr
-
-               for i in [1 .. length[ do
-                       prev = curr
-                       curr = self[i]
-                       if prev.is_letter then
-                               buf[i] = curr.to_lower
-                       else
-                               buf[i] = curr.to_upper
-                       end
-               end
-
+               buf.capitalize(keep_upper=keep_upper, src=self)
                return buf.to_s
        end
 end
@@ -1478,6 +1467,13 @@ abstract class Buffer
        # Letters that follow a letter are lowercased
        # Letters that follow a non-letter are upcased.
        #
+       # If `keep_upper = true`, uppercase letters are not lowercased.
+       #
+       # When `src` is specified, this method reads from `src` instead of `self`
+       # but it still writes the result to the beginning of `self`.
+       # This requires `self` to have the capacity to receive all of the
+       # capitalized content of `src`.
+       #
        # SEE: `Char::is_letter` for the definition of a letter.
        #
        #     var b = new FlatBuffer.from("jAVAsCriPt")
@@ -1489,16 +1485,32 @@ abstract class Buffer
        #     b = new FlatBuffer.from("ab_c -ab0c ab\nc")
        #     b.capitalize
        #     assert b == "Ab_C -Ab0C Ab\nC"
-       fun capitalize do
+       #
+       #     b = new FlatBuffer.from("12345")
+       #     b.capitalize(src="foo")
+       #     assert b == "Foo45"
+       #
+       #     b = new FlatBuffer.from("preserve my ACRONYMS")
+       #     b.capitalize(keep_upper=true)
+       #     assert b == "Preserve My ACRONYMS"
+       fun capitalize(keep_upper: nullable Bool, src: nullable Text) do
+               src = src or else self
+               var length = src.length
                if length == 0 then return
-               var c = self[0].to_upper
+               keep_upper = keep_upper or else false
+
+               var c = src[0].to_upper
                self[0] = c
                var prev = c
                for i in [1 .. length[ do
                        prev = c
-                       c = self[i]
+                       c = src[i]
                        if prev.is_letter then
-                               self[i] = c.to_lower
+                               if keep_upper then
+                                       self[i] = c
+                               else
+                                       self[i] = c.to_lower
+                               end
                        else
                                self[i] = c.to_upper
                        end
diff --git a/share/nitweb/directives/entity/card.html b/share/nitweb/directives/entity/card.html
new file mode 100644 (file)
index 0000000..aa9ccb3
--- /dev/null
@@ -0,0 +1,11 @@
+<div class='card'>
+       <div class='card-left text-center'>
+               <entity-tag mentity='mentity' />
+       </div>
+       <div class='card-body'>
+               <h5 class='card-heading'>
+                       <entity-signature mentity='mentity'/>
+               </h5>
+               <span class='synopsis' ng-bind-html='mentity.mdoc.html_synopsis' />
+       </div>
+</div>
diff --git a/share/nitweb/directives/entity/list.html b/share/nitweb/directives/entity/list.html
new file mode 100644 (file)
index 0000000..cc026a4
--- /dev/null
@@ -0,0 +1,19 @@
+<div class='entity-list'
+       ng-if='(listEntities | filter:listObjectFilter).length > 0'>
+       <h3 id={{listId}}>
+               <span>{{listTitle}}</span>
+               <button class='btn btn-link btn-xs pull-right btn-filter' ng-click='toggleFilters()'>
+                       <span class='glyphicon glyphicon-filter text-muted' />
+               </button>
+       </h3>
+               <div ng-if='showFilters'>
+                       <ui-filter-form
+                               search-filter='listObjectFilter'
+                               visibility-filter='visibilityFilter'>
+               </div>
+               <div class='card-list'>
+                       <entity-card mentity='mentity'
+                               ng-repeat='mentity in listEntities | filter:listObjectFilter | visibility:visibilityFilter' />
+               </div>
+       </div>
+</div>
diff --git a/share/nitweb/directives/entity/location.html b/share/nitweb/directives/entity/location.html
new file mode 100644 (file)
index 0000000..7e817c6
--- /dev/null
@@ -0,0 +1,5 @@
+<span ng-if='mentity.location'>
+       <a ng-href="{{mentity.web_url}}">{{mentity.location.file}}
+               <span ng-if='mentity.location.line_start'>:{{mentity.location.line_start}}</span>
+       </a>
+</span>
diff --git a/share/nitweb/directives/entity/tag.html b/share/nitweb/directives/entity/tag.html
new file mode 100644 (file)
index 0000000..7c69f1e
--- /dev/null
@@ -0,0 +1,5 @@
+<span class="glyphicon glyphicon-tag" ng-class='{
+               "text-success": mentity.visibility == "public",
+               "text-warning": mentity.visibility == "protected",
+               "text-danger": mentity.visibility == "private",
+}' />
diff --git a/share/nitweb/directives/ui-filter-button-vis.html b/share/nitweb/directives/ui-filter-button-vis.html
new file mode 100644 (file)
index 0000000..8d4f8d4
--- /dev/null
@@ -0,0 +1,6 @@
+<button
+       class='btn btn-link btn-xs'
+       ng-click='toggle()'>
+       <span ng-if='property' ng-class='classesOn'/>
+       <span ng-if='!property' ng-class='classesOff'/>
+</button>
diff --git a/share/nitweb/directives/ui-filter-field.html b/share/nitweb/directives/ui-filter-field.html
new file mode 100644 (file)
index 0000000..05c51e4
--- /dev/null
@@ -0,0 +1,4 @@
+<div class='form-group has-icon'>
+       <input type='text' class='form-control' ng-model='property' placeholder='Filter...'>
+       <span class='glyphicon glyphicon-search form-control-icon text-muted'></span>
+</div>
diff --git a/share/nitweb/directives/ui-filter-form.html b/share/nitweb/directives/ui-filter-form.html
new file mode 100644 (file)
index 0000000..9782af8
--- /dev/null
@@ -0,0 +1,6 @@
+<form class='form-inline'>
+       <ui-filter-field property='searchFilter.$' />
+       <div class='pull-right'>
+               <ui-filter-group-vis property='visibilityFilter' />
+       </div>
+</form>
diff --git a/share/nitweb/directives/ui-filter-group-vis.html b/share/nitweb/directives/ui-filter-group-vis.html
new file mode 100644 (file)
index 0000000..7fcd475
--- /dev/null
@@ -0,0 +1,14 @@
+<div class='form-group'>
+       <ui-filter-button-vis property='property.public'
+                       classes-on='"glyphicon glyphicon-eye-open text-success"'
+                       classes-off='"glyphicon glyphicon-eye-close text-success"'
+                       title='Toggle public' />
+       <ui-filter-button-vis property='property.protected'
+                       classes-on='"glyphicon glyphicon-eye-open text-warning"'
+                       classes-off='"glyphicon glyphicon-eye-close text-warning"'
+                       title='Toggle protected' />
+       <ui-filter-button-vis property='property.private'
+                       classes-on='"glyphicon glyphicon-eye-open text-danger"'
+                       classes-off='"glyphicon glyphicon-eye-close text-danger"'
+                       title='Toggle private' />
+</div>
index 2a4bdde..17bcd23 100644 (file)
                                <div class='col-xs-3 navbar-header'>
                                        <a class='navbar-brand' ng-href='/'>Nitdoc</a>
                                </div>
+                               <div class='col-xs-7'>
+                                       <form ng-controller='SearchCtrl as searchCtrl' >
+                                               <div class='form-group has-icon'>
+                                                       <input placeholder='Search...' type='text' class='form-control search-input'
+                                                               ng-model-options='{ debounce: 150 }' ng-model='query'
+                                                               ng-keydown='update($event)' ng-change='search()'>
+                                                       <span class='glyphicon glyphicon-search form-control-icon text-muted'></span>
+                                               </div>
+                                               <div ng-if='results.length > 0' class='search-results'>
+                                                       <div class='card-list'>
+                                                               <entity-card ng-click='reset()' ng-class='{active: activeItem == $index}' mentity='mentity' ng-repeat='mentity in results' />
+                                                       </div>
+                                               </div>
+                                       </form>
+                               </div>
                        </div>
                </nav>
                <div ng-view></div>
@@ -40,5 +55,6 @@
                <script src='/javascripts/nitweb.js'></script>
                <script src='/javascripts/model.js'></script>
                <script src='/javascripts/entities.js'></script>
+               <script src='/javascripts/ui.js'></script>
        </body>
 </html>
index 4fe181b..255602c 100644 (file)
@@ -16,7 +16,7 @@
 
 (function() {
        angular
-               .module('entities', ['model'])
+               .module('entities', ['ui', 'model'])
 
                .controller('EntityCtrl', ['Model', '$routeParams', '$scope', function(Model, $routeParams, $scope) {
                        Model.loadEntity($routeParams.id,
                                templateUrl: '/directives/entity/signature.html'
                        };
                })
+
+               .directive('entityTag', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       mentity: '='
+                               },
+                               replace: true,
+                               templateUrl: '/directives/entity/tag.html'
+                       };
+               })
+
+               .directive('entityLocation', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       mentity: '='
+                               },
+                               templateUrl: '/directives/entity/location.html'
+                       };
+               })
+
+               .directive('entityCard', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       mentity: '='
+                               },
+                               replace: true,
+                               templateUrl: '/directives/entity/card.html'
+                       };
+               })
+
+               .directive('entityList', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       listEntities: '=',
+                                       listTitle: '@',
+                                       listObjectFilter: '=',
+                               },
+                               templateUrl: '/directives/entity/list.html',
+                               link: function ($scope, element, attrs) {
+                                       $scope.showFilters = false;
+                                       if(!$scope.listObjectFilter) {
+                                               $scope.listObjectFilter = {};
+                                       }
+                                       if(!$scope.visibilityFilter) {
+                                               $scope.visibilityFilter = {
+                                                       public: true,
+                                                       protected: true,
+                                                       private: false
+                                               };
+                                       }
+                                       $scope.toggleFilters = function() {
+                                               $scope.showFilters = !$scope.showFilters;
+                                       };
+                               }
+                       };
+               })
 })();
index b5e8bbf..7d5051b 100644 (file)
                                        $http.get(apiUrl + '/entity/' + id)
                                                .success(cb)
                                                .error(cbErr);
-                               }
+                               },
 
+                               search: function(q, n, cb, cbErr) {
+                                       $http.get(apiUrl + '/search?q=' + q + '&n=' + n)
+                                               .success(cb)
+                                               .error(cbErr);
+                               }
                        };
                }])
 })();
diff --git a/share/nitweb/javascripts/ui.js b/share/nitweb/javascripts/ui.js
new file mode 100644 (file)
index 0000000..cd30d06
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * 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('ui', [ 'model' ])
+
+               .controller('SearchCtrl', ['Model', '$routeParams', '$scope', '$window', function(Model, $routeParams, $scope, $window) {
+                       $scope.query = '';
+
+                       $scope.reset = function() {
+                               $scope.activeItem = 0;
+                               $scope.results = [];
+                       }
+
+                       $scope.update = function(e) {
+                               if(e.keyCode == 38) {
+                                       $scope.selectUp();
+                               } else if(e.keyCode == 40) {
+                                       $scope.selectDown();
+                               } else if(e.keyCode == 27) {
+                                       $scope.selectEscape();
+                               } else if(e.keyCode == 13) {
+                                       $scope.selectEnter();
+                               }
+                       }
+
+                       $scope.selectUp = function() {
+                               if($scope.activeItem > 0) {
+                                       $scope.activeItem -= 1;
+                               }
+                       }
+
+                       $scope.selectDown = function() {
+                               if($scope.activeItem < $scope.results.length - 1) {
+                                       $scope.activeItem += 1;
+                               }
+                       }
+
+                       $scope.selectEnter = function() {
+                               $window.location.href = $scope.results[$scope.activeItem].web_url;
+                               $scope.reset();
+                       }
+
+                       $scope.selectEscape = function() {
+                               $scope.reset();
+                       }
+
+                       $scope.search = function() {
+                               if(!$scope.query) {
+                                       $scope.reset();
+                                       return;
+                               }
+                               Model.search($scope.query, 10,
+                                       function(data) {
+                                               $scope.reset();
+                                               $scope.results = data;
+                                       }, function(err) {
+                                               $scope.reset();
+                                               $scope.error = err;
+                                       });
+                       }
+
+                       $scope.reset();
+               }])
+
+               .directive('uiFilters', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       property: '=',
+                                       classesOn: '=',
+                                       classesOff: '='
+                               },
+                               replace: true,
+                               templateUrl: '/directives/ui-filter-button-vis.html',
+                               link: function ($scope, element, attrs) {
+                                       $scope.toggle = function() {
+                                               $scope.property = !$scope.property;
+                                       }
+                               }
+                       };
+               })
+
+               .filter('visibility', function() {
+                       return function(input, visibilityFilter) {
+                               var res = [];
+                               input.forEach(function(entry) {
+                                       if(visibilityFilter.public == false && entry.visibility == "public") {
+                                               return;
+                                       }
+                                       if(visibilityFilter.protected == false && entry.visibility == "protected") {
+                                               return;
+                                       }
+                                       if(visibilityFilter.private == false && entry.visibility == "private") {
+                                               return;
+                                       }
+                                       res.push(entry);
+                               });
+                               return res;
+                       };
+               })
+
+               .directive('uiFilterForm', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       searchFilter: '=',
+                                       visibilityFilter: '='
+                               },
+                               replace: true,
+                               templateUrl: '/directives/ui-filter-form.html'
+                       };
+               })
+
+               .directive('uiFilterField', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       property: '='
+                               },
+                               replace: true,
+                               templateUrl: '/directives/ui-filter-field.html'
+                       };
+               })
+
+               .directive('uiFilterGroupVis', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       property: '='
+                               },
+                               replace: true,
+                               templateUrl: '/directives/ui-filter-group-vis.html'
+                       };
+               })
+
+               .directive('uiFilterButtonVis', function() {
+                       return {
+                               restrict: 'E',
+                               scope: {
+                                       property: '=',
+                                       classesOn: '=',
+                                       classesOff: '='
+                               },
+                               replace: true,
+                               templateUrl: '/directives/ui-filter-button-vis.html',
+                               link: function ($scope, element, attrs) {
+                                       $scope.toggle = function() {
+                                               $scope.property = !$scope.property;
+                                       }
+                               }
+                       };
+               })
+})();
index 2baad72..2d38457 100644 (file)
@@ -48,11 +48,43 @@ a {
     width: 10000px;
 }
 
-.card-body {
+.card-body, .card-right, .card-left {
     display: table-cell;
     vertical-align: top;
 }
 
+.card-left, .card>.pull-left {
+    padding: 15px;
+       padding-right: 0px;
+}
+.card-right, .card>.pull-right {
+    padding: 15px;
+       padding-left: 0px;
+}
+
+.card-list {
+       margin-top: 10px;
+}
+
+.card-list > .card:first-child {
+       border-top: 1px solid #ccc;
+}
+
+.card-list > .card {
+       margin-top: 0;
+       border-top: none;
+}
+
+/* ui */
+
+entity-list .btn-filter {
+       visibility: hidden;
+}
+
+entity-list:hover .btn-filter {
+       visibility: visible;
+}
+
 /* doc */
 
 .nitdoc .synopsys {
@@ -90,6 +122,89 @@ a {
     background-color: #ff9c0f;
 }
 
+/* forms */
+
+.has-icon {
+    position: relative;
+}
+
+.has-icon .form-control {
+       padding-left: 35px;
+}
+
+.form-control-icon {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 2;
+    display: block;
+    width: 34px;
+    height: 34px;
+    line-height: 34px;
+    text-align: center;
+    pointer-events: none;
+}
+
+/* search */
+
+.search-input {
+       width: 100%;
+}
+
+.search-results {
+       position: absolute;
+       right: 0;
+}
+
+.search-results .card.active {
+       background: #eee;
+       border-color: #eee;
+}
+
+/* navs */
+
+.nav-tabs li { cursor: pointer; }
+
+.navbar-fixed-top {
+       background-color: #1E9431;
+       box-shadow: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);
+}
+
+.navbar-fixed-top .form-control:hover, .navbar-fixed-top .form-control:focus {
+       background: rgba(255, 255, 255, 0.2);
+}
+
+.navbar-fixed-top .form-control {
+       background: rgba(255, 255, 255, 0.1);
+    border: none;
+    color: #fff;
+    box-shadow: none;
+}
+
+.navbar-fixed-top .form-control-icon {
+       color: #fff;
+}
+
+.navbar-fixed-top *::-webkit-input-placeholder {
+    color: #fff;
+}
+.navbar-fixed-top *:-moz-placeholder {
+    /* FF 4-18 */
+    color: #fff;
+}
+.navbar-fixed-top *::-moz-placeholder {
+    /* FF 19+ */
+    color: #fff;
+}
+.navbar-fixed-top *:-ms-input-placeholder {
+    /* IE 10+ */
+    color: #fff;
+}
+
+.navbar-fixed-top .form-group {
+       margin-top: 8px;
+       margin-bottom: 0px;
+}
 /*
  * Code Highlighting
  */
index ac7c432..52bd1e9 100644 (file)
        <div class='tab-content'>
                <div class='tab-pane fade in active' id='doc'>
                        <entity-doc mentity='mentity.intro'/>
+
+                       <entity-list list-title='Parents'
+                               list-entities='mentity.parents'
+                               list-object-filter='{}' />
+
+                       <entity-list list-title='Constructors'
+                               list-entities='mentity.all_mproperties'
+                               list-object-filter='{is_init: true}' />
+
+                       <entity-list list-title='Introduced properties'
+                               list-entities='mentity.intro_mproperties'
+                               list-object-filter='{is_init: "!true"}' />
+
+                       <entity-list list-title='Redefined properties'
+                               list-entities='mentity.redef_mproperties'
+                               list-object-filter='{is_init: "!true"}' />
                </div>
        </div>
 </div>
index ef07e48..ca19974 100644 (file)
                <div class='tab-content'>
                        <div class='tab-pane fade in active' id='doc'>
                                <entity-doc mentity='mentity'/>
+
+                               <entity-list list-title='Parent group' list-entities='[mentity.parent]'
+                                       list-object-filter='{}' ng-if='mentity.parent' />
+
+                               <entity-list list-title='Subgroups' list-entities='mentity.mgroups'
+                                       list-object-filter='{}' />
+
+                               <entity-list list-title='Modules' list-entities='mentity.mmodules'
+                                       list-object-filter='{}' />
                        </div>
                </div>
        </div>
index 23f6d52..79d905d 100644 (file)
        <div class='tab-content'>
                <div role='tabpanel' class='tab-pane fade in active' id='doc'>
                        <entity-doc mentity='mentity'/>
+
+                       <entity-list list-title='Imported modules' list-entities='mentity.imports'
+                               list-object-filter='{}' />
+
+                       <entity-list list-title='Introduced classes' list-entities='mentity.intro_mclasses'
+                               list-object-filter='{}' />
+
+                       <entity-list list-title='Class redefinitions' list-entities='mentity.redef_mclassdefs'
+                               list-object-filter='{}' />
+
                </div>
        </div>
 </div>
index 4abcbc7..ee19aa5 100644 (file)
@@ -14,6 +14,9 @@
        <div class='tab-content'>
                <div role='tabpanel' class='tab-pane fade in active' id='doc'>
                        <entity-doc mentity='mentity'/>
+
+                       <entity-list list-title='Groups' list-entities='mentity.mgroups'
+                               list-object-filter='{}' />
                </div>
        </div>
 </div>
index 85a2360..9bbe5ad 100644 (file)
@@ -77,26 +77,6 @@ class APIRouter
        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.
@@ -144,9 +124,24 @@ class APIList
        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
+               res.json new JsonArray.from(mentities)
+       end
+end
+
+# Search mentities from a query string.
+#
+# Example: `GET /search?q=Arr`
+class APISearch
+       super APIList
+
+       redef fun list_mentities(req) do
+               var q = req.string_arg("q")
+               var mentities = new Array[MEntity]
+               if q == null then return mentities
+               for mentity in view.mentities do
+                       if mentity.name.has_prefix(q) then mentities.add mentity
+               end
+               return mentities
        end
 end
 
@@ -167,9 +162,7 @@ class APIRandom
                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
+               res.json new JsonArray.from(mentities)
        end
 end