diff --git a/collects/tests/web-server/template/examples/blog-posted.html b/collects/tests/web-server/template/examples/blog-posted.html new file mode 100644 index 0000000000..b3284cdd9e --- /dev/null +++ b/collects/tests/web-server/template/examples/blog-posted.html @@ -0,0 +1,4 @@ +

@|title|

+

@|body|

+ +

Continue

\ No newline at end of file diff --git a/collects/tests/web-server/template/examples/blog-posts.html b/collects/tests/web-server/template/examples/blog-posts.html new file mode 100644 index 0000000000..669399ac7c --- /dev/null +++ b/collects/tests/web-server/template/examples/blog-posts.html @@ -0,0 +1,16 @@ +@in[p posts]{ +

@(post-title p)

+

@(post-body p)

+ +} + +

New Post

+
+ + + +
diff --git a/collects/tests/web-server/template/examples/blog-xexpr.ss b/collects/tests/web-server/template/examples/blog-xexpr.ss new file mode 100644 index 0000000000..807b7c4a62 --- /dev/null +++ b/collects/tests/web-server/template/examples/blog-xexpr.ss @@ -0,0 +1,93 @@ +#lang scheme +(require web-server/servlet + xml + web-server/servlet-env) + +(define-struct post (title body comments)) + +(define posts + (list + (make-post + "(Y Y) Works: The Why of Y" + "..." + (list + "First post! - A.T." + "Didn't I write this? - Matthias")) + (make-post + "Church and the States" + "As you may know, I grew up in DC, not technically a state..." + (list + "Finally, A Diet That Really Works! As Seen On TV")))) + +(define (template section body) + `(html + (head (title "Alonzo's Church: " ,section) + (style ([type "text/css"]) + ,(make-cdata #f #f " + body { + margin: 0px; + padding: 10px; + } + + #main { + background: #dddddd; + }"))) + (body + (script ([type "text/javascript"]) + ,(make-cdata #f #f " + var gaJsHost = ((\"https:\" == document.location.protocol) ? + \"https://ssl.\" : \"http://www.\"); + document.write(unescape(\"%3Cscript src='\" + gaJsHost + + \"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E\")); +")) + (script ([type "text/javascript"]) + ,(make-cdata #f #f " + var pageTracker = _gat._getTracker(\"UA-YYYYYYY-Y\"); + pageTracker._trackPageview(); +")) + + (h1 "Alonzo's Church: " ,section) + (div ([id "main"]) + ,@body)))) + +(define (blog-posted title body k-url) + `((h2 ,title) + (p ,body) + (h1 (a ([href ,k-url]) "Continue")))) + +(define (extract-post req) + (define title (extract-binding/single 'title (request-bindings req))) + (define body (extract-binding/single 'body (request-bindings req))) + (set! posts + (list* (make-post title body empty) + posts)) + (send/suspend + (lambda (k-url) + (template "Posted" (blog-posted title body k-url)))) + (display-posts)) + +(define (blog-posts k-url) + (append + (apply append + (for/list ([p posts]) + `((h2 ,(post-title p)) + (p ,(post-body p)) + (ul + ,@(for/list ([c (post-comments p)]) + `(li ,c)))))) + `((h1 "New Post") + (form ([action ,k-url]) + (input ([name "title"])) + (input ([name "body"])) + (input ([type "submit"])))))) + +(define (display-posts) + (extract-post + (send/suspend + (lambda (k-url) + (template "Posts" (blog-posts k-url)))))) + +(define (start req) + (display-posts)) + +(serve/servlet start) diff --git a/collects/tests/web-server/template/examples/blog.html b/collects/tests/web-server/template/examples/blog.html new file mode 100644 index 0000000000..c5dc3f41bd --- /dev/null +++ b/collects/tests/web-server/template/examples/blog.html @@ -0,0 +1,32 @@ + + + Alonzo's Church: @|section| + + + + + + +

Alonzo's Church: @|section|

