├── .gitignore
├── LICENSE
├── README.md
├── bootstrap
├── src
│ └── acavailhez
│ │ └── html
│ │ └── bootstrap
│ │ ├── Bootstrap4AlertTrait.groovy
│ │ ├── Bootstrap4BtnTrait.groovy
│ │ ├── Bootstrap4Color.groovy
│ │ ├── Bootstrap4GridTrait.groovy
│ │ ├── Bootstrap4ModalTrait.groovy
│ │ ├── Bootstrap4NavTrait.groovy
│ │ ├── Bootstrap4Size.groovy
│ │ └── Bootstrap4Trait.groovy
└── test
│ └── acavailhez
│ └── html
│ └── bootstrap
│ ├── BootstrapAlertTests.groovy
│ ├── BootstrapButtonTests.groovy
│ ├── BootstrapGridTests.groovy
│ ├── BootstrapModalTests.groovy
│ └── BootstrapNavTests.groovy
├── build.gradle
├── core
├── README.md
├── src
│ └── acavailhez
│ │ ├── html
│ │ ├── Html.groovy
│ │ ├── HtmlFragment.groovy
│ │ ├── HtmlPage.groovy
│ │ ├── HtmlRenderMode.groovy
│ │ ├── HtmlStyle.groovy
│ │ ├── Javascript.groovy
│ │ ├── builder
│ │ │ ├── EscapedHtmlBuilder.groovy
│ │ │ ├── HtmlBuilder.groovy
│ │ │ └── RawHtmlBuilder.groovy
│ │ ├── internal
│ │ │ └── HtmlEngine.java
│ │ ├── scope
│ │ │ ├── HtmlMapDomScoped.java
│ │ │ ├── HtmlScopable.groovy
│ │ │ ├── HtmlScope.java
│ │ │ └── HtmlScopeListener.groovy
│ │ ├── traits
│ │ │ ├── AttemptTrait.groovy
│ │ │ ├── CaptureTrait.groovy
│ │ │ ├── HeadTrait.groovy
│ │ │ ├── Html5Trait.groovy
│ │ │ ├── HtmlFragmentTrait.groovy
│ │ │ ├── HtmlTrait.groovy
│ │ │ └── ShortcutTrait.groovy
│ │ └── utils
│ │ │ ├── HtmlAttributes.java
│ │ │ └── HtmlUtils.groovy
│ │ └── optget
│ │ ├── CastUtils.java
│ │ ├── OptGet.java
│ │ ├── OptGetMap.java
│ │ └── OptGetWrapper.java
├── test-resources
│ ├── grails.vm
│ └── modal.vm
└── test
│ ├── acavailhez
│ ├── html
│ │ ├── AttemptTests.groovy
│ │ ├── CaptureTests.groovy
│ │ ├── EscapeTests.groovy
│ │ ├── FragmentTests.groovy
│ │ ├── Html5TraitTests.groovy
│ │ ├── PageTests.groovy
│ │ ├── ScopeTests.groovy
│ │ ├── ShortcutTests.groovy
│ │ ├── SimpleTests.groovy
│ │ ├── TagTests.groovy
│ │ ├── performance
│ │ │ ├── GrailsHomepage.groovy
│ │ │ ├── ModalHtml.groovy
│ │ │ └── PerformanceTests.groovy
│ │ └── scope
│ │ │ └── HtmlScopeListenerTests.groovy
│ └── optget
│ │ ├── GetOptTests.groovy
│ │ └── OptGetWrapperTests.groovy
│ └── perf-velocity.vm
├── resources
├── README.md
├── src
│ └── acavailhez
│ │ └── html
│ │ └── resources
│ │ ├── HtmlResource.groovy
│ │ ├── HtmlResourceDisposition.groovy
│ │ ├── HtmlResourcesPack.groovy
│ │ └── HtmlResourcesTrait.groovy
└── test
│ └── acavailhez
│ └── html
│ └── resources
│ └── SimpleTests.groovy
├── scripts
├── src
│ ├── GenerateBootstrap4GridCols.groovy
│ ├── GenerateHtml5Trait.groovy
│ ├── GenerateHtml5TraitTest.groovy
│ ├── GrailsHomepageToVelocity.groovy
│ ├── HtmlToGroovyConverter.groovy
│ └── HtmlToVelocityConverter.groovy
└── test
│ └── acavailhez
│ └── html
│ └── ConverterTest.groovy
├── settings.gradle
├── spark-demo
├── resources
│ └── web
│ │ ├── css
│ │ └── example.css
│ │ └── js
│ │ └── example.js
└── src
│ ├── Main.groovy
│ └── acavailhez
│ └── html
│ └── demo
│ └── Frontpage.groovy
├── spark
└── src
│ └── acavailhez
│ └── html
│ └── spark
│ ├── AbstractSparkRoute.java
│ └── HtmlPageSparkRoute.java
└── tests
└── src
└── acavailhez
└── html
└── tests
└── AbstractTests.groovy
/.gitignore:
--------------------------------------------------------------------------------
1 | **/target
2 | **/.settings
3 |
4 | **/.gradle
5 | **/build
6 |
7 | **/.idea
8 | **/.idea/misc.xml
9 | **/.idea/tasks.xml
10 | **/.idea/workspace.xml
11 | **/*.iml
12 |
13 | **/*.iws
14 | **/*.log
15 | **/*.log.*
16 | **/deploy/
17 | **/atlassian-ide-plugin.xml
18 | **/out
19 |
20 | gradlew
21 | gradlew.bat
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # groovy-html-renderer
2 |
3 | in-code html5 rendering engine leveraging the groovy syntax:
4 |
5 | ```
6 | div(class:'box'){
7 | div(class:'box-body'){
8 | escape << "content of the box"
9 | }
10 | }
11 | ```
12 |
13 | renders:
14 |
15 | ```
16 |
17 |
18 | content of the box
19 |
20 |
21 | ```
22 |
23 | ## Why?
24 |
25 | Server-side rendering of complex html is not everyone's cup of tea, but if you fancy it, `groovy-html-renderer` provides a reliable way to do it
26 |
27 | In java, your choice is usually limited to jsp or similar templating engine, working on html and adding clumsy `if` and `for` statements in custom tags (``).
28 | In `groovy-html-renderer` you are rendering the html in the code itself, so you can leverage the power of the tools you already have, as well as type-check your objects.
29 |
30 | Once you are in code, all sorts of interesting features become possible, such as the `attempt` function, which will act like a try-catch:
31 |
32 | ```
33 | div {
34 | attempt {
35 | div {
36 | escape << 'text'
37 | throw new Exception("oups")
38 | }
39 | } { Throwable t ->
40 | span {
41 | escape << 'failed with:' << t.getMessage()
42 | }
43 | }
44 | }
45 | ```
46 |
47 | renders:
48 |
49 | ```
50 |
51 | oups
52 |
53 | ```
54 |
55 | This will prevent the whole document from crashing if only a non-critical part failed to render
56 |
57 | Using Groovy's `Trait`, it's easy to add new shortcuts and features to the syntax.
58 | Taking the example of Bootstrap's modal ([http://getbootstrap.com/javascript/#static-example](http://getbootstrap.com/javascript/#static-example)), writing it directly requires typing:
59 |
60 | ```
61 |
78 | ```
79 |
80 | With `Bootstrap4Trait` it can be simplified to:
81 |
82 | ```
83 | public class Snippet extends Html implements Bootstrap4Trait{
84 | public void build() {
85 |
86 | modal(title: "Modal title", closeLabel: "Close") {
87 | p("One fine body…")
88 | } {
89 | button(btn('data-dismiss': 'modal'), "Close")
90 | button(btn(color: Bootstrap4Color.PRIMARY), "Save changes")
91 | }
92 |
93 | }
94 | }
95 | ```
96 |
97 | Exposing only the important content and making the code easier to read
98 |
99 |
100 | ## Performances
101 |
102 | The order of magnitude you should expect for the rendering of a html page is < 10ms.
103 |
104 |
105 | This is a number to consider relative to what you are doing around the generation of the page. If you are making even a single SQL query in order to generate the page, then using `groovy-html-renderer` will not have any noticeable impact on your performances.
106 | Now if you are rendering static content, then `groovy-html-renderer` is probably not the right tool for the job.
107 |
108 | Here are some tips to improve performances of your html pages:
109 |
110 | - Use `@CompileStatic` in your pages. This will have a 5x performance boost on pages with a lot of subclosures
111 | - Opt for a basic rendering engine for pages with a lot of static content. `groovy-html-renderer` will perform poorly there
112 |
113 | Let's have a look at the best and worst case scenarios, we compare against 1000 run `groovy-html-renderer` to Apache's `velocity` which is build for speed:
114 |
115 | *Rendering the boostrap modal example - over 1000 runs*
116 |
117 | | Velocity | groovy-html-renderer |
118 | |---|---|
119 | | 0.086ms | 0.254 ms |
120 |
121 | In this run, a small snippet with a for-loop, `groovy-html-renderer` performs 3x slower than the highly optimized `velocity`
122 |
123 | *Rendering the grails homepage - 1000 runs*
124 |
125 | | Velocity | groovy-html-renderer |
126 | |---|---|
127 | | 0.037ms | 1.262 ms |
128 |
129 | In the worst case scenario, a long static page with no customized content, `groovy-html-renderer` performs 34x slower than `velocity`
130 |
131 | But even in the worst-case scenario, the rendering of a long static page containing a lot of tags written with closures, `groovy-html-renderer` renders the page in a matter of milliseconds.
132 | Any SQL request made by your application will dwarf those times to negligeable amounts.
133 |
134 | ## Detailed documentation by component
135 |
136 | ### [Core features](core/README.md)
137 | ### [Resources (js and css)](resources/README.md)
138 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4AlertTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.traits.Html5Trait
4 | import acavailhez.html.utils.HtmlAttributes
5 | import groovy.transform.CompileStatic
6 |
7 | // http://v4-alpha.getbootstrap.com/components/alerts/
8 | @CompileStatic
9 | trait Bootstrap4AlertTrait extends Html5Trait {
10 |
11 | public void alert(Map map, Closure body) {
12 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
13 | Bootstrap4Color color = attrs.opt('color', Bootstrap4Color, Bootstrap4Color.DEFAULT)
14 | attrs.remove('color')
15 | attrs.put('role', 'alert')
16 | attrs.addToClass('alert alert-' + color.name().toLowerCase())
17 | div(attrs) {
18 | body()
19 | }
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4BtnTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.traits.Html5Trait
4 | import acavailhez.html.utils.HtmlAttributes
5 | import groovy.transform.CompileStatic
6 |
7 | // shortcuts for bootstrap 4 buttons
8 | // http://v4-alpha.getbootstrap.com/components/buttons/
9 | @CompileStatic
10 | trait Bootstrap4BtnTrait extends Html5Trait {
11 |
12 | // Reconfigures the attributes of a tag
13 | HtmlAttributes btn(Map map) {
14 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
15 |
16 | attrs.addToClass('btn')
17 | attrs.put('type', 'button')
18 |
19 | // color
20 | Bootstrap4Color color = attrs.opt('color', Bootstrap4Color, Bootstrap4Color.DEFAULT)
21 | attrs.remove('color')
22 | String btnClass = 'btn-' + color.name().toLowerCase()
23 | if (attrs.opt('outline', Boolean, false)) {
24 | btnClass += '-outline'
25 | }
26 | attrs.remove('outline')
27 | attrs.addToClass(btnClass)
28 |
29 | // size
30 | Bootstrap4Size size = attrs.opt('size', Bootstrap4Size, null)
31 | attrs.remove('size')
32 | if (size) {
33 | attrs.addToClass('btn-' + size.name().toLowerCase())
34 | }
35 |
36 | return attrs
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4Color.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | enum Bootstrap4Color {
4 |
5 | DEFAULT,
6 | PRIMARY,
7 | SECONDARY,
8 | SUCCESS,
9 | INFO,
10 | WARNING,
11 | DANGER,
12 | LINK
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4GridTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.traits.Html5Trait
4 | import acavailhez.html.utils.HtmlAttributes
5 | import groovy.transform.CompileStatic
6 | import org.apache.log4j.Logger
7 |
8 | // http://v4-alpha.getbootstrap.com/layout/grid/
9 | // Will manage the rows automatically
10 | @CompileStatic
11 | trait Bootstrap4GridTrait extends Html5Trait {
12 |
13 | private static final Logger log = Logger.getLogger(Bootstrap4GridTrait)
14 | private static final String COLS_COUNT_KEY = '_bnoccr' // Bootstrap Number Of Columns In Current Row
15 |
16 | public void container(Closure body) {
17 | container([:], body)
18 | }
19 |
20 | public void container(Map map, Closure body) {
21 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
22 | div(attrs.addToClass('container')) {
23 | body()
24 | if (scope.opt(COLS_COUNT_KEY, Integer, 0) > 0) {
25 | log.warn('row forced closed with only ' + scope.get(COLS_COUNT_KEY, Integer) + ' cols')
26 | html << ''
27 | scope.put(COLS_COUNT_KEY, 0)
28 | }
29 | }
30 | }
31 |
32 | private void bootstrapCol(Map map, int cols, Bootstrap4Size size, Closure body) {
33 |
34 | //open the row if first one
35 | if (scope.opt(COLS_COUNT_KEY, Integer, 0) == 0) {
36 | scope.put(COLS_COUNT_KEY, 0)
37 | html << ''
38 | }
39 |
40 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
41 | attrs.addToClass('col-' + size.name().toLowerCase() + '-' + cols)
42 |
43 | div(attrs) {
44 | if (body) {
45 | body()
46 | } else {
47 | html << ' ' // empty col for spacing
48 | }
49 | }
50 |
51 | scope.put(COLS_COUNT_KEY, scope.get(COLS_COUNT_KEY, Integer) + cols)
52 |
53 | //close the row if too big
54 | if (scope.get(COLS_COUNT_KEY, Integer) >= 12) {
55 | if (scope.get(COLS_COUNT_KEY, Integer) > 12) {
56 | log.warn('current bootstrap row is more than 12 cols')
57 | }
58 | html << '
'
59 | scope.put(COLS_COUNT_KEY, 0)
60 | }
61 | }
62 |
63 | public void colSm1(Closure body) {
64 | bootstrapCol([:], 1, Bootstrap4Size.SM, body)
65 | }
66 |
67 | public void colSm1(Map map, Closure body) {
68 | bootstrapCol(map, 1, Bootstrap4Size.SM, body)
69 | }
70 |
71 | public void colSm2(Closure body) {
72 | bootstrapCol([:], 2, Bootstrap4Size.SM, body)
73 | }
74 |
75 | public void colSm2(Map map, Closure body) {
76 | bootstrapCol(map, 2, Bootstrap4Size.SM, body)
77 | }
78 |
79 | public void colSm3(Closure body) {
80 | bootstrapCol([:], 3, Bootstrap4Size.SM, body)
81 | }
82 |
83 | public void colSm3(Map map, Closure body) {
84 | bootstrapCol(map, 3, Bootstrap4Size.SM, body)
85 | }
86 |
87 | public void colSm4(Closure body) {
88 | bootstrapCol([:], 4, Bootstrap4Size.SM, body)
89 | }
90 |
91 | public void colSm4(Map map, Closure body) {
92 | bootstrapCol(map, 4, Bootstrap4Size.SM, body)
93 | }
94 |
95 | public void colSm5(Closure body) {
96 | bootstrapCol([:], 5, Bootstrap4Size.SM, body)
97 | }
98 |
99 | public void colSm5(Map map, Closure body) {
100 | bootstrapCol(map, 5, Bootstrap4Size.SM, body)
101 | }
102 |
103 | public void colSm6(Closure body) {
104 | bootstrapCol([:], 6, Bootstrap4Size.SM, body)
105 | }
106 |
107 | public void colSm6(Map map, Closure body) {
108 | bootstrapCol(map, 6, Bootstrap4Size.SM, body)
109 | }
110 |
111 | public void colSm7(Closure body) {
112 | bootstrapCol([:], 7, Bootstrap4Size.SM, body)
113 | }
114 |
115 | public void colSm7(Map map, Closure body) {
116 | bootstrapCol(map, 7, Bootstrap4Size.SM, body)
117 | }
118 |
119 | public void colSm8(Closure body) {
120 | bootstrapCol([:], 8, Bootstrap4Size.SM, body)
121 | }
122 |
123 | public void colSm8(Map map, Closure body) {
124 | bootstrapCol(map, 8, Bootstrap4Size.SM, body)
125 | }
126 |
127 | public void colSm9(Closure body) {
128 | bootstrapCol([:], 9, Bootstrap4Size.SM, body)
129 | }
130 |
131 | public void colSm9(Map map, Closure body) {
132 | bootstrapCol(map, 9, Bootstrap4Size.SM, body)
133 | }
134 |
135 | public void colSm10(Closure body) {
136 | bootstrapCol([:], 10, Bootstrap4Size.SM, body)
137 | }
138 |
139 | public void colSm10(Map map, Closure body) {
140 | bootstrapCol(map, 10, Bootstrap4Size.SM, body)
141 | }
142 |
143 | public void colSm11(Closure body) {
144 | bootstrapCol([:], 11, Bootstrap4Size.SM, body)
145 | }
146 |
147 | public void colSm11(Map map, Closure body) {
148 | bootstrapCol(map, 11, Bootstrap4Size.SM, body)
149 | }
150 |
151 | public void colSm12(Closure body) {
152 | bootstrapCol([:], 12, Bootstrap4Size.SM, body)
153 | }
154 |
155 | public void colSm12(Map map, Closure body) {
156 | bootstrapCol(map, 12, Bootstrap4Size.SM, body)
157 | }
158 |
159 | public void colMd1(Closure body) {
160 | bootstrapCol([:], 1, Bootstrap4Size.MD, body)
161 | }
162 |
163 | public void colMd1(Map map, Closure body) {
164 | bootstrapCol(map, 1, Bootstrap4Size.MD, body)
165 | }
166 |
167 | public void colMd2(Closure body) {
168 | bootstrapCol([:], 2, Bootstrap4Size.MD, body)
169 | }
170 |
171 | public void colMd2(Map map, Closure body) {
172 | bootstrapCol(map, 2, Bootstrap4Size.MD, body)
173 | }
174 |
175 | public void colMd3(Closure body) {
176 | bootstrapCol([:], 3, Bootstrap4Size.MD, body)
177 | }
178 |
179 | public void colMd3(Map map, Closure body) {
180 | bootstrapCol(map, 3, Bootstrap4Size.MD, body)
181 | }
182 |
183 | public void colMd4(Closure body) {
184 | bootstrapCol([:], 4, Bootstrap4Size.MD, body)
185 | }
186 |
187 | public void colMd4(Map map, Closure body) {
188 | bootstrapCol(map, 4, Bootstrap4Size.MD, body)
189 | }
190 |
191 | public void colMd5(Closure body) {
192 | bootstrapCol([:], 5, Bootstrap4Size.MD, body)
193 | }
194 |
195 | public void colMd5(Map map, Closure body) {
196 | bootstrapCol(map, 5, Bootstrap4Size.MD, body)
197 | }
198 |
199 | public void colMd6(Closure body) {
200 | bootstrapCol([:], 6, Bootstrap4Size.MD, body)
201 | }
202 |
203 | public void colMd6(Map map, Closure body) {
204 | bootstrapCol(map, 6, Bootstrap4Size.MD, body)
205 | }
206 |
207 | public void colMd7(Closure body) {
208 | bootstrapCol([:], 7, Bootstrap4Size.MD, body)
209 | }
210 |
211 | public void colMd7(Map map, Closure body) {
212 | bootstrapCol(map, 7, Bootstrap4Size.MD, body)
213 | }
214 |
215 | public void colMd8(Closure body) {
216 | bootstrapCol([:], 8, Bootstrap4Size.MD, body)
217 | }
218 |
219 | public void colMd8(Map map, Closure body) {
220 | bootstrapCol(map, 8, Bootstrap4Size.MD, body)
221 | }
222 |
223 | public void colMd9(Closure body) {
224 | bootstrapCol([:], 9, Bootstrap4Size.MD, body)
225 | }
226 |
227 | public void colMd9(Map map, Closure body) {
228 | bootstrapCol(map, 9, Bootstrap4Size.MD, body)
229 | }
230 |
231 | public void colMd10(Closure body) {
232 | bootstrapCol([:], 10, Bootstrap4Size.MD, body)
233 | }
234 |
235 | public void colMd10(Map map, Closure body) {
236 | bootstrapCol(map, 10, Bootstrap4Size.MD, body)
237 | }
238 |
239 | public void colMd11(Closure body) {
240 | bootstrapCol([:], 11, Bootstrap4Size.MD, body)
241 | }
242 |
243 | public void colMd11(Map map, Closure body) {
244 | bootstrapCol(map, 11, Bootstrap4Size.MD, body)
245 | }
246 |
247 | public void colMd12(Closure body) {
248 | bootstrapCol([:], 12, Bootstrap4Size.MD, body)
249 | }
250 |
251 | public void colMd12(Map map, Closure body) {
252 | bootstrapCol(map, 12, Bootstrap4Size.MD, body)
253 | }
254 |
255 | public void colLg1(Closure body) {
256 | bootstrapCol([:], 1, Bootstrap4Size.LG, body)
257 | }
258 |
259 | public void colLg1(Map map, Closure body) {
260 | bootstrapCol(map, 1, Bootstrap4Size.LG, body)
261 | }
262 |
263 | public void colLg2(Closure body) {
264 | bootstrapCol([:], 2, Bootstrap4Size.LG, body)
265 | }
266 |
267 | public void colLg2(Map map, Closure body) {
268 | bootstrapCol(map, 2, Bootstrap4Size.LG, body)
269 | }
270 |
271 | public void colLg3(Closure body) {
272 | bootstrapCol([:], 3, Bootstrap4Size.LG, body)
273 | }
274 |
275 | public void colLg3(Map map, Closure body) {
276 | bootstrapCol(map, 3, Bootstrap4Size.LG, body)
277 | }
278 |
279 | public void colLg4(Closure body) {
280 | bootstrapCol([:], 4, Bootstrap4Size.LG, body)
281 | }
282 |
283 | public void colLg4(Map map, Closure body) {
284 | bootstrapCol(map, 4, Bootstrap4Size.LG, body)
285 | }
286 |
287 | public void colLg5(Closure body) {
288 | bootstrapCol([:], 5, Bootstrap4Size.LG, body)
289 | }
290 |
291 | public void colLg5(Map map, Closure body) {
292 | bootstrapCol(map, 5, Bootstrap4Size.LG, body)
293 | }
294 |
295 | public void colLg6(Closure body) {
296 | bootstrapCol([:], 6, Bootstrap4Size.LG, body)
297 | }
298 |
299 | public void colLg6(Map map, Closure body) {
300 | bootstrapCol(map, 6, Bootstrap4Size.LG, body)
301 | }
302 |
303 | public void colLg7(Closure body) {
304 | bootstrapCol([:], 7, Bootstrap4Size.LG, body)
305 | }
306 |
307 | public void colLg7(Map map, Closure body) {
308 | bootstrapCol(map, 7, Bootstrap4Size.LG, body)
309 | }
310 |
311 | public void colLg8(Closure body) {
312 | bootstrapCol([:], 8, Bootstrap4Size.LG, body)
313 | }
314 |
315 | public void colLg8(Map map, Closure body) {
316 | bootstrapCol(map, 8, Bootstrap4Size.LG, body)
317 | }
318 |
319 | public void colLg9(Closure body) {
320 | bootstrapCol([:], 9, Bootstrap4Size.LG, body)
321 | }
322 |
323 | public void colLg9(Map map, Closure body) {
324 | bootstrapCol(map, 9, Bootstrap4Size.LG, body)
325 | }
326 |
327 | public void colLg10(Closure body) {
328 | bootstrapCol([:], 10, Bootstrap4Size.LG, body)
329 | }
330 |
331 | public void colLg10(Map map, Closure body) {
332 | bootstrapCol(map, 10, Bootstrap4Size.LG, body)
333 | }
334 |
335 | public void colLg11(Closure body) {
336 | bootstrapCol([:], 11, Bootstrap4Size.LG, body)
337 | }
338 |
339 | public void colLg11(Map map, Closure body) {
340 | bootstrapCol(map, 11, Bootstrap4Size.LG, body)
341 | }
342 |
343 | public void colLg12(Closure body) {
344 | bootstrapCol([:], 12, Bootstrap4Size.LG, body)
345 | }
346 |
347 | public void colLg12(Map map, Closure body) {
348 | bootstrapCol(map, 12, Bootstrap4Size.LG, body)
349 | }
350 |
351 | public void colXl1(Closure body) {
352 | bootstrapCol([:], 1, Bootstrap4Size.XL, body)
353 | }
354 |
355 | public void colXl1(Map map, Closure body) {
356 | bootstrapCol(map, 1, Bootstrap4Size.XL, body)
357 | }
358 |
359 | public void colXl2(Closure body) {
360 | bootstrapCol([:], 2, Bootstrap4Size.XL, body)
361 | }
362 |
363 | public void colXl2(Map map, Closure body) {
364 | bootstrapCol(map, 2, Bootstrap4Size.XL, body)
365 | }
366 |
367 | public void colXl3(Closure body) {
368 | bootstrapCol([:], 3, Bootstrap4Size.XL, body)
369 | }
370 |
371 | public void colXl3(Map map, Closure body) {
372 | bootstrapCol(map, 3, Bootstrap4Size.XL, body)
373 | }
374 |
375 | public void colXl4(Closure body) {
376 | bootstrapCol([:], 4, Bootstrap4Size.XL, body)
377 | }
378 |
379 | public void colXl4(Map map, Closure body) {
380 | bootstrapCol(map, 4, Bootstrap4Size.XL, body)
381 | }
382 |
383 | public void colXl5(Closure body) {
384 | bootstrapCol([:], 5, Bootstrap4Size.XL, body)
385 | }
386 |
387 | public void colXl5(Map map, Closure body) {
388 | bootstrapCol(map, 5, Bootstrap4Size.XL, body)
389 | }
390 |
391 | public void colXl6(Closure body) {
392 | bootstrapCol([:], 6, Bootstrap4Size.XL, body)
393 | }
394 |
395 | public void colXl6(Map map, Closure body) {
396 | bootstrapCol(map, 6, Bootstrap4Size.XL, body)
397 | }
398 |
399 | public void colXl7(Closure body) {
400 | bootstrapCol([:], 7, Bootstrap4Size.XL, body)
401 | }
402 |
403 | public void colXl7(Map map, Closure body) {
404 | bootstrapCol(map, 7, Bootstrap4Size.XL, body)
405 | }
406 |
407 | public void colXl8(Closure body) {
408 | bootstrapCol([:], 8, Bootstrap4Size.XL, body)
409 | }
410 |
411 | public void colXl8(Map map, Closure body) {
412 | bootstrapCol(map, 8, Bootstrap4Size.XL, body)
413 | }
414 |
415 | public void colXl9(Closure body) {
416 | bootstrapCol([:], 9, Bootstrap4Size.XL, body)
417 | }
418 |
419 | public void colXl9(Map map, Closure body) {
420 | bootstrapCol(map, 9, Bootstrap4Size.XL, body)
421 | }
422 |
423 | public void colXl10(Closure body) {
424 | bootstrapCol([:], 10, Bootstrap4Size.XL, body)
425 | }
426 |
427 | public void colXl10(Map map, Closure body) {
428 | bootstrapCol(map, 10, Bootstrap4Size.XL, body)
429 | }
430 |
431 | public void colXl11(Closure body) {
432 | bootstrapCol([:], 11, Bootstrap4Size.XL, body)
433 | }
434 |
435 | public void colXl11(Map map, Closure body) {
436 | bootstrapCol(map, 11, Bootstrap4Size.XL, body)
437 | }
438 |
439 | public void colXl12(Closure body) {
440 | bootstrapCol([:], 12, Bootstrap4Size.XL, body)
441 | }
442 |
443 | public void colXl12(Map map, Closure body) {
444 | bootstrapCol(map, 12, Bootstrap4Size.XL, body)
445 | }
446 | }
447 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4ModalTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.internal.HtmlEngine
4 | import acavailhez.html.traits.Html5Trait
5 | import acavailhez.html.utils.HtmlAttributes
6 | import groovy.transform.CompileStatic
7 |
8 | // shortcuts for bootstrap 4 modals
9 | // http://v4-alpha.getbootstrap.com/components/modal/
10 | @CompileStatic
11 | trait Bootstrap4ModalTrait extends Html5Trait {
12 |
13 | // shortcut
14 | void modal(Map attrs,
15 | Closure body) {
16 | modal(attrs, null, body, null)
17 | }
18 |
19 | // shortcut
20 | void modal(Map attrs,
21 | Closure body,
22 | Closure footer) {
23 | modal(attrs, null, body, footer)
24 | }
25 |
26 | // A bootstrap modal with basic options
27 | // if header is null, a default header will be added
28 | // if footer is null, no footer will be added
29 | // Available attributes:
30 | // title, String: if present and header is null, the modal will have a head with this string (escaped)
31 | // tabindex, int: if present will set the tab-index
32 | // closeLabel, String: the label of the close cross
33 | void modal(Map map,
34 | Closure header,
35 | Closure body,
36 | Closure footer) {
37 |
38 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
39 |
40 | String title = attrs.opt('title', String)
41 | attrs.remove('title')
42 |
43 | String close = attrs.opt('closeLabel', String)
44 | attrs.remove('closeLabel')
45 |
46 | attrs.addToClass('modal fade')
47 | attrs.put('role', 'dialog')
48 | attrs.put('tabindex', attrs.opt('tabindex', Integer, -1))
49 |
50 | // speed up and go around groovy trait bug (https://issues.apache.org/jira/browse/GROOVY-7843)
51 | HtmlEngine.openTag(html.stringBuilder, 'div', attrs)
52 | HtmlEngine.openTag(html.stringBuilder, 'div', [class: 'modal-dialog'])
53 | HtmlEngine.openTag(html.stringBuilder, 'div', [class: 'modal-content'])
54 |
55 | div(class: 'modal-header') {
56 | if (header) {
57 | header()
58 | } else {
59 | // default header
60 | button(type: 'button', class: 'close', 'data-dismiss': 'modal', 'aria-label': close) {
61 | html << '× '
62 | }
63 | if (title && !title.isEmpty()) {
64 | h4(class: 'modal-title') {
65 | escape << title
66 | }
67 | }
68 |
69 | }
70 | }
71 |
72 | div(class: 'modal-body') {
73 | body()
74 | }
75 |
76 | if (footer) {
77 | div(class: 'modal-footer') {
78 | footer()
79 | }
80 | }
81 | HtmlEngine.closeTag(html.stringBuilder, 'div')
82 | HtmlEngine.closeTag(html.stringBuilder, 'div')
83 | HtmlEngine.closeTag(html.stringBuilder, 'div')
84 | }
85 |
86 | // when applied to , will open a modal
87 | HtmlAttributes modal(Map map) {
88 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
89 |
90 | String modalId = attrs.get('modal', String)
91 | attrs.remove('modal')
92 | attrs.put('data-toggle', 'modal')
93 | attrs.put('data-target', '#' + modalId)
94 |
95 | return attrs
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4NavTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.traits.CaptureTrait
4 | import acavailhez.html.traits.Html5Trait
5 | import acavailhez.html.utils.HtmlAttributes
6 | import groovy.transform.CompileStatic
7 |
8 | // shortcuts for bootstrap 4 navs
9 | // http://v4-alpha.getbootstrap.com/components/navs
10 | // http://v4-alpha.getbootstrap.com/components/navbar
11 | @CompileStatic
12 | trait Bootstrap4NavTrait implements Html5Trait, CaptureTrait {
13 |
14 | void navbar(Closure body) {
15 | navbar([:], body)
16 | }
17 |
18 | void navbar(Map map, Closure body) {
19 | HtmlAttributes attrs = HtmlAttributes.copy(map)
20 | nav(attrs.addToClass('navbar navbar-light')) {
21 | scope.put('navbar', true)
22 | body()
23 | }
24 | }
25 |
26 | // Navbar
27 | void brand(String brandName) {
28 | brand([:]) {
29 | escape << brandName
30 | }
31 | }
32 |
33 | void brand(Map map, Closure body) {
34 | a(HtmlAttributes.copy(map).addToClass('navbar-brand'), body)
35 | }
36 |
37 | //
38 | void ulNav(Closure body) {
39 | ulNav([:], body)
40 | }
41 |
42 | void ulNav(Map map, Closure body) {
43 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
44 | attrs.addToClass('nav')
45 | if (scope.get('navbar')) {
46 | attrs.addToClass('navbar-nav')
47 | }
48 | ul(attrs, body)
49 | }
50 |
51 | void ulTabs(Closure body) {
52 | ulTabs([:], body)
53 | }
54 |
55 | void ulTabs(Map map, Closure body) {
56 | scope.put('tabs', true)
57 | ulNav(map, body)
58 | }
59 |
60 | // The attributes will be the attributes of only
61 | void liANav(Map map, Closure body) {
62 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
63 | li([class: 'nav-item']) {
64 | attrs.addToClass('nav-link')
65 | a(attrs) {
66 | body()
67 | }
68 | }
69 | }
70 |
71 | void tabs(Closure body) {
72 | tabs([:], body)
73 | }
74 |
75 | void tabs(Map map, Closure body) {
76 | List allTabs = []
77 | scope.put('tabs', allTabs)
78 | scope.put('navbar', true)
79 |
80 | // Let the user tell us what the tabs are named and contain
81 | capture(body)
82 |
83 | // Write the header
84 | ulNav(role: 'tab-list') {
85 | for (Map tab : allTabs) {
86 | String id = tab.id
87 | liANav(role: 'tab', 'data-toggle': 'tab', href: '#' + id) {
88 | escape << tab.title
89 | }
90 | }
91 | }
92 |
93 | // Write the content
94 | div(class: 'tab-content') {
95 | for (Map tab : allTabs) {
96 | String id = tab.id
97 | div(class: 'tab-pane', id: id, role: 'tabpanel') {
98 | html << tab.html
99 | }
100 | }
101 | }
102 | }
103 |
104 | void tab(String title, Closure body) {
105 | tab(title, [:], body)
106 | }
107 |
108 | void tab(String title, Map map, Closure body) {
109 | HtmlAttributes attrs = HtmlAttributes.wrap(map)
110 | List tabs = scope.opt('tabs', List)
111 | if (tabs == null) {
112 | throw new IllegalStateException("tab() used without being within a tabs{}, this cannot work, please consult the documentation of BootstrapNavTrait")
113 | }
114 | String id = attrs.opt('id', String)
115 | if (!id) {
116 | id = title.trim().replaceAll(" ", "-").replaceAll("[^a-zA-Z-]", "").toLowerCase();
117 | }
118 | String tabHtml = capture(body)
119 | tabs.add([
120 | title: title,
121 | id : id,
122 | html : tabHtml
123 | ])
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4Size.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | enum Bootstrap4Size {
4 |
5 | SM,
6 | MD,
7 | LG,
8 | XL,
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/bootstrap/src/acavailhez/html/bootstrap/Bootstrap4Trait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.traits.HtmlTrait
4 | import groovy.transform.CompileStatic
5 |
6 | // shortcuts for bootstrap 4
7 | @CompileStatic
8 | trait Bootstrap4Trait extends HtmlTrait
9 | implements Bootstrap4ModalTrait,
10 | Bootstrap4BtnTrait,
11 | Bootstrap4NavTrait,
12 | Bootstrap4GridTrait,
13 | Bootstrap4AlertTrait {
14 | // All bootstrap traits at once
15 | }
16 |
--------------------------------------------------------------------------------
/bootstrap/test/acavailhez/html/bootstrap/BootstrapAlertTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.Html
4 | import acavailhez.html.tests.AbstractTests
5 | import org.junit.Test
6 |
7 | public class BootstrapAlertTests extends AbstractTests {
8 |
9 | static abstract class BootstrapHtml extends Html implements Bootstrap4Trait {}
10 |
11 | @Test
12 | public void testInfo() throws Exception {
13 | String html = (new BootstrapHtml() {
14 | @Override
15 | public void build() {
16 | alert(color:Bootstrap4Color.SUCCESS) {
17 | escape << "Yeah"
18 | }
19 | }
20 | }).getRawHtml()
21 |
22 | assert renderEquals(html, 'Yeah
')
23 | }
24 | }
--------------------------------------------------------------------------------
/bootstrap/test/acavailhez/html/bootstrap/BootstrapButtonTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.Html
4 | import acavailhez.html.tests.AbstractTests
5 | import org.junit.Test
6 |
7 | public class BootstrapButtonTests extends AbstractTests {
8 |
9 | static abstract class BootstrapHtml extends Html implements Bootstrap4Trait {}
10 |
11 | @Test
12 | public void testSimple() throws Exception {
13 | String html = (new BootstrapHtml() {
14 | @Override
15 | public void build() {
16 | a(btn([:]), "Click me")
17 | }
18 | }).getRawHtml()
19 |
20 | assert renderEquals(html, 'Click me ')
21 | }
22 |
23 | @Test
24 | public void testBody() throws Exception {
25 | String html = (new BootstrapHtml() {
26 | @Override
27 | public void build() {
28 | button(btn(id: 'abc')) {
29 | escape << 'Click ' + ' me'
30 | }
31 | }
32 | }).getRawHtml()
33 |
34 | assert renderEquals(html, 'Click me ')
35 | }
36 |
37 | @Test
38 | public void testColor() throws Exception {
39 | String html = (new BootstrapHtml() {
40 | @Override
41 | public void build() {
42 | a(btn(color: Bootstrap4Color.DANGER), "Do the thing")
43 | }
44 | }).getRawHtml()
45 |
46 | assert renderEquals(html, 'Do the thing ')
47 | }
48 |
49 | @Test
50 | public void testColorString() throws Exception {
51 | String html = (new BootstrapHtml() {
52 | @Override
53 | public void build() {
54 | a(btn(color: 'danger'), "Do the thing")
55 | }
56 | }).getRawHtml()
57 |
58 | assert renderEquals(html, 'Do the thing ')
59 | }
60 |
61 | @Test
62 | public void testButtonWrongColor() throws Exception {
63 |
64 | try {
65 | String html = (new BootstrapHtml() {
66 | @Override
67 | public void build() {
68 | a(btn(color: 'danger2'), "Do the thing")
69 | }
70 | }).getRawHtml()
71 |
72 | throw new RuntimeException("Should have crashed")
73 | }
74 | catch (IllegalArgumentException ex) {
75 | // we're good
76 | }
77 | }
78 |
79 | @Test
80 | public void testButtonSize() throws Exception {
81 | String html = (new BootstrapHtml() {
82 | @Override
83 | public void build() {
84 | a(btn(color: Bootstrap4Color.DANGER, size: Bootstrap4Size.LG), "Do the thing")
85 | }
86 | }).getRawHtml()
87 |
88 | assert renderEquals(html, 'Do the thing ')
89 | }
90 |
91 | @Test
92 | public void testOutline() throws Exception {
93 | String html = (new BootstrapHtml() {
94 | @Override
95 | public void build() {
96 | a(btn(color: Bootstrap4Color.DANGER, outline: true, size: Bootstrap4Size.LG), "Do the thing")
97 | }
98 | }).getRawHtml()
99 |
100 | assert renderEquals(html, 'Do the thing ')
101 | }
102 |
103 | @Test
104 | public void testAddClass() throws Exception {
105 | String html = (new BootstrapHtml() {
106 | @Override
107 | public void build() {
108 | a(btn(class: 'my-button-class'), "Click me")
109 | }
110 | }).getRawHtml()
111 |
112 | assert renderEquals(html, 'Click me ')
113 | }
114 | }
--------------------------------------------------------------------------------
/bootstrap/test/acavailhez/html/bootstrap/BootstrapGridTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.Html
4 | import acavailhez.html.tests.AbstractTests
5 | import org.junit.Test
6 |
7 | public class BootstrapGridTests extends AbstractTests {
8 |
9 | static abstract class BootstrapHtml extends Html implements Bootstrap4Trait {}
10 |
11 |
12 | @Test
13 | public void testNoColumn() throws Exception {
14 | String html = (new BootstrapHtml() {
15 | @Override
16 | public void build() {
17 | container {
18 | }
19 | }
20 | }).getRawHtml()
21 |
22 | assert renderEquals(html, '
')
23 | }
24 |
25 |
26 | @Test
27 | public void testOneColumn() throws Exception {
28 | String html = (new BootstrapHtml() {
29 | @Override
30 | public void build() {
31 | container {
32 | colLg12 {
33 | escape << "huge column"
34 | }
35 | }
36 | }
37 | }).getRawHtml()
38 |
39 | assert renderEquals(html, '')
40 | }
41 |
42 | @Test
43 | public void testOneColumnTooSmall() throws Exception {
44 | String html = (new BootstrapHtml() {
45 | @Override
46 | public void build() {
47 | container {
48 | colLg10 {
49 | escape << "column too small"
50 | }
51 | }
52 | }
53 | }).getRawHtml()
54 |
55 | assert renderEquals(html, '')
56 | }
57 |
58 | @Test
59 | public void testThreeColumns() throws Exception {
60 | String html = (new BootstrapHtml() {
61 | @Override
62 | public void build() {
63 | container {
64 | colMd4 { escape << "one" }
65 | colMd4 { escape << "two" }
66 | colMd4 { escape << "three" }
67 | }
68 | }
69 | }).getRawHtml()
70 |
71 | assert renderEquals(html, '''
72 |
73 |
74 |
one
75 |
two
76 |
three
77 |
78 |
''')
79 | }
80 |
81 | @Test
82 | public void testTwoRows() throws Exception {
83 | String html = (new BootstrapHtml() {
84 | @Override
85 | public void build() {
86 | container {
87 | colSm6 { escape << "one" }
88 | colSm6 { escape << "two" }
89 | colSm6 { escape << "three" }
90 | colSm6 { escape << "four" }
91 | }
92 | }
93 | }).getRawHtml()
94 |
95 | assert renderEquals(html, '''
96 |
97 |
101 |
102 |
three
103 |
four
104 |
105 |
''')
106 | }
107 | }
--------------------------------------------------------------------------------
/bootstrap/test/acavailhez/html/bootstrap/BootstrapModalTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.Html
4 | import acavailhez.html.tests.AbstractTests
5 | import org.junit.Test
6 |
7 | public class BootstrapModalTests extends AbstractTests {
8 |
9 | static abstract class BootstrapHtml extends Html implements Bootstrap4Trait {}
10 |
11 | @Test
12 | public void testEmpty() throws Exception {
13 | String html = (new BootstrapHtml() {
14 | @Override
15 | public void build() {
16 | modal([:]) {
17 |
18 | }
19 | }
20 | }).getRawHtml()
21 |
22 | assert renderEquals(html, '''
23 |
36 | ''')
37 | }
38 |
39 | @Test
40 | public void testTitle() throws Exception {
41 | String html = (new BootstrapHtml() {
42 | @Override
43 | public void build() {
44 | modal([title: 'A nice title', tabindex: 2, id:'my-modal']) {
45 |
46 | }
47 | }
48 | }).getRawHtml()
49 |
50 | assert renderEquals(html, '''
51 |
65 | ''')
66 | }
67 |
68 | @Test
69 | public void testBootstrapExample() throws Exception {
70 | String html = (new BootstrapHtml() {
71 | @Override
72 | public void build() {
73 | modal(title: "Modal title", closeLabel: "Close") {
74 | p("One fine body…")
75 | } {
76 | button(btn('data-dismiss': 'modal'), "Close")
77 | button(btn(color: Bootstrap4Color.PRIMARY), "Save changes")
78 | }
79 | }
80 | }).getRawHtml()
81 |
82 | assert renderEquals(html, '''
83 |
101 | ''')
102 | }
103 |
104 | @Test
105 | public void testModalA() throws Exception {
106 | String html = (new BootstrapHtml() {
107 | @Override
108 | public void build() {
109 | a(btn(modal(color:Bootstrap4Color.PRIMARY, modal:'my-modal')),"Open modal")
110 | }
111 | }).getRawHtml()
112 |
113 | assert renderEquals(html, 'Open modal ')
114 | }
115 | }
--------------------------------------------------------------------------------
/bootstrap/test/acavailhez/html/bootstrap/BootstrapNavTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.bootstrap
2 |
3 | import acavailhez.html.Html
4 | import acavailhez.html.tests.AbstractTests
5 | import org.junit.Test
6 |
7 | public class BootstrapNavTests extends AbstractTests {
8 |
9 | static abstract class BootstrapHtml extends Html implements Bootstrap4Trait {}
10 |
11 | @Test
12 | public void tabs() throws Exception {
13 | String html = (new BootstrapHtml() {
14 | @Override
15 | public void build() {
16 | tabs {
17 | tab("Profile") {
18 | p {
19 | escape << "Profile text"
20 | }
21 | }
22 | tab("More") {
23 | p {
24 | escape << "More text"
25 | }
26 | }
27 | }
28 | }
29 | }).getRawHtml()
30 |
31 | assert renderEquals(html, '')
32 | }
33 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | group 'acavailhez'
3 |
4 | apply plugin: 'java'
5 | apply plugin: 'groovy'
6 | apply plugin: 'maven'
7 | apply plugin: 'signing'
8 |
9 | sourceCompatibility = 1.8
10 |
11 | group = 'com.github.acavailhez'
12 | version = '2.1.1-a-SNAPSHOT'
13 |
14 | repositories {
15 | mavenCentral()
16 | }
17 |
18 | sourceSets {
19 | main {
20 | java { srcDirs = [] } // everything compiled with groovy
21 | groovy { srcDirs = ["src"] }
22 | resources { srcDirs = ["resources"] }
23 | }
24 | test {
25 | java { srcDirs = ["test"] }
26 | groovy { srcDirs = ["test"] }
27 | resources { srcDirs = ["test-resources"] }
28 | }
29 | }
30 |
31 | // Add sources to the published artifact
32 | task sourcesJar(type: Jar) {
33 | classifier = 'sources'
34 | from sourceSets.main.allSource
35 | }
36 | task javadocJar(type: Jar) {
37 | classifier = 'javadoc'
38 | from javadoc
39 | }
40 |
41 | artifacts {
42 | archives javadocJar, sourcesJar
43 | }
44 | signing {
45 | sign configurations.archives
46 | }
47 |
48 | uploadArchives {
49 | repositories {
50 | mavenDeployer {
51 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
52 |
53 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
54 | authentication(userName: ossrhUsername, password: ossrhPassword)
55 | }
56 |
57 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
58 | authentication(userName: ossrhUsername, password: ossrhPassword)
59 | }
60 |
61 | pom.project {
62 | name 'groovy-html-renderer'
63 | packaging 'jar'
64 | description 'in-code html5 rendering engine leveraging the groovy syntax'
65 | url 'https://github.com/acavailhez/groovy-html-renderer'
66 |
67 | scm {
68 | connection 'scm:git:git://github.com/acavailhez/groovy-html-renderer'
69 | developerConnection 'scm:git:git@github.com:acavailhez/groovy-html-renderer'
70 | url 'https://github.com/acavailhez/groovy-html-renderer'
71 | }
72 |
73 | licenses {
74 | license {
75 | name 'The Apache License, Version 2.0'
76 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
77 | }
78 | }
79 |
80 | developers {
81 | developer {
82 | id 'acavailhez'
83 | name 'Arnaud CAVAILHEZ'
84 | email 'arnaud@cavailhez.fr'
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | project(':core') {
94 |
95 | archivesBaseName = 'groovy-html-renderer'
96 |
97 | dependencies {
98 | compile 'org.codehaus.groovy:groovy-all:2.4.6'
99 | compile 'log4j:log4j:1.2.17'
100 | compile 'org.apache.commons:commons-lang3:3.0'
101 | compile 'org.jsoup:jsoup:1.8.3' // for html prettyfier
102 | compile 'org.apache.commons:commons-lang3:3.4'
103 |
104 | testCompile project(':tests')
105 | testCompile project(':bootstrap')
106 | testCompile 'org.apache.velocity:velocity:1.7' // for perf comparison
107 | testCompile 'org.apache.velocity:velocity-tools:2.0'
108 |
109 | }
110 | }
111 |
112 | project(':bootstrap') {
113 |
114 | archivesBaseName = 'groovy-html-renderer-bootstrap'
115 |
116 | dependencies {
117 | compile project(':core')
118 |
119 | testCompile project(':tests')
120 | }
121 | }
122 |
123 | project(':resources') {
124 |
125 | archivesBaseName = 'groovy-html-renderer-resources'
126 |
127 | dependencies {
128 | compile project(':core')
129 |
130 | testCompile project(':tests')
131 | }
132 | }
133 |
134 | project(':scripts') {
135 |
136 | dependencies {
137 | compile project(':core')
138 | compile project(':bootstrap')
139 | compile 'org.jodd:jodd:3.4.1'
140 | compile 'org.jodd:jodd-http:3.7'
141 |
142 | testCompile project(':tests')
143 | }
144 | }
145 |
146 | project(':spark') {
147 |
148 | dependencies {
149 | compile project(':core')
150 | compile 'com.sparkjava:spark-core:2.5'
151 |
152 | testCompile project(':tests')
153 | }
154 | }
155 |
156 | project(':spark-demo') {
157 |
158 | dependencies {
159 | compile project(':spark')
160 | compile project(':bootstrap')
161 |
162 | testCompile project(':tests')
163 | }
164 | }
165 |
166 | project(':tests') {
167 |
168 | dependencies {
169 | compile project(':core')
170 |
171 | compile 'junit:junit:4.11'
172 | }
173 | }
--------------------------------------------------------------------------------
/core/README.md:
--------------------------------------------------------------------------------
1 | # groovy-html-renderer-core
2 |
3 | Core feature of groovy-html-renderer
4 |
5 | ## Basic rendering
6 |
7 | To render a simple HTML piece, extend `Html`:
8 |
9 | ```
10 | class BasicHtml extends Html{
11 | @Override
12 | public void build() {
13 | div(class: 'foo bar') {
14 | escape << 'html5 forever <3'
15 | }
16 | }
17 | }
18 |
19 | String html = new BasicHtml().render()
20 | ```
21 |
22 | This will render the compact html:
23 | `html5 forever <3
`
24 |
25 | To render it in a visually appealing way, do:
26 | `String html = new BasicHtml().withStyle(HtmlStyle.PRETTY).render()`
27 |
28 | This will render:
29 |
30 | ```
31 |
32 | html5 forever <3
33 |
34 | ```
35 |
36 | ## Tags
37 |
38 | Html tags are rendered using a groovy closure, so:
39 |
40 | ```
41 | div(attribute:"value",'attribute-two':"value 2"){
42 | html << ' '
43 | div{
44 | escape << 'text to print'
45 | }
46 | }
47 | ```
48 |
49 | Will render:
50 |
51 | ```
52 |
53 |
54 |
55 | text to print
56 |
57 |
58 | ```
59 |
60 | ## Full page
61 |
62 | To render a full html page, extend `HtmlPage`:
63 |
64 | ```
65 | class BasicPage extends HtmlPage{
66 |
67 | @Override
68 | protected String title() {
69 | return 'Page about politics'
70 | }
71 |
72 | protected void body() {
73 | div {
74 | escape << "text"
75 | }
76 | }
77 | }
78 | ```
79 |
80 | renders:
81 |
82 | ```
83 |
84 |
85 |
86 |
87 | Page about politics
88 |
89 |
90 |
91 |
92 | text
93 |
94 |
95 | ```
96 |
97 | See `HtmlPage.groovy` for more details on what you can configure
98 |
99 | ## Attempt
100 |
101 | If the generation of your html throws an exception, then all html is lost
102 | For complex pages, you may want to attempt rendering some elements and continue if that element failed, even providing a fallback:
103 |
104 | ```
105 | div {
106 | attempt {
107 | div {
108 | escape << 'text'
109 | throw new Exception("oups")
110 | }
111 | } { Throwable t ->
112 | span {
113 | escape << 'failed with:' << t.getMessage()
114 | }
115 | }
116 | }
117 | ```
118 |
119 | Will render:
120 |
121 | ```
122 |
123 | failed with:oups
124 |
125 | ```
126 |
127 | ## Scope
128 |
129 | You can store some variables at the current DOM element level to retrieve later
130 |
131 | ```
132 | div {
133 | scope.put("key",1)
134 | div {
135 | int value = scope.get("key")
136 | escape << 'text'
137 | }
138 | }
139 | ```
140 |
141 | This can be useful when building traits that add new behavior
142 |
143 | ## HtmlFragment
144 |
145 | Html fragments are snippet of html with some added features:
146 |
147 | ### Javascript
148 |
149 | It's convenient to write small javascript snippets where they are relevant, only to have them be deferred to the end of the document,
150 |
151 | ```
152 | div{
153 | div{
154 | escape << 'text to print'
155 | js << 'execute();'
156 | }
157 | }
158 | ```
159 |
160 | The javascript will be collected separately.
161 | In a `HtmlPage`, this javascript will be appended to a `'
41 | }
42 | }
43 |
44 | // Render to be sent via API
45 | // scopes the different statements
46 | public String getRawJavascript() {
47 | StringBuilder js = new StringBuilder()
48 | for (String statement : getCurrentScope()) {
49 | js << 'try{'
50 | js << statement
51 | js << '}catch(e){console.log(e)}'
52 | }
53 | return js.toString()
54 | }
55 |
56 | @Override
57 | void rollbackCurrentScope() {
58 | super.rollbackCurrentScope()
59 | }
60 |
61 | @Override
62 | void prepareForNewScope() {
63 | super.prepareForNewScope()
64 | }
65 |
66 | @Override
67 | void commitToPreviousScope() {
68 | super.commitToPreviousScope()
69 | }
70 | }
--------------------------------------------------------------------------------
/core/src/acavailhez/html/builder/EscapedHtmlBuilder.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.builder
2 |
3 | import acavailhez.html.utils.HtmlUtils
4 | import groovy.transform.CompileStatic
5 |
6 | // append string directly
7 | @CompileStatic
8 | class EscapedHtmlBuilder {
9 |
10 | private StringBuilder stringBuilder
11 |
12 | public EscapedHtmlBuilder() {
13 | // Nothing to do
14 | }
15 |
16 | void setStringBuilder(StringBuilder stringBuilder) {
17 | this.stringBuilder = stringBuilder
18 | }
19 |
20 | public EscapedHtmlBuilder leftShift(def s) {
21 | if (s == null) {
22 | return this
23 | }
24 | stringBuilder << HtmlUtils.escapeTextToHtml(s)
25 | return this
26 | }
27 |
28 | @Override
29 | public String toString() {
30 | return stringBuilder.toString()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/builder/HtmlBuilder.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.builder
2 |
3 | import acavailhez.html.scope.HtmlScopeListener
4 | import groovy.transform.CompileStatic
5 |
6 | @CompileStatic
7 | class HtmlBuilder extends HtmlScopeListener {
8 |
9 | private final RawHtmlBuilder html = new RawHtmlBuilder()
10 | private final EscapedHtmlBuilder escape = new EscapedHtmlBuilder()
11 |
12 | public HtmlBuilder() {
13 | html.stringBuilder = getCurrentScope()
14 | escape.stringBuilder = getCurrentScope()
15 | }
16 |
17 | public RawHtmlBuilder getHtml() {
18 | return html
19 | }
20 |
21 | public EscapedHtmlBuilder getEscape() {
22 | return escape
23 | }
24 |
25 | @Override
26 | protected StringBuilder create() {
27 | StringBuilder stringBuilder = new StringBuilder()
28 | return stringBuilder
29 | }
30 |
31 | @Override
32 | protected StringBuilder merge(StringBuilder toMergeInto, StringBuilder objectToMerge) {
33 | return toMergeInto.append(objectToMerge)
34 | }
35 |
36 | @Override
37 | protected void afterScopeChanged(StringBuilder previousScope, StringBuilder newScope) {
38 | if (html) {
39 | html.stringBuilder = newScope
40 | }
41 | if (escape) {
42 | escape.stringBuilder = newScope
43 | }
44 | }
45 |
46 | public String getRawHtml() {
47 | return getCurrentScope().toString()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/builder/RawHtmlBuilder.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.builder
2 | // append string directly
3 | class RawHtmlBuilder {
4 |
5 | private StringBuilder stringBuilder
6 |
7 | public RawHtmlBuilder() {
8 | // Nothing to do
9 | }
10 |
11 | void setStringBuilder(StringBuilder stringBuilder) {
12 | this.stringBuilder = stringBuilder
13 | }
14 |
15 | public RawHtmlBuilder leftShift(def s) {
16 | if (s == null) {
17 | return this
18 | }
19 | stringBuilder << s.toString()
20 | return this
21 | }
22 |
23 | // unprotected (but faster) append
24 | public void append(String string) {
25 | stringBuilder.append(string)
26 | }
27 |
28 | public StringBuilder getStringBuilder(){
29 | return stringBuilder
30 | }
31 |
32 | @Override
33 | public String toString() {
34 | return stringBuilder.toString()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/internal/HtmlEngine.java:
--------------------------------------------------------------------------------
1 | package acavailhez.html.internal;
2 |
3 | import acavailhez.html.utils.HtmlUtils;
4 |
5 | import java.util.Map;
6 | import java.util.Set;
7 |
8 | // Everything heavy is coded in java for performances
9 | public class HtmlEngine {
10 |
11 | public static void openTag(StringBuilder html, String tagName, Map attrs) {
12 | html.append("<");
13 | html.append(tagName);
14 | if (attrs != null) {
15 | Set keyObjects = attrs.keySet();
16 | for (Object keyO : keyObjects) {
17 | String key = keyO.toString();
18 | Object value = attrs.get(key);
19 | if (value != null) {
20 | String escaped = HtmlUtils.escapeHtmlAttribute(value);
21 | html.append(" ");
22 | html.append(key);
23 | html.append("=\"");
24 | html.append(escaped);
25 | html.append("\"");
26 | }
27 | }
28 | }
29 | html.append(">");
30 | }
31 |
32 | public static void closeTag(StringBuilder html, String tagName) {
33 | html.append("");
34 | html.append(tagName);
35 | html.append(">");
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/scope/HtmlMapDomScoped.java:
--------------------------------------------------------------------------------
1 | package acavailhez.html.scope;
2 |
3 | import acavailhez.optget.OptGet;
4 |
5 | import java.util.HashMap;
6 | import java.util.Map;
7 |
8 | // Keeps track of properties at the current scope level
9 | // eg:
10 | // div{
11 | // scope.put("current-color","red")
12 | // scope.get("current-color") >> red
13 | // div{
14 | // scope.put("current-color","blue")
15 | // scope.get("current-color") >> blue
16 | // }
17 | // scope.get("current-color") >> red
18 | // }
19 | public class HtmlMapDomScoped extends HtmlScopeListener implements OptGet {
20 |
21 |
22 | @Override
23 | protected Map create() {
24 | return new HashMap();
25 | }
26 |
27 | @Override
28 | protected Map merge(Map toMergeInto, Map objectToMerge) {
29 | // Drop all state, do not actually merge
30 | return toMergeInto;
31 | }
32 |
33 | // save something in the current scope
34 | public void put(Object key, Object value) {
35 | getCurrentScope().put(key, value);
36 | }
37 |
38 | @Override
39 | public void onMissingKey(Object key, Class classToCast) {
40 | throw new IllegalArgumentException("Scope does not contain:" + key + " of class:" + classToCast);
41 | }
42 |
43 | @Override
44 | public Object opt(Object key) {
45 | Object value = null;
46 | // look into all maps in stack, and keep the latest (freshest) one
47 | for (Map map : stack) {
48 | if (map.containsKey(key)) {
49 | value = map.get(key);
50 | }
51 | }
52 | return value;
53 | }
54 |
55 | // make calls explicit for groovy classes
56 | public T opt(String key, Class classToCast) {
57 | return OptGet.super.opt(key, classToCast);
58 | }
59 |
60 | public T opt(String key, Class classToCast, T defaultValue) {
61 | return OptGet.super.opt(key, classToCast, defaultValue);
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/scope/HtmlScopable.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.scope
2 |
3 | // Classes implementing HtmlScopable will hold some html state, scoped to a DOM element
4 | interface HtmlScopable {
5 |
6 | // Save the current scope and prepareForNewScope for a new one
7 | void prepareForNewScope()
8 |
9 | // Commit the current scope and re-scope down one level
10 | void commitToPreviousScope()
11 |
12 | // Abandon changes to the current scope
13 | void rollbackCurrentScope()
14 | }
15 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/scope/HtmlScope.java:
--------------------------------------------------------------------------------
1 | package acavailhez.html.scope;
2 |
3 | import acavailhez.optget.OptGet;
4 |
5 | import java.util.HashMap;
6 | import java.util.LinkedList;
7 | import java.util.List;
8 | import java.util.Map;
9 |
10 | // Scoped state of the html
11 | public class HtmlScope implements OptGet {
12 |
13 | private List> stack = new LinkedList<>();
14 | private List scopables = new LinkedList<>();
15 |
16 | public void addScopable(HtmlScopable scopable) {
17 | scopables.add(scopable);
18 | }
19 |
20 | // save something in the current scope
21 | public void put(String key, Object value) {
22 | stack.get(stack.size() - 1).put(key, value);
23 | }
24 |
25 | @Override
26 | public void onMissingKey(Object key, Class classToCast) {
27 | throw new IllegalArgumentException("Scope does not contain:" + key + " of class:" + classToCast);
28 | }
29 |
30 | @Override
31 | public Object opt(Object key) {
32 | Object value = null;
33 | for (Map map : stack) {
34 | if (map.containsKey(key)) {
35 | value = map.get(key);
36 | }
37 | }
38 | return value;
39 | }
40 |
41 | public void prepareForNewScope() {
42 | for (HtmlScopable scopable : scopables) {
43 | scopable.prepareForNewScope();
44 | }
45 | stack.add(new HashMap<>());
46 | }
47 |
48 | public void commitToPreviousScope() {
49 | for (HtmlScopable scopable : scopables) {
50 | scopable.commitToPreviousScope();
51 | }
52 | stack.remove(stack.size() - 1); // pop
53 | }
54 |
55 | public void rollbackCurrentScope() {
56 | for (HtmlScopable scopable : scopables) {
57 | scopable.rollbackCurrentScope();
58 | }
59 | stack.remove(stack.size() - 1); // pop
60 | }
61 |
62 | // make calls explicit for groovy classes
63 | public T opt(String key, Class classToCast) {
64 | return OptGet.super.opt(key, classToCast);
65 | }
66 |
67 | public T opt(String key, Class classToCast, T defaultValue) {
68 | return OptGet.super.opt(key, classToCast, defaultValue);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/scope/HtmlScopeListener.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.scope
2 |
3 | import groovy.transform.CompileStatic
4 |
5 | // HtmlScoped will hold an SCOPED_OBJECT instance for each DOM tag open/close
6 | //
7 | // When a new DOM tag open, a new SCOPED_OBJECT is created
8 | // when the DOM tag closes, this object is committed and saved with all the other committed SCOPED_OBJECT
9 | // it is possible to rollback the current scope and drop all the SCOPED_OBJECT that were committed during the current DOM scope
10 | @CompileStatic
11 | abstract class HtmlScopeListener {
12 |
13 | // Current stack of scoped objects, keeping track of what has been open for all current open DOM levels
14 | // If the scope currently looks like this (<< means "create new scoped object):
15 | // {
16 | // << A
17 | // {
18 | // << B
19 | // {
20 | // << C
21 | // }
22 | // {
23 | // << D
24 | // rollback
25 | // }
26 | // {
27 | // << E
28 | //
29 | // then the stack will look like:
30 | // [
31 | // A,
32 | // B+C
33 | // E
34 | // ]
35 | //
36 | // where B+C have been merged together, and D is gone
37 | //
38 | protected List stack = [];
39 |
40 | // Create a new scoped object for the current scope
41 | protected abstract SCOPED_OBJECT create();
42 |
43 | // Merge the current scope (most likely in the previous scope)
44 | protected abstract SCOPED_OBJECT merge(SCOPED_OBJECT toMergeInto, SCOPED_OBJECT objectToMerge);
45 |
46 | protected HtmlScopeListener() {
47 | // Open the "global" scope
48 | prepareForNewScope();
49 | }
50 |
51 | public SCOPED_OBJECT getCurrentScope() {
52 | if (stack.isEmpty()) {
53 | throw new IllegalStateException("stack is empty, this probably means that it was committed or rolled back one time too many")
54 | }
55 | return stack.get(stack.size() - 1);
56 | }
57 |
58 | private SCOPED_OBJECT popCurrentScope() {
59 | if (stack.isEmpty()) {
60 | throw new IllegalStateException("stack is empty, this probably means that it was committed or rolled back one time too many")
61 | }
62 | return stack.remove(stack.size() - 1);
63 | }
64 |
65 | public void prepareForNewScope() {
66 | SCOPED_OBJECT currentScope = null;
67 | if (stack.size() > 0) {
68 | currentScope = getCurrentScope()
69 | }
70 | SCOPED_OBJECT newScope = create()
71 | stack.add(newScope)
72 | afterScopeChanged(currentScope, newScope)
73 | }
74 |
75 | public void commitToPreviousScope() {
76 | SCOPED_OBJECT currentScope = popCurrentScope();
77 |
78 | // Merge the current scope into the previous one
79 | SCOPED_OBJECT previousScope = getCurrentScope();
80 | SCOPED_OBJECT merged = merge(previousScope, currentScope)
81 |
82 | // Replace the previous scope by the merged object
83 | if (merged != previousScope) {
84 | popCurrentScope()
85 | stack.addAll(merged)
86 | }
87 | afterScopeChanged(currentScope, previousScope)
88 | }
89 |
90 | public void rollbackCurrentScope() {
91 | SCOPED_OBJECT currentScope = popCurrentScope();
92 | afterScopeChanged(currentScope, getCurrentScope())
93 | }
94 |
95 | protected void afterScopeChanged(SCOPED_OBJECT previousScope, SCOPED_OBJECT newScope) {
96 | // override if you want to listen to scope change events
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/traits/AttemptTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.traits
2 |
3 | import groovy.transform.CompileStatic
4 |
5 | @CompileStatic
6 | trait AttemptTrait extends HtmlTrait {
7 |
8 | // Attempt to execute the code, if fails, rollsback and execute the fallback
9 | void attempt(Closure main, Closure fallback) {
10 | prepareForNewScope()
11 | try {
12 | main()
13 | commitToPreviousScope()
14 | }
15 | catch (Throwable e) {
16 | rollbackCurrentScope()
17 | if (fallback) {
18 | prepareForNewScope()
19 | try {
20 | // This might throw
21 | fallback(e)
22 | }
23 | finally {
24 | commitToPreviousScope()
25 | }
26 | }
27 | }
28 | }
29 |
30 | // Shortcut
31 | void attempt(Closure main) {
32 | attempt(main, null)
33 | }
34 |
35 |
36 | }
37 |
38 |
39 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/traits/CaptureTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.traits
2 |
3 | import groovy.transform.CompileStatic
4 |
5 | @CompileStatic
6 | trait CaptureTrait extends HtmlTrait {
7 |
8 | // Runs the code but does not write to the main html
9 | // Instead you get the html in a String
10 | // This loses all other state (such as defers or javascript)
11 | String capture(Closure capturable) {
12 | prepareForNewScope()
13 | capturable()
14 | String html = getRootHtmlBuilder().html.toString()
15 | rollbackCurrentScope()
16 | return html
17 | }
18 |
19 | }
20 |
21 |
22 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/traits/HeadTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.traits
2 |
3 | import groovy.transform.CompileStatic
4 |
5 | @CompileStatic
6 | trait HeadTrait extends HtmlTrait {
7 | abstract void tag(String tag, Map attrs, Closure body)
8 |
9 | void meta(Map attrs) {
10 | tag('meta', attrs, null)
11 | }
12 |
13 | void css(String url) {
14 | tag('link', [href: url, rel: "stylesheet", type: "text/css"], null)
15 | }
16 |
17 | void javascript(String url) {
18 | tag('script', [src: url], {
19 | html ' '
20 | })
21 | }
22 | }
23 |
24 |
25 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/traits/HtmlFragmentTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.traits
2 |
3 | import groovy.transform.CompileStatic
4 |
5 | @CompileStatic
6 | abstract trait HtmlFragmentTrait {
7 |
8 | abstract void defer(Closure toDefer)
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/traits/HtmlTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.traits
2 |
3 | import acavailhez.html.builder.EscapedHtmlBuilder
4 | import acavailhez.html.builder.HtmlBuilder
5 | import acavailhez.html.builder.RawHtmlBuilder
6 | import acavailhez.html.scope.HtmlMapDomScoped
7 | import acavailhez.html.scope.HtmlScopable
8 | import acavailhez.html.scope.HtmlScope
9 | import acavailhez.html.scope.HtmlScopeListener
10 | import groovy.transform.CompileStatic
11 |
12 | @CompileStatic
13 | abstract trait HtmlTrait {
14 |
15 | abstract void tag(String tag, Map attrs, Closure body)
16 |
17 | abstract HtmlMapDomScoped getScope()
18 |
19 | abstract HtmlBuilder getRootHtmlBuilder()
20 |
21 | abstract RawHtmlBuilder html(Object input)
22 |
23 | abstract RawHtmlBuilder getHtml()
24 |
25 | abstract EscapedHtmlBuilder escape(Object input)
26 |
27 | abstract EscapedHtmlBuilder getEscape()
28 |
29 | // Scope
30 |
31 | abstract void prepareForNewScope();
32 |
33 | abstract void commitToPreviousScope();
34 |
35 | abstract void rollbackCurrentScope();
36 |
37 | abstract void subscribeToScopeChanges(HtmlScopeListener scopable);
38 | }
--------------------------------------------------------------------------------
/core/src/acavailhez/html/traits/ShortcutTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.traits
2 |
3 | import groovy.transform.CompileStatic
4 |
5 |
6 | // Functions to simplify writing groups of tags
7 |
8 | @CompileStatic
9 | trait ShortcutTrait extends HtmlTrait {
10 |
11 | // shortcut for including a simple
12 | // use:
13 | // dl{
14 | // ddt("Title"){
15 | // escape << "content of the dd"
16 | // }
17 | // }
18 | public void ddt(String title, Closure body) {
19 | tag('dt', null) { escape(title) }
20 | tag('dd', null, body)
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/utils/HtmlAttributes.java:
--------------------------------------------------------------------------------
1 | package acavailhez.html.utils;
2 |
3 |
4 | import acavailhez.optget.OptGetMap;
5 |
6 | import java.util.HashMap;
7 | import java.util.Map;
8 |
9 | // Keeps a local copy of a map and adds extra methods
10 | // Two ways to create from an existing map:
11 | // HtmlAttributes.copy() will copy the content of the argument
12 | // HtmlAttributes.wrap() will keep the existing map and modify it
13 | //
14 | // Added methods: get/opt
15 | // - opt(key, String, "default") will get the [key] value or default if not set
16 | // - get(key, String) will get the [key] value and raise a IllegalArgumentException if not present
17 | // - addToClass(String) will append new classes to the existing one (or create one)
18 | public class HtmlAttributes extends OptGetMap {
19 |
20 | private HtmlAttributes(Map map) {
21 | super(map);
22 | }
23 |
24 | public static HtmlAttributes copy(Map map) {
25 | HtmlAttributes attrs = new HtmlAttributes(new HashMap<>());
26 | if (map != null) {
27 | for (Entry entry : map.entrySet()) {
28 | attrs.put(entry.getKey().toString(), entry.getValue());
29 | }
30 | }
31 | return attrs;
32 | }
33 |
34 | public static HtmlAttributes wrap(Map map) {
35 | if (map == null) {
36 | map = new HashMap<>();
37 | }
38 | return new HtmlAttributes(map);
39 | }
40 |
41 | // addToClass(String) will append new classes to the existing one (or create one)
42 | public HtmlAttributes addToClass(String newClass) {
43 | if (newClass == null) {
44 | return this;
45 | }
46 | if (!this.containsKey("class")) {
47 | this.put("class", newClass);
48 | } else {
49 | String classesString = get("class", String.class);
50 | this.put("class", classesString + " " + newClass);
51 | }
52 | return this;
53 | }
54 |
55 | // make call explicit for groovy classes
56 | public T opt(String key, Class classToCast) {
57 | return super.opt(key, classToCast);
58 | }
59 |
60 | public T opt(String key, Class classToCast, T defaultValue) {
61 | return super.opt(key, classToCast, defaultValue);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/core/src/acavailhez/html/utils/HtmlUtils.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.utils
2 |
3 | import groovy.transform.CompileStatic
4 | import org.apache.commons.lang3.StringEscapeUtils
5 | import org.jsoup.Jsoup
6 | import org.jsoup.nodes.Document
7 | import org.jsoup.nodes.Entities
8 | import org.jsoup.parser.Parser
9 |
10 | import java.nio.charset.Charset
11 |
12 | @CompileStatic
13 | class HtmlUtils {
14 |
15 | // Escape the value of a html tag attribute
16 | public static String escapeHtmlAttribute(Object attribute) {
17 | if (attribute == null) {
18 | return ''
19 | }
20 | if (!attribute instanceof String) {
21 | return attribute
22 | }
23 | return (attribute as String).replaceAll('"', '"')
24 | }
25 |
26 | // Escaoe text to be written in html
27 | public static String escapeTextToHtml(Object text) {
28 | if (text == null) {
29 | return ''
30 | }
31 | return StringEscapeUtils.escapeHtml4(text.toString())
32 | }
33 |
34 | public static Document getHtmlDocumentFromSource(String html, String baseUri = '') {
35 | Document doc
36 | if (html.contains(' T cast(Object unknown, Class target) {
10 | if (String.class.isAssignableFrom(target)) {
11 | return (T) castToString(unknown);
12 | } else if (Integer.class.isAssignableFrom(target)) {
13 | return (T) castToInteger(unknown);
14 | } else if (Long.class.isAssignableFrom(target)) {
15 | return (T) castToLong(unknown);
16 | } else if (Float.class.isAssignableFrom(target)) {
17 | return (T) castToFloat(unknown);
18 | } else if (Double.class.isAssignableFrom(target)) {
19 | return (T) castToDouble(unknown);
20 | } else if (Short.class.isAssignableFrom(target)) {
21 | return (T) castToShort(unknown);
22 | } else if (Byte.class.isAssignableFrom(target)) {
23 | return (T) castToByte(unknown);
24 | }else if (target.isEnum()) {
25 | return castToEnum(unknown, target);
26 | } else if (OptGet.class.isAssignableFrom(target)) {
27 | return (T) castToOptGet(unknown);
28 | }
29 | // unknown class, attempt to cast
30 | return (T) unknown;
31 | }
32 |
33 | public static String castToString(Object unknown) {
34 | return unknown.toString();
35 | }
36 |
37 | @SuppressWarnings("unchecked")
38 | public static ENUM castToEnum(Object unknown, Class enumClass) {
39 | if (enumClass.isAssignableFrom(unknown.getClass())) {
40 | return (ENUM) unknown;
41 | }
42 | // try to find the correct enum, ignore case
43 | for (Object enumValue : enumClass.getEnumConstants()) {
44 | if (enumValue.toString().toLowerCase().equals(unknown.toString().toLowerCase())) {
45 | return (ENUM) enumValue;
46 | }
47 | }
48 | return (ENUM) unknown;
49 | }
50 |
51 | public static OptGet castToOptGet(Object unknown) {
52 | if (unknown instanceof OptGet) {
53 | return (OptGet) unknown;
54 | }
55 | if (unknown instanceof Map) {
56 | return new OptGetMap((Map) unknown);
57 | }
58 | // Wrap the object directly, accessing its fields
59 | return new OptGetWrapper(unknown);
60 | }
61 |
62 | public static Integer castToInteger(Object unknown) {
63 | if (unknown instanceof String) {
64 | return Integer.valueOf((String) unknown);
65 | }
66 | if (unknown instanceof Number) {
67 | return ((Number) unknown).intValue();
68 | }
69 | return (Integer) unknown;
70 | }
71 |
72 | public static Long castToLong(Object unknown) {
73 | if (unknown instanceof String) {
74 | return Long.valueOf((String) unknown);
75 | }
76 | if (unknown instanceof Number) {
77 | return ((Number) unknown).longValue();
78 | }
79 | return (Long) unknown;
80 | }
81 |
82 | public static Short castToShort(Object unknown) {
83 | if (unknown instanceof String) {
84 | return Short.valueOf((String) unknown);
85 | }
86 | if (unknown instanceof Number) {
87 | return ((Number) unknown).shortValue();
88 | }
89 | return (Short) unknown;
90 | }
91 |
92 | public static Float castToFloat(Object unknown) {
93 | if (unknown instanceof String) {
94 | return Float.valueOf((String) unknown);
95 | }
96 | if (unknown instanceof Number) {
97 | return ((Number) unknown).floatValue();
98 | }
99 | return (Float) unknown;
100 | }
101 |
102 | public static Double castToDouble(Object unknown) {
103 | if (unknown instanceof String) {
104 | return Double.valueOf((String) unknown);
105 | }
106 | if (unknown instanceof Number) {
107 | return ((Number) unknown).doubleValue();
108 | }
109 | return (Double) unknown;
110 | }
111 |
112 | public static Byte castToByte(Object unknown) {
113 | if (unknown instanceof String) {
114 | return Byte.valueOf((String) unknown);
115 | }
116 | if (unknown instanceof Number) {
117 | return ((Number) unknown).byteValue();
118 | }
119 | return (Byte) unknown;
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/core/src/acavailhez/optget/OptGet.java:
--------------------------------------------------------------------------------
1 | package acavailhez.optget;
2 |
3 | public interface OptGet {
4 |
5 | void onMissingKey(Object key, Class classToCast);
6 |
7 | Object opt(Object key);
8 |
9 | // Accessors
10 | default T opt(String key, Class classToCast) {
11 | return opt(key, classToCast, null);
12 | }
13 |
14 | @SuppressWarnings("unchecked")
15 | default T opt(String key, Class classToCast, T defaultValue) {
16 | Object nonCast = recursiveOpt(key);
17 | if (nonCast == null) {
18 | return defaultValue;
19 | }
20 | return CastUtils.cast(nonCast, classToCast);
21 |
22 | }
23 |
24 | // Will transform getString("key.sub") to getGetOpt("key").getString("sub")
25 | default Object recursiveOpt(Object key) {
26 | Object value = opt(key);
27 | if (value != null) {
28 | return value;
29 | }
30 | // Search recursively in the underlying opt object
31 | String stringKey = key.toString();
32 | String[] subkeys = stringKey.split("\\.");
33 | OptGet optGet = this;
34 | for (int i = 0; i < subkeys.length - 1; i++) {
35 | optGet = optGet.opt(subkeys[i], OptGet.class);
36 | if (optGet == null) {
37 | return null;
38 | }
39 | }
40 | return optGet.opt(subkeys[subkeys.length - 1]);
41 | }
42 |
43 | default Object get(String key) {
44 | return get(key, Object.class);
45 | }
46 |
47 | default T get(String key, Class classToCast) throws IllegalArgumentException {
48 | T value = opt(key, classToCast);
49 | if (value == null) {
50 | onMissingKey(key, classToCast);
51 | }
52 | return value;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/core/src/acavailhez/optget/OptGetMap.java:
--------------------------------------------------------------------------------
1 | package acavailhez.optget;
2 |
3 | import groovy.transform.CompileStatic;
4 |
5 | import java.util.Collection;
6 | import java.util.Map;
7 | import java.util.Set;
8 |
9 | // Simple map wrapper exposing GetOpt function
10 | @CompileStatic
11 | public class OptGetMap implements Map, OptGet {
12 |
13 | private final Map map;
14 |
15 | public OptGetMap(Map map) {
16 | this.map = map;
17 | }
18 |
19 | @Override
20 | public void onMissingKey(Object key, Class classToCast) {
21 | throw new IllegalArgumentException("Missing key:" + key + " of class:" + classToCast);
22 | }
23 |
24 | @Override
25 | public Object opt(Object key) {
26 | return map.get(key);
27 | }
28 |
29 | // Implementation of Map
30 |
31 | @Override
32 | public int size() {
33 | return map.size();
34 | }
35 |
36 | @Override
37 | public boolean isEmpty() {
38 | return map.isEmpty();
39 | }
40 |
41 | @Override
42 | public boolean containsKey(Object key) {
43 | return map.containsKey(key);
44 | }
45 |
46 | @Override
47 | public boolean containsValue(Object value) {
48 | return map.containsValue(value);
49 | }
50 |
51 | @Override
52 | public Object get(Object key) {
53 | return map.get(key);
54 | }
55 |
56 | @Override
57 | public Object put(String key, Object value) {
58 | return map.put(key, value);
59 | }
60 |
61 | @Override
62 | public Object remove(Object key) {
63 | return map.remove(key);
64 | }
65 |
66 | @Override
67 | public void putAll(Map extends String, ?> m) {
68 | map.putAll(m);
69 | }
70 |
71 | @Override
72 | public void clear() {
73 | map.clear();
74 | }
75 |
76 | @Override
77 | public Set keySet() {
78 | return map.keySet();
79 | }
80 |
81 | @Override
82 | public Collection values() {
83 | return map.values();
84 | }
85 |
86 | @Override
87 | public Set> entrySet() {
88 | return map.entrySet();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/core/src/acavailhez/optget/OptGetWrapper.java:
--------------------------------------------------------------------------------
1 | package acavailhez.optget;
2 |
3 | import org.apache.commons.lang3.text.WordUtils;
4 |
5 | import java.lang.reflect.Field;
6 | import java.lang.reflect.InvocationTargetException;
7 | import java.lang.reflect.Method;
8 | import java.lang.reflect.Modifier;
9 |
10 | // Wrap an object to use its getters and properties as optget members
11 | public class OptGetWrapper implements OptGet {
12 |
13 | private final Object wrapped;
14 |
15 | public OptGetWrapper(Object toWrap) {
16 | if (toWrap == null) {
17 | throw new RuntimeException("cannot wrap null object");
18 | }
19 | this.wrapped = toWrap;
20 | }
21 |
22 | @Override
23 | public void onMissingKey(Object key, Class classToCast) {
24 | throw new IllegalArgumentException("Missing key:" + key + " of class:" + classToCast);
25 | }
26 |
27 | @Override
28 | public Object opt(Object key) {
29 | // try to find a public getter matching the key
30 | String getterName = "get" + WordUtils.capitalize(key.toString());
31 | try {
32 | Method getter = wrapped.getClass().getDeclaredMethod(getterName);
33 | if (Modifier.isPublic(getter.getModifiers())) {
34 | return getter.invoke(wrapped);
35 | }
36 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
37 | // expected if the method does not exist
38 | }
39 |
40 | // try to find a public field matching the key
41 | try {
42 | Field field = wrapped.getClass().getField(key.toString());
43 | if (Modifier.isPublic(field.getModifiers())) {
44 | return field.get(wrapped);
45 | }
46 | } catch (NoSuchFieldException | IllegalAccessException ex) {
47 | // expected if the field does not exist
48 | }
49 |
50 | // could not find anything
51 | return null;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/core/test-resources/grails.vm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | The Grails Framework
13 |
14 |
29 |
30 |
31 |
32 |
Fork me on Github
33 |
34 |
35 |
54 |
55 |
56 |
81 | A powerful Groovy-based web application framework for the JVM
82 |
83 |
84 | Grails is a powerful web framework, for the Java platform
85 | aimed at multiplying developers’ productivity thanks to a Convention-over-Configuration, sensible defaults
86 | and opinionated APIs. It integrates smoothly with the JVM, allowing you to be immediately productive whilst
87 | providing powerful features, including integrated ORM, Domain-Specific Languages ,
88 | runtime and compile-time meta-programming and Asynchronous programming.
89 |
90 |
91 |
92 |
93 |
94 | Flat learning curve
95 |
96 | Convention-over-Configuration, sensible defaults, opinionated APIS and the Groovy language combine to make Grails easy to learn for Java developers
97 |
98 |
99 |
100 | Smooth Java integration
101 |
102 |
103 | Seamlessly and transparently integrates and interoperates with Java, the JVM and existing Java EE containers
104 |
105 |
106 |
107 |
108 | Plugins
109 |
110 |
111 | Build plugins that extend and enhance Grails or reuse an existing plugin already published by the
112 | vibrant plugin community!
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | Powerful features
121 |
122 |
123 | Integrated ORM / NoSQL support, a powerful view technology, plugins, and Spring-powered dependency injection. All you need to build modern web applications.
124 |
125 |
126 |
127 |
128 | Domain-Specific Languages
129 |
130 |
131 | Expressive Domain Specific Languages (DSLs) used for validation, querying, markup rendering etc.
132 |
133 |
134 |
135 |
136 | IDE Support
137 |
138 |
139 | Great support in IDEs and text editors such as Intellij IDEA, Eclipse, Sublime, Textmate etc.
140 |
141 |
142 |
143 |
144 |
145 |
Groovy and Grails events you shouldn't miss!
April 8-9, 2016
146 |
147 | Greach, the Spanish gathering of enthusiasts of Groovy, Grails, Griffon, Gradle, Spock, Vert.x, Gaelyk,
148 | and many more. With inspirational talks from the makers and users of these projects, hands-on workshops with the rock stars,
149 | join the 150+ attendees, designers, students, designers, the best professionals together in a great atmosphere.
150 |
151 |
June 1-3, 2016
152 |
153 | Groovy, Grails and the related technologies have seen astounding growth in interest and adoption the past
154 | few years, and with good reason. To spread the word even more we have created GR8Conf.
155 |
156 |
157 | GR8Conf is an independent, affordable series of conferences.
158 | It's dedicated to the technologies in the Groovy ecosystem.
159 |
160 |
July 27-29, 2016
161 |
162 | Groovy, Grails and the related technologies have seen astounding growth in interest and adoption the past
163 | few years, and with good reason. To spread the word even more we have created GR8Conf.
164 |
165 |
166 | GR8Conf is an independent, affordable series of conferences.
167 | It's dedicated to the technologies in the Groovy ecosystem.
168 |
169 |
For more events see the Events page
170 |
171 |
172 |
They all use Groovy!
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
243 |
244 |
245 |
246 |
247 |
248 |
--------------------------------------------------------------------------------
/core/test-resources/modal.vm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 | #if( $content )
10 | #foreach( $foo in [1..5] )
11 |
$content
12 | #end
13 | #end
14 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/core/test/acavailhez/html/AttemptTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class AttemptTests extends AbstractTests {
7 |
8 | @Test
9 | public void testSuccess() throws Exception {
10 | String html = (new Html() {
11 | @Override
12 | public void build() {
13 | div {
14 | attempt {
15 | div {
16 | escape << 'text'
17 | }
18 | }
19 | }
20 | }
21 | }).getRawHtml()
22 |
23 | assert renderEquals(html, "")
24 | }
25 |
26 | @Test
27 | public void testSuccessWithCatch() throws Exception {
28 | String html = (new Html() {
29 | @Override
30 | public void build() {
31 | attempt {
32 | div {
33 | attempt {
34 | div {
35 | escape << 'text'
36 | }
37 | }
38 | }
39 | } { Throwable t ->
40 | span {
41 | escape << 'failed with:' << t.getMessage()
42 | }
43 | }
44 | }
45 | }).getRawHtml()
46 |
47 | assert renderEquals(html, "")
48 | }
49 |
50 | @Test
51 | public void testError() throws Exception {
52 | String html = (new Html() {
53 | @Override
54 | public void build() {
55 | div {
56 | attempt {
57 | div {
58 | escape << 'text'
59 | throw new Exception("oups")
60 | }
61 | }
62 | }
63 | }
64 | }).getRawHtml()
65 |
66 | assert renderEquals(html, "
")
67 | }
68 |
69 | @Test
70 | public void testErrorDoNotKeepDefers() throws Exception {
71 | HtmlFragment fragment = (new HtmlFragment() {
72 |
73 | protected void build() {
74 | div {
75 | escape << "text"
76 | attempt {
77 | defer {
78 | div(class: 'modal') {
79 | escape << 'deferred'
80 | }
81 | }
82 | js << 'executeModal();'
83 | div{
84 | escape << 'text'
85 | }
86 | div {
87 | escape << 'text'
88 | throw new Exception("oups")
89 | }
90 | }
91 | }
92 | }
93 |
94 | }).withStyle(HtmlStyle.PRETTY)
95 |
96 | assert renderEquals(fragment.getRawHtml(), '''
97 | text
98 | ''')
99 | assert renderEquals(fragment.getRawJavascript(), "")
100 | assert renderEquals(fragment.getRawDeferredHtml(), "")
101 | }
102 |
103 | @Test
104 | public void testErrorWithCatch() throws Exception {
105 | String html = (new Html() {
106 | @Override
107 | public void build() {
108 | div {
109 | attempt {
110 | div {
111 | escape << 'text'
112 | throw new Exception("oups")
113 | }
114 | } { Throwable t ->
115 | span {
116 | escape << 'failed with:' << t.getMessage()
117 | }
118 | }
119 | }
120 |
121 | }
122 | }).getRawHtml()
123 |
124 | assert renderEquals(html, "failed with:oups
")
125 | }
126 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/CaptureTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class CaptureTests extends AbstractTests {
7 |
8 | @Test
9 | public void testSuccess() throws Exception {
10 | String captured
11 | String html = (new Html() {
12 | @Override
13 | public void build() {
14 | div {
15 | captured = capture {
16 | span {
17 | escape << 'text'
18 | }
19 | }
20 | }
21 | }
22 | }).getRawHtml()
23 | assert html == "
"
24 | assert captured == "text "
25 | }
26 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/EscapeTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class EscapeTests extends AbstractTests{
7 |
8 | @Test
9 | public void testNull() throws Exception {
10 | String html = (new Html() {
11 | @Override
12 | public void build() {
13 | div {
14 | escape << null
15 | }
16 | }
17 | }).getRawHtml()
18 |
19 | assert html == '
'
20 | }
21 |
22 | @Test
23 | public void testString() throws Exception {
24 | String html = (new Html() {
25 | @Override
26 | public void build() {
27 | div {
28 | escape << "Test String" << " String2"
29 | }
30 | }
31 | }).getRawHtml()
32 |
33 | assert html == 'Test String String2
'
34 | }
35 |
36 | @Test
37 | public void testObject() throws Exception {
38 | String html = (new Html() {
39 | @Override
40 | public void build() {
41 | div {
42 | escape << [a: "b"]
43 | }
44 | }
45 | }).getRawHtml()
46 |
47 | assert html == '[a:b]
'
48 | }
49 |
50 | @Test
51 | public void testEscape() throws Exception {
52 | String html = (new Html() {
53 | @Override
54 | public void build() {
55 | div {
56 | escape << "a < b => "
57 | }
58 | }
59 | }).getRawHtml()
60 |
61 | assert html == 'a < b =>
'
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/FragmentTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class FragmentTests extends AbstractTests {
7 |
8 |
9 | @Test
10 | public void testJavascript() throws Exception {
11 | HtmlFragment fragment = (new HtmlFragment() {
12 |
13 | protected void build() {
14 | div {
15 | escape << "text"
16 | js << 'var i=0;'
17 | }
18 | }
19 |
20 | }).withStyle(HtmlStyle.PRETTY)
21 |
22 | assert renderEquals(fragment.getRawHtml(), '''
23 | text
24 | ''')
25 | assert renderEquals(fragment.getRawJavascript(), '''
26 | try{var i=0;}catch(e){console.log(e)}''')
27 | }
28 |
29 | @Test
30 | public void testJavascriptDefer() throws Exception {
31 | HtmlFragment fragment = (new HtmlFragment() {
32 |
33 | protected void build() {
34 | div {
35 | escape << "text"
36 | defer {
37 | div(class: 'modal') {
38 | escape << 'deferred'
39 | }
40 | }
41 | js << 'var i=0;'
42 | div{
43 |
44 | }
45 | }
46 | }
47 |
48 | }).withStyle(HtmlStyle.PRETTY)
49 |
50 | assert renderEquals(fragment.getRawHtml(), '''
51 |
52 | ''')
53 | assert renderEquals(fragment.getRawJavascript(), '''
54 | try{var i=0;}catch(e){console.log(e)}''')
55 |
56 | assert renderEquals(fragment.getRawDeferredHtml(), '''
57 | deferred
''')
58 | }
59 |
60 | @Test
61 | public void testDeferHtml() throws Exception {
62 | HtmlFragment fragment = (new HtmlFragment() {
63 |
64 | protected void build() {
65 | div {
66 | escape << "text"
67 | defer {
68 | div(class: 'modal') {
69 | escape << 'deferred'
70 | }
71 | }
72 | }
73 | }
74 |
75 | }).withStyle(HtmlStyle.PRETTY)
76 |
77 | assert renderEquals(fragment.getRawHtml(), '''
78 | text
79 | ''')
80 | assert renderEquals(fragment.getRawDeferredHtml(), '''
81 | deferred
''')
82 | }
83 |
84 | @Test
85 | public void testDeferHtml2() throws Exception {
86 | HtmlFragment fragment = (new HtmlFragment() {
87 |
88 | protected void build() {
89 | div {
90 | escape << "text"
91 | defer {
92 | div(class: 'modal') {
93 | escape << 'deferred'
94 | }
95 | }
96 | div{}
97 | }
98 | }
99 |
100 | }).withStyle(HtmlStyle.PRETTY)
101 |
102 | assert renderEquals(fragment.getRawHtml(), '''
103 |
104 | ''')
105 | assert renderEquals(fragment.getRawDeferredHtml(), '''
106 | deferred
''')
107 | }
108 |
109 | @Test
110 | public void testInsert() throws Exception {
111 | HtmlFragment fragment = (new HtmlFragment() {
112 |
113 | protected void build() {
114 | div {
115 | insert(new HtmlFragment() {
116 | @Override
117 | protected void build() {
118 | div {
119 | escape << "fragment"
120 | }
121 | defer {
122 | div(class: 'modal') {
123 | escape << 'deferred'
124 | }
125 | }
126 | }
127 | }, HtmlRenderMode.IMMEDIATE)
128 | escape << "text"
129 | }
130 | }
131 |
132 | }).withStyle(HtmlStyle.PRETTY)
133 |
134 | assert renderEquals(fragment.getRawHtml(), '''
135 |
136 |
137 | fragment
138 |
text
139 |
140 | ''')
141 | assert renderEquals(fragment.getRawDeferredHtml(), '''
142 | deferred
''')
143 | }
144 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/PageTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class PageTests extends AbstractTests {
7 |
8 |
9 | @Test
10 | public void testMinimal() throws Exception {
11 | String html = (new HtmlPage() {
12 |
13 | protected String title() {
14 | return 'Page'
15 | }
16 |
17 | protected void body() {
18 | div {
19 | escape << "text"
20 | }
21 | }
22 |
23 | }).withStyle(HtmlStyle.PRETTY).getRawHtml()
24 |
25 | assert renderEquals(html, '''
26 |
27 |
28 |
29 |
30 | Page
31 |
32 |
33 |
34 |
35 | text
36 |
37 |
38 | ''')
39 | }
40 |
41 | @Test
42 | public void testWithHeadOptions() throws Exception {
43 | String html = (new HtmlPage() {
44 |
45 | protected String description() {
46 | return 'Page about "interesting" things'
47 | }
48 |
49 | protected String favicon() {
50 | return '/favicon.png'
51 | }
52 |
53 |
54 | protected String viewport() {
55 | return null // remove viewport
56 | }
57 |
58 | protected String title() {
59 | return 'Page'
60 | }
61 |
62 | protected void head() {
63 | meta(property: "og:type", content: "website")
64 | css('https://cdn.com/min.css')
65 | javascript('https://cdn.com/min.js')
66 | }
67 |
68 | protected void body() {
69 | div {
70 | escape << "text"
71 | }
72 | }
73 |
74 | }).getRawHtml()
75 |
76 | assert renderEquals(html, '''
77 |
78 |
79 |
80 | Page
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | text
90 |
91 |
92 | ''')
93 | }
94 |
95 | @Test
96 | public void testJavascript() throws Exception {
97 | String html = (new HtmlPage() {
98 |
99 | protected String title() {
100 | return 'Page'
101 | }
102 |
103 | protected void body() {
104 | div {
105 | escape << "text"
106 | js << 'var i=0;'
107 | }
108 | }
109 |
110 | }).withStyle(HtmlStyle.PRETTY).getRawHtml()
111 |
112 | assert renderEquals(html, '''
113 |
114 |
115 |
116 |
117 | Page
118 |
119 |
120 |
121 |
122 | text
123 |
126 |
127 |
128 | ''')
129 | }
130 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/ScopeTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class ScopeTests extends AbstractTests {
7 |
8 | @Test
9 | public void testSameLevel() throws Exception {
10 | String value = null
11 | assert (new Html() {
12 | @Override
13 | public void build() {
14 | div {
15 | scope.put("key", "value")
16 | value = scope.get("key")
17 | }
18 | }
19 | }).render()
20 |
21 | assert value == "value"
22 | }
23 |
24 | @Test
25 | public void testSameLevelAfterStuff() throws Exception {
26 | String value = null
27 | assert (new Html() {
28 | @Override
29 | public void build() {
30 | div {
31 | scope.put("key", "value")
32 | div{
33 |
34 | }
35 | value = scope.get("key")
36 | }
37 | }
38 | }).render()
39 |
40 | assert value == "value"
41 | }
42 |
43 | @Test
44 | public void testAtRoot() throws Exception {
45 | String value = null
46 | assert (new Html() {
47 | @Override
48 | public void build() {
49 | scope.put("key", "value")
50 | div {
51 |
52 | }
53 | value = scope.get("key")
54 | }
55 | }).render()
56 |
57 | assert value == "value"
58 | }
59 |
60 |
61 | @Test
62 | public void testDeeperLevel() throws Exception {
63 | String value = null
64 | assert (new Html() {
65 | @Override
66 | public void build() {
67 | div {
68 | scope.put("key", "value")
69 | div {
70 | value = scope.get("key")
71 | }
72 | }
73 | }
74 | }).render()
75 |
76 | assert value == "value"
77 | }
78 |
79 | @Test
80 | public void testLowerLevel() throws Exception {
81 | String value = null
82 | assert (new Html() {
83 | @Override
84 | public void build() {
85 | div {
86 |
87 | div {
88 | scope.put("key", "value")
89 | }
90 | value = scope.opt("key")
91 | }
92 | }
93 | }).render()
94 |
95 | assert value == null
96 | }
97 |
98 | @Test
99 | public void testOverwriteNewValue() throws Exception {
100 | String value = null
101 | assert (new Html() {
102 | @Override
103 | public void build() {
104 | div {
105 | scope.put("key", "value1")
106 | div {
107 | scope.put("key", "value2")
108 | div {
109 | value = scope.get("key")
110 | }
111 | }
112 |
113 | }
114 | }
115 | }).render()
116 |
117 | assert value == "value2"
118 | }
119 |
120 | @Test
121 | public void testOverwriteKeptOldValue() throws Exception {
122 | String value = null
123 | assert (new Html() {
124 | @Override
125 | public void build() {
126 | div {
127 | scope.put("key", "value1")
128 | div {
129 | scope.put("key", "value2")
130 | div {
131 |
132 | }
133 | }
134 | value = scope.get("key")
135 | }
136 | }
137 | }).render()
138 |
139 | assert value == "value1"
140 | }
141 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/ShortcutTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class ShortcutTests extends AbstractTests {
7 |
8 | @Test
9 | public void testDdt() throws Exception {
10 | String html = (new Html() {
11 | @Override
12 | public void build() {
13 | dl {
14 | ddt("Title") {
15 | escape << "content"
16 | }
17 | ddt("Title2") {
18 | escape << "content2"
19 | }
20 | }
21 | }
22 | }).getRawHtml()
23 |
24 | assert renderEquals(html, '''
25 |
26 | Title
27 | content
28 | Title2
29 | content2
30 |
31 | ''')
32 | }
33 |
34 |
35 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/SimpleTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class SimpleTests extends AbstractTests {
7 |
8 | @Test
9 | public void testEmpty() throws Exception {
10 | assert (new Html() {
11 | @Override
12 | public void build() {
13 |
14 | }
15 | }).getRawHtml() == ""
16 | }
17 |
18 | @Test
19 | public void testEmptyTag() throws Exception {
20 | assert (new Html() {
21 | @Override
22 | public void build() {
23 | tag('br', [:], null)
24 | }
25 | }).getRawHtml() == " "
26 | }
27 |
28 | @Test
29 | public void testDiv() throws Exception {
30 | String html = (new Html() {
31 | @Override
32 | public void build() {
33 | div(class: 'class-of-div') {
34 | escape << 'INSIDE THE DIV'
35 | }
36 | }
37 | }).getRawHtml()
38 |
39 | assert html == 'INSIDE THE DIV
'
40 | }
41 |
42 | @Test
43 | public void testLevel() throws Exception {
44 | String html = (new Html() {
45 | @Override
46 | public void build() {
47 | div(attr: "outter") {
48 | span(attr: "inner") {
49 | escape << 'embed'
50 | }
51 | }
52 | }
53 | }).getRawHtml()
54 |
55 | assert html == 'embed
'
56 | }
57 |
58 | @Test
59 | public void testForgotStream() throws Exception {
60 | String html = (new Html() {
61 | @Override
62 | public void build() {
63 | div {
64 | 'embed'
65 | }
66 | }
67 | }).getRawHtml()
68 |
69 | assert html == '
'
70 | }
71 |
72 | @Test
73 | public void testPretty() throws Exception {
74 | String html = (new Html() {
75 | @Override
76 | public void build() {
77 | div {
78 | span {
79 | p {
80 | escape << 'embed'
81 | }
82 | }
83 | span {
84 | escape << 'embed '
85 | escape << 'several'
86 | }
87 | }
88 | }
89 | }).withStyle(HtmlStyle.PRETTY)
90 | .getRawHtml()
91 |
92 | assert renderEquals(html, '''
93 |
94 |
95 | embed
96 |
97 |
embed several
98 |
99 | ''')
100 | }
101 |
102 |
103 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/TagTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class TagTests extends AbstractTests {
7 |
8 | @Test
9 | public void testEscapeAttribute() throws Exception {
10 | String html = (new Html() {
11 | @Override
12 | public void build() {
13 | div(attribute: 'value value; value-value| value"value') {
14 | escape << 'INSIDE THE DIV'
15 | }
16 | }
17 | }).getRawHtml()
18 |
19 | assert html == 'INSIDE THE DIV
'
20 | }
21 |
22 | @Test
23 | public void testNullAttribute() throws Exception {
24 | String html = (new Html() {
25 | @Override
26 | public void build() {
27 | div(attribute: null) {
28 | escape << 'INSIDE THE DIV'
29 | }
30 | }
31 | }).getRawHtml()
32 |
33 | assert html == 'INSIDE THE DIV
'
34 | }
35 |
36 |
37 | @Test
38 | public void testSeveralAttributes() throws Exception {
39 | String html = (new Html() {
40 | @Override
41 | public void build() {
42 | div(attr1: "one", attr2: "two", 'attr-three': "three") {
43 | escape << 'INSIDE THE DIV'
44 | }
45 | }
46 | }).getRawHtml()
47 |
48 | assert renderEquals(html, 'INSIDE THE DIV
')
49 | }
50 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/performance/ModalHtml.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.performance
2 |
3 | import acavailhez.html.Html
4 | import acavailhez.html.bootstrap.Bootstrap4Color
5 | import acavailhez.html.bootstrap.Bootstrap4Trait
6 | import groovy.transform.CompileStatic
7 |
8 | @CompileStatic
9 | class ModalHtml extends Html implements Bootstrap4Trait {
10 |
11 | private final String title
12 | private final String content
13 |
14 | public ModalHtml(String title, String content) {
15 | this.title = title
16 | this.content = content
17 | }
18 |
19 | @Override
20 | protected void build() {
21 | modal(title: title, closeLabel: "Close") {
22 | if (content) {
23 | for (int i = 0; i < 5; i++) {
24 | p(content)
25 | }
26 | }
27 | } {
28 | button(btn('data-dismiss': 'modal'), "Close")
29 | button(btn(color: Bootstrap4Color.PRIMARY), "Save changes")
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/performance/PerformanceTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.performance
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.apache.log4j.BasicConfigurator
5 | import org.apache.log4j.Logger
6 | import org.apache.velocity.Template
7 | import org.apache.velocity.VelocityContext
8 | import org.apache.velocity.app.VelocityEngine
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 | public class PerformanceTests extends AbstractTests {
13 |
14 | private final static Logger log = Logger.getLogger(PerformanceTests)
15 |
16 | @Before
17 | public void before() {
18 | BasicConfigurator.configure()
19 | }
20 |
21 | @Test
22 | public void test1000BootstrapModals() throws Exception {
23 |
24 | log.info("preheat")
25 |
26 | VelocityEngine ve = new VelocityEngine();
27 | ve.init();
28 |
29 | generateModalHtmlWithVelocity(100, ve)
30 | generateModalHtmlWithGroovyHtmlRenderer(100)
31 |
32 | log.info('run')
33 |
34 | long msVelocity = generateModalHtmlWithVelocity(1000, ve)
35 | long msGroovy = generateModalHtmlWithGroovyHtmlRenderer(1000)
36 |
37 | log.info('velocity:' + msVelocity + 'ms')
38 | log.info('groovy-html-renderer:' + msGroovy + 'ms')
39 | }
40 |
41 | private static long generateModalHtmlWithGroovyHtmlRenderer(int number) {
42 | return run(number) {
43 | new ModalHtml("World", "One <>fine body…").getRawHtml()
44 | }
45 | }
46 |
47 | private static long generateModalHtmlWithVelocity(int number, VelocityEngine ve) {
48 | Template t = ve.getTemplate("./test-resources/modal.vm");
49 | return run(number) {
50 | VelocityContext context = new VelocityContext();
51 | context.put("title", "World")
52 | context.put("closeLabel", "Close")
53 | context.put("content", "One <>fine body…")
54 | StringWriter writer = new StringWriter();
55 | t.merge(context, writer);
56 | writer.toString()
57 | }
58 | }
59 |
60 | @Test
61 | public void test1000GrailsHomepages() throws Exception {
62 | log.info("preheat")
63 |
64 | VelocityEngine ve = new VelocityEngine();
65 | ve.init();
66 |
67 | generateGrailsHomepageWithVelocity(100, ve)
68 | generateGrailsHomepageWithGroovyHtmlRenderer(100)
69 |
70 | log.info('run')
71 |
72 | long msVelocity = generateGrailsHomepageWithVelocity(1000, ve)
73 | long msGroovy = generateGrailsHomepageWithGroovyHtmlRenderer(1000)
74 |
75 |
76 | log.info('velocity:' + msVelocity + 'ms')
77 | log.info('groovy-html-renderer:' + msGroovy + 'ms')
78 |
79 | }
80 |
81 | private static long generateGrailsHomepageWithGroovyHtmlRenderer(int number) {
82 | return run(number) {
83 | new GrailsHomepage().getRawHtml()
84 | }
85 | }
86 |
87 | private static long generateGrailsHomepageWithVelocity(int number, VelocityEngine ve) {
88 | Template t = ve.getTemplate("./test-resources/grails.vm");
89 | return run(number) {
90 | StringWriter writer = new StringWriter();
91 | t.merge(null, writer);
92 | writer.toString()
93 | }
94 | }
95 |
96 | private static long run(int number, Closure runnable) {
97 | long ms = System.currentTimeMillis()
98 | for (int i = 0; i < number; i++) {
99 | runnable(i)
100 | }
101 | return System.currentTimeMillis() - ms
102 | }
103 |
104 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/html/scope/HtmlScopeListenerTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.scope
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class HtmlScopeListenerTests extends AbstractTests {
7 |
8 | static class Listener extends HtmlScopeListener {
9 |
10 | @Override
11 | protected StringBuilder create() {
12 | return new StringBuilder()
13 | }
14 |
15 | @Override
16 | protected StringBuilder merge(StringBuilder toMergeInto, StringBuilder objectToMerge) {
17 | return toMergeInto.append(objectToMerge)
18 | }
19 |
20 | public String getFullString() {
21 | return getCurrentScope().toString()
22 | }
23 | }
24 |
25 | @Test
26 | public void simple() throws Exception {
27 | Listener listener = new Listener()
28 | listener.prepareForNewScope()
29 | assert listener.getCurrentScope() != null
30 | listener.getCurrentScope().append("A")
31 | listener.commitToPreviousScope()
32 | assert listener.getFullString() == "A"
33 | }
34 |
35 | @Test
36 | public void rollback() throws Exception {
37 | Listener listener = new Listener()
38 |
39 | listener.prepareForNewScope()
40 | listener.getCurrentScope().append("A")
41 | listener.rollbackCurrentScope()
42 |
43 | assert listener.getFullString() == ""
44 | }
45 |
46 | @Test
47 | public void twoLevels() throws Exception {
48 | Listener listener = new Listener()
49 |
50 | listener.prepareForNewScope()
51 | listener.getCurrentScope().append("A")
52 | listener.getCurrentScope().append("B")
53 | listener.prepareForNewScope()
54 | listener.getCurrentScope().append("C")
55 | listener.getCurrentScope().append("D")
56 | listener.commitToPreviousScope()
57 | listener.getCurrentScope().append("F")
58 | listener.getCurrentScope().append("E")
59 | listener.commitToPreviousScope()
60 |
61 | assert listener.getFullString() == "ABCDFE"
62 | }
63 |
64 | @Test
65 | public void severalLevelsWithRollback() throws Exception {
66 | Listener listener = new Listener()
67 |
68 | listener.prepareForNewScope()
69 | listener.getCurrentScope().append("A")
70 |
71 | listener.prepareForNewScope()
72 | listener.getCurrentScope().append("B")
73 | listener.commitToPreviousScope()
74 |
75 | listener.prepareForNewScope()
76 | listener.getCurrentScope().append("C")
77 | listener.rollbackCurrentScope()
78 |
79 | listener.prepareForNewScope()
80 | listener.getCurrentScope().append("D")
81 | listener.commitToPreviousScope()
82 |
83 | listener.prepareForNewScope()
84 | listener.getCurrentScope().append("E")
85 | listener.rollbackCurrentScope()
86 |
87 | listener.getCurrentScope().append("F")
88 | listener.commitToPreviousScope()
89 |
90 | assert listener.getFullString() == "ABDF"
91 | }
92 |
93 | @Test
94 | public void rollbackSeveralLevels() throws Exception {
95 | Listener listener = new Listener()
96 |
97 | listener.prepareForNewScope()
98 | listener.getCurrentScope().append("A")
99 |
100 | listener.prepareForNewScope()
101 | listener.getCurrentScope().append("B")
102 |
103 | listener.prepareForNewScope()
104 | listener.getCurrentScope().append("C")
105 | listener.commitToPreviousScope()
106 |
107 | listener.getCurrentScope().append("D")
108 | listener.rollbackCurrentScope()
109 |
110 | listener.getCurrentScope().append("F")
111 | listener.commitToPreviousScope()
112 |
113 | assert listener.getFullString() == "AF"
114 | }
115 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/optget/GetOptTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.optget
2 |
3 | import acavailhez.html.bootstrap.Bootstrap4Color
4 | import acavailhez.html.tests.AbstractTests
5 | import org.junit.Assert
6 | import org.junit.Test
7 |
8 | public class GetOptTests extends AbstractTests {
9 |
10 | @Test
11 | public void testSimple() throws Exception {
12 | OptGetMap map = new OptGetMap([
13 | a: 1,
14 | b: "string",
15 | c: Locale.FRENCH,
16 | d: [
17 | da: 1,
18 | db: [
19 | dba: "two"
20 | ]
21 | ],
22 | e: [1, 2],
23 | f: "12"
24 | ])
25 |
26 | assert map.get('a') == 1
27 | assert map.get('b') == 'string'
28 | assert map.get('c') == Locale.FRENCH
29 | assert map.get('d.da') == 1
30 | assert map.get('d.db.dba') == "two"
31 |
32 | // cast
33 | assert map.get('a', String) == '1'
34 | assert map.get('e', List)[0] == 1
35 | assert map.get('f', Long) == 12L
36 |
37 | // opt
38 | assert map.opt('z') == null
39 | assert map.opt('a.b') == null
40 | assert map.opt('d.da.da') == null
41 |
42 | // default
43 | assert map.opt('z', String, "test") == "test"
44 | }
45 |
46 | @Test
47 | public void cast() {
48 |
49 | Assert.assertEquals("test", CastUtils.cast("test", String.class))
50 | Assert.assertEquals("1", CastUtils.cast(1, String.class))
51 |
52 | Assert.assertEquals(1, CastUtils.cast(1, Integer.class))
53 | Assert.assertEquals(1, CastUtils.cast("1", Integer.class))
54 | Assert.assertEquals(1, CastUtils.cast(1L, Integer.class))
55 |
56 | Assert.assertEquals(Bootstrap4Color.DANGER, CastUtils.cast(Bootstrap4Color.DANGER, Bootstrap4Color.class))
57 | Assert.assertEquals(Bootstrap4Color.DANGER, CastUtils.cast('DANGER', Bootstrap4Color.class))
58 | Assert.assertEquals(Bootstrap4Color.DANGER, CastUtils.cast('danger', Bootstrap4Color.class))
59 |
60 | }
61 | }
--------------------------------------------------------------------------------
/core/test/acavailhez/optget/OptGetWrapperTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.optget
2 |
3 | import acavailhez.html.tests.AbstractTests
4 | import org.junit.Test
5 |
6 | public class OptGetWrapperTests extends AbstractTests {
7 |
8 | public static class Wrapped {
9 | public int publicField = 10;
10 | private int privateField = 20;
11 |
12 | public String getPublicProperty() {
13 | return "publicProperty"
14 | }
15 |
16 | private String getPrivateProperty() {
17 | return "privateProperty"
18 | }
19 |
20 | public Wrapped getNested() {
21 | return new Wrapped()
22 | }
23 | }
24 |
25 | @Test
26 | public void wrap() throws Exception {
27 | OptGetWrapper wrapper = new OptGetWrapper(new Wrapped())
28 |
29 | assert wrapper.get('publicField', Integer) == 10
30 | assert wrapper.get('publicProperty', String) == "publicProperty"
31 |
32 | assert wrapper.opt("missing") == null
33 |
34 | assert wrapper.get("nested.publicField") == 10
35 |
36 | try {
37 | wrapper.get("privateField")
38 | assert false
39 | }
40 | catch (IllegalArgumentException ex) {
41 | }
42 |
43 |
44 | try {
45 | wrapper.get("privateProperty")
46 | assert false
47 | }
48 | catch (IllegalArgumentException ex) {
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/core/test/perf-velocity.vm:
--------------------------------------------------------------------------------
1 |
2 |
3 | #set( $foo = "Velocity" )
4 | Hello $foo World!
5 |
6 |
--------------------------------------------------------------------------------
/resources/README.md:
--------------------------------------------------------------------------------
1 | # groovy-html-renderer-resources
2 |
3 | Handling css and javascript for pages
4 |
5 | ## Overview
6 |
7 | You define a pack of resources and then inject it into your page:
8 |
9 | ```
10 | HtmlResourcesPack pack = new HtmlResourcesPack()
11 | pack.add('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css')
12 | pack.add('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/js/bootstrap.min.js')
13 | .withDisposition(HtmlResourceDisposition.FOOT)
14 |
15 | public class SimplePage extends HtmlPage {
16 |
17 | protected String title() {
18 | return "Lee Sedol beaten by Alpha go"
19 | }
20 |
21 | protected void head() {
22 | resource(bootstrap)
23 | }
24 |
25 | protected void body() {
26 | div {
27 | escape << "text"
28 | }
29 | }
30 | }
31 | ```
32 |
33 | will render
34 |
35 | ```
36 |
37 |
38 |
39 | ...
40 |
41 |
42 |
43 | text
44 |
45 |
46 |
47 | ```
48 |
49 | ## Disposition
50 |
51 | When adding a resource you can specify where it should be rendered
52 |
53 | ```
54 | pack.add('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/js/bootstrap.min.js')
55 | .withDisposition(HtmlResourceDisposition.FOOT)
56 | ```
57 |
58 | will render it before the `` tag.
59 |
60 | By default the disposition is `HEAD`
61 |
62 | ### TODO
63 |
64 | - bundles
65 | - minify
--------------------------------------------------------------------------------
/resources/src/acavailhez/html/resources/HtmlResource.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.resources
2 |
3 | class HtmlResource {
4 |
5 | private String url
6 | private HtmlResourceDisposition disposition = HtmlResourceDisposition.HEAD
7 |
8 | public HtmlResource(String url) {
9 | this.url = url
10 | }
11 |
12 | public HtmlResource withDisposition(HtmlResourceDisposition disposition) {
13 | this.disposition = disposition
14 | return this
15 | }
16 |
17 | // getters
18 | public String getUrl() {
19 | return url
20 | }
21 |
22 | public HtmlResourceDisposition getDisposition() {
23 | return disposition
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/resources/src/acavailhez/html/resources/HtmlResourceDisposition.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.resources
2 |
3 | enum HtmlResourceDisposition {
4 |
5 | HEAD, // inside the tag
6 | FOOT, // just before
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/resources/src/acavailhez/html/resources/HtmlResourcesPack.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.resources
2 |
3 |
4 | class HtmlResourcesPack {
5 |
6 | private final List resources = [];
7 |
8 | public HtmlResource add(String url) {
9 | return add(new HtmlResource(url));
10 | }
11 |
12 | public HtmlResource add(HtmlResource resource) {
13 | resources.add(resource);
14 | return resource;
15 | }
16 |
17 | public List getResources() {
18 | return Collections.unmodifiableList(resources);
19 | }
20 |
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/resources/src/acavailhez/html/resources/HtmlResourcesTrait.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.resources
2 |
3 | import acavailhez.html.traits.HeadTrait
4 | import acavailhez.html.traits.HtmlFragmentTrait
5 |
6 | trait HtmlResourcesTrait implements HeadTrait, HtmlFragmentTrait {
7 |
8 | void resources(HtmlResourcesPack pack) {
9 |
10 | Closure renderResource = { HtmlResource resource ->
11 | if (resource.url.endsWith('.js')) {
12 | javascript(resource.url)
13 | } else if (resource.url.endsWith('.css')) {
14 | css(resource.url)
15 | }
16 | }
17 |
18 | for (HtmlResource resource : pack.resources) {
19 | if (resource.disposition == HtmlResourceDisposition.HEAD) {
20 | renderResource(resource)
21 | } else {
22 | defer {
23 | renderResource(resource)
24 | }
25 | }
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/resources/test/acavailhez/html/resources/SimpleTests.groovy:
--------------------------------------------------------------------------------
1 | package acavailhez.html.resources
2 |
3 | import acavailhez.html.HtmlPage
4 | import acavailhez.html.HtmlStyle
5 | import acavailhez.html.tests.AbstractTests
6 | import org.junit.Test
7 |
8 | class SimpleTests extends AbstractTests {
9 |
10 | static abstract class ResourcesPage extends HtmlPage implements HtmlResourcesTrait {
11 | @Override
12 | protected String title() {
13 | return "Resources page"
14 | }
15 | }
16 |
17 | @Test
18 | public void pack() throws Exception {
19 | HtmlResourcesPack pack = new HtmlResourcesPack()
20 | pack.add('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css')
21 | pack.add('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/js/bootstrap.min.js')
22 |
23 | String html = (new ResourcesPage() {
24 |
25 | @Override
26 | protected void head() {
27 | resources(pack)
28 | }
29 |
30 | public void body() {
31 | }
32 | }).getRawHtml()
33 |
34 | assert contains(html, '''
35 |
36 | ''')
37 | assert contains(html, '''
38 |