Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

[enhance] doc/book: Partial re-write of all wiki-based chapters.

We are now using Markdown instad of Templates. Plus took this
opportunity to do a little clean-up.
  • Loading branch information...
commit 626cab31368b4835f97f926280b7777993179afa 1 parent 76c582d
@akoprow akoprow authored
View
42 doc/book/hello_chat/hello_chat.adoc
@@ -826,9 +826,47 @@ a comparison is a boolean. We write that the type of function
========================
In Opa, booleans are values +{true = void}+ and +{false = void}+, or, more
concisely but equivalently, +\{true\}+ and +\{false\}+.
-You can check whether boolean +b+ is true or false by using +if b then ... else ...+ or,
-equivalently, +match b with \{true\} -> ... | \{false\} -> ...+.
+
+Their type declaration looks as follow: +type bool = \{true\} / \{false\}+.
+Such types, admitting one of a number of variants, are called sum types.
+========================
+
+[TIP]
+.About sum types
========================
+A value has a _sum type_ +t / u+, meaning that the values of this type are either
+of the two variants: either a value of type +t+ or a value of type +u+.
+
+A good example of sum type are the aforementioned boolean values, which are defined
+as +type bool = \{false\}/\{true\}+.
+
+Another good example of sum type is the type +list+ of linked lists; its definition
+can be summed up as +\{nil\} / \{hd: ...; tl: list\}+.
+
+Note that sum types are not limited to two cases. Sum types with tens of cases
+are not uncommon in real applications.
+=======================
+
+Safely determining which variant was used to construct a value of a sum type
+can be accomplished with pattern matching.
+
+[TIP]
+.About pattern-matching
+========================
+The operation used to branch according to the case of a sum type
+is called _pattern-matching_. A good example of pattern-matching
+is +if ... then ... else ...+ . The more general syntax for pattern matching is
++match ... with | case_1 \-> ... | case_2 \-> ... | case_3 \-> ...+
+
+The operation is actually more powerful than just determining which case of a
+sum type is used. Indeed, if we use the vocabulary of languages such as Java or
+C#, pattern-matching combines features of +if+, +switch+, +instanceof+/+is+,
+multiple assignment and dereferenciation, but without the possible safety issues
+of +instanceof+/+is+ and with fewer chances of misuse than +switch+.
+
+As an example, you can check whether boolean +b+ is true or false by using
++if b then ... else ...+ or, equivalently, +match b with \{true\} -> ... | \{false\} -> ...+.
+=======================
Distinguishing messages between users
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
View
226 doc/book/hello_web_services/hello_web_services.adoc
@@ -71,9 +71,9 @@ web application, it starts with a +server+:
topic_of_path(path) = String.capitalize(String.to_lower(List.to_string_using("", "", "::", path)))
start =
- | {path = [] ... } -> display("Hello")
- | {path = ["_rest_" | path] ...} -> rest(topic_of_path(path))
- | {~path ...} -> display(topic_of_path(path))
+| {path = [] ... } -> display("Hello")
+| {path = ["_rest_" | path] ...} -> rest(topic_of_path(path))
+| {~path ...} -> display(topic_of_path(path))
server = Server.of_bundle([@static_include_directory("resources")])
server = Server.simple_dispatch(start)
@@ -90,29 +90,23 @@ As you may see, this function is also quite simple:
.Handling rest requests
----------------
rest(topic) =
-(
match HttpServer.get_method() with
- | {some = {post}} ->
- _ = save_source(topic,
- match HttpServer.get_body() with
- | ~{some} -> some
- | {none} -> ""
- )
- Resource.raw_status({success})
- | {some = {delete}}->
- do remove_topic(topic)
- Resource.raw_status({success})
- | {some = {get}} ->
- Resource.raw_response(load_source(topic), "text/plain", {success})
- | _ ->
- Resource.raw_status({method_not_allowed})
-)
+ | {some = {post}} ->
+ _ = save_source(topic,
+ match HttpServer.get_body() with
+ | ~{some} -> some
+ | {none} -> ""
+ )
+ Resource.raw_status({success})
+ | {some = {delete}}->
+ do remove_topic(topic)
+ Resource.raw_status({success})
+ | {some = {get}} ->
+ Resource.raw_response(load_source(topic), "text/plain", {success})
+ | _ ->
+ Resource.raw_status({method_not_allowed})
----------------
-[WARNING]
-.In progress
-(API is currently evolving, update to new API)
-
First, notice that +rest+ is based on pattern-matching. Expect to meet pattern-matching
constantly in Opa. The first three patterns are built from some of to the distinct verbs
of the standard vocabulary of REST (these verbs are called _Http methods_):
@@ -156,19 +150,17 @@ With this expression, we may rewrite our extract as follows:
.Handling rest requests (shorter variant)
----------------
rest(topic) =
-(
match HttpServer.get_method() with
- | {some = {post}} ->
- _ = save_source(topic, HttpServer.get_body()?"")
- Resource.raw_status({success})
- | {some = {delete}}->
- do remove_topic(topic)
- Resource.raw_status({success})
- | {some = {get}} ->
- Resource.raw_response(load_source(topic), "text/plain", {success})
- | _ ->
- Resource.raw_status({method_not_allowed})
-)
+ | {some = {post}} ->
+ _ = save_source(topic, HttpServer.get_body() ? "")
+ Resource.raw_status({success})
+ | {some = {delete}}->
+ do remove_topic(topic)
+ Resource.raw_status({success})
+ | {some = {get}} ->
+ Resource.raw_response(load_source(topic), "text/plain", {success})
+ | _ ->
+ Resource.raw_status({method_not_allowed})
----------------
And with this, we are done! Our wiki can now be scripted by external web applications:
@@ -418,13 +410,14 @@ a distant service:
[source,opa]
---------------
-@publish load_source(topic) =
+@publish load_source(topic) =
match WebClient.Get.try_get(uri_for_topic(topic)) with
- | {failure = _} -> "Error, could not connect"
- | {~success} -> match WebClient.Result.get_class(success) with
- | {success} -> success.content
- | _ -> "Error {success.code}"
- end
+ | {failure = _} -> "Error, could not connect"
+ | {~success} ->
+ match WebClient.Result.get_class(success) with
+ | {success} -> success.content
+ | _ -> "Error {success.code}"
+ end
---------------
As in previous variants of the wiki, this version of +load_source+ attempts to
@@ -479,10 +472,8 @@ we have previously published:
[source,opa]
---------------
@publish load_rendered(topic) =
- source = load_source(topic)
- match Template.try_parse( Template.default, source) with
- | {failure = _} -> <>{source}</>
- | ~{success} -> Template.to_xhtml(Template.default, success)
+ source = load_source(topic)
+ Markdown.xhtml_of_string(Markdown.default_options, source)
---------------
Finally, we can adapt +save_source+, as follows:
@@ -490,20 +481,19 @@ Finally, we can adapt +save_source+, as follows:
[source,opa]
---------------
@publish save_source(topic, source) =
- match Template.try_parse(Template.default, source) with
- | ~{success} -> match WebClient.Post.try_post(uri_for_topic(topic), source) with
- | { failure = _ } -> {failure = "Could not reach distant server"}
- | { success = s } -> match WebClient.Result.get_class(s) with
- | {success} -> {success = Template.to_xhtml(Template.default, success)}
- | _ -> {failure = "Error {s.code}"}
- end
- end
- | {failure = _} -> {failure = "Incorrect syntax"}
+ match WebClient.Post.try_post(uri_for_topic(topic), source) with
+ | { failure = _ } ->
+ {failure = "Could not reach the distant server"}
+ | { success = s } ->
+ match WebClient.Result.get_class(s) with
+ | {success} -> {success = load_rendered(topic)}
+ | _ -> {failure = "Error {s.code}"}
+ end
---------------
This version of +save_source+ differs slightly from the original, not only because
-it uses a +\{post\}+ request to send the information, but also because it returns
-something slightly more precise than the original, for better error reporting.
+it uses a +\{post\}+ request to send the information, but also because it either
+returns the result +\{success=...\}+ or indicates an error with +\{failure=...\}+.
We take this opportunity to tweak our UI with a box meant to report such
errors:
@@ -517,34 +507,31 @@ and we update it in +edit+ and +save+, as follows:
[source,opa]
---------------
display(topic) =
- Resource.styled_page("About {topic}", ["/resources/css.css"],
- <div id=#header><div id=#logo></div>About {topic}</div>
- <div class="show_content" id=#show_content ondblclick={_ -> edit(topic)}>
- {load_rendered(topic)}
- </>
- <div class="show_messages" id=#show_messages />
- <textarea class="edit_content" id=#edit_content style="display:none"
- cols="40" rows="30" onblur={_ -> save(topic)}></>
- )
+ Resource.styled_page("About {topic}", ["/resources/css.css"],
+ <div id=#header><div id=#logo></div>About {topic}</div>
+ <div class="show_content" id=#show_content ondblclick={_ -> edit(topic)}>{load_rendered(topic)}</>
+ <div class="show_messages" id=#show_messages />
+ <textarea class="edit_content" id=#edit_content style="display:none" cols="40" rows="30" onblur={_ -> save(topic)}></>
+ )
edit(topic) =
- do Dom.transform([#show_messages <- <></>]) //Clear messages
- do Dom.set_value(#edit_content, load_source(topic))
- do Dom.hide(#show_content)
- do Dom.show(#edit_content)
- do Dom.give_focus(#edit_content)
- void
+ do Dom.transform([#show_messages <- <></>])
+ do Dom.set_value(#edit_content, load_source(topic))
+ do Dom.hide(#show_content)
+ do Dom.show(#edit_content)
+ do Dom.give_focus(#edit_content)
+ void
save(topic) =
- match save_source(topic, Dom.get_value(#edit_content)) with
- | { ~success } ->
- do Dom.transform([#show_content <- success]);
- do Dom.hide(#edit_content);
- do Dom.show(#show_content);
- void
- | {~failure} ->
- do Dom.transform([#show_messages <- <>{failure}</>])
- void
+ match save_source(topic, Dom.get_value(#edit_content)) with
+ | { ~success } ->
+ do Dom.transform([#show_content <- success]);
+ do Dom.hide(#edit_content);
+ do Dom.show(#show_content);
+ void
+ | {~failure} ->
+ do Dom.transform([#show_messages <- <>{failure}</>])
+ void
---------------
And that is all for the user interface.
@@ -683,14 +670,12 @@ In this case, expanding our horizon starts by rewriting +uri_for_topic+ as follo
[source,opa]
--------------
uri_for_topic = topic ->
-(
Uri.of_absolute({Uri.default_absolute with
schema = {some = "http"}
domain = "localhost"
port = {some = 8080}
path = ["_rest_", topic]
})
-)
--------------
So far, this is absolutely equivalent to what we had written earlier. Note
@@ -752,14 +737,18 @@ both purposes, Opa offers a module +CommandLine+:
--------------
uri_for_topic =
(
- default_uri = {Uri.default_absolute with domain = "localhost"
- schema = {some = "http"}}
- base_uri = CommandLine.filter({
- title = "Wiki arguments"
- init = default_uri
- parsers = []
- anonymous = []
- })
+ default_uri =
+ {Uri.default_absolute with
+ domain = "localhost"
+ schema = {some = "http"}
+ }
+ base_uri =
+ CommandLine.filter({
+ title = "Wiki arguments"
+ init = default_uri
+ parsers = []
+ anonymous = []
+ })
topic -> Uri.of_absolute({base_uri with
path = ["_rest_", topic]
})
@@ -784,10 +773,12 @@ parser_) to our family, as follows:
[source,opa]
.Parsing option +--wiki-server-port+
--------------
- option_port = {CommandLine.default_parser with
- names = ["--wiki-server-port"]
- on_param(x) = parser y=Rule.natural -> {no_params = {x with port = {some = y}}}
- }
+port_parser =
+ {CommandLine.default_parser with
+ names = ["--wiki-server-port"]
+ description = "The server port of the REST server for this wiki. By default, 8080."
+ on_param(x) = parser y=Rule.natural -> {no_params = {x with port = {some = y}}}
+ }
--------------
As you can see, a command-line parser is a record (it has type
@@ -849,12 +840,14 @@ the same absolute URI, but with a new content in field +port+.
We can now define in the exact same manner the command-line parser for the host:
[source,opa]
-.Parsing option +--wiki-server-host+
+.Parsing option +--wiki-server-domain+
--------------
- host_parser = {CommandLine.default_parser with
- names = ["--wiki-server-host"]
- on_param(x) = parser y=Rule.consume -> {no_params = {x with domain = y}}
- }
+domain_parser =
+ {CommandLine.default_parser with
+ names = ["--wiki-server-domain"]
+ description = "The REST server for this wiki. By default, localhost."
+ on_param(x) = parser y=Rule.consume -> {no_params = {x with domain = y}}
+ }
--------------
The main difference is that we use predefined text parser +Rule.consume+ (which accepts
anything) instead of +Rule.natural+ (which only accepts non-negative integers).
@@ -865,24 +858,27 @@ additional documentation, we obtain:
.Command-line arguments (complete)
--------------
uri_for_topic =
- host_parser = {CommandLine.default_parser with
- names = ["--wiki-server-host"]
- description = "The host of a the server offering a REST API for this wiki. By default, localhost."
- on_param(x) = parser y=Rule.consume -> {no_params = {x with domain = y}}
+ domain_parser =
+ {CommandLine.default_parser with
+ names = ["--wiki-server-domain"]
+ description = "The REST server for this wiki. By default, localhost."
+ on_param(x) = parser y=Rule.consume -> {no_params = {x with domain = y}}
}
- port_parser = {CommandLine.default_parser with
- names = ["--wiki-server-port"]
- description = "The port of a the server offering a REST API for this wiki. By default, 8080."
- on_param(x) = parser y=Rule.natural -> {no_params = {x with port = {some = y}}}
+ port_parser =
+ {CommandLine.default_parser with
+ names = ["--wiki-server-port"]
+ description = "The server port of the REST server for this wiki. By default, 8080."
+ on_param(x) = parser y=Rule.natural -> {no_params = {x with port = {some = y}}}
}
- base_uri = CommandLine.filter({
- title = "Wiki arguments"
- init = {Uri.default_absolute with domain = "localhost"
- schema = {some = "http"}}
- parsers = [domain_parser, port_parser]
- anonymous = []
- })
- topic -> Uri.of_absolute({base_uri with path = ["_rest_", topic]})
+ base_uri =
+ CommandLine.filter(
+ {title = "Wiki arguments"
+ init = {Uri.default_absolute with domain = "localhost" schema = {some = "http"}}
+ parsers = [domain_parser, port_parser]
+ anonymous = []
+ }
+ )
+ topic -> Uri.of_absolute({base_uri with path = ["_rest_", topic]})
--------------
This completes our REST client. We now have a full-featured REST client that can also act as a server
View
67 doc/book/hello_web_services/hello_wiki_rest.opa
@@ -1,29 +1,35 @@
-import stdlib.web.template
+import stdlib.tools.markdown
-db /wiki: stringmap(Template.default_content)
-db /wiki[_] = Template.text("This page is empty"): Template.default_content
+db /wiki: stringmap(string)
+db /wiki[_] = "This page is empty. Double click to edit"
+
+@publish load_source(topic) =
+ /wiki[topic]
+
+@publish load_rendered(topic) =
+ source = load_source(topic)
+ Markdown.xhtml_of_string(Markdown.default_options, source)
-@publish load_source(topic) = Template.to_source(Template.default, /wiki[topic])
-@publish load_rendered(topic) = Template.to_xhtml( Template.default, /wiki[topic])
@publish save_source(topic, source) =
- match Template.try_parse(Template.default, source) with
- | ~{success} -> do /wiki[topic] <- success; Template.to_xhtml(Template.default, success)
- | {failure = _} -> do /wiki[topic] <- Template.text(source); <>Error: {source}</>
-remove_topic(topic) = Db.remove(@/wiki[topic])
+ do /wiki[topic] <- source
+ load_rendered(topic)
+
+remove_topic(topic) =
+ Db.remove(@/wiki[topic])
edit(topic) =
- do Dom.set_value(#edit_content, load_source(topic))
- do Dom.hide(#show_content)
- do Dom.show(#edit_content)
- do Dom.give_focus(#edit_content)
- void
+ do Dom.set_value(#edit_content, load_source(topic))
+ do Dom.hide(#show_content)
+ do Dom.show(#edit_content)
+ do Dom.give_focus(#edit_content)
+ void
save(topic) =
- content = save_source(topic, Dom.get_value(#edit_content))
- do Dom.transform([#show_content <- content]);
- do Dom.hide(#edit_content);
- do Dom.show(#show_content);
- void
+ content = save_source(topic, Dom.get_value(#edit_content))
+ do Dom.transform([#show_content <- content]);
+ do Dom.hide(#edit_content);
+ do Dom.show(#show_content);
+ void
display(topic) =
Resource.styled_page("About {topic}", ["/resources/css.css"],
@@ -36,19 +42,24 @@ display(topic) =
)
rest(topic) =
-(
match HttpRequest.get_method() with
- | {some = method} ->
+ | {some = method} ->
match method with
- | {post} -> _ = save_source(topic, HttpRequest.get_body()?"") Resource.raw_status({success})
- | {delete} -> do remove_topic(topic) Resource.raw_status({success})
- | {get} -> Resource.raw_response(load_source(topic), "text/plain", {success})
- | _ -> Resource.raw_status({method_not_allowed})
+ | {post} ->
+ _ = save_source(topic, HttpRequest.get_body() ? "")
+ Resource.raw_status({success})
+ | {delete} ->
+ do remove_topic(topic)
+ Resource.raw_status({success})
+ | {get} ->
+ Resource.raw_response(load_source(topic), "text/plain", {success})
+ | _ ->
+ Resource.raw_status({method_not_allowed})
end
- | _ -> Resource.raw_status({bad_request})
-)
+ | _ -> Resource.raw_status({bad_request})
-topic_of_path(path) = String.capitalize(String.to_lower(List.to_string_using("", "", "::", path)))
+topic_of_path(path) =
+ String.capitalize(String.to_lower(List.to_string_using("", "", "::", path)))
start =
| {path = [] ... } -> display("Hello")
View
128 doc/book/hello_web_services/hello_wiki_rest_client.opa
@@ -1,83 +1,93 @@
-import stdlib.web.template
+import stdlib.tools.markdown
-uri_for_topic(topic) = Uri.of_absolute({Uri.default_absolute with schema = {some = "http"} : option(string)
- domain = "localhost"
- port = {some = 8080} : option(int)
- path = ["_rest_", topic]})
+uri_for_topic(topic) =
+ Uri.of_absolute(
+ {Uri.default_absolute with
+ schema = {some = "http"} : option(string)
+ domain = "localhost"
+ port = {some = 8080} : option(int)
+ path = ["_rest_", topic]
+ }
+ )
+
+@publish load_source(topic) =
+ match WebClient.Get.try_get(uri_for_topic(topic)) with
+ | {failure = _} -> "Error, could not connect"
+ | {~success} ->
+ match WebClient.Result.get_class(success) with
+ | {success} -> success.content
+ | _ -> "Error {success.code}"
+ end
-@publish load_source(topic) = match WebClient.Get.try_get(uri_for_topic(topic)) with
- | {failure = _} -> "Error, could not connect"
- | {~success} -> match WebClient.Result.get_class(success) with
- | {success} -> success.content
- | _ -> "Error {success.code}"
- end
@publish load_rendered(topic) =
- source = load_source(topic)
- match Template.try_parse( Template.default, source) with
- | {failure = _} -> <>{source}</>
- | ~{success}-> Template.to_xhtml(Template.default, success)
+ source = load_source(topic)
+ Markdown.xhtml_of_string(Markdown.default_options, source)
@publish save_source(topic, source) =
- match Template.try_parse(Template.default, source) with
- | ~{success} -> match WebClient.Post.try_post(uri_for_topic(topic), source) with
- | { failure = _ } -> {failure = "Could not reach distant server"}
- | { success = s } -> match WebClient.Result.get_class(s) with
- | {success} -> {success = Template.to_xhtml(Template.default, success)}
- | _ -> {failure = "Error {s.code}"}
- end
- end
- | {failure = _} -> {failure = "Incorrect syntax"}
+ match WebClient.Post.try_post(uri_for_topic(topic), source) with
+ | { failure = _ } ->
+ {failure = "Could not reach the distant server"}
+ | { success = s } ->
+ match WebClient.Result.get_class(s) with
+ | {success} -> {success = load_rendered(topic)}
+ | _ -> {failure = "Error {s.code}"}
+ end
remove_topic(topic) =
- _ = WebClient.Delete.try_delete(uri_for_topic(topic))
- void
+ _ = WebClient.Delete.try_delete(uri_for_topic(topic))
+ void
edit(topic) =
- do Dom.transform([#show_messages <- <></>])
- do Dom.set_value(#edit_content, load_source(topic))
- do Dom.hide(#show_content)
- do Dom.show(#edit_content)
- do Dom.give_focus(#edit_content)
- void
+ do Dom.transform([#show_messages <- <></>])
+ do Dom.set_value(#edit_content, load_source(topic))
+ do Dom.hide(#show_content)
+ do Dom.show(#edit_content)
+ do Dom.give_focus(#edit_content)
+ void
save(topic) =
- match save_source(topic, Dom.get_value(#edit_content)) with
- | { ~success } ->
- do Dom.transform([#show_content <- success]);
- do Dom.hide(#edit_content);
- do Dom.show(#show_content);
- void
- | {~failure} ->
- do Dom.transform([#show_messages <- <>{failure}</>])
- void
+ match save_source(topic, Dom.get_value(#edit_content)) with
+ | { ~success } ->
+ do Dom.transform([#show_content <- success]);
+ do Dom.hide(#edit_content);
+ do Dom.show(#show_content);
+ void
+ | {~failure} ->
+ do Dom.transform([#show_messages <- <>{failure}</>])
+ void
display(topic) =
- Resource.styled_page("About {topic}", ["/resources/css.css"],
- <div id=#header><div id=#logo></div>About {topic}</div>
- <div class="show_content" id=#show_content ondblclick={_ -> edit(topic)}>{load_rendered(topic)}</>
- <div class="show_messages" id=#show_messages />
- <textarea class="edit_content" id=#edit_content style="display:none" cols="40" rows="30" onblur={_ -> save(topic)}></>
- )
+ Resource.styled_page("About {topic}", ["/resources/css.css"],
+ <div id=#header><div id=#logo></div>About {topic}</div>
+ <div class="show_content" id=#show_content ondblclick={_ -> edit(topic)}>{load_rendered(topic)}</>
+ <div class="show_messages" id=#show_messages />
+ <textarea class="edit_content" id=#edit_content style="display:none" cols="40" rows="30" onblur={_ -> save(topic)}></>
+ )
rest(topic) =
-(
match HttpRequest.get_method() with
- | {some = method} ->
+ | {some = method} ->
match method with
- | {post} -> _ = save_source(topic, HttpRequest.get_body()?"") Resource.raw_status({success})
- | {delete} -> do remove_topic(topic) Resource.raw_status({success})
- | {get} -> Resource.source(load_source(topic), "text/plain")
- | _ -> Resource.raw_status({method_not_allowed})
+ | {post} ->
+ _ = save_source(topic, HttpRequest.get_body()?"")
+ Resource.raw_status({success})
+ | {delete} ->
+ do remove_topic(topic)
+ Resource.raw_status({success})
+ | {get} ->
+ Resource.source(load_source(topic), "text/plain")
+ | _ ->
+ Resource.raw_status({method_not_allowed})
end
- | _ -> Resource.raw_status({bad_request})
-)
+ | _ -> Resource.raw_status({bad_request})
-topic_of_path(path) = String.capitalize(String.to_lower(List.to_string_using("", "", "::", path)))
+topic_of_path(path) =
+ String.capitalize(String.to_lower(List.to_string_using("", "", "::", path)))
start =
- | {path = [] ... } -> display("Hello")
- | {path = ["rest" | path] ...} -> rest(topic_of_path(path))
- | {~path ...} -> display(topic_of_path(path))
+| {path = [] ... } -> display("Hello")
+| {path = ["rest" | path] ...} -> rest(topic_of_path(path))
+| {~path ...} -> display(topic_of_path(path))
server = Server.of_bundle([@static_include_directory("resources")])
server = Server.simple_dispatch(start)
View
158 doc/book/hello_web_services/hello_wiki_rest_client_customizable.opa
@@ -1,108 +1,106 @@
-import stdlib.web.template
+import stdlib.tools.markdown
uri_for_topic =
- domain_parser = {CommandLine.default_parser with
- names = ["--wiki-server-domain"]
- description = "The REST server for this wiki. By default, localhost."
- on_param(x) = parser y=Rule.consume -> {no_params = {x with domain = y}}
+ domain_parser =
+ {CommandLine.default_parser with
+ names = ["--wiki-server-domain"]
+ description = "The REST server for this wiki. By default, localhost."
+ on_param(x) = parser y=Rule.consume -> {no_params = {x with domain = y}}
}
- port_parser = {CommandLine.default_parser with
- names = ["--wiki-server-port"]
- description = "The server port of the REST server for this wiki. By default, 8080."
- on_param(x) = parser y=Rule.natural -> {no_params = {x with port = {some = y}}}
+ port_parser =
+ {CommandLine.default_parser with
+ names = ["--wiki-server-port"]
+ description = "The server port of the REST server for this wiki. By default, 8080."
+ on_param(x) = parser y=Rule.natural -> {no_params = {x with port = {some = y}}}
}
- base_uri = CommandLine.filter({title = "Wiki arguments"
- init = {Uri.default_absolute with domain = "localhost"
- schema = {some = "http"}}
- parsers = [domain_parser, port_parser]
- anonymous = []
- })
- topic -> Uri.of_absolute({base_uri with path = ["_rest_", topic]})
+ base_uri =
+ CommandLine.filter(
+ {title = "Wiki arguments"
+ init = {Uri.default_absolute with domain = "localhost" schema = {some = "http"}}
+ parsers = [domain_parser, port_parser]
+ anonymous = []
+ }
+ )
+ topic -> Uri.of_absolute({base_uri with path = ["_rest_", topic]})
-@publish load_source(topic) =
- match WebClient.Get.try_get(uri_for_topic(topic)) with
- | {failure = _} -> "Error, could not connect"
- | {~success} -> match WebClient.Result.get_class(success) with
- | {success} -> success.content
- | _ -> "Error {success.code}"
- end
+@publish load_source(topic) =
+ match WebClient.Get.try_get(uri_for_topic(topic)) with
+ | {failure = _} -> "Error, could not connect"
+ | {~success} ->
+ match WebClient.Result.get_class(success) with
+ | {success} -> success.content
+ | _ -> "Error {success.code}"
+ end
@publish load_rendered(topic) =
- source = load_source(topic)
- match Template.try_parse( Template.default, source) with
- | {failure = _} -> <>{source}</>
- | ~{success}-> Template.to_xhtml(Template.default, success)
+ source = load_source(topic)
+ Markdown.xhtml_of_string(Markdown.default_options, source)
@publish save_source(topic, source) =
- match Template.try_parse(Template.default, source) with
- | ~{success} -> match WebClient.Post.try_post(uri_for_topic(topic), source) with
- | { failure = _ } -> {failure = "Could not reach distant server"}
- | { success = s } -> match WebClient.Result.get_class(s) with
- | {success} -> {success = Template.to_xhtml(Template.default, success)}
- | _ -> {failure = "Error {s.code}"}
- end
- end
- | {failure = _} -> {failure = "Incorrect syntax"}
+ match WebClient.Post.try_post(uri_for_topic(topic), source) with
+ | { failure = _ } ->
+ {failure = "Could not reach the distant server"}
+ | { success = s } ->
+ match WebClient.Result.get_class(s) with
+ | {success} -> {success = load_rendered(topic)}
+ | _ -> {failure = "Error {s.code}"}
+ end
remove_topic(topic) =
- _ = WebClient.Delete.try_delete(uri_for_topic(topic))
- void
+ _ = WebClient.Delete.try_delete(uri_for_topic(topic))
+ void
edit(topic) =
- do Dom.transform([#show_messages <- <></>])
- do Dom.set_value(#edit_content, load_source(topic))
- do Dom.hide(#show_content)
- do Dom.show(#edit_content)
- do Dom.give_focus(#edit_content)
- void
+ do Dom.transform([#show_messages <- <></>])
+ do Dom.set_value(#edit_content, load_source(topic))
+ do Dom.hide(#show_content)
+ do Dom.show(#edit_content)
+ do Dom.give_focus(#edit_content)
+ void
save(topic) =
- match save_source(topic, Dom.get_value(#edit_content)) with
- | { ~success } ->
- do Dom.transform([#show_content <- success]);
- do Dom.hide(#edit_content);
- do Dom.show(#show_content);
- void
- | {~failure} ->
- do Dom.transform([#show_messages <- <>{failure}</>])
- void
+ match save_source(topic, Dom.get_value(#edit_content)) with
+ | { ~success } ->
+ do Dom.transform([#show_content <- success]);
+ do Dom.hide(#edit_content);
+ do Dom.show(#show_content);
+ void
+ | {~failure} ->
+ do Dom.transform([#show_messages <- <>{failure}</>])
+ void
display(topic) =
- Resource.styled_page("About {topic}", ["/resources/css.css"],
- <div id=#header><div id=#logo></div>About {topic}</div>
- <div class="show_content" id=#show_content ondblclick={_ -> edit(topic)}>
- {load_rendered(topic)}
- </>
- <div class="show_messages" id=#show_messages />
- <textarea class="edit_content" id=#edit_content style="display:none"
- cols="40" rows="30" onblur={_ -> save(topic)}></>
- )
+ Resource.styled_page("About {topic}", ["/resources/css.css"],
+ <div id=#header><div id=#logo></div>About {topic}</div>
+ <div class="show_content" id=#show_content ondblclick={_ -> edit(topic)}>{load_rendered(topic)}</>
+ <div class="show_messages" id=#show_messages />
+ <textarea class="edit_content" id=#edit_content style="display:none" cols="40" rows="30" onblur={_ -> save(topic)}></>
+ )
rest(topic) =
-(
match HttpRequest.get_method() with
- | {some = method} ->
+ | {some = method} ->
match method with
- | {post} ->
- _ = save_source(topic, HttpRequest.get_body()?"")
- Resource.raw_status({success})
- | {delete} ->
- do remove_topic(topic)
- Resource.raw_status({success})
- | {get} -> Resource.source(load_source(topic), "text/plain")
- | _ -> Resource.raw_status({method_not_allowed})
+ | {post} ->
+ _ = save_source(topic, HttpRequest.get_body()?"")
+ Resource.raw_status({success})
+ | {delete} ->
+ do remove_topic(topic)
+ Resource.raw_status({success})
+ | {get} ->
+ Resource.source(load_source(topic), "text/plain")
+ | _ ->
+ Resource.raw_status({method_not_allowed})
end
- | _ -> Resource.raw_status({bad_request})
-)
+ | _ -> Resource.raw_status({bad_request})
-topic_of_path(path) = String.capitalize(String.to_lower(
- List.to_string_using("", "", "::", path)
-))
+topic_of_path(path) =
+ String.capitalize(String.to_lower(List.to_string_using("", "", "::", path)))
start =
- | {path = [] ... } -> display("Hello")
- | {path = ["rest" | path] ...} -> rest(topic_of_path(path))
- | {~path ...} -> display(topic_of_path(path))
+| {path = [] ... } -> display("Hello")
+| {path = ["rest" | path] ...} -> rest(topic_of_path(path))
+| {~path ...} -> display(topic_of_path(path))
server = Server.of_bundle([@static_include_directory("resources")])
server = Server.simple_dispatch(start)
View
255 doc/book/hello_wiki/hello_wiki.adoc
@@ -18,8 +18,8 @@ Let us start with a picture of the wiki application we will develop in this chap
image::hello_wiki/result.png[]
-This web application stores pages and lets users edit them, using a HTML-like syntax
-that supports paragraphs, tables, etc.
+This web application stores pages and lets users edit them, using http://en.wikipedia.org/wiki/Markdown[Markdown syntax],
+which is a popular markup language that supports headings, links, lists, images etc.
If you are curious, this is the full source code of the application.
@@ -29,8 +29,8 @@ include::hello_wiki.opa[]
------------------------
[run]#http://tutorials.opalang.org/hello_wiki[Run]#
-In this listing, we define a database for storing the content of the pages in a
-safe format, we define the user interface and finally, the main application. In
+In this listing, we define a database for storing the content of the pages in the
+Markdown syntax format, we define the user interface and finally, the main application. In
the rest of the chapter, we will walk you through all the concepts and
constructions introduced.
@@ -45,13 +45,13 @@ need to set up some storage to receive these pages, as follows:
[source,opa]
-----------------
-db /wiki: stringmap(Template.default_content)
+db /wiki: stringmap(string)
-----------------
This defines a _database path_, i.e. a place where we can store information, and specifies the type
-of information that will be stored there. Here, we use type +stringmap(Template.default_content)+.
+of information that will be stored there. Here, we use type +stringmap(string)+.
This is a +stringmap+ (i.e. an association from +string+ keys to values) containing values of type
-+Template.default_content+.
++string+ (used to denote Markdown code).
[TIP]
.About database paths
@@ -67,21 +67,24 @@ subtle incompatibilities between successive versions of an application, the type
must be given during the definition of the path.
=====================
-In turn, +Template.default_content+ is the default content format for templates,
-i.e. the default method of representing rich text that can be modified by the user.
-
[TIP]
-.About template
+.HTML VS markup
=====================
-On the web, letting users write HTML code that can be seen at a later stage by
-other users is a very bad idea, as it opens the way for numerous forms of
-attacks, sometimes quite subtle and hard to detect. Opa's _templating_ mechanism
-is a manner of representing rich text that can be provided by a user, stored in
-the database and displayed to other users, without compromising security.
-
-Although this topic is not covered by this chapter, the templating mechanism
-is designed for full extensibility - indeed, http://opalang.org[our website] is
-assembled using this mechanism, including our own on-line editor.
+On the web, letting users directly write HTML code that can be seen at a later
+stage by other users is not a good idea, as it opens the way for numerous forms of
+attacks, sometimes quite subtle and hard to detect.
+
+Opa offers at least two ways to circumvent this problem. Firstly, there is
+a _templating_ mechanism (see package +stdlib.web.template+), which is an
+XML-based markup that covers a safe subset of HTML. Moreover this templating
+mechanism is designed for full extensibility -- indeed, http://opalang.org[our website],
+including our own on-line editor, is developed using templating.
+
+Another, more lightweight approach, and one that we are using here, is to
+use Markdown (package +stdlib.tools.markdown+), which is a very popular
+markup language that convers easy to read and write plain text format
+into structurally valid and completely safe XHTML (and supports headings,
+links, images, code snippets etc.).
=====================
It is generally a good idea to associate a default value to each path, as this
@@ -89,20 +92,18 @@ makes manipulation of data easier:
[source,opa]
-----------------
-db /wiki[_] = Template.text("This page is empty. Double-click to edit.")
+db /wiki[_] = "This page is empty. Double-click to edit."
-----------------
The square brackets +[_]+ are a convention to specify that we are talking about
the contents of a map and, more precisely, providing a default value. Here, the
-default value is +Template.text("This page is empty. Double-click to edit.")+,
-i.e. a simple text in our templating system. As expected, it has type
-+Template.default_content+.
+default value is +"This page is empty. Double-click to edit."+, i.e. a simple text
+in Markdown syntax (here it's just plain text).
With these two lines, the database is set. Any data written to the database will
-be kept persistent, versioned and with regular snapshots. Should you stop and
-restart your application, the data will be checked and made available. If the
-data has been corrupted or is somehow incompatible with your application, you
-will be informed.
+be kept persistent. Should you stop and restart your application, the data will
+be checked and made accessible to your application as it was at the point you
+stopped it.
Loading, parsing and writing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -115,130 +116,30 @@ present it as +xhtml+ that can be displayed immediately:
[source,opa]
-----------------
-load_source(topic) = Template.to_source(Template.default, /wiki[topic])
-load_rendered(topic) = Template.to_xhtml( Template.default, /wiki[topic])
+load_source(topic) = /wiki[topic]
+load_rendered(topic) =
+ source = load_source(topic)
+ Markdown.xhtml_of_string(Markdown.default_options, source)
-----------------
In this extract, +topic+ is the topic we wish to display or edit -- i.e. the name
-of the page. Both functions use +Template.default+, the default template engine.
-This engine uses as source language a safe subset of HTML and produces content with
-type +Template.default_content+. The content associated with +topic+ can be found
-in the database at path +/wiki[topic]+. Once we have this content, depending on
-our needs, we convert it to source code, as a +string+, or to a +xhtml+ data
-structure used for display.
-
-For parsing and saving data, the logic is slightly more complicated:
+of the page. The content associated with +topic+ can be found in the database at path +/wiki[topic]+.
+Once we have this content, depending on our needs, we just return it as a +string+,
+or convert to +xhtml+ data structure for display, using the +Markdown.xhtml_of_string+
+function, which parses the string and builds its corresponding +xhtml+ representation,
+with respect to the Markdown syntax.
+
+Saving data is equally simple:
[source,opa]
-----------------
save_source(topic, source) =
-(
- match Template.try_parse(Template.default, source) with
- | {success = x} ->
- (
- do /wiki[topic] <- x
- Template.to_xhtml(Template.default, x)
- )
- | {failure = _} ->
- (
- do /wiki[topic] <- Template.text(source)
- <>Syntax error<br/>: {source}</>
- )
-)
+ do /wiki[topic] <- source
+ load_rendered(topic)
-----------------
This function takes two arguments: a +topic+, with the same meaning as above,
-and a +source+, i.e. a +string+ meant to be understood by our templating engine.
-The call to function +Template.try_parse+ submits this +source+ to the default
-templating engine, which may either accept it (if the syntax was correct) or
-reject it (otherwise). For this wiki, we do not try to fix the syntax
-automatically or to fallback to an approximate syntax.
-
-Function +Template.try_parse+ can produce two kinds of results: _success_ results and
-_failure_ results. More precisely, by definition, the value returned by this function
-is always either a record +\{ success = something \}+ or +\{ failure = something\}+. We
-call this a _sum_ of records, and it has type +\{ success = ... \} /\{ failure = ...\}+ --
-we will fill in the dots later.
-
-[TIP]
-.About sum types
-========================
-A value has a _sum type_ +t/u+ if, depending on the execution path, it can
-have either values of type +t+ or values of type +u+ . A good example of sum type
-is booleans, which are defined as +\{false\}/\{true\}+ . Another good example of
-sum type is the type +list+ of linked lists, whose definition can be summed up
-as +\{nil\} / \{hd: ...; tl: list\}+.
-
-Note that sum types are not limited to two cases. Advanced applications commonly
-manipulate sum types with ten cases or more.
-=======================
-
-As you can see, we apply a +match ... with ...+ operation to this result. This
-operation is known as _pattern-matching_, and it lets us determine safely whether
-the result was a _success_ or a _failure_.
-
-[TIP]
-.About pattern-matching
-========================
-The operation used to branch according to the case of a sum type
-is called _pattern-matching_. A good example of pattern-matching
-is +if ... then ... else ...+ . The more general syntax for pattern matching is
-+match ... with | case_1 \-> ... | case_2 \-> ... | case_3 \-> ...+
-
-The operation is actually more powerful than just determining which case of a
-sum type is used. Indeed, if we use the vocabulary of languages such as Java or
-C#, pattern-matching combines features of +if+, +switch+, +instanceof+/+is+,
-multiple assignment and dereferenciation, but without the possible safety issues
-of +instanceof+/+is+ and with fewer chances of misuse than +switch+.
-=======================
-
-This pattern-matching has two cases. Let us concentrate on the first one:
-
-[source,opa]
------------------
- | {success = x} ->
- (
- do /wiki[topic] <- x
- Template.to_xhtml(Template.default, x)
- )
------------------
-
-The items between the pipe (+|+) and the arrow (+\->+) are known as the
-_pattern_. The corresponding case is executed if the _pattern_ _matches_ the
-value. Here, we have a pattern that accepts all records containing exactly one
-field called +success+. When our pattern-matching encounters such a record, it calls
-+x+ the contents of field and executes everything that appears after the arrow
-(the _body_).
-
-The first instruction writes +x+ to the database at path +/wiki[topic]+.
-The second instruction produces a +xhtml+-formatted version of +x+, which
-is the result of our function.
-
-Let us now examine the second case of our pattern-matching:
-[source,opa]
------------------
- | {failure = _} ->
- (
- do /wiki[topic] <- Template.text(source)
- <>Syntax error<br/>: {source}</>
- )
------------------
-
-By definition, cases are executed in order. Here, the pattern matches all
-records containing exactly one field called +failure+. When our pattern-matching
-encounters such a record, it ignores the contents of the field -- recall that
-+_+ is pronounced "I don't care" -- and executes the body.
-
-Here, we are in the error case, i.e. the user has entered a syntactically
-incorrect text. We could decide to perform sophisticated error reporting, but
-that goes beyond the scope of this chapter. We will rather employ an alternative
-strategy: we store the text entered by the user, but as source code,
-so that it can be modified at a later stage. For this purpose, we again call
-function +Template.text+ to adopt the source code as immediate text,
-we write this to the database at path +/wiki[topic]+ and then we produce
-a message stating that there was a syntax error.
-
-With this, we have covered everything related to database, parsing or rendering.
-We can now move to the user interface.
+and a +source+, i.e. a +string+, which is a representation of the page with Markdown syntax.
+The instruction after +do+ writes +source+ to the database at path +/wiki[topic]+.
User interface
~~~~~~~~~~~~~~
@@ -407,20 +308,6 @@ This is a total of 30 effective lines of code + CSS.
Questions
~~~~~~~~~
-What about the wiki syntax?
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-For this tutorial chapter, we have used the default syntax provided by +Template+, which
-we have not detailed. That is because you already know this syntax: +Template+ uses a subset
-of the syntax of HTML, without JavaScript. As mentioned earlier, this syntax can be extended.
-The general idea is to plug new _engines_ into +Template+ which can interpret new namespaces.
-We will discuss the specifics in another chapter, or you can already look at the documentation
-of module +Template+.
-
-Opa also features a powerful mechanism for parsing simple or complex syntaxes,
-which you can easily use to replace the default syntax of +Template+ with
-something closer to the usual Wiki syntax. We will introduce this mechanism
-in another chapter.
-
What about user security?
^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -435,8 +322,8 @@ stealing identities.
You may attempt to reproduce this with the wiki, the chat, or any other Opa
application. This will fail. Indeed, while lower-level web technologies make no
difference between JavaScript code, text, or structured data, Opa does, and
-ensures that data that has been provided as the one can never be interpreted
-as one of the others.
+ensures that data that has been provided as one can never be interpreted
+as the other one.
[CAUTION]
.Careful with the +<script>+
@@ -461,7 +348,7 @@ What about database security?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Now that we have a database, it is high time to think about _what_ can be
-modified and _in which circumstances_. By default, Opa takes a conservative
+modified and _under what circumstances_. By default, Opa takes a conservative
approach and ensures that malicious clients have access to as few entry points
as possible -- we call this _publishing_ a function. By default, the only
published functions are functions that users could trigger themselves by
@@ -485,11 +372,8 @@ By default, Opa will _automatically_ publish event handlers.
================
Here, this means functions +edit+ and +save+. In our listing, no other functions
-are _published_. Consequently, the +Template+ mechanism is invoked only on the
-server. A direct consequence is that we can be certain that the +stringmap+ is
-always well-formed and that +/wiki[x]+ can only ever contain well-formed
-template content.
-
+are _published_. Consequently, the +Markdown+ syntax analysis is invoked only on the
+server.
What about client-server performance?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -514,8 +398,8 @@ compiler, namely to let it know that +load_source+, +load_rendered+ and
+save_source+ have been designed to handle anything that can be thrown at them,
and thus do not need to be kept hidden.
-For this purpose, Opa offers a _publication_ primitive: +@publish+.
-To apply it to our three functions, we simply add this directive where needed:
+For this purpose, Opa offers a special directive: +@publish+.
+To apply it to our three functions, we simply add it where needed:
[source,opa]
------------------------
@@ -526,7 +410,7 @@ To apply it to our three functions, we simply add this directive where needed:
And we are done!
-With this simple modification, saving is now just one request. We will discuss
+With this simple modification, saving now requires only one request. We will discuss
+@publish+ and the details of client-server _slicing_ in a later chapter.
Once again, we may take a look at the complete application:
@@ -537,44 +421,15 @@ Once again, we may take a look at the complete application:
include::hello_wiki.opa[]
------------------------
-What about database performance?
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Generally speaking, the Opa database is very fast. In particular, in our
-experience, a +stringmap+ or an +intmap+ is as fast as an _indexed_ table in a
-relational database, sometimes much faster.
-
-Of course, you do not have to restrict yourself to a +map+ indexed by a +string+
-or an +int+. Indeed, any type that can be stored in the database can be used as
-an index. The chapter dedicated to the database will detail how you can do this
-and how to ensure that the performance of your database remains optimal.
-
Exercises
~~~~~~~~~
Time to put your new knowledge to the test.
-Better handling of errors
-^^^^^^^^^^^^^^^^^^^^^^^^^
-When the user attempts to save content containing an error, instead of saving the result:
-
-* keep editing;
-* change the background color of +edit_content+ to red;
-* show a message informing that the user can simply reload the page to cancel changes.
-
-You will need to use a CSS class for setting the background color. You may also wish
-to use the following functions:
-
-[source, opa]
------------------
-Dom.add_class: dom, /*class*/string -> void
-Dom.remove_class: dom, /*class*/string -> void
------------------
-
Changing the default content
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Customize the wiki so that the database contains not a +Template.default_content+ but
-a +option(Template.default_content)+, i.e. a value that can be either +\{none\}+
-or +\{some = x\}+, where +x+ is a +Template.default_content+.
+Customize the wiki so that the database contains not a +string+ but
+a +option(string)+, i.e. a value that can be either +\{none\}+
+or +\{some = x\}+, where +x+ is a +string+.
Use this change to ensure that the default content of a page with topic _topic_ is
"We have no idea about _topic_. Could you please enter some information?".
View
49 doc/book/hello_wiki/hello_wiki.opa
@@ -6,9 +6,9 @@
import stdlib.themes.bootstrap
/**
- * {1 Import templates}
+ * {1 Import markdown syntax}
*/
-import stdlib.web.template
+import stdlib.tools.markdown
/**
* {1 Database and database interaction}
@@ -18,21 +18,20 @@ import stdlib.web.template
* Contents of the wiki.
*
* Pages which do not exist have content "This page is empty".
- * Note: By definition, pages stored in the database are always well-formed.
*/
-db /wiki: stringmap(Template.default_content)
-db /wiki[_] = Template.text("This page is empty. Double-click to edit.")
-
+db /wiki: stringmap(string)
+db /wiki[_] = "This page is empty. Double-click to edit."
/**
* Read the content associated to a topic from the database and return the
- * corresponding source code.
+ * corresponding Markdown source.
*
* @param topic A topic (arbitrary string).
- * @return If a page has been saved in for [topic], the source code for this
- * page. Otherwise, the source code for the default page.
+ * @return If a page has been saved in for [topic], the source for this
+ * page. Otherwise, the source for the default page.
*/
-@publish load_source(topic) = Template.to_source(Template.default, /wiki[topic])
+@publish load_source(topic) =
+ /wiki[topic]
/**
* Read the content associated to a topic from the database and return the
@@ -44,29 +43,20 @@ db /wiki[_] = Template.text("This page is empty. Double-click to edit.")
*
* Note: This function does not perform any caching.
*/
-@publish load_rendered(topic) = Template.to_xhtml(Template.default, /wiki[topic])
+@publish load_rendered(topic) =
+ source = load_source(topic)
+ Markdown.xhtml_of_string(Markdown.default_options, source)
/**
- * Accept source code, save the corresponding document in the database.
+ * Accept source and save the corresponding document in the database.
*
* @param topic A topic (arbitrary string).
- * @param source Source code to store at this topic. If this source code
- * is syntactically valid, store the template datastructure
- * corresponding to its content [Template.content].
- * Otherwise, the source code is implicitly replaced by the document
- * representing this raw code and this document is saved in the database.
- *
- * @return In case of success, the xhtml for the page that has just been
- * saved. In case of failure, an error message.
+ * @param source Markdown source to store at this topic.
+ * @return The xhtml for the page that has just been saved.
*/
@publish save_source(topic, source) =
- match Template.try_parse(Template.default, source) with
- | ~{success} ->
- do /wiki[topic] <- success;
- Template.to_xhtml(Template.default, success)
- | {failure = _} ->
- do /wiki[topic] <- Template.text(source);
- <>Error: {source}</>
+ do /wiki[topic] <- source
+ load_rendered(topic)
/**
* {1 User interface}
@@ -75,7 +65,8 @@ db /wiki[_] = Template.text("This page is empty. Double-click to edit.")
/**
* Set the user interface in edition mode.
*
- * Load the source code for a topic, display an editable zone for this source code.
+ * Load the Markdown source for a topic, display an editable zone
+ * for this markdown.
*
* @param topic The topic to edit.
*/
@@ -89,7 +80,7 @@ edit(topic) =
/**
* Set the user interface in reading mode.
*
- * Save the source code for a topic (extracted from [#edit_content]),
+ * Save the Markdown source for a topic (extracted from [#edit_content]),
* display the rendered version.
*
* @param topic The topic to save.
View
51 doc/book/hello_wiki/hello_wiki_simple.opa
@@ -6,9 +6,9 @@
import stdlib.themes.bootstrap
/**
- * {1 Import templates}
+ * {1 Import markdown syntax}
*/
-import stdlib.web.template
+import stdlib.tools.markdown
/**
* {1 Database and database interaction}
@@ -18,21 +18,20 @@ import stdlib.web.template
* Contents of the wiki.
*
* Pages which do not exist have content "This page is empty".
- * Note: By definition, pages stored in the database are always well-formed.
*/
-db /wiki: stringmap(Template.default_content)
-db /wiki[_] = Template.text("This page is empty. Double-click to edit.")
-
+db /wiki: stringmap(string)
+db /wiki[_] = "This page is empty. Double-click to edit."
/**
* Read the content associated to a topic from the database and return the
- * corresponding source code.
+ * corresponding Markdown source.
*
* @param topic A topic (arbitrary string).
- * @return If a page has been saved in for [topic], the source code for this
- * page. Otherwise, the source code for the default page.
+ * @return If a page has been saved in for [topic], the source for this
+ * page. Otherwise, the source for the default page.
*/
-load_source(topic) = Template.to_source(Template.default, /wiki[topic])
+load_source(topic) =
+ /wiki[topic]
/**
* Read the content associated to a topic from the database and return the
@@ -44,29 +43,20 @@ load_source(topic) = Template.to_source(Template.default, /wiki[topic])
*
* Note: This function does not perform any caching.
*/
-load_rendered(topic) = Template.to_xhtml( Template.default, /wiki[topic])
+load_rendered(topic) =
+ source = load_source(topic)
+ Markdown.xhtml_of_string(Markdown.default_options, source)
/**
- * Accept source code, save the corresponding document in the database.
+ * Accept source and save the corresponding document in the database.
*
* @param topic A topic (arbitrary string).
- * @param source Source code to store at this topic. If this source code
- * is syntactically valid, store the template datastructure
- * corresponding to its content [Template.content].
- * Otherwise, the source code is implicitly replaced by the document
- * representing this raw code and this document is saved in the database.
- *
- * @return In case of success, the xhtml for the page that has just been
- * saved. In case of failure, an error message.
+ * @param source Markdown source to store at this topic.
+ * @return The xhtml for the page that has just been saved.
*/
save_source(topic, source) =
- match Template.try_parse(Template.default, source) with
- | ~{success} ->
- do /wiki[topic] <- success;
- Template.to_xhtml(Template.default, success)
- | {failure = _} ->
- do /wiki[topic] <- Template.text(source);
- <>Error: {source}</>
+ do /wiki[topic] <- source
+ load_rendered(topic)
/**
* {1 User interface}
@@ -75,7 +65,8 @@ save_source(topic, source) =
/**
* Set the user interface in edition mode.
*
- * Load the source code for a topic, display an editable zone for this source code.
+ * Load the Markdown source for a topic, display an editable zone
+ * for this markdown.
*
* @param topic The topic to edit.
*/
@@ -89,7 +80,7 @@ edit(topic) =
/**
* Set the user interface in reading mode.
*
- * Save the source code for a topic (extracted from [#edit_content]),
+ * Save the Markdown source for a topic (extracted from [#edit_content]),
* display the rendered version.
*
* @param topic The topic to save.
@@ -113,7 +104,7 @@ display(topic) =
<div class="content container">
<div class="page-header"><h1>About {topic}</></>
<div class="well" id=#show_content ondblclick={_ -> edit(topic)}>{load_rendered(topic)}</>
- <textarea clas="xxlarge" rows="30" id=#edit_content onblur={_ -> save(topic)}></>
+ <textarea rows="30" id=#edit_content onblur={_ -> save(topic)}></>
</div>
)
Please sign in to comment.
Something went wrong with that request. Please try again.