+
+ @body +
+ + diff --git a/collects/tests/web-server/template/examples/blog.ss b/collects/tests/web-server/template/examples/blog.ss new file mode 100644 index 0000000000..34e0c12a82 --- /dev/null +++ b/collects/tests/web-server/template/examples/blog.ss @@ -0,0 +1,46 @@ +#lang scheme +(require web-server/templates + web-server/servlet + web-server/servlet-env) + +(define-struct post (title body comments)) + +(define posts + (list + (make-post + "(Y Y) Works: The Why of Y" + "..." + (list + "First post! - A.T." + "Didn't I write this? - Matthias")) + (make-post + "Church and the States" + "As you may know, I grew up in DC, not technically a state..." + (list + "Finally, A Diet That Really Works! As Seen On TV")))) + +(define (template section body) + (list TEXT/HTML-MIME-TYPE + (include-template "blog.html"))) + +(define (extract-post req) + (define title (extract-binding/single 'title (request-bindings req))) + (define body (extract-binding/single 'body (request-bindings req))) + (set! posts + (list* (make-post title body empty) + posts)) + (send/suspend + (lambda (k-url) + (template "Posted" (include-template "blog-posted.html")))) + (display-posts)) + +(define (display-posts) + (extract-post + (send/suspend + (lambda (k-url) + (template "Posts" (include-template "blog-posts.html")))))) + +(define (start req) + (display-posts)) + +(serve/servlet start) diff --git a/collects/web-server/scribblings/templates.scrbl b/collects/web-server/scribblings/templates.scrbl index a42b233b04..f202eee7ad 100644 --- a/collects/web-server/scribblings/templates.scrbl +++ b/collects/web-server/scribblings/templates.scrbl @@ -2,11 +2,13 @@ @(require "web-server.ss") @(require (for-label web-server/servlet web-server/templates + scheme/promise scheme/list xml)) @(define xexpr @tech[#:doc '(lib "xml/xml.scrbl")]{X-expression}) @(define at-reader-ref @secref[#:doc '(lib "scribblings/scribble/scribble.scrbl")]{reader}) +@(define text-ref @secref[#:doc '(lib "scribblings/scribble/scribble.scrbl")]{preprocessor}) @title[#:tag "templates"]{Templates} @@ -15,6 +17,9 @@ The @web-server provides a powerful Web template system for separating the presentation logic of a Web application and enabling non-programmers to contribute to PLT-based Web applications. +@margin-note{Although all the examples here generate HTML, the template language and the @text-ref it is based on can + be used to generate any text-based format: C, SQL, form emails, reports, etc.} + @local-table-of-contents[] @section{Static} @@ -62,8 +67,8 @@ Then ] evaluates to the same content as the static example. -There is no constraints on the values, the way they are used, or the way they are defined, that are made accessible to the template. -For example, +There are no constraints on how the lexical context of the template is populated. For instance, you can built template abstractions +by wrapping the inclusion of a template in a function: @schemeblock[ (define (fast-template thing) (include-template "simple.html")) @@ -94,18 +99,71 @@ and }| +Furthermore, there are no constraints on the Scheme used by templates: they can use macros, structs, continuation marks, threads, etc. +However, Scheme values that are ultimately returned must be printable by the @text-ref@"." +For example, consider the following outputs of the +title line of different calls to @scheme[fast-template]: + +@itemize{ + +@item{ +@schemeblock[ + (fast-template 'Templates) +] +@verbatim[#:indent 2]|{ + Fastest Templates in the West! +}| +} + +@item{ +@schemeblock[ + (fast-template 42) +] +@verbatim[#:indent 2]|{ + Fastest 42 in the West! +}| +} + +@item{ +@schemeblock[ + (fast-template (list "Noo" "dles")) +] +@verbatim[#:indent 2]|{ + Fastest Noodles in the West! +}| +} + +@item{ +@schemeblock[ + (fast-template (lambda () "Thunks")) +] +@verbatim[#:indent 2]|{ + Fastest Thunks in the West! +}| +} + +@item{ +@schemeblock[ + (fast-template (delay "Laziness")) +] +@verbatim[#:indent 2]|{ + Fastest Laziness in the West! +}| +} +} + @section{Gotchas} -One of the most important things to remember about the @at-reader-ref syntax is that the @"@" symbol must be escaped in content: +To obtain an @"@" symbol in template output, you must escape the @"@" symbol, because it is the escape character of the @at-reader-ref syntax. +For example, to obtain: @verbatim[#:indent 2]|{ - - Fastest @"@"s in the West! - -

Bang!

-

Bang!

- - + Fastest @s in the West! }| +You must write: +@verbatim[#:indent 2]|{ + Fastest @"@"s in the West! +}| +as your template: literal @"@"s must be replaced with @"@\"@\"". The other gotcha is that since the template is compiled into a Scheme program, only its results will be printed. For example, suppose we have the template: @@ -117,7 +175,7 @@ we have the template: }| -If this is included in a lexical context with @scheme[clients] bound to @scheme[(list (cons "Young" "Brigham") (cons "Smith" "Joseph"))], +If this is included in a lexical context with @scheme[clients] bound to @schemeblock[(list (cons "Young" "Brigham") (cons "Smith" "Joseph"))] then the template will be printed as: @verbatim[#:indent 2]|{ @@ -225,4 +283,218 @@ the template to be unescaped, then create a @scheme[cdata] structure: (in c clients "") ] } - \ No newline at end of file + +@section{Conversion Example} + +Alonzo Church has been maintaining a blog with PLT Scheme for some years and would like to convert to @schememodname[web-server/templates]. + +Here's the code he starts off with: +@schememod[ + scheme +(require xml + web-server/servlet + web-server/servlet-env) + +(code:comment "He actually Church-encodes them, but we'll use structs.") +(define-struct post (title body comments)) + +(define posts + (list + (make-post + "(Y Y) Works: The Why of Y" + "..." + (list + "First post! - A.T." + "Didn't I write this? - Matthias")) + (make-post + "Church and the States" + "As you may know, I grew up in DC, not technically a state..." + (list + "Finally, A Diet That Really Works! As Seen On TV")))) + +(code:comment "A function that is the generic template for the site") +(define (template section body) + `(html + (head (title "Alonzo's Church: " ,section) + (style ([type "text/css"]) + (code:comment "CDATA objects were useful for returning raw data") + ,(make-cdata #f #f "\n body {\n margin: 0px;\n padding: 10px;\n }\n\n #main {\n background: #dddddd;\n }"))) + (body + (script ([type "text/javascript"]) + (code:comment "Which is particularly useful for JavaScript") + ,(make-cdata #f #f "\n var gaJsHost = ((\"https:\" == document.location.protocol) ?\n \"https://ssl.\" : \"http://www.\");\n document.write(unescape(\"%3Cscript src='\" + gaJsHost +\n \"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E\"));\n")) + (script ([type "text/javascript"]) + ,(make-cdata #f #f "\n var pageTracker = _gat._getTracker(\"UA-YYYYYYY-Y\");\n pageTracker._trackPageview();\n")) + + (h1 "Alonzo's Church: " ,section) + (div ([id "main"]) + (code:comment "He had to be careful to use splicing here") + ,@body)))) + +(define (blog-posted title body k-url) + `((h2 ,title) + (p ,body) + (h1 (a ([href ,k-url]) "Continue")))) + +(define (extract-post req) + (define binds + (request-bindings req)) + (define title + (extract-binding/single 'title binds)) + (define body + (extract-binding/single 'body binds)) + (set! posts + (list* (make-post title body empty) + posts)) + (send/suspend + (lambda (k-url) + (template "Posted" (blog-posted title body k-url)))) + (display-posts)) + +(define (blog-posts k-url) + (code:comment "append or splicing is needed") + (append + (code:comment "Each element of the list is another list") + (apply append + (for/list ([p posts]) + `((h2 ,(post-title p)) + (p ,(post-body p)) + (ul + ,@(for/list ([c (post-comments p)]) + `(li ,c)))))) + `((h1 "New Post") + (form ([action ,k-url]) + (input ([name "title"])) + (input ([name "body"])) + (input ([type "submit"])))))) + +(define (display-posts) + (extract-post + (send/suspend + (lambda (k-url) + (template "Posts" (blog-posts k-url)))))) + +(define (start req) + (display-posts)) + +(serve/servlet start) +] + +Luckily, Alonzo has great software engineering skills, so he's already separated the presentation logic into the functions +@scheme[blog-posted], @scheme[blog-posts], and @scheme[template]. Each one of these will turn into a different +template. + +@filepath{blog.html}: +@verbatim[#:indent 2]|{ + + + Alonzo's Church: @|section| + + + + + + +

Alonzo's Church: @|section|

+
+ @body +
+ + +}| + +Notice that this part of the presentation is much simpler, because the CSS and JavaScript +can be included verbatim, without resorting to any special escape-escaping patterns. +Similarly, since the @scheme[body] is represented as a string, there is no need to +remember if splicing is necessary. + +@filepath{blog-posts.html}: +@verbatim[#:indent 2]|{ +@in[p posts]{ +

@(post-title p)

+

@(post-body p)

+ +} + +

New Post

+ + + + + +}| + +This template is even simpler, because there is no list management whatsoever. The defaults "just work". +For completeness, we show the final template: + +@filepath{blog-posted.html}: +@verbatim[#:indent 2]|{ +

@|title|

+

@|body|

+ +

Continue

+}| + +The code associated with these templates is very simple as well: +@schememod[ + scheme +(require web-server/templates + web-server/servlet + web-server/servlet-env) + +(define-struct post (title body comments)) + +(define posts ...) + +(define (template section body) + (list TEXT/HTML-MIME-TYPE + (include-template "blog.html"))) + +(define (extract-post req) + (define binds + (request-bindings req)) + (define title + (extract-binding/single 'title binds)) + (define body + (extract-binding/single 'body binds)) + (set! posts + (list* (make-post title body empty) + posts)) + (send/suspend + (lambda (k-url) + (template "Posted" (include-template "blog-posted.html")))) + (display-posts)) + +(define (display-posts) + (extract-post + (send/suspend + (lambda (k-url) + (template "Posts" (include-template "blog-posts.html")))))) + +(define (start req) + (display-posts)) + +(serve/servlet start) +] \ No newline at end of file
" (car c) ", " (cdr c) "