├── .gitignore
├── CHANGELOG.md
├── LICENSE-2.0.txt
├── README.asciidoc
├── dev-resources
├── META-INF
│ └── assets
│ │ ├── aviso-logo.png
│ │ ├── coffeescript-source.coffee
│ │ ├── colors.less
│ │ ├── common.jade
│ │ ├── f1.txt
│ │ ├── invalid-coffeescript.coffee
│ │ ├── invalid-less.less
│ │ ├── jade-helper.jade
│ │ ├── jade-source.jade
│ │ ├── jade-uris-helper.jade
│ │ ├── logo.less
│ │ ├── sample.less
│ │ ├── stack
│ │ ├── barney.js
│ │ ├── bedrock.stack
│ │ ├── bootstrap.js
│ │ ├── bootstrap.stack
│ │ ├── compiled.stack
│ │ ├── fred.js
│ │ ├── jquery.js
│ │ ├── local.less
│ │ ├── meta.stack
│ │ ├── missing-component.stack
│ │ ├── stack.coffee
│ │ └── style.stack
│ │ └── sub
│ │ ├── f2.txt
│ │ ├── f3.txt
│ │ ├── jade-include.jade
│ │ └── samedir.jade
├── example.clj
├── expected
│ ├── bedrock.js
│ ├── bootstrap-webjars.css
│ ├── coffeescript-source.js
│ ├── coffeescript-source.map
│ ├── compiled-stack.js
│ ├── jade-include.html
│ ├── jade-source.html
│ ├── jade-uris-helper.html
│ ├── logo.css
│ ├── meta.js
│ ├── minimized
│ │ ├── bootstrap.css
│ │ ├── bootstrap.js
│ │ ├── fred.js
│ │ ├── meta.js
│ │ └── sample.css
│ ├── sample.css
│ ├── sample.map
│ └── style.css
├── logback-test.xml
└── user.clj
├── java
├── com
│ └── yahoo
│ │ └── platform
│ │ └── yui
│ │ └── compressor
│ │ └── CssCompressor.java
└── io
│ └── aviso
│ └── twixt
│ └── shims
│ └── CompilerShim.java
├── manual
├── _template.html
├── dexy.yaml
└── en
│ ├── caching.ad
│ ├── configuration.ad
│ ├── cssmin.ad
│ ├── direct-uris.ad
│ ├── exceptions.ad
│ ├── getting-started.ad
│ ├── index.ad
│ ├── jade.ad
│ ├── jsmin.ad
│ ├── meta.edn
│ ├── notes.ad
│ ├── stacks.ad
│ ├── uris.ad
│ └── webjars.ad
├── project.clj
├── resources
└── META-INF
│ ├── assets
│ └── twixt
│ │ ├── exception.coffee
│ │ └── exception.less
│ └── twixt
│ ├── coffee-script.js
│ └── invoke-coffeescript.js
├── spec
└── io
│ └── aviso
│ ├── twixt_spec.clj
│ └── utils_spec.clj
└── src
└── io
└── aviso
├── twixt.clj
└── twixt
├── asset.clj
├── coffee_script.clj
├── compress.clj
├── css_minification.clj
├── css_rewrite.clj
├── exceptions.clj
├── export.clj
├── fs_cache.clj
├── jade.clj
├── js_minification.clj
├── less.clj
├── memory_cache.clj
├── rhino.clj
├── ring.clj
├── schemas.clj
├── stacks.clj
├── startup.clj
└── utils.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | doc
3 | .lein-failures
4 | .DS_Store
5 | twixt.sublime-*
6 | pom.*
7 | .*
8 | *.iml
9 | .dexy
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.25 - 21 Mar 2017
2 |
3 | Update to Clojure 1.8.0
4 | Fix bad ns declaration in io.aviso.twixt.fs-cache
5 |
6 | ## 0.1.24 - 18 Feb 2016
7 |
8 | Redeployed on Clojars, POM problematic in previous deploy.
9 |
10 | ## 0.1.23 - 18 Feb 2016
11 |
12 | Bugfixes
13 | - Not passing the merged 'twixt-options' to 'wrap-with-exporter' in 'startup/wrap-with-twixt'
14 | - Wrong arg passed to 'ex-info' in 'js-minification/minimize-javascript-asset'
15 | - [Issue 26](https://github.com/AvisoNovate/twixt/issues/26)
16 |
17 | ## 0.1.22 - 30 Sep 2015
18 |
19 | Updated a bunch of dependencies to latest version, most important: clojure 1.7 and schema 1.0.1 .
20 |
21 | ## 0.1.21 - 21 Aug 2015
22 |
23 | Ensured that small amount of Java in the project is compiled for JDK 1.6.
24 |
25 | Removed a few reflection warnings.
26 |
27 | Upgraded various dependencies to latest.
28 |
29 | The twixt helper provided to Jade templates now has a uris() method, which accepts an array of asset path strings.
30 |
31 | ## 0.1.20 - 12 Aug 2015
32 |
33 | Added support for CSS Compression (using YUICompressor).
34 | This is normally enabled only in production mode.
35 |
36 | Revised the approach to caching significantly.
37 | File-system caching is now enabled for both production and development, but uses
38 | different sub-directories of the specified root cache folder.
39 |
40 | There have been some non-backwards compatible changes to several functions as a result.
41 |
42 | In the Twixt options map, the :cache-folder key has been renamed to :cache-dir, and moved
43 | under a new key, :cache.
44 |
45 | The new caching and locking approach helps ensure that multiple threads do no attempt
46 | to perform the same compilation steps in parallel.
47 | There is now a per-asset lock used to prevent duplicate conflicting work across
48 | multiple threads.
49 |
50 | ## 0.1.19 - 7 Aug 2015
51 |
52 | Fixed bug where compressed assets would cause a double exception (an exception, caused by
53 | an exception building the exception report).
54 |
55 | ## 0.1.18 - 31 Jul 2015
56 |
57 | Reverted Clojure compatibility to 1.6.0.
58 |
59 | ## 0.1.17 - 29 Jul 2015
60 |
61 | Updated dependencies, including Clojure to 1.7.0, and CoffeeScript compiler to 1.9.3.
62 |
63 | Clarified many APIs by introducing Prismatic Schema signatures.
64 |
65 | Added support for finding assets inside [WebJars](http://www.webjars.org/).
66 |
67 | Twixt can now export assets to specific locations (typically, under public/resources).
68 | Exports are live: monitored for changes and dynamically re-exported as needed.
69 | This is intended largely to allow Less stylesheets to live-update when using
70 | [Figwheel](https://github.com/bhauman/lein-figwheel).
71 |
72 | ## 0.1.16 - 10 Jun 2015
73 |
74 | Update dependencies to latest.
75 |
76 | ## 0.1.15 - 7 Nov 2014
77 |
78 | Added medley (0.5.3) as a dependency.
79 |
80 | ## 0.1.14 - Oct 24 2014
81 |
82 | * In-line source maps for compiled Less files (in development mode)
83 | * Update Less4J dependency to 0.8.3
84 |
85 | No closed issues
86 |
87 | ## 0.1.13 - 6 Jun 2014
88 |
89 | * Source maps for CoffeeScript
90 | * Source maps for Less (partial, pending improvements to Less4J)
91 | * Support for stacks: aggregated assets that combine into a single virtual asset
92 | * JavaScript Minification (via Google Closure)
93 |
94 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.13)
95 |
96 | ## 0.1.12 - 29 Apr 2014
97 |
98 | * Minor bug fixes; some relative paths computed incorrectly
99 |
100 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.12)
101 |
102 | ## 0.1.11 - 13 Mar 2014
103 |
104 | * Adds support for Jade helpers and variables
105 |
106 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.11)
107 |
108 | ## 0.1.10 - 7 Mar 2014
109 |
110 | * Jade `include` directive supported, with dependency tracking
111 |
112 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.10)
113 |
114 | ## 0.1.9 - 26 Feb 2014
115 |
116 | * Exception report displays map keys in sorted order
117 | * Exception report displays some system properties as lists (e.g., `java.class.path`)
118 |
119 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.9)
120 |
--------------------------------------------------------------------------------
/LICENSE-2.0.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.asciidoc:
--------------------------------------------------------------------------------
1 | == Twixt - Awesome asset management for Clojure web applications
2 |
3 | image:http://clojars.org/io.aviso/twixt/latest-version.svg[Clojars Project, link="http://clojars.org/io.aviso/twixt"]
4 |
5 | image:https://drone.io/github.com/AvisoNovate/twixt/status.png[Build Status, link="https://drone.io/github.com/AvisoNovate/twixt"]
6 |
7 | Twixt is an extensible asset pipeline for use in Clojure web applications.
8 | It is designed to complement an application built using Ring and related libraries, such as Compojure.
9 | Twixt provides content transformation (such as Less to CSS), support for efficient immutable resources,
10 | and best-of-breed exception reporting.
11 |
12 | Twixt is available under the terms of the Apache Software License 2.0.
13 |
14 | link:https://portal.aviso.io/#/docs/open-source[Full Documentation]
15 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/aviso-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvisoNovate/twixt/21fff9321cd760459346604263bea8f747cfb1e2/dev-resources/META-INF/assets/aviso-logo.png
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/coffeescript-source.coffee:
--------------------------------------------------------------------------------
1 | println "CoffeeScript is fun!"
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/colors.less:
--------------------------------------------------------------------------------
1 | @body-color: #898989;
2 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/common.jade:
--------------------------------------------------------------------------------
1 | p Content from common.jade
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/f1.txt:
--------------------------------------------------------------------------------
1 | file 1
2 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/invalid-coffeescript.coffee:
--------------------------------------------------------------------------------
1 | define ["dep1", "dep2"],
2 | (dep1, dep2) ->
3 |
4 | # Error is the missing comma after the string:
5 | dep1.doSomething "atrocious"
6 | argument: dep2
7 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/invalid-less.less:
--------------------------------------------------------------------------------
1 | body {
2 |
3 | p {
4 | color: red;
5 |
6 | // Missing close brace:
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/jade-helper.jade:
--------------------------------------------------------------------------------
1 | img(src=twixt.uri("aviso-logo.png"), title=logoTitle)
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/jade-source.jade:
--------------------------------------------------------------------------------
1 | .sidebar-placeholder
2 | h1 Sidebar Empty
3 | p.lead
4 | | The sidebar is used when selecting certain objects or operations
5 | | in the main user interface. It will open when needed,
6 | | and can be closed using the
7 | button: i.icon-chevron-left
8 | | button.
9 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/jade-uris-helper.jade:
--------------------------------------------------------------------------------
1 | ul: for s in twixt.uris(["stack/compiled.stack", "/colors.less"])
2 | li= s
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/logo.less:
--------------------------------------------------------------------------------
1 | div.logo {
2 | background: data-uri("aviso-logo.png");
3 | }
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/sample.less:
--------------------------------------------------------------------------------
1 | @import "colors.less";
2 |
3 | body {
4 | color: @body-color;
5 | }
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/barney.js:
--------------------------------------------------------------------------------
1 | var barney = "Barney Rubble";
2 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/bedrock.stack:
--------------------------------------------------------------------------------
1 | {:content-type "text/javascript"
2 | :components ["fred.js" "barney.js"]}
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/bootstrap.stack:
--------------------------------------------------------------------------------
1 | {:content-type "text/javascript"
2 | :components ["jquery.js", "bootstrap.js"]}
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/compiled.stack:
--------------------------------------------------------------------------------
1 | {:content-type "text/javascript"
2 | :components ["../coffeescript-source.coffee" "stack.coffee"]}
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/fred.js:
--------------------------------------------------------------------------------
1 | var fred = "Fred Flintstone";
2 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/local.less:
--------------------------------------------------------------------------------
1 | div.logo {
2 | background: url("../aviso-logo.png");
3 | }
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/meta.stack:
--------------------------------------------------------------------------------
1 | {:content-type "text/javascript"
2 | :components ["bedrock.stack" "compiled.stack"]}
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/missing-component.stack:
--------------------------------------------------------------------------------
1 | {:content-type "text/javascript"
2 | :components ["does-not-exist.coffee"]}
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/stack.coffee:
--------------------------------------------------------------------------------
1 | showDialog = (text) -> window.alert text
2 |
3 | $("#logo").click showDialog
4 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/stack/style.stack:
--------------------------------------------------------------------------------
1 | {:content-type "text/css"
2 | :components ["local.less" "../sample.less"]}
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/sub/f2.txt:
--------------------------------------------------------------------------------
1 | file 2
2 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/sub/f3.txt:
--------------------------------------------------------------------------------
1 | file 3
2 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/sub/jade-include.jade:
--------------------------------------------------------------------------------
1 | include samedir.jade
2 | include ../common
3 | p Content from "sub/jade-include.jade"
4 |
--------------------------------------------------------------------------------
/dev-resources/META-INF/assets/sub/samedir.jade:
--------------------------------------------------------------------------------
1 | p Content from "sub/samedir.jade"
2 |
--------------------------------------------------------------------------------
/dev-resources/example.clj:
--------------------------------------------------------------------------------
1 | (ns example
2 | (:use ring.adapter.jetty
3 | io.aviso.twixt.startup)
4 | (:require [io.aviso.tracker :as t]
5 | [io.aviso.twixt :refer [get-asset-uris default-options]]))
6 |
7 | (defn handler
8 | [request]
9 | (t/track "Invoking handler (that throws exceptions)"
10 | (if (= (:uri request) "/fail")
11 | ;; This will fail at some depth:
12 | (doall
13 | (get-asset-uris (:twixt request) "invalid-coffeescript.coffee")))))
14 |
15 | (defn app
16 | []
17 | (wrap-with-twixt handler (assoc-in default-options [:cache :cache-dir] "target/twixt-cache") false))
18 |
19 | (defn launch
20 | []
21 | (run-jetty (app) {:port 8888 :join? false}))
22 |
23 | (defn shutdown
24 | [server]
25 | (.stop server))
--------------------------------------------------------------------------------
/dev-resources/expected/bedrock.js:
--------------------------------------------------------------------------------
1 | var fred = "Fred Flintstone";
2 | var barney = "Barney Rubble";
3 |
--------------------------------------------------------------------------------
/dev-resources/expected/coffeescript-source.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.9.3
2 | (function() {
3 | println("CoffeeScript is fun!");
4 |
5 | }).call(this);
6 |
7 | //# sourceMappingURL=coffeescript-source.coffee@source.map
--------------------------------------------------------------------------------
/dev-resources/expected/coffeescript-source.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "file": "",
4 | "sourceRoot": "",
5 | "sources": [
6 | "coffeescript-source.coffee"
7 | ],
8 | "names": [],
9 | "mappings": ";AAAA;EAAA,OAAA,CAAQ,sBAAR;AAAA",
10 | "sourcesContent": [
11 | "println \"CoffeeScript is fun!\""
12 | ]
13 | }
--------------------------------------------------------------------------------
/dev-resources/expected/compiled-stack.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.9.3
2 | (function() {
3 | println("CoffeeScript is fun!");
4 |
5 | }).call(this);
6 |
7 | //# sourceMappingURL=coffeescript-source.coffee@source.map
8 | // Generated by CoffeeScript 1.9.3
9 | (function() {
10 | var showDialog;
11 |
12 | showDialog = function(text) {
13 | return window.alert(text);
14 | };
15 |
16 | $("#logo").click(showDialog);
17 |
18 | }).call(this);
19 |
20 | //# sourceMappingURL=stack.coffee@source.map
21 |
--------------------------------------------------------------------------------
/dev-resources/expected/jade-include.html:
--------------------------------------------------------------------------------
1 |
Content from "sub/samedir.jade"
2 | Content from common.jade
3 | Content from "sub/jade-include.jade"
--------------------------------------------------------------------------------
/dev-resources/expected/jade-source.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dev-resources/expected/jade-uris-helper.html:
--------------------------------------------------------------------------------
1 |
2 | /assets/54753589/coffeescript-source.coffee
3 | /assets/82834aa5/stack/stack.coffee
4 | /assets/9053106b/colors.less
5 |
--------------------------------------------------------------------------------
/dev-resources/expected/logo.css:
--------------------------------------------------------------------------------
1 | div.logo {
2 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAACMCAYAAACuwEE+AAAgAElEQVR42u19d3wcxdn/d/fuVE469XLqtqot2ZYlN1my3K2CMRBMCSHEkJe8BAKGFEp+CQFCe0MgoWO6SQCbYBPcVWxZbrIlS5blLqva6sVqJ510d1t+f+ze3u7tnbpkkXj4DGftzs4+O/PMM08fgmVZFlaFZVkQBAF710f6O1gf4usAhGv2YBgtTCN53lyG8/6xjMtwvtF6XK43LCSuU7EGzt7fNvB5QuEZ7sSM9xiIv3Oyvnk0sFw3hBktYv0Qy0i/YSK/eaywkP+pkzRRZTSrf6KeuR6wkP/tCDBVVv9QW/RUgYWc6hP0n4CE1/ObxxuW/3oKM1W2rR8KLDcQ5j+cio43LP91CGOgKDR0d9/A6lEW5X/6SjMrpZp1OpxuasD5lmaQBIlHF6fAQakc9JmxbAPWeg2CIEY1HlMNFuVYEMBa2TVWBBntRwwG24WWFpy4WotGXQ/E415xrR1x/tpJhWUqjcuoKcxosXeo564n49fY0w1PZzWclEq063vR0NMja3O6sQGxfv4TzlyO9PpUh0U5Eowbrl3mekgBAxSFCy3NKG1sQHOvDmlRMVgYEorZ2gAcqakBwzKS5650dqBroB+ezupxhcUW+b8e4zNRsIyYwthrP9m2H3PpNRqRX1WJi20tMFC0cL2sqRELgkPg4eSMaZ6eqO64JnmOZlmcbWpC6vTwCV3dQ/09mSL1eMBCmhmg4VTrvdHWvaGujUftN5nQ0qsDQRBQkAQutEqRBQBaenVo0nFb0dzAIJsfX9bcCHYcYbT+3qH+nsg6UbCMmsKIze6DuQaM54qq6+5CaWMDLrW2ws3JCQ8tWgxnpQoxvr4429xkBSdHZQI0bojy9oFa5YA+o1HSpkvfj9rODkz39Bo3ibCvrw9qtfq6WqKtx348YVGOx546URy5uZxqqEdR3VW09fUJ16719aG+uwuhHp6YGxAkQxgAON/SjNWR0XBQKhHn74+iujopUvHM73RPr3GBlaZpbPrwfWi1Abh57Tq4ubldN/WCvXdeNynJWk8wVn2BuDAsi8aebgS6uYMkCDTpekTIwgIgwIJFWVMjQtw9EOLhAS+1Gh16vdACAPQmE8rbWhHnr0V8QBBO1tWBhRhGAuVtreg3meCsUo0Z7sLCE2hvb0d7WxuqqiqRkZ6J+fMXXHd9lD0KM5pCjtdeOdb9GQD6jEacuHoFHxWdwBeninGlqxMEQSA+IBAEWBEqcL8XW1tgpGkoSBJztAFgWW4rgqiebmwAAPhrNNBqNMJ1rh0LE0XjfEvzmHkGvV6PgwfzzHiI/n49/v39d9i8+TPQND1pvIu9sRUjz1jqqCmMPTe/0ZR2fR8O11Sjor0NJtoi/pY1NmK6pxcC3dzh6+qK1t5emZr/YmsL5gYGYbY2EIerq0FbwVDb2Ymu/n54ODsjPiAQjSKdDCvSycwPDhnTyjuYn4e+vl4LDSS41ezm7g6FQoHmlmZ4e3tDqVD+d1IYe5re0VSaYXCxtQUmmpaQh/L2VvRTJpAEgTnaQPBEQVJPNzaAZVm4OzlhmpeXrAHDMChragRBEIjTBkBFkrI2TT09aNbpRg1/e3sbCgtPAASHKdxwEHB0dMKa1WtgNBrxxReb8f777+Lq1SuTKi2NN4UhWZbFeFRrCWok1d9VgwCNRobNJprG+eZmsCyLWdoAKAm5rbS+uwvtPH9jFp+tdiWcaWoEzTBQq1SI9vW1IB7/yzCsgHijqdk52aBMJqFDbhhYpKamws3NHceOHUVXZweam5rw0ccfYtfunejv78d4jf1w5saWlDuaSo633D9aLI4PsKMraeJ0Ja4ODoj09hFtJlxlWFagINE+vlCrHGQY06nvR21HBwiCwNzAYB5Z+EHgf882NcHEMCOGu6amGhcunOf2IL4SANzdPbAkJRU9PT04fOQwfw9gGAbHjxfg/Q/eA0VRk6qTGY/dgBwrJRnqd7g11s8fKlIh23KaenrQ2qsDAMQHBopIg4XBPdfcBIqmoVIoEKfVghX/x/df2lgPlmUx3csLHk5OFoTiWaa+AQMut7aOCGaGYbAvay8YhpECTRBYvToNjo6OOHAgFwMD/RKSxrIsoqKioFQqUVlZgZ6ennGnKMOZk9FQminjD+Os4hRwMkUhLJJOpLcPNI6OwlZivt/dP4Cqa9eEbYkQeB2WxwsW5S2t0JuMIAkC8YGB3D2GlfyestLTDFVOl51GfX09CPDUhf8NCgpCwtwENDU1oeRUiey+i4sLVq5Yhf7+fmz9ZgvefOtvOFl8ctJDakbF9GIKFW5bYmVM6fnmZphoGiRBYLY2UCAtLH+fBYvShnoAgFajgdbVTcqksICRpnGuqYmnVEEgbayuqvZ2dPX3DwtWo9GI3NxsDlzuf4KOJyPjJpAkiaysvWBohqd0lnbLl62Ai4sL8g8dRF9fH/R6Pf797+349LNP0NbWhqlcyPHGyrGskmmenvB0cpYxrb1GIy63twnbEgFC2I7MvxXt7egZGAAAJAQFcVTDqp6q55DKS61GmKdFojIjDM0wKGtoGBasxwqOoquziyMa4PkCEJg5MxYR4REoL7+EisoK4T7B3/f28UFS0mJ0dFzDiRPHhefAAtXVVfj0s49BUdSkU45xR5jhIsJYgCYIAnMCAuWyM8uilJ9IHxcXBLm7y6gDRdM429gIAIjTauGgUMjaNHV1o4nXwyQGB4NhWTASpGJQVl8/5Lf29PTgyJHDIsrCFYVSgYz0TNA0jazsLAtSm3U+BJCelg6lUonsnGwYjUbJ8wCweHEylEolSktPoa7u6rh4ElwXhJmsfXROQCBIgpDhTM21a8J2kRAUJEUGEQVhAagdHBDj5yehHizLSVSnrnKTMEOrhbNSBZZlRJVFS3cPrnZ2Dgojx8gOCFyWWQu9cOEi+Pr6orj4JFpamoXrBF+nTZuGWbNmo/ZKLc6dOysyUHDtPD29kJK8BDqdDrt278SHH23C7j27YDAYpox5gZxskjbUavBwdkaYp5eMOtAMgzKe+Z3pr4WTQillXFkWbTod6js5c0JicDBPPRhJPdPQABNNw0GhwKyAADA0I60Mg5O1tXatvU1NjSg5VWIxUhAcOqjVFkb2QN5+y5ZKcOhAKhTIzLgJLMtyvI1ZSiHM1IdARnoGlEqlgJA0zeDYsaN4+503cfHihUmdI/F7xFp88no59wxWEgJtU5DSek655qRUYqa/vwRZzBSkpK4OLMtimrc3vJydJTwMw7DQDRhwsbkZADAvNJS/LkWas/UN6DeZbI5LVtY+0AwNXpnLUQ8CWLZ8OVxcXHDocD50Op1w3fw7e/YchISE4szZM7h69SpPVSD0ExbGUZ/m5maUnCrhJ4oFCKCjswPbtn+LgYGBQedoPJHJnhsEOdyQzMnE7Bg/P7ioVDIK0tnXh5qODo4HCQkR+A5xPdfYCANFceJzcDCHDAwDWkAIGidrOAoS7OkBrZsGLC1ljvUDBpyrb5CtsvLL5RwjK5GMAG9vbyQtWozOzk4cLyiwqBb5+w4Ojkhbkw6KopCTkyWI++bJIAmO+hAEgazsvaAoiv9mriMCBJamLoOTk9OgiDGRC16yJVlr/2wZGccCjC27hq2/zcVBqUScNkBOQRgGJTwPEuLhAV9XFwn1YGiGm2ye+U0MDQH462bEYRkWlS2t6ODNCfPDwoR7HKWhwdIMCiurJSEZFEUhK2uvYI0WS0bpaRlQqVTIzsmCiTJaJB/+Nzk5BZ6enjh27Cg6OzulzxME4ufEIzQ0FJcvl+Py5cu8uobgyQ/g5eWNxYuTBVisHdXGGrUxlO+MzdjqwZyfxkJhxN5ftsicvX4Tg4MBGQVhcbGpCXqjESRJIiE4GDRNg6ZpMDRt4UF4CuKpViPC15fThfCVoRlQFIWSmiucqSAsFEqCtGxLPPLVtrWjubtbgLu4+CSaebsWWAv806ZNR1zcLFy9egVnz54RKRS5f2g0GixNXQZdrw6HDufLnndwcMCatDRestonvc9y45O2Jg0qM8W1YRuy1uaOdvuxlfxJtiUNRs7GYxsyf5AtX9LBPlLr7g6tm0bGkPYbTTjDi9gJISEgWUjuMzSD2rZ2tPZw1uf506wpCAOGZnGyugY0w8DV0REzA7SS5zmkolFUWQMA6O/vR17eAZ4yQKAAJEkiMyMTALAvax9Y3hZlpiwEQWDVytVwcnJC3oEDGOjvl41BSvISeHp4ouRUMZqammSrOiw0DLNnzxnUJjQe6ozhZt8ibdkV7NkYRmpHGmwl2KM4AoIBSAwOEXgPmmYESnKyphYsy0Lj5IRof3+ZpENRNIr5NnFBgVCrHKRIxTBo69GhsrkFALAwIlyGdDRNo6iyCjTD4MiRw+jR9Vh0JjwFmDMnHiEhoTh77iyuXKnl+RbLd2i1WsyfvwDNLc0oLjkpu+/m5oalS5dhYGAABw7sl40DSZLIyLxJMpnDGe/R2vwG44cEKWkoj3N7++RIfkfr6R4fEgwlQQjMqnlS69o70NDZBYIgsGD6NBsUhEExT0FUCgXmhobIkIqmaRyvqAIARAf4w1OtFt3jqFCnTo/zdQ1QKBQ8fBadrYODA9LWpMNkMiEnJ4tnTmHR6RIEMtIzQZIksrP2gaZo2f3Vq9Pg5OSEw4cPQdejk43b7FmzERYaNqJxnMg5Gpbibrylo5Hss2oHB8RqtTxlEU02ReNkNbddxARooXF0skIIGh26PlxqbOIpyHSwDMtTKZ5SUQzKaq6iz2CAUqHA/PBpkufNteBSBeYmJIAkSbH9EMnJKfDy8sLxEwXo6OgQnKfM21Z0dAyio2NwueIyyi+Xi/YyrllgYBASExLR0dGBYwXHLIwuP+YqlQppaRk/DFvSVMmxZqYgrLVyjWZQUl0LI0VBSZJIDAsFQ3GIxDHBHFIUlFdy4rO3F4I8PDgkoDhkYWgGAwYjink+JSk6EmBY0JQUOc/WXoXC0RnTp4cLYq6Zke3t1eHQoXyZXYszEWSAYRhkZe2zaKz57YwgCGRmcNQnN1dkIhDZS5OSFsPb2/uHgTBTKSFOpNYfHs5qYcWbkaK7T49zdRzzuzAynBOtKUZABppicLa2Dj39/SAJAklR4VaMLYcYBRcvg2FZ+LppEOHvJ6EuNEXDYDDhxKUKzJ83X6AwK1euhrOzMw7kHUB/f795LxLuz0uch4CAQJSUFKO5uUm4Z74/Y8ZMREREoq6uDmVnyiTPEwA0GlcsX7bih2OtnkoJcRQkifnh00BTjKXyCFFw6TIAIMDDHWE+XhbKQHETbjSZUHiZ41PmRYZDRZAcwlE03xeN6qZW1LVd4ySWmVFW7+GQ8+j5csTMmAm1Wg1/fy3mz5uP1tYWnDxZJDF4sSwLJycnrFq1Bv39/dh/IFdmEVcqlcjIyATLstiXtQesyPnK3GblylVQq9U/HISZamVhxHQQLPjtghYQ4sLVBnT09oIgCCyOjhQm2IIUNI5dKAfDMHB1csTs0GDLPb4dRdE4cu4SJ6ZHTIezSsUjJC1QrLqWdlxt78CcOfHIzMiEQqFAVnYWKJqykAWeMVy6dBk0Gg0OHzkEna5HcAon+N9Fi5Lg6+OL8+fPoba2VvQ8909/fy0WzF84JRcvQRCjQ5jJ/ggfNw0i/H15/oOrNEXDZDTh+MUKTtEXMQ2OpEJAAjNiXG1pR3VzK0dBYmM4XodmBArDUDROXKjAgNEER5USC6IjeIRiLJWicajsAlatXI3o6BhUVlagvPySxC+BBQsPDw+kJC9BZ2cnCgqOidT/3H212gUrlq/kJKvcbBtbv8UAOV7swXhGo47aRXOyeRyCIJAcEwWKRwaKokGZKFAUg6Pny0HTDJwdHJAQHgbaREu3FRONQ2cuclbu0CB4aVwFhDMjRKeuF6WVNSAIAqmzZoDlkYSmaKG/E+cvg1CqRIwsK6juzeJ22pp0qFQq5O7PgdFoFNT/nGM4gZUrV8PFxUWIkAQhRZaIiEjMmDFzSlN7cqInerzK3PAwqFUOoE00KJOFB2ls70B5PWc7WjJrhmQ7MtfCCxXQDxigIEksiY3mKJBJXCnknToHAIgI9EeQtyeHVCYKNMVVXa8ehecvo7KyEo18JINFkcciJCQE8fFzOUa27LTgS2y+7+fnh0ULF6G3txf5+QeF54WJIEnBADlV+ckxb0mTmbLMUaXCgqhwGY9CUwwOnj4PgiAQFaiFv4e7iDpQoCga3b19KLpUKSAVwbACItAUBcpE4WxlLVo6OGXg8vhYUCZK+h4Thf0nyxAUFASVSsWrVTgaQpIkMtJvAkGAiyLgNdWCmo5X4ikUCuQdPIC+fr3wvJm6JCQkIjAwcFxUERO5qMmxdG5P1TxRqyJ19gyBIlAmivs1Uii6UIEefT9IksTS2TOE6+Y2tIlCXvEZsCwLP093zAgJ5JDF3A9Fw2AwIr+UozLJs2dARRIWpDJRoCgKF6qvoLNvADNnxgqUgwWL2Ng4TJ8+HefPn0dtbQ1/HcJvVFQUYmJmoK2tDUVFhYJEZA56c3J0xOpVa8Z13Iay1Y32XeR4UoSJJp/TtX4I8fGyIIuJAmUyobdPj+NnOUlnyeyZIFkWtMkkIAttonC+6goaePF5ZeIsbmujKEmbA4WnQTMMPDWuSIgKF7Ys7n00TEYK+4tKOZ0MTxlUShXS0zJAURSys7NEPA1HQRQkiQx+q8nO3geapgWJijMRAEtSUuHh4THl2QKJ8XE8QjEnI+RzeXwcaCMF2kiBMpqEf+cWnQbLsvByc8Wc8DAemfhqpGA0GHHgZBlYlsWCmVFwdXLknhUhX31zO85U1IBlWaxZOJe/LkW8/cdLERwSBi8vLxBgsWhREnx8fHCi8Dja29sAlvfxZQGCZTF//gIEaANQVVWFCxcvcPfNrgtg4e7ujtTU1EkZu/Hoh5yoFFkTVVPmzICSIECZTPy2YwJlMqG8ph7VDZzr5ar5cyyIwCMWZaSw/0QpKJqBk6MDUmbPsEIqEyijCVnHigEAc6Kmw8fNVdjazG2a2zpQeqkKCQmJULtwYnJfXx/y8w8CIAQfXxCAs7Maq1et4SSr7L1CWgfzfRAEVq9aA0dHp0lPYTZlgvEnurq5qDFvRgRM/ARzCEHBOGBAzvFTnAJuRgQ8XdT8faOADM0t11By4TJHQZISBCpFGfhqNOFo8Tn09OqhVJBYuSBeoGA0/y7aSGHvoUIkJCRi9ao1UKvVOJifB32fHgALQiT+LFu+AhqNBqdPl6KhoQFS/wggMDAQCQmJkx6M/19FYQiCQNqiBJgGTDAZjJK6/1gJjCYKjioVliXOhkmECJSRgslgwp78Qk58Dg5ARJBWoEBmiqTr6UPeiVLuPcnzwNKMQM3MbY6fOg+aVSApabGFkTWL0BzGwNvbB8mLk2EwDCB3f47Ij9ciGJglp/+qdB+2HHEmus6Jmg6tp7tAGcyI0dbeiYLS8wCA9JR5YIQtiRIo0rHis7jW1QOCIJCeMg+UwUKBzBRpd14BGIZBoJ834qOniZCOq329euQcOQkAyM7eBxNFCco5wRcmIxNKpRJHjh5Bd3c3N+AiK2RMTAwiI6OuS7qP60phRussNaa0WUoFViclWJBlwMBNvMGIXQcKwLIsQgP8EBceKlw3t9H36pFz+CTnPrk4ESqCEBDBZDCCMphw/lI1KmrqQRAE1i5bBNpoEiplMII2mrAz9wgoisJVUXQiS3BUJDIyCrEzY9Hd040jR45YMknw1EepUCA9PfO/M6HQYC5/E1kzly4AGBrUgEGyLR0/eQaNLe0AgJtXJIEyGnlEsNQd2YdB0TTcXNVYumCOhYfhKYlxwIjvsw+DZVmkLoyHm4saJqNJqJTRhMqqOpy5VI2lqct4EZvjXxxUKty8dh0IgkBubg6MxgGLNx7PvyQmzoO/v/91Syg0pSjMZFEafx8vLFsQD6PBCBNPaUwGE/S9enz81U4AwMrkefB2d+OQSTThFy/XIPdQEQiCwN3rVoGhab6NpX635yAaW9rh7OSI29KW8MyxkWd+TTAajHjnk2+wcFESQkNDecmHsyf5+/ujqqoSpaWnBC87FgRYAnB1dcOa1Wt+sCnLFM8999zzI1UEDecM5Mkoc2ZEYHfOEfT29PIGRQoMRaO8ohbR4aGIjghFaKAf9u0/BspE8a4RFFiaQknZRaxZloSIsCD09epx6vR5sDQNlm9jHDCgouoKMlelYPbMSBw8UoT29g6hDUtRaGhsAQHgvnvWo7qqEpGRUcjMuAnd3d344h+bMTAwwGn/eQrj4eGBDRvuh7eQSWvibT/iMwXGY65GjDDDtSdNhtHMRe2EJQviUVRyBu1tHWApSpjwQ0eLMT0sCCuWzMe0YC2OF57GQJ8eLM210ffpceR4CRYviEf6isUwDBhw5uwl0CYKYGiAZlDf0IzKmjqsWroQacsXoezMJTQ3twIMI9SS0xdAgMQvHrgPs2fPRl9fHzZ/8RmuXbsmaHM93D2QtHgx7rrzx/D28p4URJmoOSIYhmEngsJMJqUxGIzIzivAwSMncaWuCQY+RbyCJJGxKgW3r1sFpVKJHXsPorDkLFpar4GiaYAFXFycce+da7Fy6QI0Nbfj+70HcfZ8Bbq6e4SA+ZAgLX5+722YFRuJYydKsT+/ENVX6tHfPyAYnGNjwvHgfbcjOjIMOp0OXV2dYBgGGo0bPDw8oFQqJ21cbMUYjdcc/UcgzA8BhsmEaSJhuXFI6I1yA2FulBsIc6PcQJixHX55o1xnhBlM1JqMU1GnCiKM1B1yKsE9KVk0rV35bHHNY+Xqh3Oa6WCpJsbzNI6RiKPD+a7JyPo0lWAhh6sAGu/sRvba2Jo0e7lrJiJRwHglUhrruNhT6V9vWMjhOgmP9/nW9pIKWauy7Q3YaFbUUD7Ig33zYDaZiSojocCTBQvBTqXI+xvlhyclDbZyx5KFajC3CHv9D4eSjCUT1mA820hOkR/tuNjL/jWVYRmxaeBG+e8uSjEfMdx/j9ROMdjeau9d9hhRe8+PFgZ73ydmwIfiDUYzLiPpc6yw2JMGR9Oncix6EIZhUF5eLrvu4OCAyMhImyLx5cuXuWAu672RJBETEzPujFp1dTUMBoN8pSiViI6Olg1kc3Mzl4LMqri5uSE4ONjuu/R6PS5evIjy8ktobGxET08PKIqCSqWCRqOBr68vQkPDEB0djYCAgBFLScOZI5Zl0dLSgvPnz6OqqhKNjY3o6uoSzipwdlbDx8cbwcEhiI6ORmxsLFxdXUckLCiHI+baK4WFJ/Diiy9COEua5dKkq1QO+PDDj6DVamXPbN26hT/2hfcsEt5D4u233xEQbTxKZ2cnnnjiCVCUSXgXy6cMW7ZsOZ555hnJ9zEMg5deehGVlZVW38/i4Yd/JUMYlmVRX1+P7du34+jRI+izPltbtopZEASJgIAALFmyBKtXr0FwcPCYpRudToe8vAPYv38/qqurwTDmBUkMAgvg6OiI+Ph4ZGRkYP78BVAN4+xu5XD0Efba7NixwyaimUxG7N27Fz//+c9l99euXYsTJ06Irwr95ebmICIiYtzEw7y8AzyyWN5D8I7Ya9eulX1refklVFVVwfr1arULli9fLrlmMpmwZcsWbN++jctRJ/1S/l3Wi46bsMbGBvzrX//Cd999h8WLk3HfffeNCnEMBgO+//7f+O6779DT0w1L/hBiGLBwzxcVFaKoqAhhYdOwYcMGJCUlDQrHqFOWVVVV4uzZswAsp6iKueucnGwu/5tV4bIUBAm59MWSU35+vmz7GC3yMAyD3NxcS85+cwIOFpg+PRyxsbGS/lmWxd69e0VwWY4BTE1dCjc3N6Hv/v5+/PnPL+Drr7+CwWDgv5m18Qu711mWhclkwpEjh/Hoo7/Cl1/+EybRgRhDLdby8nJs3PgYPv/8c/7MSLF0M3JYamtr8Oc/v4BXXnkF3d3dI0OY4ZQdO3aAE7As5zSbY3IA7hCqvLw82XMKhQKZmZlCCi+x0qmnpwcFBcfGhbpcvHiBOzWEEGXV4FfbTTdxAWTi0t3djaNHjwpwmVclQRC46aabRIhI4/XX/4ri4mIrZZk4wJ6QrHZx4D1A2FzpX3/9NZ566knROUz2kSUnJwdPPfUkrl69akexOXpYjhw5jCeeeBw1NTXjhzDXrl3DkSNHJKvW8m8L1u7cuYM7cdWqrFq1Ck5OTjIKw7IssrOzx8UmkpWVbUXFuF9XV1csX77CxvaVJzpLGgK/wwWcWfiq7OxsFJhPLLGju9BoNAgLm4aoqCgEBATA2dlZ6G8w/VBAQAAcHR0HRZZ///s7vPXWmyLKBruwkCQJjUYDLy9veHl5CX2Lx8MWLE1NTXjmmae5gzKGw8MMheFZWVmSU8LEK1Jc6urqUFJSggULFkiue3p6IiUlBQcOHJD1f+7cOdTX1yMkJGTUyNLT04PjxwtsbmfLli2XSQbmVB222mdmcgd+AtzBoF999bXd7XLx4mTccccdiIyMgIODozAR/f39qK2tRVFRIQ4ePIjW1lbZtqvVavHww48MKv7v378fn3zyiU0eU8zIJicnIzk5BTExMXB3d4dKxaVa6+/vR2NjI0pKSpCXl4f6+jq77EhPTw+ef/45vPHG3xAQEDB6CmMwGLBv315hxdj6FUs/O3Z8b5NirF17s81B4XiPnDFRlyNHDqOvT2/du8B0y5H0rIS8m2HRaDRYunSpcK2kpATXrrWLVqal/YYNG/Dss88iNjYWDg6OkglQq9WIjY3Fhg3345NPPsXvfvekJNuUQqHAr3/9G2g0GrsTWFlZiffeexcsy9jU1BIEgTVr1uDjjz/BU089jZSUFPj6+sLBwUE4SMPV1RXR0dG45557sGnTJjz11NPw8fGxmkNLv52dnXj11VcEvnJUKcuOHDki6CnElCUuLg4ODioZl15aWoorV67I+omJiUFUVJTNwcnLy7OpOxmurp1GWQUAAB2pSURBVCA7OxvyhUoImaKsy549e2z2t3LlSuFQKwAoKzsteZf5HTExMbjzzruGBadKpcKqVavw/vsf4N5774VKpcKdd96F2bNn233GaDTi73//Gz8m8lARZ2dn/L//9wf8+te/ga+Ns79t8iIkiRUrVuDdd9/DvHnzZFKU+beiogLffLPV8txIQyV37twh2o8tAPzsZxuQnJws4sa5X5pmsHPnTlk/YmZSLJGwLMcjFRcXjyrNSGVlJSorKyVUwAynWJQ21/b2dhQWFlpJR/xJIvzRNua29fX1Nvf+efPmC9vWYLCJ/3ZwcMC99/4Ub775Fn7yk5/YtLeZ6+7du1FTUyN7N8sCTk5OeOGFF5CSkmKXlxnsbzc3N/zpT88hOTnZ5lywLIvvvvsOjY2NIw+VPXfunKDUEq/gyMhIxMXF4ZZbbpFw32ZuPD//oCWDgaia+QmLJGORULKysiTc/3DDQXNycvgBkVIBT08vJCcny57Lzc3lxVlCAkdcXBzCwsIkbfX6ftn3mbXGo02BEh4eziVZtHO/v78f27Z9yyscYSUJAY89tlE4T2m06VgcHBzwu989iYiICNGYWaRYg8GAb7/9duTB+Bw/ApkkcfPNN0OhUGDmzFhER8fIVoFer0dOTrasPy7N+iqJrsSM4adPl6K1tWVEAecDAwM4dChf4FnE99asWQMHBwfpWdc8syt9N6etvukmOTVSqZQiidDyTFVV5YQlWcrPz+eO/QNrNfYslixZghUrVoxLwicnJyf85je/hVKpkI0Hy1oW/bApTFNTIwoLCyVp0AkC8PT0wNKlywQMvvXWW6x0LNzvnj17YDKZZP1mZt7EJ9WR9ktRFPbv3z+iQPGCggLodDrZalKplEhPT5e1Ly4uRktLiwxWLy9vQeMprt7ePpJVbn6msLAQpaWnhr2qh0s5AeDAAfEYWN6pUqmwYcP9Y36n+Pr06dOxZk2abDwIgluMBQUFw6cwu3btBk1TMp1LWlqawBiyLIuUlCXw9vaWtWttbUVBQYGs39DQUIHhs9Yr5ObmgqbpYa9GMxWz9MVdnzdvHgICAmTt9+7dY+VHwj27evVqODo6ytqbmXRrHsZkMuGFF17AN99shV6vHzLFxnApT1dXJ8rLy638U7jf+fMXIMj6wHc7fQ8XFgC47bZbQZKk7H0sy6KoqHB4FKa3t5c/Yk6q1VUqlVi7dq0EUx0cHHhNrlj7y73azDBb928WsaVGT6ClpQWlpaXDWo0NDfU4d+6cldRECKK0dfvm5macOnVK1l6hUNikRgRBYPHiJNHpbFJKYzQasXnzZvz85w9g06YPcO7cWZhMJsl5lyOtFy5cBE3TMi0uACxdunRC0n2EhoYhPDxc9j6CIHD+/PnhUZi8vAO8vUJq/0lKWgxfXz/ZiktPz7BaoWZ1/UVcunRJ1v/ChQvh6+trdZ3jZTgeY+j9OScnV6JVNlMMe4kHs7OzJOdDm68nJibapEYsy8LfXyvwDNYaU3Pt7u7Cjh078OSTv8PPfnYfXn75JezcuRM1NTUjopaca0aV7aQ+JIlZs2ZNSEIhAJg7d67Nb+vp6RmawtA0jV27dtm0sXBSkXwv9Pb2xpIlS2w6bu/Y8b0M0x0dHbFmTZrMJkIQQFFRETo7OweF0Wg04ODBPBsWYwIZGRkyKcZoNAr8kZgCcqL+2kHf9eCDvxC5OUgprnXt6urCsWPH8P777+ORRx7Ghg0/w9tvv4XS0lMSymGfb2yW2YEIgoC7uzs8PT0nLKHQ9OnhMqnRXIekMCdPnkRDQ4NMMoqIiMCsWbPs+s/ecsutVqehctcLCgrQ2toqe096erpEijH3ZzAYkJeXNyiMxcUlaG9vt9I4c7qONWvWyNqfOHEC165ds7LcAn5+fpg/f/7gaV/d3PDSSy8jPDxcYvUV82sWPRQkVuL29nbs27cPf/jDH/Dgg/+Dbdu2oa+vz+67OJcFVib1ubu7Q6FQjCiN3GA+v9aV0/6yMks/MIQexqzaN6vVCdFRdDffvE5QVtna0yMjIxEbGyt5hiA4BnHv3r2yd/n6+mLBgoU2JAIuVxzDnwltq2ZnZ/MwSt+1ZMkSuLt7yL6JY3ateQtuK7WnUxFXrVaL119/A3fddRecnJxsSBWEDBbxfYDzjPv004/x8MO/xLFjx2yufk6qlPcxmN5mqKRBw0kv5+joaFNSIoghNL01NTUoKyuTaf48PDyxdOnSQT3QuS3rVt5pSKr9zcnJFqQJ8aoQa2LFq+rq1au4cOGCTRhbW1tx6lSJzJ5FEKRNXcrVq1dw7tw5mf+ISmWbGg2mt3jggZ/jo48+xl13/Zg/0JOw44Ni/7etrRWvvPIyPvvsMzAMI3kHl4RIrK1meXueccQJK0dCYUwmo+R9Yp5yUApj5jcsfIvZyJUGtVo9ZFLE5ORkkW3Dshd3dnYiPz9f9r74+HiEhITItJmcfSjLJowHDnBedda2o/DwcJ7CSdvv25clMMcWvozAokWL4OPjM2JJxs/PDw888AA2b/4Cr776f7j11tsQGhoKgiBlfI7Uqi/2wmOwbdu32LJli2T8NBo3yfibn+nu7gJFUeN2hrV1tfg0E1bjNAiF6ejowKFDh2Q2GaVSKbIBDR7rolAocPPNN8vkeU7E3imTGkiSRGbmTTb9WAoKCkSSGldpmkZubo5sBQKswLyK2/f39yMv74Bs5XBOVWvHpJlVKpWIj4/HQw/9Eps2fYgvvvgHnn76GWRmZiIgIJCHRWqrEWuLWZbFli1fo7q6WiSV+YsogwXenp4enmcbnlZ3qLmyrrW1tTakwCH8YcQ+L2Z1OcsCarUa//rXNyNyULZQDIsj8pUrtTh9+jQSExMl7VeuXIl//vMf0Ov1knfr9XocOnRIhIDAmTNn0NTUJMBm3hI0GjeJW4LY0s5pgqUO6CEhIYNai0djNff19cXy5cuxYsUK0DSN2tpaHDx4ELm5ObxdTQozQRCgKArbt2/Dk08+BQA2LOsECIKb1DNnzth0srcVTjLScubMGYkDvNkX2cvLyzbCGI1G7Nu310rzanFl3Lt375hCP8QitjXCuLm5ITU1VeJ5Z353Tk62BGGsvfOEI3KWr4CLi4vsvdbfZC7p6Rkyl83xLCRJIiIiAuHh4bj77ruxefNmgfG2XvEnT56EyWSCSqVCbOxMkCQp0i9Z+J/8/INIS0sbd1hbW1uF0CFrbe+sWbNt+8McPXoU7e3tE55wuLi42KbX1003rQVJkrL2lZWVqKioEBD3xInj8tTmJCnxwRU7rV+6dEnW3tnZGatWrcJkFY1Gg0cffdSm9tmsVW9paeGplB+mTw+32e7MmTOScJjxKrt377LLHyUlJdmOrTaHj0h9I1ib2j/5fohht2MYBjt27JQBHRUVhejoGJt7rvnUs/z8fAwMGGS6lNmz5yAsLEzWp5kqWsOQkpICd3d3THa54447rbSvFti6urpEW/QK2dgDAE3T+OyzT8c1J0xLSwt27dpl0+/G1dUVixYtkiPM+fPnUVlZIZGMxFpGsa5B7nVO2NRJyONlLPcPHsxDT0+PbO81UwlrPcLhw4fR19eH3Nwcm9EKa9fKT2bV6XS807o1P2BhdkdS6urqxjxRnp6egnO4tRe/2Q2TIAisXr2a314JmT/M6dOnsXPnznFBFoqi8Oabb2JgwCCLKCAIIC0tHWq1Wo4wZk9/KXXg9A5ubm6S6u4++N/W1zlfC2k8TF+f3qYPb2pqKtzc3GUaZp1Oh88//0zwQBNrQL29vZGUtFjW16FD+ejt7bXiiViEh4dj5syZI4qDunDhPDZufAxvv/3WiN1IrX2jOc9/uW7G7NvLaZbdsW7dLTb9YVgW+PTTT3D8+PExIQvDMNi06QOcPn1aBgvLsnBxccHtt9/OSUliTrq5uZl/OSHi4gGSJPDaa38dcxjrRx99iO+//x7Wi3PPnj245ZZbhVBNgiDg5OSE1atXYfv27yS6CJYFH3AmpRQsy60C63BPc4CapZ1F4rOmLkNJFc3NzXjppZcwMGBAdnY2ysvL8cQTTwjb50hKQcEx0DQjGWeAgKOjA/z9tRJ47rzzTuTnH0Rzc4voOwhec07h//7vVfzyl79EZuZNI4ZjYGAA7777DvLy8mSwmGG4996f8i4rrJTC7NmzGxRFCXYDguBqXFycLLh+NHnszcysWY3PtWPR3NyMwsITMmmHC/EghPBWaQSAuQ+zppZzkrK1xdbW1vJ9WPpxdXXFsmXLbEpwtkpfXx9efPHP6OrqFPqpqanBb3/7W7z11pv8EX3DKxUVFfj8888hjS3n4IuJiRHFMXH31Wo1Hn/8cSiVCtFYWOw7JpMJ77zzDl588c+C3W840mpxcTEef3yjYKuzhoUgWCQkJGDdunXCfaVZjd/X14ecnBxBwSQu69atG7VMLw5FDQ4ORkJCAkpKimXay++/34ElS1IlzwQFBSEhIYH3W2GtLNHS/hcsWCCcQWQtSkuf4c9jXLYMrq6udlOMiPumKAqvv/5XVFdX83u65T5N08jOzkZeXh4SEhKwZEkq5syZw+kslEoJj9DU1IScnBzs3r3LctKJlQ9tWlq6zbGeOzcBGzZswGeffSbTy5jL8ePHUVxcjAULFiA1NRVxcbMEQ6XZ0au5uRmlpaXIzz/IB6qxdrTQLAICAvG73z0pUTkoLaEdB0TujRaQfH19Bb5gpNTFVhaIm29eh5KSEtnEX7x4AeXl5ZgxY4bkmbVr1+LUqVNDnsBh7YRFEAS6ujoFw561ed9MvoeTVoNhGIkpRBrsDmEyioqKUFRUJOiTPDw84OTkBJOJQldXJ++bCxuIz/1OmzZNCPq3Bdcdd9yJ7u5ubN++3S4sJpMJBQUFQnSmq6srnJ2dwbIsdDqdDb6LsPnr7++PF198EV5eXlKdkhn7d+3aJVOXc9tCJhwcHIZlChhOyrL58+cLkXTWRsadO3fInH4WLFgIPz8/G31amLLg4BDEx8fL3p2bu1/CWJqvz5w5U/CQH06aMLNX/UMPPSS4YAz+3Zz6/sqVK7h8+TKqq6vQ2dk56HPOzs747W9/J6FK1rAQBIH/+Z8Hcf/990vCWgb71el60dbWhra2Nkl47WC/4eHh+MtfXkNQkDy9CcmyLE6dKrESFc1e8ipkZGQO2yYxmK1CHOVnthdZ6yDMCkPxc0qlEunpGTZ1KGYkyMzMEPxDzPe5rSJLhlxm3mgwe4ot7zTOwf02vPXW2wJy2v/ukY2XRqPBH//4LCIiIoaEBQDuuutuvPDCnwXD7uDvGD4sZqXnX//6us0jBgUK8/33OySxPGZsTk1dCg8PjxFbP21tR+K6evVqqNVqmyYJa18Zc4gI56MhJ5/Ozs5YuXKV7N1lZWUiRtTS3sPDAykpKTY9Be1to2J4pk2bhldeeRXPPvsnxMTECCt9MH8T621e7EE3a9YsvPHGG0hMTLRrabYFy/z58/Hee+/jjjvuEJjk4fi+2IKFIAjMmDEDr7zyKh57bKOw/dqCRVlbW4uystMi5tTSYN26m8fNGGettFqyZAlyc3NltqqsrH24++674OTkLOGjFi1aJCjfzIwZywJLlqTCw8NDthotEQFSpdjKlatECrOhYbV1TaFQIDk5GUlJSbh06RLy8vJQWHhCoI72+AIz7Gaf3FtvvRVJSYtlSDdcWDQaDR588Bf40Y9uFxjvhoZ6yXjag4UgAEdHJyxcuBCZmRmIj59rEw4Z4p49e4Y9c+asrJFa7Yxbb71N5pM7Xsn/6uvrcfjwYZv3VqxYIeFzCIJAbW0NCgqO21TwhYSESN5JURR27NgBk8koa79y5SqJ28BgBtLhJjMSh9JWVFzG1at1aGtrQ2+vDiaTCQqFEhqNK/z9tQgPD8esWbMEn9zhGmuH27a+noueqK6uQnNzM7q6umE0GkCSJFxcXODj44vQ0FDMmBGD2Ng4K8o9dP82Ezvbm+iJyBY51HvHI4vmcLN4jgVhJmpcphoso05ZNlllomG5cZzOCF01bgzBDx/pJxMWciJePhUG6HrC8J9MoceEMCPJgT/Z5caZGze2pBsUYgrAQo7lI8b75PWxDM5QZx9N5ERN9HdOKVhunGZyo4ykKEezEq1l96H+niyeZSw6oImEaSqU8YJl3CjMaP1lbpQfGH/3n3aE3/WkbmO9/kOARTle+DIVKMxkHVc8HDF+KFgmEr6JhEW5bds29Pfrbd6Mj58r8f2YKCnn2rV25Obuh0ajsZmpe7ylnCNHDkOhUAq5acerlJWVCcmfAcDd3R1LlqTymR3+M8R0pU6nQ29vLwAWBw/mIyoqEsHBXJ5/6wMQBqMso6UwFGXC73//e6jVaqxde/Oo/YZtMd72ytWrdVCpVOO+ys+dO4t9+/Zh8eJkPhlTMb755hu8/vobknz9U5XyfvPNVmg0bjYjRwWEMR+CRdM0SkpKsGrVasH73t550gzDQKfTQaPRCCGt1oD09PTAxcVFkkTQ7Ffq5OQkhIPU1zegsbERn332OXx9fWVtHR0dhXz5tj7YGkaj0QiapgWfF4Zh0NfXB41GI/RhzrxtDbfJZILBYOCTTUsH2Rpue6vV31+LjRs38ouBwuOPb8Tu3bvwv//7kOB6odf3QaNxs8pWCbvfaC79/XooFErBZdZkMsFkMln5G3PP6/V6kCQpSX0v7re/n7vv6OgkjENNTS28vb1kC1Gn08HZ2RlKpdI2D2O+9vrrr8PHxxvt7e04ffo0vvzyK2zevBl79uwGwzBQqVTYuPFxJCcno6+vD/ffvwGPPPIIvvrqKzQ2NsLNzQ1//OOzmDVrFhoaGvDSSy+isbERBEHgzjvvxD333IMXXngeNM3gmWeexurVq3HPPT/ByZMn8fHHHwkxxosWLcIjj/wKHh4eqKqqwvPPP4eHHvolPvjgA9x///1QKEjs3r0byckp+OqrL2EymbB+/XrMnj0bb775Jjo6OhAVFYWXX34Frq6ueOedd6BQKPDII49g7969OHr0COLj52Lr1q0wGAYwZ84cPP/8C3ByckJjYyNeffVV1NbWwMHBESkpKTh9uhQfffSxZDJsqRUUCgWCg0Nw7do1MAyDd999F/n5B/ngMFc8+eSTiI2NxaOP/gppaelCsFhNTQ2eeeZpvPHG37B79y6YTBRMJiPy8vKgUqnwxBO/xrVr7fjyyy9hMBiwfPkK/Pa3vwVJkmhsbMTf//531NVdBU3TWLhwIR5//Ak4ODjg5ZdfQkhIKOrqrnI5d0kSd9xxBzZsuB/btn2LkyeLoFAoUFRUhL/85TV0d3fj5ZdfRltbK0iSxE9/ep8loZA1CeNShg9g//79UKlUePbZPwEAZs2ahbfeehtbtmzF+vXr8f777/GxTFzsztatW/HYYxvx9ddbEBUVjW++2SokJ3JxccGWLVvxwQebMG/ePJCkAg8++AuoVEo8/fTTyMy8CRUVFXjllZeRnp6BLVu24sMPP4JOp8OLL74IhmHAMAx6enqwdesWPPLII1i4cCEoikJNTQ0qKyvw6aef4de//g2+++47bN68GS+88Gd8/PEnaGlpEQ7QMhgMMBoNQlqwS5cuobq6Cps2fYC3334Hly9fRmFhIQDg7bffgre3N7788it88sknaG9vg16vl4yTLUrDMAzq6upQVnYaM2bMAEEQmDdvHjZt+hBbtmxFamoqNm36AEqlks9WkSUgWl7eAYSEhCA4OBhGoxH5+Qeh1WqxZctWpKdn4N1338WpU6ewadOHeP75F3DoUD6uXLkChmHw4ot/RnR0FP7xj3/i008/Q3V1tZDudmDAgF27diImZga+/noLHnroIWzbtg09Pd1YsyYN0dExWLx4MZ5//nm4u7tj+/bt0Gr9sXXrN3jnnXcxZ84cS0Ihawpjvubm5oZHH31MOPF17ty5aG5uxp49u9HS0oKOjg7JKWJ33/1jxMfHw8PDA/PnzxOSD4aFheHq1as4dOgQPDw8MGMGF6Lq5+cHgiAQGBgEDw8PZGXtQ1zcLKxfvx5qtRparRYbNz6OiorLQrYCk8mIn/70PkkgPcuyeOSRX8HLywvz5s0Dy7JYv349IiIiEBQUhNDQUHR0XLOZHkShUGDjxsfh769FZGQkgoKCcO3aNej1epw7dw533XWnkLnytttuk42TeLyqqirx05/ei/vu+ykee+wxxMbGCsmN5s2bh5qaauzevQvd3V1oamoCwzBIS0tHa2srLl68AJPJhEOHDgkHYwBAQEAA7rnnJ9BoNEhMTER/vx4PP/ww/Pz8kJCQAAcHB3R2dqCyshJXrlyBs7Mzdu/ejf3798PNzR2lpRZGPDY2FnfccQfc3d2RnJwChmHQ1dUNd3d3ODs7wdVVg6CgYCgUCoSFhaGqqgrHjh2Fj48PoqKibGt6xddmzJgphD709vbimWeehkqlwrx58wRHbjF/Yz7QAQBIUiE6uWQtnJycsX37Nvzzn//AI4/8CqmpqbL3dnV1Qav1l8Dg6ekJBwcHdHd3wcPDEwqFUli14jZm5BEnKZbCYtux2tfXV3Lolhluc2Jm8z4PQOJrbGvsgoKC8Otf/0ZYbFqtFgRBoL29HU8//RR8ff0wZ84cIVbI/P6kpCTk5ORCr++H0WgUHNUBICQkVPC3VShIqNVq+Pj4CulNuDkAdLoegS8yGAb4BR4PPz9/O/ND2tTFmP+9fv16aDSu+Pbbb7F582Y8/vgTg/MwACTSREkJl970iy/+AUdHR0n6r6EkFoZhsGrVKqxcuRJff/0V3nvvPeHIFnG76Oho5OTkQK/XC4xrWVkZTCYTpk8PR2dnJ0iSlISVWH+85RoxJJM8mA7Jzc0N/v7+OHr0qBDHJBabbTGsTk7OiI6OlkmPR48eBUmSeOWVV0AQBLZt2yZ517p1t+BPf3oWvb29WLZsmQShrL/DvCCsYZ42bToIgkBc3CxJoiZzajjuWdKu8KBSqYS0cOZrGRmZSEtLx+eff4b3339vZEf4BQQEoK+vDzt27ICHhzsf/DY8uX7Tpk3o6+tFfPxcXLp0Cd7e3jZX6M03r8Phw4fx+98/g7S0dHR2dmDHjh24++4fw8fHx0b04MSKow888HO88cbrqK6uhkqlQnV11aj6CgwMRGtrK/bu3SMcuSwuM2fORFBQEE6cOI63335nVO/w9vbG+vV34LXX/oJbbrkVnp6eOHHiBObOnYsf/ehHQz6/YMFCvPPO23B2dsKdd96JL774B0iSRFxcHCoqKuDr6wvFc88997z5Ab1ej9jYWOFYN4NhAMHBIQgNDRUACgsLQ0lJMbq7e/DAAw9AqeS2J4VCgf7+ASQmJgpinslkgqurK+Li4hAePh0NDQ04d+4stNoAPPzwL+Hi4sqn4+KCvpVKTmRcvnw5BgYGUFZWhoEBA37yk58gIyNDYCZZFkhMTBS2Soqi4OysluSpMxgMmDs3Qdg2+/v7MX36dAQGBsJgMCAwMBDTpk0DRVHQaFwRGxsnEjn7ERkZCa1Wi7CwMCxcuAgsyyA8PBxJSYtRWFiI229fL4lSNGcY9/Tk+DNrF4+goCD4+PiiqKgIFEXjZz/7GRwdHSXxSB0dHTAaDbj77h8LzxkMBmi1AUK+O5qmoVAoMXeuJSxEr+/DnDlz4O7ujvj4eAQEBPDHEtZh9uxZSE/PgIODAwYGBjBtWpiQyZzbuoxITEyEs7MzwsPDERISjObmZkRHxyA+Ph5XrlzBhQvnERISgv/934dGbnwcqUf6ZFGCiYLBaDSCoiio1WpQFIW//e0NtLe347XX/jpu48Lpjwx4/PHHcfvt68ecu24i50iJG2XQUlJSjNdeew1+fn7o6uqCp6cn/vCHP45b/wzDYOPGx9DR0YHg4GCsWLFialurh0thRhPrMpEURtz/RMOg0+mg0/VApXKAl5eXzYybYxmXzs5OGAwGjkcQ9T2WFCvjPUfCMze2pNFb3UeCtBM9PpMFCzkVA7nG2z93olw0xjLhw3l2pFGjkwELOdyBnEzEsg4PtfdB4w3TYAM33u8aSVz1VIJFac8tYKjJGikw9qyytlb1RFAYe984mthqe+3HupUONu5TBZb/OBfNG2Vii81M4ENFNI4lZdlIzvIZCp6RwmCrv+GkLBtqBY9Hdi5775xqsPx/RA0Ht3Y81B8AAAAASUVORK5CYII=");
3 | }
4 |
5 | /*# sourceMappingURL=logo.less@source.map */
--------------------------------------------------------------------------------
/dev-resources/expected/meta.js:
--------------------------------------------------------------------------------
1 | var fred = "Fred Flintstone";
2 | var barney = "Barney Rubble";
3 | // Generated by CoffeeScript 1.9.3
4 | (function() {
5 | println("CoffeeScript is fun!");
6 |
7 | }).call(this);
8 |
9 | //# sourceMappingURL=coffeescript-source.coffee@source.map
10 | // Generated by CoffeeScript 1.9.3
11 | (function() {
12 | var showDialog;
13 |
14 | showDialog = function(text) {
15 | return window.alert(text);
16 | };
17 |
18 | $("#logo").click(showDialog);
19 |
20 | }).call(this);
21 |
22 | //# sourceMappingURL=stack.coffee@source.map
--------------------------------------------------------------------------------
/dev-resources/expected/minimized/fred.js:
--------------------------------------------------------------------------------
1 | var fred="Fred Flintstone";
2 |
--------------------------------------------------------------------------------
/dev-resources/expected/minimized/meta.js:
--------------------------------------------------------------------------------
1 | var fred="Fred Flintstone",barney="Barney Rubble";(function(){println("CoffeeScript is fun!")}).call(this);(function(){$("#logo").click(function(a){return window.alert(a)})}).call(this);
2 |
--------------------------------------------------------------------------------
/dev-resources/expected/minimized/sample.css:
--------------------------------------------------------------------------------
1 | body{color:#898989}
2 |
--------------------------------------------------------------------------------
/dev-resources/expected/sample.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: #898989;
3 | }
4 |
5 | /*# sourceMappingURL=sample.less@source.map */
6 |
--------------------------------------------------------------------------------
/dev-resources/expected/sample.map:
--------------------------------------------------------------------------------
1 | {
2 | "version":3,
3 | "file":"sample.css",
4 | "lineCount":1,
5 | "mappings":"ACEAA;",
6 | "sources":["colors.less","sample.less"],
7 | "sourcesContent":["@body-color: #898989;\n","@import \"colors.less\";\n\nbody {\n color: @body-color;\n}"],
8 | "names":["body"]
9 | }
--------------------------------------------------------------------------------
/dev-resources/expected/style.css:
--------------------------------------------------------------------------------
1 | div.logo {
2 | background: url("/assets/8ee745bf/aviso-logo.png");
3 | }
4 |
5 | /*# sourceMappingURL=local.less@source.map */
6 | body {
7 | color: #898989;
8 | }
9 |
10 | /*# sourceMappingURL=sample.less@source.map */
--------------------------------------------------------------------------------
/dev-resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %date{HH:mm:ss.SSS} %-5level [%thread] %logger{} - %msg%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/dev-resources/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:use speclj.config
3 | clojure.pprint)
4 | (:require [schema.core :as s]))
5 |
6 | (alter-var-root #'default-config assoc :color true :reporters ["documentation"])
7 |
8 | (s/set-fn-validation! true)
--------------------------------------------------------------------------------
/java/com/yahoo/platform/yui/compressor/CssCompressor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * YUI Compressor
3 | * http://developer.yahoo.com/yui/compressor/
4 | * Author: Julien Lecomte - http://www.julienlecomte.net/
5 | * Author: Isaac Schlueter - http://foohack.com/
6 | * Author: Stoyan Stefanov - http://phpied.com/
7 | * Contributor: Dan Beam - http://danbeam.org/
8 | * Copyright (c) 2013 Yahoo! Inc. All rights reserved.
9 | * The copyrights embodied in the content of this file are licensed
10 | * by Yahoo! Inc. under the BSD (revised) open source license.
11 | */
12 | package com.yahoo.platform.yui.compressor;
13 |
14 | import java.io.IOException;
15 | import java.io.Reader;
16 | import java.io.Writer;
17 | import java.util.regex.Pattern;
18 | import java.util.regex.Matcher;
19 | import java.util.ArrayList;
20 |
21 | public class CssCompressor {
22 |
23 | private StringBuffer srcsb = new StringBuffer();
24 |
25 | public CssCompressor(Reader in) throws IOException {
26 | // Read the stream...
27 | int c;
28 | while ((c = in.read()) != -1) {
29 | srcsb.append((char) c);
30 | }
31 | }
32 |
33 | /**
34 | * @param css - full css string
35 | * @param preservedToken - token to preserve
36 | * @param tokenRegex - regex to find token
37 | * @param removeWhiteSpace - remove any white space in the token
38 | * @param preservedTokens - array of token values
39 | * @return
40 | */
41 | protected String preserveToken(String css, String preservedToken,
42 | String tokenRegex, boolean removeWhiteSpace, ArrayList preservedTokens) {
43 |
44 | int maxIndex = css.length() - 1;
45 | int appendIndex = 0;
46 |
47 | StringBuffer sb = new StringBuffer();
48 |
49 | Pattern p = Pattern.compile(tokenRegex);
50 | Matcher m = p.matcher(css);
51 |
52 | while (m.find()) {
53 | int startIndex = m.start() + (preservedToken.length() + 1);
54 | String terminator = m.group(1);
55 |
56 | // skip this, if CSS was already copied to "sb" upto this position
57 | if (m.start() < appendIndex) {
58 | continue;
59 | }
60 |
61 | if (terminator.length() == 0) {
62 | terminator = ")";
63 | }
64 |
65 | boolean foundTerminator = false;
66 |
67 | int endIndex = m.end() - 1;
68 | while(foundTerminator == false && endIndex+1 <= maxIndex) {
69 | endIndex = css.indexOf(terminator, endIndex+1);
70 |
71 | if (endIndex <= 0) {
72 | break;
73 | } else if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
74 | foundTerminator = true;
75 | if (!")".equals(terminator)) {
76 | endIndex = css.indexOf(")", endIndex);
77 | }
78 | }
79 | }
80 |
81 | // Enough searching, start moving stuff over to the buffer
82 | sb.append(css.substring(appendIndex, m.start()));
83 |
84 | if (foundTerminator) {
85 | String token = css.substring(startIndex, endIndex);
86 | if(removeWhiteSpace)
87 | token = token.replaceAll("\\s+", "");
88 | preservedTokens.add(token);
89 |
90 | String preserver = preservedToken + "(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
91 | sb.append(preserver);
92 |
93 | appendIndex = endIndex + 1;
94 | } else {
95 | // No end terminator found, re-add the whole match. Should we throw/warn here?
96 | sb.append(css.substring(m.start(), m.end()));
97 | appendIndex = m.end();
98 | }
99 | }
100 |
101 | sb.append(css.substring(appendIndex));
102 |
103 | return sb.toString();
104 | }
105 |
106 | public void compress(Writer out, int linebreakpos)
107 | throws IOException {
108 |
109 | Pattern p;
110 | Matcher m;
111 | String css = srcsb.toString();
112 |
113 | int startIndex = 0;
114 | int endIndex = 0;
115 | int i = 0;
116 | int max = 0;
117 | ArrayList preservedTokens = new ArrayList(0);
118 | ArrayList comments = new ArrayList(0);
119 | String token;
120 | int totallen = css.length();
121 | String placeholder;
122 |
123 |
124 | StringBuffer sb = new StringBuffer(css);
125 |
126 | // collect all comment blocks...
127 | while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) {
128 | endIndex = sb.indexOf("*/", startIndex + 2);
129 | if (endIndex < 0) {
130 | endIndex = totallen;
131 | }
132 |
133 | token = sb.substring(startIndex + 2, endIndex);
134 | comments.add(token);
135 | sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___");
136 | startIndex += 2;
137 | }
138 | css = sb.toString();
139 |
140 |
141 | css = this.preserveToken(css, "url", "(?i)url\\(\\s*([\"']?)data\\:", true, preservedTokens);
142 | css = this.preserveToken(css, "calc", "(?i)calc\\(\\s*([\"']?)", false, preservedTokens);
143 | css = this.preserveToken(css, "progid:DXImageTransform.Microsoft.Matrix", "(?i)progid:DXImageTransform.Microsoft.Matrix\\s*([\"']?)", false, preservedTokens);
144 |
145 |
146 | // preserve strings so their content doesn't get accidentally minified
147 | sb = new StringBuffer();
148 | p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')");
149 | m = p.matcher(css);
150 | while (m.find()) {
151 | token = m.group();
152 | char quote = token.charAt(0);
153 | token = token.substring(1, token.length() - 1);
154 |
155 | // maybe the string contains a comment-like substring?
156 | // one, maybe more? put'em back then
157 | if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
158 | for (i = 0, max = comments.size(); i < max; i += 1) {
159 | token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString());
160 | }
161 | }
162 |
163 | // minify alpha opacity in filter strings
164 | token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
165 |
166 | preservedTokens.add(token);
167 | String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote;
168 | m.appendReplacement(sb, preserver);
169 | }
170 | m.appendTail(sb);
171 | css = sb.toString();
172 |
173 |
174 | // strings are safe, now wrestle the comments
175 | for (i = 0, max = comments.size(); i < max; i += 1) {
176 |
177 | token = comments.get(i).toString();
178 | placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";
179 |
180 | // ! in the first position of the comment means preserve
181 | // so push to the preserved tokens while stripping the !
182 | if (token.startsWith("!")) {
183 | preservedTokens.add(token);
184 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
185 | continue;
186 | }
187 |
188 | // \ in the last position looks like hack for Mac/IE5
189 | // shorten that to /*\*/ and the next one to /**/
190 | if (token.endsWith("\\")) {
191 | preservedTokens.add("\\");
192 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
193 | i = i + 1; // attn: advancing the loop
194 | preservedTokens.add("");
195 | css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
196 | continue;
197 | }
198 |
199 | // keep empty comments after child selectors (IE7 hack)
200 | // e.g. html >/**/ body
201 | if (token.length() == 0) {
202 | startIndex = css.indexOf(placeholder);
203 | if (startIndex > 2) {
204 | if (css.charAt(startIndex - 3) == '>') {
205 | preservedTokens.add("");
206 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
207 | }
208 | }
209 | }
210 |
211 | // in all other cases kill the comment
212 | css = css.replace("/*" + placeholder + "*/", "");
213 | }
214 |
215 | // preserve \9 IE hack
216 | final String backslash9 = "\\9";
217 | while (css.indexOf(backslash9) > -1) {
218 | preservedTokens.add(backslash9);
219 | css = css.replace(backslash9, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
220 | }
221 |
222 | // Normalize all whitespace strings to single spaces. Easier to work with that way.
223 | css = css.replaceAll("\\s+", " ");
224 |
225 | // Remove the spaces before the things that should not have spaces before them.
226 | // But, be careful not to turn "p :link {...}" into "p:link{...}"
227 | // Swap out any pseudo-class colons with the token, and then swap back.
228 | sb = new StringBuffer();
229 | p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)");
230 | m = p.matcher(css);
231 | while (m.find()) {
232 | String s = m.group();
233 | s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
234 | s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" );
235 | m.appendReplacement(sb, s);
236 | }
237 | m.appendTail(sb);
238 | css = sb.toString();
239 | // Remove spaces before the things that should not have spaces before them.
240 | css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1");
241 | // Restore spaces for !important
242 | css = css.replaceAll("!important", " !important");
243 | // bring back the colon
244 | css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":");
245 |
246 | // retain space for special IE6 cases
247 | sb = new StringBuffer();
248 | p = Pattern.compile("(?i):first\\-(line|letter)(\\{|,)");
249 | m = p.matcher(css);
250 | while (m.find()) {
251 | m.appendReplacement(sb, ":first-" + m.group(1).toLowerCase() + " " + m.group(2));
252 | }
253 | m.appendTail(sb);
254 | css = sb.toString();
255 |
256 | // no space after the end of a preserved comment
257 | css = css.replaceAll("\\*/ ", "*/");
258 |
259 | // If there are multiple @charset directives, push them to the top of the file.
260 | sb = new StringBuffer();
261 | p = Pattern.compile("(?i)^(.*)(@charset)( \"[^\"]*\";)");
262 | m = p.matcher(css);
263 | while (m.find()) {
264 | String s = m.group(1).replaceAll("\\\\", "\\\\\\\\").replaceAll("\\$", "\\\\\\$");
265 | m.appendReplacement(sb, m.group(2).toLowerCase() + m.group(3) + s);
266 | }
267 | m.appendTail(sb);
268 | css = sb.toString();
269 |
270 | // When all @charset are at the top, remove the second and after (as they are completely ignored).
271 | sb = new StringBuffer();
272 | p = Pattern.compile("(?i)^((\\s*)(@charset)( [^;]+;\\s*))+");
273 | m = p.matcher(css);
274 | while (m.find()) {
275 | m.appendReplacement(sb, m.group(2) + m.group(3).toLowerCase() + m.group(4));
276 | }
277 | m.appendTail(sb);
278 | css = sb.toString();
279 |
280 | // lowercase some popular @directives (@charset is done right above)
281 | sb = new StringBuffer();
282 | p = Pattern.compile("(?i)@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)");
283 | m = p.matcher(css);
284 | while (m.find()) {
285 | m.appendReplacement(sb, '@' + m.group(1).toLowerCase());
286 | }
287 | m.appendTail(sb);
288 | css = sb.toString();
289 |
290 | // lowercase some more common pseudo-elements
291 | sb = new StringBuffer();
292 | p = Pattern.compile("(?i):(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)");
293 | m = p.matcher(css);
294 | while (m.find()) {
295 | m.appendReplacement(sb, ':' + m.group(1).toLowerCase());
296 | }
297 | m.appendTail(sb);
298 | css = sb.toString();
299 |
300 | // lowercase some more common functions
301 | sb = new StringBuffer();
302 | p = Pattern.compile("(?i):(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\\(");
303 | m = p.matcher(css);
304 | while (m.find()) {
305 | m.appendReplacement(sb, ':' + m.group(1).toLowerCase() + '(');
306 | }
307 | m.appendTail(sb);
308 | css = sb.toString();
309 |
310 | // lower case some common function that can be values
311 | // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
312 | sb = new StringBuffer();
313 | p = Pattern.compile("(?i)([:,\\( ]\\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)");
314 | m = p.matcher(css);
315 | while (m.find()) {
316 | m.appendReplacement(sb, m.group(1) + m.group(2).toLowerCase());
317 | }
318 | m.appendTail(sb);
319 | css = sb.toString();
320 |
321 | // Put the space back in some cases, to support stuff like
322 | // @media screen and (-webkit-min-device-pixel-ratio:0){
323 | css = css.replaceAll("(?i)\\band\\(", "and (");
324 |
325 | // Remove the spaces after the things that should not have spaces after them.
326 | css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1");
327 |
328 | // remove unnecessary semicolons
329 | css = css.replaceAll(";+}", "}");
330 |
331 | // Replace 0(px,em,%) with 0.
332 | css = css.replaceAll("(?i)(^|[^.0-9])(?:0?\\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)", "$10");
333 | // Replace x.0(px,em,%) with x(px,em,%).
334 | css = css.replaceAll("([0-9])\\.0(px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz| |;)", "$1$2");
335 |
336 | // Replace 0 0 0 0; with 0.
337 | css = css.replaceAll(":0 0 0 0(;|})", ":0$1");
338 | css = css.replaceAll(":0 0 0(;|})", ":0$1");
339 | css = css.replaceAll(":0 0(;|})", ":0$1");
340 |
341 |
342 | // Replace background-position:0; with background-position:0 0;
343 | // same for transform-origin
344 | sb = new StringBuffer();
345 | p = Pattern.compile("(?i)(background-position|webkit-mask-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})");
346 | m = p.matcher(css);
347 | while (m.find()) {
348 | m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2));
349 | }
350 | m.appendTail(sb);
351 | css = sb.toString();
352 |
353 | // Replace 0.6 to .6, but only when preceded by : or a white-space
354 | css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2");
355 |
356 | // Shorten colors from rgb(51,102,153) to #336699
357 | // This makes it more likely that it'll get further compressed in the next step.
358 | p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)");
359 | m = p.matcher(css);
360 | sb = new StringBuffer();
361 | while (m.find()) {
362 | String[] rgbcolors = m.group(1).split(",");
363 | StringBuffer hexcolor = new StringBuffer("#");
364 | for (i = 0; i < rgbcolors.length; i++) {
365 | int val = Integer.parseInt(rgbcolors[i]);
366 | if (val < 16) {
367 | hexcolor.append("0");
368 | }
369 |
370 | // If someone passes an RGB value that's too big to express in two characters, round down.
371 | // Probably should throw out a warning here, but generating valid CSS is a bigger concern.
372 | if (val > 255) {
373 | val = 255;
374 | }
375 | hexcolor.append(Integer.toHexString(val));
376 | }
377 | m.appendReplacement(sb, hexcolor.toString());
378 | }
379 | m.appendTail(sb);
380 | css = sb.toString();
381 |
382 | // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
383 | // the color is not preceded by either ", " or =. Indeed, the property
384 | // filter: chroma(color="#FFFFFF");
385 | // would become
386 | // filter: chroma(color="#FFF");
387 | // which makes the filter break in IE.
388 | // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} )
389 | // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
390 | p = Pattern.compile("(\\=\\s*?[\"']?)?" + "#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})");
391 |
392 | m = p.matcher(css);
393 | sb = new StringBuffer();
394 | int index = 0;
395 |
396 | while (m.find(index)) {
397 |
398 | sb.append(css.substring(index, m.start()));
399 |
400 | boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));
401 |
402 | if (isFilter) {
403 | // Restore, as is. Compression will break filters
404 | sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
405 | } else {
406 | if( m.group(2).equalsIgnoreCase(m.group(3)) &&
407 | m.group(4).equalsIgnoreCase(m.group(5)) &&
408 | m.group(6).equalsIgnoreCase(m.group(7))) {
409 |
410 | // #AABBCC pattern
411 | sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
412 |
413 | } else {
414 |
415 | // Non-compressible color, restore, but lower case.
416 | sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
417 | }
418 | }
419 |
420 | index = m.end(7);
421 | }
422 |
423 | sb.append(css.substring(index));
424 | css = sb.toString();
425 |
426 | // Replace #f00 -> red
427 | css = css.replaceAll("(:|\\s)(#f00)(;|})", "$1red$3");
428 | // Replace other short color keywords
429 | css = css.replaceAll("(:|\\s)(#000080)(;|})", "$1navy$3");
430 | css = css.replaceAll("(:|\\s)(#808080)(;|})", "$1gray$3");
431 | css = css.replaceAll("(:|\\s)(#808000)(;|})", "$1olive$3");
432 | css = css.replaceAll("(:|\\s)(#800080)(;|})", "$1purple$3");
433 | css = css.replaceAll("(:|\\s)(#c0c0c0)(;|})", "$1silver$3");
434 | css = css.replaceAll("(:|\\s)(#008080)(;|})", "$1teal$3");
435 | css = css.replaceAll("(:|\\s)(#ffa500)(;|})", "$1orange$3");
436 | css = css.replaceAll("(:|\\s)(#800000)(;|})", "$1maroon$3");
437 |
438 | // border: none -> border:0
439 | sb = new StringBuffer();
440 | p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|})");
441 | m = p.matcher(css);
442 | while (m.find()) {
443 | m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2));
444 | }
445 | m.appendTail(sb);
446 | css = sb.toString();
447 |
448 | // shorter opacity IE filter
449 | css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
450 |
451 | // Find a fraction that is used for Opera's -o-device-pixel-ratio query
452 | // Add token to add the "\" back in later
453 | css = css.replaceAll("\\(([\\-A-Za-z]+):([0-9]+)\\/([0-9]+)\\)", "($1:$2___YUI_QUERY_FRACTION___$3)");
454 |
455 | // Remove empty rules.
456 | css = css.replaceAll("[^\\}\\{/;]+\\{\\}", "");
457 |
458 | // Add "\" back to fix Opera -o-device-pixel-ratio query
459 | css = css.replaceAll("___YUI_QUERY_FRACTION___", "/");
460 |
461 | // TODO: Should this be after we re-insert tokens. These could alter the break points. However then
462 | // we'd need to make sure we don't break in the middle of a string etc.
463 | if (linebreakpos >= 0) {
464 | // Some source control tools don't like it when files containing lines longer
465 | // than, say 8000 characters, are checked in. The linebreak option is used in
466 | // that case to split long lines after a specific column.
467 | i = 0;
468 | int linestartpos = 0;
469 | sb = new StringBuffer(css);
470 | while (i < sb.length()) {
471 | char c = sb.charAt(i++);
472 | if (c == '}' && i - linestartpos > linebreakpos) {
473 | sb.insert(i, '\n');
474 | linestartpos = i;
475 | }
476 | }
477 |
478 | css = sb.toString();
479 | }
480 |
481 | // Replace multiple semi-colons in a row by a single one
482 | // See SF bug #1980989
483 | css = css.replaceAll(";;+", ";");
484 |
485 | // restore preserved comments and strings
486 | for(i = 0, max = preservedTokens.size(); i < max; i++) {
487 | css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString());
488 | }
489 |
490 | // Trim the final string (for any leading or trailing white spaces)
491 | css = css.trim();
492 |
493 | // Write the output...
494 | out.write(css);
495 | }
496 | }
--------------------------------------------------------------------------------
/java/io/aviso/twixt/shims/CompilerShim.java:
--------------------------------------------------------------------------------
1 | package io.aviso.twixt.shims;
2 |
3 | import com.google.javascript.jscomp.*;
4 | import com.google.javascript.jscomp.Compiler;
5 |
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.nio.charset.StandardCharsets;
9 | import java.util.ArrayList;
10 | import java.util.Arrays;
11 | import java.util.Collections;
12 | import java.util.List;
13 |
14 |
15 | /**
16 | * A shim for creating and executing the Google Closure JavaScript compiler.
17 | */
18 | public class CompilerShim {
19 |
20 | public static List runCompiler(CompilerOptions options,
21 | CompilationLevel level,
22 | String filePath,
23 | InputStream source) throws IOException {
24 | Compiler compiler = new Compiler();
25 |
26 | compiler.disableThreads();
27 |
28 | level.setOptionsForCompilationLevel(options);
29 |
30 | SourceFile sourceFile = SourceFile.fromInputStream(filePath, source, StandardCharsets.UTF_8);
31 |
32 | List externs = Collections.emptyList();
33 | List sources = Arrays.asList(sourceFile);
34 |
35 | Result result = compiler.compile(externs, sources, options);
36 |
37 | List resultTuple = new ArrayList();
38 |
39 | if (result.success) {
40 | resultTuple.add(null);
41 | resultTuple.add(compiler.toSource());
42 | } else {
43 | resultTuple.add(result.errors);
44 | }
45 |
46 | return resultTuple;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/manual/_template.html:
--------------------------------------------------------------------------------
1 | title: {{ page_title }}
2 | ---
3 | {{ content }}
4 |
--------------------------------------------------------------------------------
/manual/dexy.yaml:
--------------------------------------------------------------------------------
1 | docs:
2 | - "*/*.ad|jinja|yamlargs|asciidoctor|soups":
3 | - asciidoctor: { ext : .html , args: '-s' }
4 | - apply-ws-to-content: True
5 | - code
6 |
7 | meta:
8 | - "*/meta.edn":
9 | - output: True
10 | code:
11 | - "*/*.clj"
--------------------------------------------------------------------------------
/manual/en/caching.ad:
--------------------------------------------------------------------------------
1 | title: Caching
2 | ---
3 |
4 | It is desirable to have Twixt be able to serve-up files quickly, especially in production.
5 | However, that is counter-balanced by the need to ensure the *correct* content is served.
6 |
7 | The Twixt options specifies the location of the file system cache used to store compiled assets.
8 |
9 | The location of this is specified in the key `[:cache :cache-dir]` in the Twixt options.
10 |
11 | The cache-dir is the root level directory; actually caching occurs in sub-folders for "development"
12 | or "production".
13 |
14 | The cache folder is not cleared on startup; this allow compilation and other transformations
15 | from previous executions
16 |
17 | Generally, when a request for an asset is received, and the asset is in the cache, then
18 | the response is served from the cache.
19 |
20 | Periodically (as defined by the `[:cache :cache-interval-ms]` options key), a request for an asset
21 | will perform a check against the dependencies of the asset; if any of the underlying resources have changed, then the
22 | cache entry for the asset will be discarded and rebuilt.
23 |
24 | In production, you may want to set the cache interval to be much higher than in development.
25 |
26 | On concern is that a change in configuration can also invalidate caches.
27 | For example, if you upgrade to a newer version of Twixt, or make changes to the configuration
28 | of Twixt between executions, then the cached versions of the assets may not match what
29 | Twixt would generate.
30 |
31 | You should take this into account while developing and especially when deploying: it may be necessary
32 | to manually delete the file system cache as part of deploying an application upgrade.
33 |
34 |
--------------------------------------------------------------------------------
/manual/en/configuration.ad:
--------------------------------------------------------------------------------
1 | title: Configuration
2 | ---
3 |
4 | Twixt's configuration is used to determine where to locate asset resources on the classpath,
5 | and what folder to serve them under. It also maps file name extensions to MIME types, and
6 | configures the file system cache.
7 |
8 | The default options:
9 |
10 | [source,clojure]
11 | ----
12 | (def default-options
13 | {:path-prefix "/assets/"
14 | :content-types mime/default-mime-types
15 | :resolver-factories [...]
16 | :twixt-template {}
17 | :content-transformers {}
18 | :js-optimizations :default
19 | :compressable #{"text/*" "application/edn" "application/json"}
20 | :cache {:cache-dir (System/getProperty "twixt.cache-dir" (System/getProperty "java.io.tmpdir"))
21 | :check-interval-ms 1000}
22 | :exports {:interval-ms 5000
23 | :output-dir "resources/public"
24 | :output-uri ""
25 | :assets []}}
26 | ----
27 |
28 | You can override :path-prefix to change the root URL for assets; / is an acceptable value.
29 |
30 | The :content-types key maps file extensions to MIME types.
31 |
32 | The :content-transformers key is a map of content type keys (as strings, such as "text/coffeescript") to a
33 | transformation function; The CoffeeScript, Jade, and Less compilers operate by adding entries to :content-types and :content-transformers.
34 |
35 | The :compressable key is a set used to identify which content types are compressable; note the use of the /* suffix to indicate
36 | that all text content types are compressable. Anything not explicitly compressable is considered non-compressable.
37 |
38 | The :twixt-template key is a map that provides default values for the :twixt request key.
39 | This is often used to provide information specific to particular content transformers.
40 |
41 | The :js-optimisations key is a keyword that specifies which Google Closure compilation level should be used to minimize
42 | JavaScript files.
43 | This may be one of :default, :none, :simple, :whitespace, :advanced.
44 |
45 | The :default option corresponds to :none in development mode, and :simple in production mode.
46 |
47 | The :resolver-factories key is a list of functions that create Asset resolvers.
48 | Each factory function is passed the Twixt configuration and returns an Asset handler.
49 | The two built-in factories search for standard resources (under META-INF/assets) and WebJars resources (under META-INF/resources/webjars).
50 |
51 | The :cache key defines where the file system cache (of compiled assets) is stored, and
52 | a what interval should invalidation checks occur (invalidation checks detect when any of the files from which an asset
53 | is obtained and compiled have changed).
54 |
55 | :exports is used to configure static exports. A preset list of exports are located by their asset name and output under
56 | the configured output directory.
57 |
58 | Each value in the :assets collection can be a string, or a tuple of asset name and output alias.
59 | This allows an asset to be exported and renamed at the same time.
60 |
61 | For example, to make the asset css/site.less visible as css/site.css, you would supply a tuple:
62 |
63 | [source,clojure]
64 | ----
65 | (-> default-options
66 | (update-in [:exports :assets] conj ["css/site.less" "css/site.css"]))
67 | ----
68 |
69 | This would locate the resource file META-INF/assets/css/site.less, compile it from Less to CSS, and copy it
70 | to resources/public/css/site.css.
71 | A call to the io.aviso.twixt/get-asset-uri function would return the URI "/css/site.css",
72 | which is exactly what the client will need to read the downloaded file.
73 |
74 | Assets are scanned for changes at intervals, and exports occur automatically.
75 | The thread which performs asset exports is started as needed, and will shutdown
76 | once the Ring handler pipeline is garbage collected; you should generally include a call to (System/gc) to
77 | ensure this happens promptly.
78 |
79 | The output-uri is the URI corresponding to the output-dir.
80 |
81 | Twixt does *not* provide a resource handler (to pick up files exported to the output directory) on its own; this is something
82 | you must supply when setting up your Ring pipeline.
83 |
84 |
85 |
--------------------------------------------------------------------------------
/manual/en/cssmin.ad:
--------------------------------------------------------------------------------
1 | title: CSS Minification
2 | ---
3 |
4 | CSS minification is enabled by default in production mode, but is normally disabled in development mode.
5 |
6 | The :minimize-css key in the link:configuration.html[Twixt options] overrides the default.
7 |
8 |
--------------------------------------------------------------------------------
/manual/en/direct-uris.ad:
--------------------------------------------------------------------------------
1 | title: Direct URIs
2 | ---
3 |
4 | Sometimes it is not possible to determine the full asset URI ahead of time;
5 | a common example would be a client-side
6 | framework, such as http://angularjs.org[AngularJS] that wants to load HTML templates dynamically, at runtime. It will know
7 | the path to the asset, but will not know the checksum.
8 |
9 | In this case, an *optional* Ring middleware can be used: +wrap-with-asset-redirector+.
10 |
11 | This middleware identifies requests that match existing assets and responds with a 302 redirect to the proper asset URL.
12 | For example, the asset stored as +META-INF/assets/blueant/cla.html+ can be accessed as +/blueant/cla.html+, and will be sent a redirect
13 | to +/assets/123abc/blueant/cla.html+.
14 |
--------------------------------------------------------------------------------
/manual/en/exceptions.ad:
--------------------------------------------------------------------------------
1 | title: Exception Reporting
2 | ---
3 |
4 | Twixt features a very readable HTML exception report page, which displays:
5 |
6 | * The entire stack of nested exceptions, top to bottom
7 | * The stack trace for only the root exception
8 | * Demangled namespace and function names for Clojure stack frames: `io.aviso.twixt/new-twixt/reify/middleware/fn` instead of
9 | `io.aviso.twixt$new_twixt$reify__954$middleware__959$fn__960.invoke()`.
10 | * The contents of the Ring request map
11 | * All JVM system properties
12 |
13 | Twixt provides middleware that wraps the request in a `try` block; exceptions are caught and converted to the HTML exception report, which is passed down to the client.
14 | Twixt does not attempt to perform content negotiation: it always sends a `text/html` stream.
15 |
--------------------------------------------------------------------------------
/manual/en/getting-started.ad:
--------------------------------------------------------------------------------
1 | title: Getting Started
2 | ---
3 |
4 | Twixt serves resources located on the classpath, in the +META-INF/assets/+ folder.
5 | The contents of this folder is accessible to clients, by default, via the URL +/assets/+.
6 |
7 | By design, assets are segregated from the rest of your code.
8 | This prevents a malicious client from directly accessing your code or configuration files.
9 | Anything outside the +META-INF/assets/+ folder is inaccessible via Twixt.
10 |
11 | Twixt maps file extensions to MIME types; it will then transform certain MIME types; for example +.coffee+ files are compiled to JavaScript.
12 |
13 | [source,clojure]
14 | ----
15 | (ns example.app
16 | (:use compojure.core
17 | ring.adapter.jetty
18 | [io.aviso.twixt.startup :as startup]))
19 |
20 | ;;; Use Compojure to map routes to handlers
21 | (defroutes app ...)
22 |
23 | ;;; Create twixt wrappers and handler to handle /asset/ URLs;
24 | ;;; true -> in development mode
25 | (def app-handler
26 | (startup/wrap-with-twixt app true)
27 |
28 | ;;; Use the Ring Jetty adapter to serve your application
29 | (run-jetty app-handler)
30 | ----
31 |
32 | NOTE: Twixt changes its behavior in a number of ways in development mode (as opposed to the normal
33 | production mode).
34 | For example, Less compilation will pretty-print the generated HTML markup in development
35 | mode.
36 | In production mode, the markup omits all unnecessary white space.
37 |
38 | The Twixt middleware intercepts requests for the +/assets/+ URI that map to actual files; non-matching requests, or
39 | requests for assets that do not exist, are delegated down to the wrapped handlers.
40 |
41 | In development mode, Twixt will write compiled files to the file system (you can configure where if you like).
42 | On a restart of the application, it will use those cached files if the source files have not changed. This is important,
43 | as compilation of some resources, such as CoffeeScript, can take several seconds (due to the speed, or lack thereof, of
44 | the Rhino JavaScript engine).
45 |
46 | The Twixt API includes alternate functions for constructing both the Ring middleware, and Twixt's own
47 | asset pipeline; this allows you to add new features, or exclude unwanted features. Please reference the
48 | code to see how to configure Twixt options, assemble the Twixt asset pipeline, and finally, provide the necessary
49 | Ring middleware.
--------------------------------------------------------------------------------
/manual/en/index.ad:
--------------------------------------------------------------------------------
1 | title: About
2 | ---
3 |
4 | == Awesome asset management for Clojure web applications
5 |
6 | %={:type :github :user "AvisoNovate" :repo "twixt"}%
7 |
8 | image:http://clojars.org/io.aviso/twixt/latest-version.svg[Clojars Project, link="http://clojars.org/io.aviso/twixt"]
9 |
10 | image:https://drone.io/github.com/AvisoNovate/twixt/status.png[Build Status, link="https://drone.io/github.com/AvisoNovate/twixt"]
11 |
12 | Twixt is an extensible asset pipeline for use in Clojure web applications.
13 | It is designed to complement an application built using Ring and related libraries, such as Compojure.
14 | Twixt provides content transformation (such as Less to CSS), support for efficient immutable resources,
15 | and best-of-breed exception reporting.
16 |
17 | Twixt draws inspiration from http://tapestry.apache.org[Apache Tapestry] and https://github.com/edgecase/dieter[Dieter].
18 |
19 | Twixt currently supports:
20 |
21 | * link:exceptions.html[Best-of-breed exception report]
22 | * link:http://coffeescript.org/[CoffeeScript] to JavaScript compilation (using Rhino) - including source maps
23 | * JavaScript Minimization (using https://developers.google.com/closure/compiler/[Google Closure])
24 | * Jade to HTML compilation (using https://github.com/neuland/jade4j[jade4j])
25 | * Less to CSS compilation (using https://github.com/SomMeri/less4j[less4j])
26 | * link:stacks.html[JavaScript and CSS aggregation]
27 | * File system caching of compiled content
28 | * Automatic GZip compression
29 | * Automatic CSS Minification (using http://yui.github.io/yuicompressor/[YUICompressor])
30 | * Works with link:webjars.html[WebJars]
31 |
32 | == Stability
33 |
34 | *Alpha*: Many features are not yet implemented and the code is likely to change in many ways going forward ... but still very useful!
35 |
--------------------------------------------------------------------------------
/manual/en/jade.ad:
--------------------------------------------------------------------------------
1 | title: Jade
2 | ---
3 |
4 | link:http://jade-lang.com/[Jade] is a wonderful template engine ... as long as you
5 | are comfortable with significant indentation.
6 |
7 | Like many such tools, the real power comes from being able to extend Jade in various ways.
8 |
9 | == twixt helper
10 |
11 | Twixt places a helper object, +twixt+, into scope for your templates. +twixt+ supplies a single method, +uri+.
12 | You can pass the +uri+ method a relative path, or an absolute path (starting with a slash).
13 |
14 | ----
15 | img(src=twixt.uri("logo.png"))
16 | ----
17 |
18 | WARNING: When the path is relative, it is evaluated relative to the main Jade asset
19 | (and explicitly not relative to any +include+ -ed Jade sources).
20 |
21 | This will output a fully qualified asset URI:
22 |
23 | ----
24 |
25 | ----
26 |
27 | The +uris+ method accepts an array of paths; an returns an array of individual asset URIs.
28 | This is useful when the URI references a stack (which will be a single asset in production,
29 | but multiple assets in development).
30 |
31 | ---
32 | for script in twixt.uris(["js/app.stack"])
33 | script(src=script)
34 | ----
35 |
36 | == Defining your own helpers
37 |
38 | It is possible to define your own helper objects.
39 |
40 | Helper objects are defined inside the Twixt context under keys +:jade+ +:helpers+.
41 | This is a map of _string_ keys to creator functions.
42 |
43 | Each creator function is passed the main Jade asset, and the Twixt context.
44 | It uses this to initialize and return a helper object.
45 | A new set of helper objects is created for each individual Jade compilation.
46 |
47 | Generally, you will want to define a protocol, then use +reify+. For example, this is the implementation of the +twixt+ helper:
48 |
49 | ----
50 | (defprotocol TwixtHelper
51 | "A Jade4J helper object that is used to allow a template to resolve asset URIs."
52 | (uri
53 | [this path]
54 | "Used to obtain the URI for a given asset, identified by its asset path.
55 | The path may be relative to the currently compiling asset, or may be absolute (with a leading slash).
56 |
57 | Throws an exception if the asset it not found.")
58 | (uris
59 | [this paths]
60 | "Used to obtain multiple URIs for any number of assets, each identified by a path.
61 |
62 | paths is a seq of asset path strings (eitehr relative to the current asset, or absolute).
63 |
64 | *Added in 0.1.21*"))
65 |
66 | (defn- create-twixt-helper
67 | [asset context]
68 | (reify TwixtHelper
69 | (uri [_ path]
70 | (twixt/get-asset-uri context (complete-path asset path)))
71 | (uris [_ paths]
72 | (->> paths
73 | (map (partial complete-path asset))
74 | (mapcat (partial twixt/get-asset-uris context))))))
75 | ----
76 |
77 | NOTE: Any asset URI will cause the asset in question to be added as a dependency of the main Jade template. This means
78 | that changing the referenced asset will cause the Jade template to be re-compiled. This makes sense: changing an image
79 | file will change the URI for the image file (due to content snapshotting), which means that the Jade output should also change.
80 |
81 | Creator functions can be added to the Twixt context using Ring middleware:
82 |
83 | ----
84 | (handler (assoc-in request [:twixt :jade :helpers "adrotation"]
85 | create-ad-rotation-helper))
86 | ----
87 |
88 | However, more frequently, you will just add to the Twixt options in your application's startup code:
89 |
90 | ----
91 | (assoc-in twixt/default-options [:twixt-template :jade :helpers "adrotation"]
92 | create-ad-rotation-helper))
93 | ----
94 |
95 | This +:twixt-template+ key is used to create the +:twixt+ Ring request key.
96 |
97 | == Defining your own variables
98 |
99 | Variables are much the same as helpers, with two differences:
100 |
101 | * The key is +:variables+ (under +:jade+, in the Twixt context)
102 | * The value is the exact object to expose to the template
103 |
104 | You can expose Clojure functions as variables if you wish; the Jade template should use +func.invoke()+ to call the function.
105 |
106 | == Helper / Variable pitfalls
107 |
108 | The main issue with helpers and variables relates to cache invalidation.
109 | Twixt bases cache invalidation entirely on the contents of the underlying files.
110 | There is no way for Twixt to know to invalidate the cache just because the implementation
111 | of a helper has changed, even if that means different markup is being rendered. This
112 | is one of the primary reasons that link:caching.html[disk cache is disabled in production].
113 |
114 | There is currently an ambiguity that comes into play when the referenced asset is a compressable file type (e.g., not an image
115 | file). This can cause the Jade compiler to generate a compressed URI that, for a different request and client, will not be useful.
116 |
117 |
--------------------------------------------------------------------------------
/manual/en/jsmin.ad:
--------------------------------------------------------------------------------
1 | title: JavaScript Minimization
2 | ---
3 |
4 | JavaScript minimization is enabled by default only in production mode.
5 |
6 | It is set up for _simple optimizations_; this removes whitespace and may shorten variable and function names.
7 |
8 | The optimization level is configurable using the +:js-optimisations+ key as described in %={:type :link :page "configuration"}.
9 |
10 | It is recommended that you make use of JavaScript stacks; this allows the compiler to only be executed once
11 | on a larger sample of JavaScript.
--------------------------------------------------------------------------------
/manual/en/meta.edn:
--------------------------------------------------------------------------------
1 | {:name "Twixt Manual"
2 | :description "Awesome asset management for Clojure web applications."
3 | :github "AvisoNovate/twixt"
4 | :api "http://howardlewisship.com/io.aviso/twixt/"
5 | :pages ["index"
6 | "getting-started"
7 | "uris"
8 | "exceptions"
9 | "jade"
10 | "stacks"
11 | "caching"
12 | "jsmin"
13 | "cssmin"
14 | "configuration"
15 | "direct-uris"
16 | "webjars"
17 | "notes"]}
18 |
--------------------------------------------------------------------------------
/manual/en/notes.ad:
--------------------------------------------------------------------------------
1 | title: Notes
2 | ---
3 |
4 | == Future Plans
5 |
6 | The goal is to achieve at least parity with Apache Tapestry, plus some additional features specific to Clojure. This means:
7 |
8 | * E-Tags support
9 | * ClojureScript compilation
10 | * CSS Minification
11 | * RequireJS support and AMD modules
12 | * Break out the the Less, Jade, CoffeeScript, exception reporting support, etc. into a-la-carte modules
13 | * "Warm up" the cache at startup (in production)
14 |
15 |
16 | == A note about feedback
17 |
18 | http://tapestryjava.blogspot.com/2013/05/once-more-feedback-please.html[Feedback] is very important to me; I often find
19 | Clojure just a bit frustrating, because if there is an error in your code, it can be a bit of a challenge to track the problem
20 | backwards from the failure to the offending code. Part of this is inherent in functional programming, part of it is related to lazy evaluation,
21 | and part is the trade-off between a typed and untyped language.
22 |
23 | In any case, it is very important to me that when thing go wrong, you are provided with a detailed description of the failure.
24 | Twixt has a mechanism for tracking the operations it is attempting, to give you insight into what exactly failed if there
25 | is an error. For example, (from the test suite):
26 |
27 | ----
28 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script An exception has occurred:
29 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script [ 1] - Invoking handler (that throws exceptions)
30 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script [ 2] - Accessing asset `invalid-coffeescript.coffee'
31 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script [ 3] - Compiling `META-INF/assets/invalid-coffeescript.coffee' to JavaScript
32 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script META-INF/assets/invalid-coffeescript.coffee:6:1: error: unexpected INDENT
33 | argument: dep2
34 | ^^^^^^
35 | java.lang.RuntimeException: META-INF/assets/invalid-coffeescript.coffee:6:1: error: unexpected INDENT
36 | argument: dep2
37 | ^^^^^^
38 | ....
39 | ----
40 |
41 | In other words, when there's a failure, Twixt can tell you the steps that led up the failure, which is 90% of solving the problem in the first place.
42 |
43 | Twixt's exception report captures all of this and presents it as readable HTML.
44 | The exception report page also does a decent job of de-mangling Java class names to Clojure namespaces and function names.
45 |
46 | == How does Twixt differ from Dieter?
47 |
48 | On the application I was building, I had a requirement to deploy as a JAR; Dieter expects all the assets to be on the filesystem; I spent some time attempting to hack the Dieter code to allow resources on the classpath as well.
49 | When that proved unsuccessful, I decided to build out something a bit more ambitious, that would support the features that have accumulated in Tapestry over the last few years.
50 |
51 | Twixt also embraces http://www.infoq.com/presentations/Clojure-Large-scale-patterns-techniques[system as transient state], meaning nothing is stored statically.
52 |
53 | Twixt will grow further apart from Dieter as the more advanced pieces are put into place.
--------------------------------------------------------------------------------
/manual/en/stacks.ad:
--------------------------------------------------------------------------------
1 | title: Stacks
2 | ---
3 |
4 | In development, you often want to have many small source files downloaded individually to the browser.
5 | This is simpler to debug, and faster ... a change to one file will be a small recompile of just that file.
6 |
7 | In production, it's a different story; you want the client to make as few requests to the server as possible.
8 |
9 | This can be accomplished using _stacks_.
10 |
11 | Stacks allow you to group together related files of the same type into a single asset. Commonly, this is used
12 | to aggregate JavaScript or CSS.
13 |
14 | In development mode, you will see the individual files of the stack;
15 | in production mode, the stack is represented by a single URI which maps to the aggregated content of all the
16 | files in the stack.
17 |
18 | A stack file is written in https://github.com/edn-format/edn[EDN].
19 | Each stack file contains a +:content-type+ key, and a +:components+ key.
20 | Stack files have a +.stack+ extension.
21 |
22 | [source,clojure]
23 | ----
24 | {:content-type "text/css"
25 | :components "bootstrap3/bootstrap.css"
26 | "app.less"
27 | "ie-fixes.less"}
28 | ----
29 |
30 | When using stacks, you will want a slight tweak to your page template:
31 |
32 | [source,clojure]
33 | ----
34 | (defhtml index
35 | [{context :twixt :as request}]
36 | (html
37 | (doctype :html5
38 | [:html
39 | [:head
40 | [:title "My Clojure App"]
41 | (apply include-css (get-asset-uris context "css/app-css.stack"))
42 | ...
43 | ----
44 |
45 | Since +get-asset-uris+ will return a collection of URIs (unlike +get-asset-uri+ which always returns just one),
46 | we must change +include-css+ to +apply include-css+.
47 |
48 | This template will work in development (+get-asset-uris+ returning several URIs) and in production (just a
49 | single URI).
50 |
51 | It is possible for stacks to include other stacks as components.
52 |
53 | Stack components are _included_ not _imported_; if a component asset is listed more than once, its content will
54 | be aggregated more than once.
--------------------------------------------------------------------------------
/manual/en/uris.ad:
--------------------------------------------------------------------------------
1 | title: URIs
2 | ---
3 |
4 | At its core, Twixt is a set of Ring middleware that maps certain URI patterns to matching files on the classpath,
5 | and does some transformations along the way.
6 |
7 | Currently, the mapping is very straightforward: the path +/assets/123abc/css/style.less+ is mapped to resource
8 | +META-INF/assets/css/style.less+ which is read into memory and transformed from Less to CSS.
9 | Embedded in the middle of the URL is the content checksum for the file (+123abc+).
10 |
11 | NOTE: Placing this information directly into the URI is called _fingerprinting_.
12 |
13 | In your application, assets will change during development, or between production deployments. The URIs provided to
14 | the client agent (the web browser) is a _resource_; on the web, resources are immutable, even though
15 | in your workspace, files change all the time.
16 | The checksum in the URI is based on the actual content of the file;
17 | whenever the underlying content changes, then a new checksum, new URI, and therefore, new resource will be referenced by the URI.
18 |
19 | Twixt sets headers to indicate a far-future expiration date for the resource;
20 | the upshot of which is that, once the resource for an asset is downloaded to the client browser, the browser will not ask for it again.
21 | This is great for overall performance, since it reduces the amount of network I/O
22 | the client will encounter, especially on subsequent visits to the same site.
23 |
24 | It also reduces the load on the server in two ways:
25 |
26 | * Fewer requests will need to be sent to the server as client will use their local cache.
27 | * Content can be cached by intermediary servers.
28 |
29 | The checksum has a optional "z" prefix; this indicates a GZip compressed resource.
30 |
31 | On a request that renders markup, Twixt will detect if the client supports GZip compression.
32 | If so, then for assets where compression makes sense (such as JavaScript, and
33 | specifically excluding image formats) then Twixt will generate an alternate URI that
34 | indicates access to the compressed asset.
35 |
36 | Because of this, when referencing assets inside your templates, you must pass paths (relative to +META-INF/assets+)
37 | through Twixt to get URIs that will work in the browser:
38 |
39 | [source,clojure]
40 | ----
41 | (defhtml index
42 | [{context :twixt :as request}]
43 | (html
44 | (doctype :html5
45 | [:html
46 | [:head
47 | [:title "My Clojure App"]
48 | (include-css (get-asset-uri context "css/style.less"))
49 | ...
50 | ----
51 |
52 | The parameter to +defhtml+ is the Ring request map; Twixt has injected middleware that provides the Twixt context under
53 | the +:twixt+ key.
54 |
55 | +get-asset-uri+ is defined in the +io.aviso.twixt+ namespace.
56 |
57 | Twixt must do all necessary compilations and other transformations, to arrive at final content for which a checksum
58 | can be computed.
59 | Although this can slow the initial HTML render, it is also good because any exceptions, such as compilation errors, will occur immediately,
60 | rather than when the asset's content is later retrieved by the client.
61 |
62 | When a client requests an asset that exists, but supplies an incorrect checksum,
63 | Twixt will respond with a 301 redirect HTTP response,
64 | directing the client to the correct resource (with the correct checksum).
65 | This is an extremely unlikely scenario that would involve a running client in a race with a redeployed application.
--------------------------------------------------------------------------------
/manual/en/webjars.ad:
--------------------------------------------------------------------------------
1 | title: WebJars
2 | ---
3 |
4 | link:http://www.webjars.org/[Web Jars] is any easy way to include any number of pre-packaged sets of resources
5 | as a JAR file.
6 | Why mess around with getting a distribution, extracting it, and possibly checking those files into your project's
7 | source code control?
8 | Just add the WebJar to the classpath (inside your project.clj):
9 |
10 | [source,clojure]
11 | ----
12 | [org.webjars/d3js "3.5.5-1"]
13 | ----
14 |
15 | And you can now reference the files within that WebJar:
16 |
17 | [source,clojure]
18 | ----
19 | (find-asset-uri context "d3js/d3.js)
20 | ----
21 |
22 | That's it ... you don't even need to use the version number in the call to find-asset-uri; the version
23 | number needed internally to build the path to the referenced file (in this example, /META-INF/resources/webjars/d3j3/3.5.5-1/d3.js)
24 | is determined automatically.
25 |
26 | The referenced file will be read as normal and processed just like any file under META-INF/assets.
27 |
28 | Twixt uses dependencies on Bootstrap (3.3.5) and jQuery (1.11.1), which are used in the link:exceptions.html[exception report page].
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject io.aviso/twixt "0.1.25"
2 | :description "An extensible asset pipeline for Clojure web applications"
3 | :url "https://github.com/AvisoNovate/twixt"
4 | :license {:name "Apache Sofware Licencse 2.0"
5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"}
6 | :dependencies [[org.clojure/clojure "1.8.0"]
7 | [io.aviso/pretty "0.1.19"]
8 | [io.aviso/tracker "0.1.7"]
9 | [medley "0.7.0"]
10 | [ring/ring-core "1.4.0"]
11 | [com.google.javascript/closure-compiler "v20150729"]
12 | [org.mozilla/rhino "1.7.7"]
13 | [com.github.sommeri/less4j "1.14.0"]
14 | [de.neuland-bfi/jade4j "0.4.3"]
15 | [prismatic/schema "1.0.1"]
16 | [hiccup "1.0.5"]
17 | [org.webjars/bootstrap "3.3.5"]
18 | [org.webjars/webjars-locator-core "0.28"]]
19 | ;; We keep a local copy of YUICompressor's CSSCompressor; we don't want the rest of the dependency
20 | ;; since it does evil things to patch Rhino to support JavaScript compression.
21 | :java-source-paths ["java"]
22 | :javac-options ["-target" "1.6" "-source" "1.6"]
23 | :test-paths ["spec"]
24 | :plugins [[speclj "3.2.0"]
25 | [lein-shell "0.4.0"]]
26 | :shell {:commands {"scp" {:dir "doc"}}}
27 | :aliases {"deploy-doc" ["shell"
28 | "scp" "-r" "." "hlship_howardlewisship@ssh.phx.nearlyfreespeech.net:io.aviso/twixt"]
29 | "release" ["do"
30 | "clean,"
31 | "spec,",
32 | "doc,"
33 | "deploy-doc,"
34 | "deploy" "clojars"]}
35 | :codox {:src-dir-uri "https://github.com/AvisoNovate/twixt/blob/master/"
36 | :src-linenum-anchor-prefix "L"
37 | :defaults {:doc/format :markdown}}
38 | :profiles {:dev {:dependencies [[ch.qos.logback/logback-classic "1.1.3"]
39 | [speclj "3.3.1"]
40 | [ring/ring-jetty-adapter "1.4.0"]]
41 | :jvm-opts ["-Xmx1g"]}
42 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]
43 | [speclj "3.2.0"]
44 | [medley "0.6.0"]]}})
45 |
--------------------------------------------------------------------------------
/resources/META-INF/assets/twixt/exception.coffee:
--------------------------------------------------------------------------------
1 | # Adds a simple event handler that toggles body.hide-filtered when the toggle filter button is clicked.
2 |
3 | $ ->
4 | $("body").on "click", "[data-action=toggle-filter]", ->
5 | $("body").toggleClass "hide-filtered"
--------------------------------------------------------------------------------
/resources/META-INF/assets/twixt/exception.less:
--------------------------------------------------------------------------------
1 | // CSS used on the exception report view; expects Bootstrap CSS to be present as well.
2 |
3 | body {
4 | margin-top: 5px;
5 | }
6 |
7 | dl:not(.dl-horizontal) dd {
8 | margin-left: 25px;
9 | }
10 |
11 | td.function-name, td.source-location {
12 | text-align: right;
13 | }
14 |
15 | .package-name {
16 | font-size: x-small;
17 | }
18 |
19 | .filtered {
20 | color: rgb(136, 136, 136);
21 | }
22 |
23 | .hide-filtered .filtered {
24 | display: none;
25 | }
26 |
27 | .spacing-below {
28 | margin-bottom: 5px;
29 | }
--------------------------------------------------------------------------------
/resources/META-INF/twixt/invoke-coffeescript.js:
--------------------------------------------------------------------------------
1 | // Compiles CoffeeScript source to JavaScript.
2 | //
3 | // input - string containing contents of the file
4 | // filepath - name of file, used to report errors
5 | // filename - last term in filepath
6 | //
7 | // Returns { output: , sourceMap: source-map-as-string } or { exception: }
8 | function compileCoffeeScriptSource(input, filepath, filename) {
9 | try {
10 | var result = CoffeeScript.compile(input, {
11 | header: true,
12 | filename: filepath,
13 | sourceFiles: [filename],
14 | sourceMap: true,
15 | inline: true});
16 |
17 | return {
18 | output: result.js + "\n//# sourceMappingURL=" + filename + "@source.map\n" ,
19 | sourceMap: result.v3SourceMap
20 | };
21 | }
22 | catch (err) {
23 | return { exception: err.toString() };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/spec/io/aviso/twixt_spec.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt-spec
2 | (:use speclj.core
3 | clojure.pprint
4 | clojure.template
5 | [io.aviso.twixt :exclude [find-asset]]
6 | io.aviso.twixt.utils)
7 | (:require [clojure.java.io :as io]
8 | [ring.middleware.resource :as resource]
9 | [io.aviso.twixt
10 | [coffee-script :as cs]
11 | [jade :as jade]
12 | [less :as less]
13 | [ring :as ring]
14 | [startup :as startup]
15 | [stacks :as stacks]]
16 | [clojure.string :as str]
17 | [clojure.tools.logging :as l]
18 | [io.aviso.twixt :as twixt]))
19 |
20 | (defn read-as-trimmed-string
21 | [content]
22 | (-> content
23 | read-content
24 | String.
25 | .trim))
26 |
27 | (defn read-asset-content
28 | [asset]
29 | (assert asset "Can't read content from nil asset.")
30 | (->
31 | asset
32 | :content
33 | read-as-trimmed-string))
34 |
35 | (defn read-attachment-content
36 | [asset attachment-name]
37 | (assert asset "Can't read content from nil asset.")
38 | (->
39 | asset
40 | (get-in [:attachments attachment-name])
41 | :content
42 | read-as-trimmed-string))
43 |
44 | (defn read-resource-content
45 | [path]
46 | (->
47 | path
48 | io/resource
49 | read-as-trimmed-string))
50 |
51 | (defmacro should-have-content
52 | [expected actual]
53 | `(let [expected# ~expected
54 | actual# ~actual]
55 | (when (not (= expected# actual#))
56 | (-fail (format "Expected content did not match actual content:%n----%n%s%n----"
57 | actual#)))))
58 |
59 | (defn have-same-content
60 | [expected-content-resource asset]
61 | (let [expected (read-resource-content expected-content-resource)
62 | actual (read-asset-content asset)]
63 | (if (= expected actual)
64 | true
65 | (printf "Content of `%s' did not match `%s'\n----\n%s\n----\n"
66 | (:resource-path asset)
67 | expected-content-resource
68 | actual))))
69 |
70 | (defn- sorted-dependencies
71 | [asset]
72 | (->> asset :dependencies vals (map :asset-path) sort))
73 |
74 | (defn- remove-hash-from-uri
75 | [^String asset-uri]
76 | (str/join "/" (->
77 | asset-uri
78 | (.split "/")
79 | vec
80 | (assoc 2 "[hash]"))))
81 |
82 | ;; Note: updating the CoffeeScript compiler will often change the outputs, including checksums, not least because
83 | ;; the compiler injects a comment with the compiler version.
84 |
85 | (def compiled-coffeescript-checksum "54753589")
86 |
87 | (describe "io.aviso.twixt"
88 |
89 | (with-all cache-dir (format "%s/%x" (System/getProperty "java.io.tmpdir") (System/nanoTime)))
90 |
91 | (defn with-sub-cache-dir [twixt-options subdir]
92 | (update-in twixt-options [:cache :cache-dir] str "/" subdir))
93 |
94 | (defn get-asset-with-options [twixt-options asset-path]
95 | (let [pipeline (default-asset-pipeline twixt-options)]
96 | (pipeline asset-path (assoc twixt-options :asset-pipeline pipeline))))
97 |
98 | (with-all options (->
99 | default-options
100 | (assoc :development-mode true
101 | :js-optimizations :none)
102 | (assoc-in [:cache :cache-dir] @cache-dir)
103 | ;; This is usually done by the startup namespace:
104 | cs/register-coffee-script
105 | jade/register-jade
106 | less/register-less
107 | stacks/register-stacks))
108 |
109 | (with-all pipeline (default-asset-pipeline @options))
110 |
111 | (with-all twixt-context (assoc @options :asset-pipeline @pipeline))
112 |
113 | (defn find-asset
114 | [asset-path]
115 | (@pipeline asset-path @twixt-context))
116 |
117 | (context "asset pipeline"
118 |
119 | (it "returns nil when an asset is not found"
120 |
121 | (should-be-nil (@pipeline "does/not/exist.gif" @options))))
122 |
123 | (context "get-asset-uri"
124 |
125 | (it "throws IllegalArgumentException if an asset does not exist"
126 | (should-throw Exception
127 | "Asset path `does/not/exist.png' does not map to an available resource."
128 | (get-asset-uri @twixt-context "does/not/exist.png")))
129 |
130 | (it "returns the correct asset URI"
131 | (should= (str "/assets/" compiled-coffeescript-checksum "/coffeescript-source.coffee")
132 | (find-asset-uri @twixt-context "coffeescript-source.coffee")))
133 |
134 |
135 | (it "can find WebJars assets"
136 | (should
137 | (have-same-content "META-INF/resources/webjars/bootstrap/3.3.5/js/alert.js"
138 | (find-asset "bootstrap/js/alert.js"))))
139 |
140 | (it "can process compilation of WebJars assets"
141 | (should
142 | (have-same-content "expected/bootstrap-webjars.css"
143 | (find-asset "bootstrap/less/bootstrap.less")))))
144 |
145 | (context "CoffeeScript compilation"
146 |
147 | (with-all asset (@pipeline "coffeescript-source.coffee" @twixt-context))
148 |
149 | (it "can read and transform a source file"
150 |
151 | (should-not-be-nil @asset)
152 | (should-not-be-nil (:modified-at @asset))
153 |
154 | (do-template [key expected]
155 | (should= expected (key @asset)
156 | {:resource-path "META-INF/assets/coffeescript-source.coffee"
157 | :asset-path "coffeescript-source.coffee"
158 | :content-type "text/javascript"
159 | :compiled true
160 | :size 160
161 | :checksum compiled-coffeescript-checksum})))
162 | (it "has the correct compiled content"
163 | (should (have-same-content "expected/coffeescript-source.js" @asset)))
164 |
165 | (it "has the correct source.map attachment"
166 | (should-have-content
167 | (read-resource-content "expected/coffeescript-source.map")
168 | (read-attachment-content @asset "source.map")))
169 |
170 | (it "has the expected dependencies"
171 | (should= (sorted-dependencies @asset)
172 | ["coffeescript-source.coffee"]))
173 |
174 | (it "throws an exception if the source is not valid"
175 | (try
176 | (@pipeline "invalid-coffeescript.coffee" @twixt-context)
177 | (should-fail)
178 | (catch Exception e
179 | (should (-> e .getMessage (.startsWith "META-INF/assets/invalid-coffeescript.coffee:6:1: error: unexpected indentation")))))))
180 |
181 | (context "Jade compilation"
182 |
183 | (context "simple Jade source"
184 | (with-all asset (@pipeline "jade-source.jade" @twixt-context))
185 |
186 |
187 | (it "can read and transform a source file"
188 |
189 | (should= "text/html" (:content-type @asset))
190 | (should (:compiled @asset)))
191 |
192 | (it "has the correct compiled content"
193 | (should (have-same-content "expected/jade-source.html" @asset))))
194 |
195 | (context "using Jade includes"
196 |
197 | (with-all asset (@pipeline "sub/jade-include.jade" @twixt-context))
198 |
199 | (it "has the correct compiled content"
200 | (should (have-same-content "expected/jade-include.html" @asset)))
201 |
202 | (it "has the expected dependencies"
203 | (should= ["common.jade" "sub/jade-include.jade" "sub/samedir.jade"]
204 | (sorted-dependencies @asset))))
205 |
206 | (context "using Jade helpers"
207 |
208 | (with-all context' (->
209 | @twixt-context
210 | ;; The merge is normally part of the Ring code.
211 | (merge (:twixt-template @options))
212 | ;; This could be done by Ring middleware, or by
213 | ;; modifying the :twixt-template as well.
214 | (assoc-in [:jade :variables "logoTitle"] "Our Logo")))
215 |
216 |
217 | (with-all asset (@pipeline "jade-helper.jade" @context'))
218 |
219 | (it "has the correct compiled content ignoring attr order"
220 | (should (or (= " " (read-asset-content @asset))
221 | (= " " (read-asset-content @asset)))))
222 |
223 | (it "includes a dependency on the asset accessed by twixt.uri()"
224 | (should= ["aviso-logo.png" "jade-helper.jade"]
225 | (sorted-dependencies @asset)))
226 |
227 |
228 | (it "supports multiple assets via twixt.uris()"
229 | (should (have-same-content "expected/jade-uris-helper.html"
230 | (@pipeline "jade-uris-helper.jade" @context'))))))
231 |
232 | (context "Less compilation"
233 |
234 | (context "basic compilation"
235 | (with-all asset (@pipeline "sample.less" @twixt-context))
236 |
237 | (it "can read and transform a source file"
238 | (should= "text/css" (:content-type @asset))
239 | (should (:compiled @asset)))
240 |
241 | (it "has the correct compiled content"
242 | (should (have-same-content "expected/sample.css" @asset)))
243 |
244 | (it "has an attached source.map"
245 | (should-have-content (read-resource-content "expected/sample.map")
246 | (read-attachment-content @asset "source.map")))
247 |
248 | (it "includes dependencies for @import-ed files"
249 | (should= ["colors.less" "sample.less"]
250 | (sorted-dependencies @asset)))
251 |
252 | (it "throws an exception for compilation failures"
253 | (try
254 | (@pipeline "invalid-less.less" @twixt-context)
255 | (should-fail)
256 | (catch Exception e
257 | #_(-> e .getMessage println)
258 | (should (-> e .getMessage (.contains "META-INF/assets/invalid-less.less:3:5: no viable alternative at input 'p'")))))))
259 |
260 | (context "using data-uri and getData method"
261 |
262 | (with-all asset (@pipeline "logo.less" @twixt-context))
263 |
264 | (it "has the correct compiled content"
265 | (should (have-same-content "expected/logo.css" @asset)))))
266 |
267 | (context "stack support"
268 |
269 | (context "simple stack"
270 |
271 | (with-all asset (@pipeline "stack/bedrock.stack" @twixt-context))
272 |
273 | (it "has the content type from the stack file"
274 | (should= "text/javascript" (:content-type @asset)))
275 |
276 | (it "is marked as compiled"
277 | (should (:compiled @asset)))
278 |
279 | (it "identifies the correct aggregated asset paths"
280 | (should= ["stack/fred.js" "stack/barney.js"]
281 | (-> @asset :aggregate-asset-paths)))
282 |
283 | (it "includes dependencies on every file in the stack"
284 | (should= ["stack/barney.js" "stack/bedrock.stack" "stack/fred.js"]
285 | (sorted-dependencies @asset)))
286 |
287 | (it "has the correct aggregated content"
288 | (should (have-same-content "expected/bedrock.js" @asset))))
289 |
290 | (context "stack with compilation"
291 |
292 | (with-all asset (@pipeline "stack/compiled.stack" @twixt-context))
293 |
294 | (it "identifies the correct aggregated asset path"
295 | (should= ["coffeescript-source.coffee" "stack/stack.coffee"]
296 | (-> @asset :aggregate-asset-paths)))
297 |
298 | (it "includes dependencies on every file in the stack"
299 | (should= ["coffeescript-source.coffee" "stack/compiled.stack" "stack/stack.coffee"]
300 | (sorted-dependencies @asset)))
301 |
302 | ;; Note: the compiled output includes the sourceMappingURL comments.
303 | ;; It is possible that will not work well in the client and may need to be filtered out.
304 | (it "has the correct aggregated content"
305 | (should (have-same-content "expected/compiled-stack.js" @asset))))
306 |
307 | (context "stack of stacks"
308 | (with-all asset (@pipeline "stack/meta.stack" @twixt-context))
309 |
310 | (it "identifies the correct aggregated asset path"
311 | (should= ["stack/fred.js" "stack/barney.js" "coffeescript-source.coffee" "stack/stack.coffee"]
312 | (-> @asset :aggregate-asset-paths)))
313 |
314 | (it "includes dependencies on every file in the stack"
315 | (should= ["coffeescript-source.coffee" "stack/barney.js" "stack/bedrock.stack" "stack/compiled.stack" "stack/fred.js" "stack/meta.stack" "stack/stack.coffee"]
316 | (sorted-dependencies @asset)))
317 |
318 |
319 | (it "has the correct aggregated content"
320 | (should (have-same-content "expected/meta.js" @asset))))
321 |
322 | (context "stack with missing component"
323 |
324 | (it "throws a reasonable exception when a component is missing"
325 | (try
326 | (get-asset-uri @twixt-context "stack/missing-component.stack")
327 | (should-fail)
328 | (catch Exception e
329 | (should= "Could not locate resource `stack/does-not-exist.coffee' (a component of `stack/missing-component.stack')."
330 | (.getMessage e))))))
331 |
332 | (context "CSS stack"
333 |
334 | (with-all asset (@pipeline "stack/style.stack" @twixt-context))
335 |
336 | (it "has the content type from the stack file"
337 | (should= "text/css" (:content-type @asset)))
338 |
339 | (it "identifies the correct aggregated asset paths"
340 | (should= ["stack/local.less" "sample.less"]
341 | (-> @asset :aggregate-asset-paths)))
342 |
343 | (it "identifies the correct dependencies"
344 | ;; Remember that a change to aviso-logo.png will change the checksum in the
345 | ;; compiled and CSS-rewritten stack/local.css, so it must be a dependency
346 | ;; of the final asset.
347 | (should= ["aviso-logo.png" "colors.less" "sample.less" "stack/local.less" "stack/style.stack"]
348 | (sorted-dependencies @asset)))
349 |
350 | ;; Again, need to think about stripping out the sourceMappingURL lines.
351 | (it "contains the correct aggregated content"
352 | (should (have-same-content "expected/style.css" @asset))))
353 |
354 | (context "get-asset-uri"
355 | (it "always returns the stack asset URI"
356 | (should= "/assets/[hash]/stack/bedrock.stack"
357 | (->> (get-asset-uri @twixt-context "stack/bedrock.stack")
358 | remove-hash-from-uri))))
359 |
360 | (context "get-asset-uris"
361 |
362 | (it "returns a list of component asset URIs in place of a stack"
363 |
364 | (should= ["/assets/[hash]/stack/fred.js" "/assets/[hash]/stack/barney.js"]
365 | (->> (get-asset-uris @twixt-context "stack/bedrock.stack")
366 | (map remove-hash-from-uri))))
367 |
368 | (it "returns just the stack asset URI in production mode"
369 | (should= ["/assets/[hash]/stack/bedrock.stack"]
370 | (->> (get-asset-uris (assoc @twixt-context :development-mode false) "stack/bedrock.stack")
371 | (map remove-hash-from-uri))))))
372 |
373 | (context "JavaScript Minimization"
374 | (context "is enabled by default in production mode"
375 |
376 | (with-all prod-options (-> @options
377 | (assoc :development-mode false
378 | :js-optimizations :default)
379 | (with-sub-cache-dir "js-min-1")))
380 |
381 | (with-all prod-pipeline (default-asset-pipeline @prod-options))
382 |
383 | (with-all prod-twixt-context (assoc @prod-options :asset-pipeline @prod-pipeline))
384 |
385 | (with-all asset (@prod-pipeline "stack/meta.stack" @prod-twixt-context))
386 |
387 | (it "contains the correct minimized content"
388 | (should (have-same-content "expected/minimized/meta.js" @asset)))
389 |
390 | (it "can handle much larger files"
391 | (should (have-same-content "expected/minimized/bootstrap.js"
392 | (@prod-pipeline "stack/bootstrap.stack" @prod-twixt-context)))))
393 |
394 | (context "can be disabled in production mode"
395 |
396 | (with-all prod-options (-> @options
397 | (assoc :development-mode false
398 | :js-optimizations :none)
399 | (with-sub-cache-dir "js-min-2")))
400 |
401 | (with-all prod-pipeline (default-asset-pipeline @prod-options))
402 |
403 | (with-all prod-twixt-context (assoc @prod-options :asset-pipeline @prod-pipeline))
404 |
405 | (with-all asset (@prod-pipeline "stack/meta.stack" @prod-twixt-context))
406 |
407 | (it "contains the correct unoptimized content"
408 | (should (have-same-content "expected/meta.js" @asset))))
409 |
410 | (context "can be enabled in development mode"
411 |
412 | (with-all dev-options (-> @options
413 | (assoc :js-optimizations :simple)
414 | (with-sub-cache-dir "js-min-3")))
415 |
416 | (with-all dev-pipeline (default-asset-pipeline @dev-options))
417 |
418 | (with-all dev-twixt-context (assoc @dev-options :asset-pipeline @dev-pipeline))
419 |
420 | (with-all asset (@dev-pipeline "stack/fred.js" @dev-twixt-context))
421 |
422 | (it "contains the correct unoptimized content"
423 | (should (have-same-content "expected/minimized/fred.js" @asset)))))
424 |
425 | (context "CSS minification"
426 | (it "can be expicitly enabled"
427 | (let [asset (-> @options
428 | (assoc :minimize-css true)
429 | (with-sub-cache-dir "css-min-1")
430 | (get-asset-with-options "sample.less"))]
431 | (should
432 | (have-same-content "expected/minimized/sample.css" asset))))
433 |
434 | (it "is enabled by default in production mode"
435 | (let [asset (-> @options
436 | (assoc :development-mode false)
437 | (with-sub-cache-dir "css-min-2")
438 | ;; This is not a terrific idea in that a change to the Bootstrap dependency
439 | ;; will invalidate this test. On the other hand, it ensures (on the side)
440 | ;; that YUICompressor can handle compiling all of Bootstrap.
441 | (get-asset-with-options "bootstrap/less/bootstrap.less"))]
442 | (should
443 | (have-same-content "expected/minimized/bootstrap.css" asset)))))
444 |
445 | (context "asset redirector"
446 | (with-all wrapped (ring/wrap-with-asset-redirector (constantly nil)))
447 |
448 | (with-all request {:uri "/sample.less" :twixt @twixt-context})
449 |
450 | (with-all response (@wrapped @request))
451 |
452 | (it "sends a 302 response"
453 |
454 | (should= 302 (:status @response)))
455 |
456 | (it "sends an empty body"
457 | (should= "" (:body @response)))
458 |
459 | (it "sends a proper Location header"
460 | (should (re-matches #"/assets/.*/sample.less" (get-in @response [:headers "Location"]))))
461 |
462 | (it "returns nil for non-matching paths"
463 | (do-template [path]
464 | (should-be-nil (@wrapped {:uri path :twixt @twixt-context}))
465 | "/"
466 | "/a-folder"
467 | "/another/folder/")))
468 |
469 | (context "exporter"
470 |
471 | (with-all ring-handler (startup/wrap-with-twixt
472 | (-> (fn [request]
473 | (-> request
474 | :twixt
475 | (get-asset-uri "sub/jade-include.jade")))
476 | (resource/wrap-resource "target/exported"))
477 | (-> twixt/default-options
478 | (assoc-in [:exports :output-dir] "target/exported")
479 | (update-in [:exports :assets] into ["sub/jade-include.jade"]))
480 | true))
481 |
482 |
483 | (it "can export a file"
484 | (let [response (@ring-handler {:request-method :get
485 | :uri "/sub/jade-include.jade"})]
486 |
487 |
488 | (should-have-content (read-resource-content "expected/jade-include.html")
489 | (-> "target/exported/sub/jade-include.jade"
490 | io/file
491 | read-as-trimmed-string))))
492 |
493 | (it "exposes the exported alias as the asset URI"
494 | (should= "/sub/jade-include.jade"
495 | (@ring-handler {:request-method :get
496 | :uri "any-match-ok"}))))
497 |
498 | ;; Slightly bogus; this lets the mass of exceptions written out by the executing tests have a chance to finish
499 | ;; before speclj outputs the report; without it, you often get a jumble of console output (including formatted exceptions)
500 | ;; and the report as well. Perhaps another solution is to get speclj to pipe its output through clojure.tools.logging?
501 | (it "needs to slow down to let the console catch up"
502 | (Thread/sleep 1000))
503 |
504 | (after-all (System/gc)))
505 |
506 |
507 |
508 | (run-specs)
--------------------------------------------------------------------------------
/spec/io/aviso/utils_spec.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.utils-spec
2 | (:use
3 | clojure.template
4 | io.aviso.twixt.utils
5 | speclj.core)
6 | (:require
7 | [clojure.java.io :as io]))
8 |
9 |
10 | (defn- get-modified-at
11 | [resource-path]
12 | (-> resource-path io/resource modified-at .getTime))
13 |
14 | (describe "io.aviso.twixt.utils"
15 |
16 | (context "compute-relative-path"
17 |
18 | (it "can compute relative paths"
19 |
20 | (do-template [start relative expected]
21 |
22 | (should= expected
23 | (compute-relative-path start relative))
24 |
25 | "foo/bar.gif" "baz.png" "foo/baz.png"
26 |
27 | "foo/bar.gif" "./baz.png" "foo/baz.png"
28 |
29 | "foo/bar.gif" "../zip.zap" "zip.zap"
30 |
31 | "foo/bar/baz/biff.gif" "../gnip/zip.zap" "foo/bar/gnip/zip.zap"
32 |
33 | "foo/bar/gif" "../frozz/pugh.pdf" "foo/frozz/pugh.pdf"))
34 |
35 |
36 | (it "throws IllegalArgumentException if ../ too many times"
37 | (should-throw IllegalArgumentException
38 | (compute-relative-path "foo/bar.png" "../../too-high.pdf"))))
39 |
40 | (context "access to time modified"
41 |
42 | ;; This is to verify some underpinnings
43 |
44 | (it "can access time modified for a local file"
45 |
46 | ;; This will be on the classpath, but on the filesystem since it is part of the current project.
47 |
48 | (should (-> "META-INF/assets/colors.less"
49 | get-modified-at
50 | pos?)))
51 |
52 | (it "can access time modified for a resource in a JAR"
53 | (should (-> "META-INF/leiningen/io.aviso/pretty/LICENSE"
54 | get-modified-at
55 | pos?)))))
56 |
57 |
58 | (run-specs)
--------------------------------------------------------------------------------
/src/io/aviso/twixt.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt
2 | "Pipeline for accessing and transforming assets for streaming.
3 |
4 | Twixt integrates with Ring, as a set of Ring request filters.
5 |
6 | Twixt plugs into the Ring pipeline, but most of its work is done in terms of the asset pipeline.
7 |
8 | An asset pipeline handler is passed two values: an asset path and a Twixt context, and returns an asset.
9 |
10 | The asset path is a string, identifying the location of the asset on the classpath, beneath
11 | `META-INF/assets`.
12 |
13 | The Twixt context provides additional information that may be needed
14 | when resolving the asset.
15 |
16 | The asset itself is a map with a specific set of keys.
17 |
18 | As with Ring, there's the concept of middleware; functions that accept an asset handler (plus optional
19 | additional parameters) and return a new asset handler."
20 | (:require [clojure.java.io :as io]
21 | [io.aviso.toolchest.macros :refer [cond-let]]
22 | [io.aviso.tracker :as t]
23 | [io.aviso.twixt
24 | [asset :as asset]
25 | [compress :as compress]
26 | [css-rewrite :as rewrite]
27 | [css-minification :as cssmin]
28 | [fs-cache :as fs]
29 | [memory-cache :as mem]
30 | [js-minification :as js]
31 | [schemas :refer [Asset AssetHandler AssetPath AssetURI TwixtContext ResourcePath]]
32 | [utils :as utils]]
33 | [ring.util.mime-type :as mime]
34 | [schema.core :as s]
35 | [clojure.string :as str])
36 | (:import [java.net URL]
37 | org.webjars.WebJarAssetLocator
38 | [java.io File]))
39 |
40 | ;;; Lots of stuff from Tapestry 5.4 is not yet implemented
41 | ;;; - multiple domains (the context, the file system, etc.)
42 | ;;; - CSS minification
43 | ;;; - AMD/RequireJS modules
44 |
45 | (defn- extract-file-extension [^String path]
46 | (let [dotx (.lastIndexOf path ".")]
47 | (.substring path (inc dotx))))
48 |
49 | (defn- extract-content-type
50 | "Uses the resource-path's file extension to identify the content type."
51 | [content-types resource-path]
52 | (get content-types (extract-file-extension resource-path) "application/octet-stream"))
53 |
54 | (s/defn new-asset :- Asset
55 | "Create a new Asset.
56 |
57 | content-types
58 | : The content-types map from the Twixt options.
59 |
60 | asset-path
61 | : The path to the asset from the asset Root. This may be used in some tracking/debugging output.
62 |
63 | resource-path
64 | : The path to the asset on the classpath, used to locate the Asset's raw content.
65 |
66 | url
67 | : A URL used to access the raw content for the asset."
68 | {:size "0.1.17"}
69 | [content-types :- {s/Str s/Str}
70 | asset-path :- AssetPath
71 | resource-path :- ResourcePath
72 | url :- URL]
73 | (let [^bytes content-bytes (utils/read-content url)
74 | checksum (utils/compute-checksum content-bytes)
75 | modified-at (utils/modified-at url)]
76 | {:asset-path asset-path
77 | :resource-path resource-path
78 | :modified-at modified-at
79 | :content-type (extract-content-type content-types resource-path)
80 | :content content-bytes
81 | :size (alength content-bytes)
82 | :checksum checksum
83 | :dependencies {resource-path {:asset-path asset-path
84 | :checksum checksum
85 | :modified-at modified-at}}}))
86 |
87 | (s/defn make-asset-resolver :- AssetHandler
88 | "Factory for the standard Asset resolver function which converts a path into an Asset.
89 |
90 | The factory function is passed the Twixt options, which defines content types mappings.
91 |
92 | The resolver function is passed an asset path and a pipeline context (which is ignored);
93 | The asset path is converted to a classpath resource via the configuration;
94 | if the resource exists, it is converted to an Asset.
95 |
96 | If the Asset does not exist, the resolver returns nil.
97 |
98 | An Asset has the minimum following keys:
99 |
100 | :content
101 | : content of the asset in a form that is compatible with clojure.java.io
102 |
103 | :asset-path
104 | : path of the asset under the root folder /META-INF/assets/
105 |
106 | :resource-path
107 | : full path of the underlying resource
108 |
109 | :content-type
110 | : MIME type of the content, as determined from the path's extension
111 |
112 | :size
113 | : size of the asset in bytes
114 |
115 | :checksum
116 | : Adler32 checksum of the content
117 |
118 | :modified-at
119 | : instant at which the file was last modified (not always trustworthy for files packaged in JARs)
120 |
121 | :compiled
122 | : _optional_ - true for assets that represent some form of compilation (or aggregation) and should be cached
123 |
124 | :aggregate-asset-paths
125 | : _optional_ - seq of asset paths from which a stack asset was constructed
126 |
127 | :dependencies
128 | : _optional_ - used to track underlying dependencies; a map of resource path to details about that resource
129 | (keys :asset-path, :checksum, and :modified-at)
130 |
131 | :attachments
132 | : _optional_ - map of string name to attachment (with keys :content, :size, and :content-type)"
133 | [twixt-options]
134 | (let [{:keys [content-types]} twixt-options]
135 | (fn [asset-path _context]
136 | (let [resource-path (str "META-INF/assets/" asset-path)]
137 | (if-let [url (io/resource resource-path)]
138 | (new-asset content-types asset-path resource-path url))))))
139 |
140 | (s/defn make-webjars-asset-resolver :- AssetHandler
141 | "As with [[make-asset-resolver]], but finds assets inside WebJars.
142 |
143 | The path must start with a WebJar libary name, such as \"bootstrap\".
144 | The version number is injected into the resource path."
145 | {:since "0.1.17"}
146 | [twixt-options]
147 | (let [webjar-versions (into {} (-> (WebJarAssetLocator.) .getWebJars))
148 | content-types (:content-types twixt-options)]
149 | (fn [asset-path _context]
150 | (cond-let
151 | [[webjar-name webjar-asset-path] (str/split asset-path #"/" 2)
152 | webjar-version (get webjar-versions webjar-name)]
153 |
154 | (nil? webjar-version)
155 | nil
156 |
157 | [resource-path (str "META-INF/resources/webjars/" webjar-name "/" webjar-version "/" webjar-asset-path)
158 | url (io/resource resource-path)]
159 |
160 | (some? url)
161 | (new-asset content-types asset-path resource-path url)))))
162 |
163 |
164 | (def default-options
165 | "Provides the default options when using Twixt; these rarely need to be changed except, perhaps, for :path-prefix
166 | or :cache-folder, or by plugins."
167 | {:path-prefix "/assets/"
168 | :content-types mime/default-mime-types
169 | :resolver-factories [make-asset-resolver make-webjars-asset-resolver]
170 | ;; Content transformer, e.g., compilers (such as CoffeeScript to JavaScript). Key is a content type,
171 | ;; value is a function passed an asset and Twixt context, and returns a new asset.
172 | :content-transformers {}
173 | ;; Identify which content types are compressable; all other content types are assumed to not be compressable.
174 | :compressable #{"text/*" "application/edn" "application/json"}
175 | :js-optimizations :default
176 | :cache {:cache-dir (System/getProperty "twixt.cache-dir" (System/getProperty "java.io.tmpdir"))
177 | :check-interval-ms 1000}
178 | :exports {:interval-ms 5000
179 | :output-dir "resources/public"
180 | :output-uri ""
181 | :assets []}})
182 |
183 |
184 | (s/defn find-asset :- (s/maybe Asset)
185 | "Looks for a particular asset by asset path, returning the asset (if found)."
186 | {:added "0.1.17"}
187 | [asset-path :- AssetPath
188 | context :- TwixtContext]
189 | ((:asset-pipeline context) asset-path context))
190 |
191 | (s/defn asset->uri :- s/Str
192 | "Converts an Asset to a URI that can be provided to a client."
193 | {:added "0.1.17"}
194 | [context :- TwixtContext
195 | asset :- Asset]
196 | (if-let [alias-uri (get (:asset-aliases context) (:asset-path asset))]
197 | alias-uri
198 | (asset/asset->request-path (:path-prefix context) asset)))
199 |
200 | (defn- get-single-asset
201 | [asset-pipeline context asset-path]
202 | {:pre [(some? asset-pipeline)
203 | (some? context)
204 | (some? asset-path)]}
205 | (or (asset-pipeline asset-path context)
206 | (throw (ex-info (format "Asset path `%s' does not map to an available resource." asset-path)
207 | context))))
208 |
209 | (s/defn get-asset-uris :- [AssetURI]
210 | "Converts a number of asset paths into client URIs.
211 | Each path must exist.
212 |
213 | An asset path does not start with a leading slash.
214 | The default asset resolver locates each asset on the classpath under `META-INF/assets/`.
215 |
216 | Unlike [[get-asset-uri]], this function will (in _development mode_) expand stack assets
217 | into the asset URIs for all component assets.
218 |
219 | context
220 | : the :twixt key, extracted from the Ring request map
221 |
222 | paths
223 | : asset paths to convert to URIs"
224 | [{:keys [asset-pipeline development-mode] :as context} :- TwixtContext
225 | & paths :- [AssetPath]]
226 | (loop [asset-uris []
227 | [path & more-paths] paths]
228 | (if-not (some? path)
229 | asset-uris
230 | (let [asset (get-single-asset asset-pipeline context path)
231 | aggregate-asset-paths (-> asset :aggregate-asset-paths seq)]
232 | (if (and development-mode aggregate-asset-paths)
233 | (recur (into asset-uris (apply get-asset-uris context aggregate-asset-paths))
234 | more-paths)
235 | (recur (conj asset-uris (asset->uri context asset))
236 | more-paths))))))
237 |
238 | (s/defn get-asset-uri :- AssetURI
239 | "Converts a single asset paths into a client URI.
240 | Throws an exception if the path does not exist.
241 |
242 | The default asset resolver locates each asset on the classpath under `META-INF/assets/`.
243 |
244 | This works much the same as [[get-asset-uris]] except that stack asset will
245 | be a URI to the stack (whose content is the aggregation of the components of the stack)
246 | rather than the list of component asset URIs.
247 | This matches the behavior of `get-asset-uris` in _production_, but you should use
248 | `get-asset-uris` in development, since in development, you want the individual
249 | component assets, rather than the aggregated whole.
250 |
251 | context
252 | : the :twixt key, extracted from the Ring request map
253 |
254 | asset-path
255 | : path to the asset; asset paths do __not__ start with a leading slash"
256 | [context :- TwixtContext
257 | asset-path :- AssetPath]
258 | (let [{:keys [asset-pipeline]} context]
259 | (->>
260 | asset-path
261 | (get-single-asset asset-pipeline context)
262 | (asset->uri context))))
263 |
264 | (s/defn find-asset-uri :- (s/maybe AssetURI)
265 | "Returns the URI for an asset, if it exists.
266 | If not, returns nil."
267 | [{:keys [asset-pipeline] :as context}
268 | asset-path :- AssetPath]
269 | (if-let [asset (asset-pipeline asset-path context)]
270 | (asset->uri context asset)))
271 |
272 | (s/defn wrap-pipeline-with-tracing :- AssetHandler
273 | "The first middleware in the asset pipeline, used to trace the construction of the asset."
274 | [asset-handler :- AssetHandler]
275 | (fn [asset-path context]
276 | (t/track
277 | #(format "Accessing asset `%s'" asset-path)
278 | (asset-handler asset-path context))))
279 |
280 | (s/defn wrap-pipeline-with-per-content-type-transformation :- AssetHandler
281 | [asset-handler :- AssetHandler
282 | {:keys [content-transformers]}]
283 | (fn [asset-path context]
284 | (let [asset (asset-handler asset-path context)
285 | content-type (:content-type asset)
286 | transformer (get content-transformers content-type)]
287 | (if transformer
288 | (transformer asset context)
289 | asset))))
290 |
291 | (s/defn default-wrap-pipeline-with-content-transformation :- AssetHandler
292 | "Used when constructing the asset pipeline, wraps a handler (normally, the asset resolver)
293 | with additional pipeline handlers based on
294 | the `:content-transformers` key of the Twixt options, plus JavaScript minification and CSS URL Rewriting
295 | and CSS Minification.
296 |
297 | The JavaScript and/or CSS minification may not be enabled in development mode."
298 | [asset-handler :- AssetHandler
299 | twixt-options]
300 | (->
301 | asset-handler
302 | (wrap-pipeline-with-per-content-type-transformation twixt-options)
303 | (js/wrap-with-javascript-minimizations twixt-options)
304 | rewrite/wrap-with-css-rewriting
305 | (cssmin/wrap-with-css-minification twixt-options)))
306 |
307 | (s/defn default-wrap-pipeline-with-caching :- AssetHandler
308 | "Used when constructing the asset pipeline to wrap the handler with file system caching
309 | of compiled assets, and in-memory caching of all assets.
310 |
311 | This is invoked before adding support for compression."
312 | [asset-handler :- AssetHandler
313 | cache-dir :- File
314 | check-interval-ms :- s/Num]
315 | (->
316 | asset-handler
317 | ;; The file system cache should come after anything downstream that might compile.
318 | (fs/wrap-with-filesystem-cache cache-dir)
319 | ;; This in-memory cache prevents constant checks agains the file system cache, or against
320 | ;; the asset resolvers.
321 | (mem/wrap-with-memory-cache check-interval-ms)))
322 |
323 | (s/defn wrap-pipeline-with-asset-resolver :- AssetHandler
324 | "Wraps the asset handler so that the :asset-resolver key is set to the asset resolver; the asset resolver
325 | is a way to bypass intermediate steps and gain access to the asset in its completely untransformed format."
326 | [asset-handler :- AssetHandler
327 | asset-resolver]
328 | (fn [asset-path context]
329 | (asset-handler asset-path (assoc context :asset-resolver asset-resolver))))
330 |
331 | (defn- merge-handlers [handlers]
332 | (fn [asset-path context]
333 | (loop [[h & more-handlers] handlers]
334 | (cond-let
335 | (nil? h)
336 | nil
337 |
338 | [result (h asset-path context)]
339 |
340 | (some? result)
341 | result
342 |
343 | :else
344 | (recur more-handlers)))))
345 |
346 | (s/defn default-asset-pipeline :- AssetHandler
347 | "Sets up the default pipeline.
348 |
349 | The asset pipeline starts with a resolver, which is then intercepted using asset pipeline middleware.
350 | As with Ring, middleware is a function that accepts an asset-handler and returns an asset-handler. The asset-handler
351 | is passed an asset path and a context. The initial context is the value of the `:twixt` key from the
352 | Ring request map.
353 |
354 | In production mode, JavaScript will be minimized.
355 |
356 | The context will contain an :asset-pipeline key whose value is the asset pipeline in use.
357 | The context will contain a :path-prefix key, extracted from the twixt options.
358 | The context may also be passed to [[get-asset-uri]] (and related functions).
359 |
360 | In some cases, middlware may modify the context before passing it forward to the next asset-handler, typically
361 | by adding additional keys."
362 | [twixt-options]
363 | (let [{:keys [development-mode resolver-factories]} twixt-options
364 | asset-resolvers (for [factory resolver-factories]
365 | (factory twixt-options))
366 | asset-resolver (merge-handlers asset-resolvers)
367 | ^String cache-path (-> twixt-options :cache :cache-dir)
368 | cache-dir (File. cache-path
369 | (if development-mode "dev" "prod"))
370 | check-interval-ms (-> twixt-options :cache :check-interval-ms)]
371 | (->
372 | asset-resolver
373 | (default-wrap-pipeline-with-content-transformation twixt-options)
374 | (default-wrap-pipeline-with-caching cache-dir check-interval-ms)
375 | (compress/wrap-with-gzip-compression twixt-options)
376 | (wrap-pipeline-with-asset-resolver asset-resolver)
377 | wrap-pipeline-with-tracing)))
378 |
379 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/asset.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.asset
2 | "Utilities for dealing with a Twixt asset map.")
3 |
4 | (defn asset->request-path
5 | "Computes the complete asset path that can be referenced by a client in order to obtain
6 | the asset content. This includes the path prefix, the checksum, and the asset path itself. Compressed assets
7 | have their checksum prefixed with 'z'."
8 | [path-prefix asset]
9 | (str path-prefix
10 | (if (:compressed asset) "z" "")
11 | (:checksum asset)
12 | "/"
13 | (:asset-path asset)))
14 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/coffee_script.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.coffee-script
2 | "Provides asset pipeline middleware to perform CoffeeScript to JavaScript compilation.
3 |
4 | CoffeeScript assets include a source map as an attachment.
5 | The source map includes the source of the CoffeeScript file.
6 |
7 | The compiled JavaScript includes the directive for the browser to load the source map."
8 | (:import
9 | [java.util Map])
10 | (:require
11 | [io.aviso.tracker :as t]
12 | [io.aviso.twixt
13 | [rhino :as rhino]
14 | [utils :as utils]]))
15 |
16 | (defn- ^String extract-value [^Map object key]
17 | (str (.get object key)))
18 |
19 | (defn- coffee-script-compiler [asset context]
20 | (let [file-path (:resource-path asset)
21 | file-name (utils/path->name file-path)]
22 | (t/timer
23 | #(format "Compiled `%s' to JavaScript in %.2f ms" file-path %)
24 | (t/track
25 | #(format "Compiling `%s' to JavaScript" file-path)
26 | (let [^Map result
27 | (rhino/invoke-javascript ["META-INF/twixt/coffee-script.js" "META-INF/twixt/invoke-coffeescript.js"]
28 | "compileCoffeeScriptSource"
29 | (-> asset :content utils/as-string)
30 | file-path
31 | file-name)]
32 |
33 | ;; The script returns an object with key "exception" or key "output":
34 | (when (.containsKey result "exception")
35 | (throw (RuntimeException. (extract-value result "exception"))))
36 | (-> asset
37 | (utils/create-compiled-asset "text/javascript" (extract-value result "output") nil)
38 | (utils/add-attachment "source.map" "application/json" (-> result (extract-value "sourceMap") utils/as-bytes))))))))
39 |
40 | (defn register-coffee-script
41 | "Updates the Twixt options with support for compiling CoffeeScript into JavaScript."
42 | [options]
43 | (-> options
44 | (assoc-in [:content-types "coffee"] "text/coffeescript")
45 | (assoc-in [:content-transformers "text/coffeescript"] coffee-script-compiler)))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/compress.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.compress
2 | "Asset pipeline middleware for handling compressable assets."
3 | (:require [clojure.string :as str]
4 | [clojure.java.io :as io]
5 | [io.aviso.twixt
6 | [memory-cache :as mem]
7 | [utils :as utils]
8 | [schemas :refer [AssetHandler]]]
9 | [schema.core :as s])
10 | (:import [java.io ByteArrayOutputStream]
11 | [java.util.zip GZIPOutputStream]))
12 |
13 | (defn- make-wildcard [^String mime-type]
14 | (->
15 | mime-type
16 | (.split "/")
17 | first
18 | (str "/*")))
19 |
20 | (defn- is-compressable-mime-type?
21 | [compressable-types mime-type]
22 | (or
23 | (compressable-types mime-type)
24 | (compressable-types (make-wildcard mime-type))))
25 |
26 | (defn- is-compressable?
27 | [compressable-types asset]
28 | (is-compressable-mime-type? compressable-types (:content-type asset)))
29 |
30 | (defn- compress-asset
31 | [asset]
32 | (let [bos (ByteArrayOutputStream. (:size asset))]
33 | (with-open [gz (GZIPOutputStream. bos)
34 | is (io/input-stream (:content asset))]
35 | (io/copy is gz))
36 | (-> asset
37 | (utils/replace-asset-content (:content-type asset) (.toByteArray bos))
38 | (assoc :compressed true))))
39 |
40 | (defn- is-gzip-supported?
41 | [request]
42 | (if-let [encodings (-> request :headers (get "accept-encoding"))]
43 | (some #(.equalsIgnoreCase ^String % "gzip")
44 | (str/split encodings #","))))
45 |
46 | (s/defn wrap-with-gzip-compression :- AssetHandler
47 | "Adds an in-memory cache that includes invalidation checks.
48 |
49 | Although compression is expensive enough that it is not desirable to
50 | perform compression on every request, it is also overkill to
51 | commit the compressed asset to a file system cache.
52 |
53 | The cache is only consulted if the :gzip-enabled options key is true."
54 | {:added "0.1.20"}
55 | [asset-handler :- AssetHandler
56 | twixt-options]
57 | (let [cache (mem/wrap-with-transforming-cache asset-handler
58 | (partial is-compressable? (:compressable twixt-options))
59 | compress-asset)]
60 | (fn [asset-path options]
61 | (let [delegate (if (:gzip-enabled options) cache asset-handler)]
62 | (delegate asset-path options)))))
63 |
64 | (defn wrap-with-compression-analyzer
65 | "Ring middleware that analyzes the incoming request to determine if the
66 | client can accept compressed streams."
67 | [handler]
68 | (fn [request]
69 | (handler (assoc-in request [:twixt :gzip-enabled] (is-gzip-supported? request)))))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/css_minification.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.css-minification
2 | "CSS Miniification based on YUICompressor."
3 | {:added "0.1.20"}
4 | (:require [schema.core :as s]
5 | [io.aviso.twixt.schemas :refer [AssetHandler]]
6 | [io.aviso.tracker :as t]
7 | [clojure.java.io :as io]
8 | [io.aviso.twixt.utils :as utils])
9 | (:import [java.io StringWriter]
10 | [com.yahoo.platform.yui.compressor CssCompressor]))
11 |
12 |
13 | (defn- run-minimizer
14 | [{:keys [resource-path content] :as asset}]
15 | (t/timer
16 | #(format "Minimized `%s' (%,d bytes) in %.2f ms"
17 | resource-path (:size asset) %)
18 | (t/track
19 | #(format "Minimizing `%s' using YUICompressor." resource-path)
20 | (let [compressed (with-open [writer (StringWriter. 1000)
21 | reader (io/reader content)]
22 | (doto (CssCompressor. reader)
23 | (.compress writer -1))
24 |
25 | (.flush writer)
26 | (-> writer .getBuffer str))]
27 | (utils/create-compiled-asset asset "text/css" compressed nil)))))
28 |
29 | (s/defn wrap-with-css-minification :- AssetHandler
30 | "Enabled CSS minification based on the :compress-css key of the Twixt options.
31 | If the key is not present, the default is to enable compression only in production mode.
32 |
33 | When not enabled, returns the provided handler unchanged."
34 | [handler :- AssetHandler {:keys [development-mode] :as twixt-options}]
35 | (if (get twixt-options :minimize-css (not development-mode))
36 | (fn [asset-path {:keys [for-aggregation] :as context}]
37 | (let [{:keys [content-type] :as asset} (handler asset-path context)]
38 | (if (and (= "text/css" content-type)
39 | (not for-aggregation))
40 | (run-minimizer asset)
41 | asset)))
42 | handler))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/css_rewrite.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.css-rewrite
2 | "Defines an asset pipeline filter used to rewrite URIs in CSS files.
3 | Relative URLs in CSS files will become invalid due to the inclusion of the individual asset's checksum in the
4 | asset request path; instead, the relative URIs are expanded to complete URIs that include the individual asset's checksum."
5 | (:require [clojure.string :as str]
6 | [io.aviso.tracker :as t]
7 | [io.aviso.twixt
8 | [asset :as asset]
9 | [utils :as utils]
10 | [schemas :refer [AssetHandler]]]
11 | [schema.core :as s]))
12 |
13 | (def ^:private url-pattern #"url\(\s*(['\"]?)(.+?)([\#\?].*?)?\1\s*\)")
14 | (def ^:private complete-url-pattern #"^[#/]|(\p{Alpha}\w*:)")
15 |
16 | (defn- rewrite-relative-url
17 | [asset-path context dependencies ^String relative-url]
18 | (if (.startsWith relative-url "data:")
19 | relative-url
20 | (t/track
21 | #(format "Rewriting relative URL `%s'" relative-url)
22 | (let [referenced-path (utils/compute-relative-path asset-path relative-url)
23 | asset ((:asset-pipeline context) referenced-path context)]
24 | (if-not asset
25 | (throw (ex-info
26 | (format "Unable to locate asset `%s'." referenced-path)
27 | {:source-asset asset-path
28 | :relative-url relative-url})))
29 |
30 | (swap! dependencies utils/add-asset-as-dependency asset)
31 | (asset/asset->request-path (:path-prefix context) asset)))))
32 |
33 | (defn- css-url-match-handler [asset-path context dependencies match]
34 | (let [url (nth match 2)
35 | parameters (nth match 3)]
36 | (str "url(\""
37 | (if (re-matches complete-url-pattern url)
38 | (str url parameters)
39 | (str (rewrite-relative-url asset-path context dependencies url)
40 | parameters))
41 | "\")")))
42 |
43 | (defn- rewrite-css
44 | [asset context]
45 | (t/track
46 | #(format "Rewriting URLs in `%s'" (:asset-path asset))
47 | (let [content (utils/as-string (:content asset))
48 | ;; Using an atom this way is clumsy.
49 | dependencies (atom (:dependencies asset))
50 | content' (str/replace content
51 | url-pattern
52 | (partial css-url-match-handler (:asset-path asset) context dependencies))]
53 | (->
54 | asset
55 | (utils/replace-asset-content "text/css" (utils/as-bytes content'))
56 | (assoc :dependencies @dependencies)))))
57 |
58 | (s/defn wrap-with-css-rewriting :- AssetHandler
59 | "Wraps the asset handler with the CSS URI rewriting logic needed for the client to be able to properly request the referenced assets.
60 |
61 | Rewriting occurs for individual CSS assets (including those created by compiling a Less source). It does not occur for
62 | aggregated CSS assets (since the individual assets will already have had URIs rewritten)."
63 | [handler :- AssetHandler]
64 | (fn [asset-path context]
65 | (let [asset (handler asset-path context)]
66 | (if (and
67 | (= "text/css" (:content-type asset))
68 | (-> asset :aggregate-asset-paths empty?))
69 | (rewrite-css asset context)
70 | asset))))
71 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/exceptions.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.exceptions
2 | "Support for generating pretty and useful HTML reports when server-side exceptions occur."
3 | (:use hiccup.core
4 | hiccup.page
5 | ring.util.response)
6 | (:require [clojure.string :as s]
7 | [io.aviso
8 | [exception :as exception]
9 | [twixt :as t]])
10 | (:import [clojure.lang APersistentMap Sequential PersistentHashMap]
11 | [java.util Map]
12 | [java.util.regex Pattern]))
13 |
14 | (def ^:private path-separator (System/getProperty "path.separator"))
15 |
16 | (defn- exception-message
17 | [^Throwable exception]
18 | (or (.getMessage exception)
19 | (-> exception .getClass .getName)))
20 |
21 | (defprotocol MarkupGeneration
22 | "Used to convert arbitrary values into markup strings.
23 |
24 | Extended onto nil, `String`, `APersistentMap`, `Sequential` and `Object`."
25 | (to-markup
26 | [value]
27 | "Returns HTML markup representing the value."))
28 |
29 | (extend-type nil
30 | MarkupGeneration
31 | (to-markup [_] "nil "))
32 | h
33 | (extend-type String
34 | MarkupGeneration
35 | (to-markup [value] (h value)))
36 |
37 | (extend-type Object
38 | MarkupGeneration
39 | (to-markup [value] (-> value .toString h)))
40 |
41 | (extend-type APersistentMap
42 | MarkupGeneration
43 | (to-markup
44 | [m]
45 | (html
46 | (if (empty? m)
47 | [:em "empty map"]
48 | [:dl
49 | (apply concat
50 | (for [[k v] (sort-by str m)]
51 | [[:dt (to-markup k)] [:dd (to-markup v)]]
52 | ))
53 | ]))))
54 |
55 | (defn- seq->markup [coll]
56 | ;; Since *print-length* is normally nil, we provide an actual limit
57 | (let [limit (or *print-length* 10)
58 | values (take (inc limit) coll)
59 | trimmed (if (<= (count values) limit)
60 | values
61 | (concat
62 | (take limit values)
63 | ["..."]))]
64 | (for [v trimmed]
65 | [:li (to-markup v)])))
66 |
67 | (extend-type Sequential
68 | MarkupGeneration
69 | (to-markup
70 | [coll]
71 | (html
72 | (if (empty? coll)
73 | [:em "none"]
74 | [:ul (seq->markup coll)]))))
75 |
76 | (extend-type Map
77 | MarkupGeneration
78 | (to-markup
79 | [^Map m]
80 | (to-markup (PersistentHashMap/create m))))
81 |
82 | (defn- element->clojure-name [element]
83 | (let [names (:names element)]
84 | (list
85 | (s/join "/" (drop-last names))
86 | "/"
87 | [:strong (last names)])))
88 |
89 | (defn- element->java-name
90 | [{:keys [package class simple-class method]}]
91 | (list
92 | (if package
93 | (list
94 | [:span.package-name package]
95 | "."
96 | simple-class)
97 | class)
98 | " — "
99 | method))
100 |
101 | (defn- apply-stack-frame-filter
102 | [filter frames]
103 | (loop [result []
104 | [frame & more-frames] frames]
105 | (if (nil? frame)
106 | result
107 | (case (filter frame)
108 | :show
109 | (recur (conj result frame) more-frames)
110 |
111 | ;; We could perhaps do this differntly, but unlike console output, we treat these the same.
112 | (:hide :omit)
113 | (recur (conj result (assoc frame :filtered true)) more-frames)
114 |
115 | :terminate
116 | result))))
117 |
118 | (defn- stack-frame->row-markup
119 | [{:keys [file line names]
120 | :as frame}]
121 | (let [clojure? (-> names empty? not)
122 | java-name (element->java-name frame)]
123 | [:tr (if (:filtered frame) {:class :filtered} {})
124 | [:td.function-name
125 | (if clojure?
126 | (list
127 | (element->clojure-name frame)
128 | [:div.filtered java-name])
129 | java-name)
130 | ]
131 | [:td.source-location file (if line ":")]
132 | [:td (or line "")]
133 | ]))
134 |
135 | (defn- single-exception->markup
136 | "Given an analyzed exception, generate markup for it."
137 | [twixt
138 | {^Throwable e :exception
139 | :keys [class-name message properties root]}]
140 | (html
141 | [:div.panel.panel-default
142 | [:div.panel-heading
143 | [:h3.panel-title class-name]
144 | ]
145 | [:div.panel-body
146 | [:h4 message]
147 | ;; TODO: sorting
148 | (when-not (empty? properties)
149 | [:dl
150 | (apply concat
151 | (for [[k v] (sort-by str properties)]
152 | [[:dt (-> k h)] [:dd (to-markup v)]]))
153 | ])]
154 | (if root
155 | (list
156 | [:div.btn-toolbar.spacing-below
157 | [:button.btn.btn-default.btn-sm {:data-action :toggle-filter} "Toggle Stack Frame Filter"]]
158 | [:table.table.table-hover.table-condensed.table-striped
159 | (->>
160 | e
161 | exception/expand-stack-trace
162 | (apply-stack-frame-filter (:stack-frame-filter twixt))
163 | (map stack-frame->row-markup))
164 | ]))
165 | ]))
166 |
167 |
168 | (defn exception->markup
169 | "Returns the markup (as a string) for a root exception and its stack of causes,
170 | including a stack trace for the deepest exception."
171 | [twixt root-exception]
172 | (->>
173 | root-exception
174 | exception/analyze-exception
175 | (map (partial single-exception->markup twixt))
176 | (apply str)))
177 |
178 | (defn- is-path?
179 | [^String k ^String v]
180 | (and (.endsWith k ".path")
181 | (.contains v path-separator)))
182 |
183 | (defn- split-string [^String s ^String sep]
184 | (s/split s (-> sep Pattern/quote Pattern/compile)))
185 |
186 | (defn- path->markup [^String path]
187 | `[:ul
188 | ~@(for [v (.split path path-separator)]
189 | [:li v])])
190 |
191 | (defn- sysproperties->markup
192 | []
193 | (let [props (System/getProperties)]
194 | [:dl
195 | (apply concat
196 | (for [k (-> props keys sort)
197 | :let [v (.get props k)]]
198 | [[:dt k]
199 | [:dd (if (is-path? k v) (path->markup v) v)]]))]))
200 |
201 | (defn build-report
202 | "Builds an HTML exception report (as a string).
203 |
204 | request
205 | : Ring request map, which must contain the `:twixt` key
206 |
207 | exception
208 | : instance of Throwable to report"
209 | [request exception]
210 | (let [twixt (:twixt request)]
211 | (html5
212 | [:head
213 | [:title "Exception"]
214 | (apply include-css (t/get-asset-uris twixt
215 | "bootstrap/css/bootstrap.css"
216 | "twixt/exception.less"))]
217 | [:body.hide-filtered
218 | [:div.container
219 | [:div.panel.panel-danger
220 | [:div.panel-heading
221 | [:h3.panel-title "An unexpected exception has occurred."]]
222 | [:div.panel-body
223 | [:h3 (h (exception-message exception))]
224 | ]
225 | ]
226 | (exception->markup twixt exception)
227 |
228 | [:h3 "Request"]
229 |
230 | (to-markup request)
231 |
232 | [:h3 "System Properties"]
233 | (sysproperties->markup)
234 | ]
235 | (apply include-js (t/get-asset-uris twixt
236 | "jquery/jquery.js"
237 | "twixt/exception.coffee"))
238 | ])))
239 |
240 | (defn wrap-with-exception-reporting
241 | "Wraps the handler to report any uncaught exceptions as an HTML exception report.
242 | This wrapper should wrap around other handlers (including the Twixt handler itself), but be nested within
243 | the twixt-setup handler (which provides the `:twixt` request map key)."
244 | [handler]
245 | (fn [request]
246 | (try
247 | (handler request)
248 | (catch Throwable t
249 | (->
250 | (build-report request t)
251 | response
252 | (content-type "text/html")
253 | (status 500))))))
254 |
255 |
256 | (defn default-stack-frame-filter
257 | "The default stack frame filter function, used by the HTML excepton report to identify frames that can be hidden
258 | by default.
259 |
260 | This implementation extends the standard frame filter (`io.aviso.repl/standard-frame-filter`),
261 | to also omit frames with no line number.
262 |
263 | The HTML exception report treats `:omit` and `:hide` identically; frames marked as either are
264 | initially hidden, but can be revealed in the client."
265 | [frame]
266 | (cond
267 | (nil? (:line frame))
268 | :omit
269 |
270 | :else
271 | (exception/*default-frame-filter* frame)))
272 |
273 | (defn register-exception-reporting
274 | "Must be invoked to configure the Twixt options with the default `:stack-frame-filter` key, [[default-stack-frame-filter]]."
275 | [options]
276 | (assoc options :stack-frame-filter default-stack-frame-filter))
277 |
278 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/export.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.export
2 | "Support for exporting assets to the file system.
3 |
4 | Ideally, exporting would be fully asynchronous; however that would require a dedicated thread, and
5 | that is a problem as there is no lifecycle for
6 | a Ring request handler to know when the Ring server is itself shutdown."
7 | {:added "0.1.17"}
8 | (:require [schema.core :as s]
9 | [io.aviso.twixt.schemas :refer [AssetPath ExportsConfiguration]]
10 | [io.aviso.tracker :as t]
11 | [io.aviso.toolchest.macros :refer [cond-let]]
12 | [io.aviso.twixt :as twixt]
13 | [clojure.java.io :as io]
14 | [clojure.tools.logging :as l]))
15 |
16 | (deftype ^:private Token [running?
17 | ^Thread thread]
18 |
19 | Object
20 | (finalize [_]
21 | (reset! running? false)
22 | (.interrupt thread)))
23 |
24 | (defn- export-asset
25 | [output-dir {:keys [asset-path content]} output-alias]
26 | (t/track
27 | (if (= asset-path output-alias)
28 | (format "Exporting `%s'." asset-path)
29 | (format "Exporting `%s' (as `%s')." asset-path output-alias))
30 |
31 | (let [output-file (io/file output-dir output-alias)]
32 | (when-not (.exists output-file)
33 | (-> output-file .getParentFile .mkdirs))
34 |
35 | (io/copy content output-file))))
36 |
37 | (defn- check-exports
38 | [context output-dir assets checksums]
39 | (t/track
40 | "Checking exported assets for changes."
41 | (let [context' (assoc context :gzip-enabled false)]
42 | (doseq [[asset-path output-alias] assets]
43 | (try
44 | (t/track
45 | (format "Checking `%s' for changes." asset-path)
46 | (cond-let
47 | [asset (twixt/find-asset asset-path context')]
48 |
49 | (nil? asset)
50 | nil
51 |
52 | [asset-checksum (:checksum asset)]
53 |
54 | (= asset-checksum (get @checksums asset-path))
55 | nil
56 |
57 | :else
58 | (do
59 | (export-asset output-dir asset output-alias)
60 | (swap! checksums assoc asset-path asset-checksum)))
61 | (catch Throwable _
62 | ;; Reported by the tracker and ignored.
63 | )))))))
64 |
65 |
66 | (defn- start-exporter-thread
67 | [context {:keys [interval-ms output-dir assets]}]
68 | (let [checksums (atom {})
69 | ;; We want the assets to all be the same shape: asset path and output path.
70 | assets' (map #(if (string? %)
71 | [% %]
72 | %)
73 | assets)
74 | running? (atom true)
75 | first-pass (promise)
76 | export-body (fn []
77 | (l/info "Twixt asset export thread started.")
78 | (while @running?
79 | (try
80 | (check-exports context output-dir assets' checksums)
81 | (deliver first-pass true)
82 | (Thread/sleep interval-ms)
83 |
84 | (catch Throwable _))
85 | ;; Real errors are reported inside check-exports, and InterruptedException is ignored.
86 | )
87 | (l/info "Shutting down Twixt asset export thread."))
88 | export-thread (doto (Thread. ^Runnable export-body)
89 | (.setName "Twixt Export")
90 | (.setDaemon true)
91 | .start)]
92 | ;; Wait for completion of first export pass:
93 |
94 | @first-pass
95 |
96 | ;; The token is returned to the call, which keeps it in an atom. At shutdown time,
97 | ;; the entire Ring request pipeline will be GC'ed, at which point the token will become
98 | ;; weakly referenced.
99 | (Token. running? export-thread)))
100 |
101 |
102 | (defn- build-asset-aliases
103 | [assets output-uri]
104 | (reduce (fn [result asset]
105 | (if (string? asset)
106 | (assoc result asset (str output-uri "/" asset))
107 | (let [[asset-path output-alias] asset]
108 | (assoc result asset-path (str output-uri "/" output-alias)))))
109 | {}
110 | assets))
111 |
112 | (s/defn wrap-with-exporter
113 | "Wraps a Ring handler so that, periodically, assets identified in the configuration
114 | are checked for changes and copied out to the file system (as needed).
115 |
116 | The first checks and exports occur before delegating to the wrapped handler; this will allow
117 | a standard resource handler (say, ring.middleware.resource/wrap-resource) to operate.
118 |
119 | Subsequent checks happen at intervals on a secondary thread.
120 | The thread will shutdown once the Ring request pipeline is GC'ed.
121 | It requires an explicit call to System/gc to ensure that the pipeline is GC'ed
122 | (and the necessary object finalizer called).
123 |
124 | Note that asset exporting is largely intended for development purposes."
125 | [ring-handler
126 | {:keys [assets output-uri] :as configuration} :- ExportsConfiguration]
127 | (if (empty? assets)
128 | ring-handler
129 | (let [token (atom nil)
130 | asset-aliases (build-asset-aliases assets output-uri)]
131 | (fn [request]
132 | (when (nil? @token)
133 | (reset! token (start-exporter-thread (:twixt request) configuration)))
134 |
135 | (ring-handler (assoc-in request [:twixt :asset-aliases] asset-aliases))))))
136 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/fs_cache.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.fs-cache
2 | "Provide asset pipeline middleware that implements a file-system cache.
3 | The cache will persist between executions of the application; this is used in development to prevent
4 | duplicated, expensive work from being performed after a restart."
5 | (:import [java.util UUID]
6 | [java.io PushbackReader File Writer])
7 | (:require
8 | [clojure.java.io :as io]
9 | [clojure
10 | [edn :as edn]
11 | [pprint :as pp]]
12 | [medley.core :as medley]
13 | [clojure.tools.logging :as l]
14 | [io.aviso.tracker :as t]))
15 |
16 |
17 | (defn- checksum-matches?
18 | [asset-resolver asset-path cached-checksum]
19 | (->
20 | asset-path
21 | (asset-resolver nil)
22 | :checksum
23 | (= cached-checksum)))
24 |
25 | (defn- is-valid?
26 | [asset-resolver cached-asset]
27 | (and cached-asset
28 | (every?
29 | (fn [{:keys [asset-path checksum]}]
30 | (checksum-matches? asset-resolver asset-path checksum))
31 | (-> cached-asset :dependencies vals))))
32 |
33 | (defn- read-cached-asset-data
34 | [^File file]
35 | (if (.exists file)
36 | (with-open [reader (-> file io/reader PushbackReader.)]
37 | (edn/read reader))))
38 |
39 | (defn- read-attachments
40 | [asset asset-cache-dir]
41 | (if (-> asset :attachments empty?)
42 | asset
43 | (update-in asset [:attachments]
44 | (fn [attachments]
45 | (medley/map-vals
46 | #(assoc % :content (io/file asset-cache-dir (:content %)))
47 | attachments)))))
48 |
49 | (def ^:private asset-file-name "asset.edn")
50 | (def ^:private content-file-name "content")
51 |
52 | (defn- read-cached-asset
53 | "Attempt to read the cached asset, if it exists.
54 |
55 | `asset-cache-dir` is the directory containing the two files (asset.edn and content)."
56 | [^File asset-cache-dir]
57 | (if (.exists asset-cache-dir)
58 | (t/track
59 | #(format "Reading from asset cache `%s'" asset-cache-dir)
60 | (some->
61 | (io/file asset-cache-dir asset-file-name)
62 | read-cached-asset-data
63 | (assoc :content (io/file asset-cache-dir content-file-name))
64 | (read-attachments asset-cache-dir)))))
65 |
66 |
67 | (defn- write-attachment
68 | "Writes the attachment's content to a file, returns the attachment with `:content` modified
69 | to be the simple name of the file written."
70 | [asset-cache-dir name attachment]
71 | (let [file-name (str (UUID/randomUUID) "-" name)
72 | content-file (io/file asset-cache-dir file-name)]
73 | (io/copy (:content attachment) content-file)
74 | (assoc attachment :content file-name)))
75 |
76 | (defn- write-attachments
77 | "Writes attachments as files in the cache dir, updating each's `:content` key into a simple file name."
78 | [asset asset-cache-dir]
79 | (if (-> asset :attachments empty?)
80 | asset
81 | (update-in asset [:attachments]
82 | (fn [attachments]
83 | (into {}
84 | (map (fn [[name attachment]]
85 | [name (write-attachment asset-cache-dir name attachment)])
86 | attachments))))))
87 |
88 | (defn- write-cached-asset
89 | [^File asset-cache-dir asset]
90 | (t/track
91 | #(format "Writing to asset cache `%s'" asset-cache-dir)
92 | (.mkdirs asset-cache-dir)
93 | (let [content-file (io/file asset-cache-dir content-file-name)
94 | asset-file (io/file asset-cache-dir asset-file-name)]
95 | ;; Write the content first
96 | (io/copy (:content asset) content-file)
97 | (with-open [^Writer writer (io/writer asset-file)]
98 | (.write writer (-> asset
99 | (write-attachments asset-cache-dir)
100 | ;; The name of the content file is (currently) a fixed value,
101 | ;; so we can just remove the key, rather than replace it with a file name.
102 | (dissoc :content)
103 | ;; Override *print-length* and *print-level* to ensure it is all written out.
104 | ^String (pp/write :length nil :level nil :stream nil)))
105 | (.write writer "\n")))))
106 |
107 | (defn- delete-dir-and-contents
108 | [^File dir]
109 | (when (.exists dir)
110 | (t/track
111 | #(format "Deleting directory `%s'" dir)
112 | (doseq [file (.listFiles dir)]
113 | (t/track
114 | #(format "Deleting file `%s'" file)
115 | (io/delete-file file)))
116 | (io/delete-file dir))))
117 |
118 | (defn wrap-with-filesystem-cache
119 | "Used to implement file-system caching of assets (this is typically only used in development, not production).
120 | File system caching improves startup and first-request time by avoiding the cost of recompling assets; instead
121 | the assets are stored on the file system. The asset checksums are used to see if the cache is still valid.
122 |
123 | Only assets that have truthy value for :compiled key will be file-system cached. This is set by the various
124 | compiler and transformers, such as the CoffeeScript to JavaScript transformer.
125 |
126 | Assets that are accessed for aggregation are not cached (the final aggregated asset will be cached).
127 |
128 | asset-handler
129 | : handler to be wrapped
130 |
131 | cache-dir-name
132 | : name of root folder to store cache in (from the Twixt options); the directory will be created as necessary"
133 | [asset-handler cache-dir-name]
134 | (let [cache-dir (io/file cache-dir-name "compiled")]
135 | (l/infof "Caching compiled assets to `%s'." cache-dir)
136 | (.mkdirs cache-dir)
137 | (fn [asset-path {:keys [asset-resolver for-aggregation] :as context}]
138 | (let [asset-cache-dir (io/file cache-dir asset-path)
139 | cached-asset (read-cached-asset asset-cache-dir)]
140 | (if (is-valid? asset-resolver cached-asset)
141 | cached-asset
142 | (do
143 | (delete-dir-and-contents asset-cache-dir)
144 | (let [asset (asset-handler asset-path context)]
145 | (if (and
146 | (:compiled asset)
147 | (not for-aggregation))
148 | (write-cached-asset asset-cache-dir asset))
149 | asset)))))))
150 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/jade.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.jade
2 | "Provides asset pipeline middleware for compiling Jade templates to HTML using jade4j."
3 | (:require [clojure.java.io :as io]
4 | [io.aviso.twixt :as twixt]
5 | [io.aviso.tracker :as t]
6 | [io.aviso.twixt.utils :as utils]
7 | [medley.core :as medley])
8 | (:import [de.neuland.jade4j JadeConfiguration]
9 | [de.neuland.jade4j.exceptions JadeException]
10 | [de.neuland.jade4j.template TemplateLoader]))
11 |
12 | (defn- add-missing-extension
13 | [^String name ext]
14 | (if (.endsWith name ext)
15 | name
16 | (str name ext)))
17 |
18 | (defn- create-template-loader [root-asset asset-resolver dependencies]
19 | (reify TemplateLoader
20 | ;; getLastModified is only needed for caching, and we disable Jade's caching
21 | ;; in favor of Twixt's.
22 | (getLastModified [_ name] -1)
23 | (getReader [_ name]
24 | (if (= name (:asset-path root-asset))
25 | (-> root-asset :content io/reader)
26 | (let [full-name (add-missing-extension name ".jade")]
27 | (t/track
28 | #(format "Including Jade source from asset `%s'." full-name)
29 | ;; Use the asset-resolver, not the asset pipeline, because we need
30 | ;; the un-compiled version of the asset.
31 | (let [included (asset-resolver full-name nil)]
32 | (utils/nil-check included "Included asset does not exist.")
33 | (swap! dependencies utils/add-asset-as-dependency included)
34 | (-> included
35 | :content
36 | ;; We have to trust that Jade will close the reader.
37 | io/reader))))))))
38 |
39 | (defn- wrap-asset-pipeline-with-dependency-tracker
40 | [asset-pipeline dependencies]
41 | (fn [asset-path context]
42 | (let [asset (asset-pipeline asset-path context)]
43 | (swap! dependencies utils/add-asset-as-dependency asset)
44 | asset)))
45 |
46 | (defn- create-shared-variables
47 | [asset {{:keys [helpers variables]} :jade :as context} dependencies]
48 | (let [context' (update-in context [:asset-pipeline]
49 | wrap-asset-pipeline-with-dependency-tracker dependencies)]
50 | (-> (medley/map-vals
51 | #(% asset context')
52 | (or helpers {}))
53 | (merge variables))))
54 |
55 | (defn- ^JadeConfiguration create-configuration
56 | [pretty-print asset {:keys [asset-resolver] :as context} dependencies]
57 | (doto (JadeConfiguration.)
58 | (.setPrettyPrint pretty-print)
59 | (.setSharedVariables (create-shared-variables asset context dependencies))
60 | (.setCaching false)
61 | (.setTemplateLoader (create-template-loader asset asset-resolver dependencies))))
62 |
63 | (defn- jade-compiler [asset context]
64 | (let [name (:resource-path asset)]
65 | (t/timer
66 | #(format "Compiled `%s' to HTML in %.2f ms" name %)
67 | (t/track
68 | #(format "Compiling `%s' from Jade to HTML" name)
69 | (try
70 | ;; Seed the dependencies with the Jade source file. Any included
71 | ;; sources will be added to dependencies.
72 | (let [dependencies (atom {name (utils/extract-dependency asset)})
73 | configuration (create-configuration (-> context :development-mode) asset context dependencies)
74 | template (.getTemplate configuration (:asset-path asset))
75 | compiled-output (.renderTemplate configuration template {})]
76 | (utils/create-compiled-asset asset "text/html" compiled-output @dependencies))
77 | (catch JadeException e
78 | (throw (RuntimeException.
79 | (format "Jade Compilation exception on line %d: %s"
80 | (.getLineNumber e)
81 | (or (.getMessage e) (-> e .getClass .getName)))
82 | e))))))))
83 |
84 | (defn- complete-path
85 | "Computes the complete path for a partial path, relative to an existing asset.
86 | Alternately, if the path starts with a leading slash (an absolute path), the the leading path is stripped.
87 |
88 | The result is a complete asset path that can be passed to [[get-asset-uri]]."
89 | [asset ^String path]
90 | (if (.startsWith path "/")
91 | (.substring path 1)
92 | (utils/compute-relative-path (:asset-path asset) path)))
93 |
94 | (defprotocol TwixtHelper
95 | "A Jade4J helper object that is used to allow a template to resolve asset URIs."
96 | (uri
97 | [this path]
98 | "Used to obtain the URI for a given asset, identified by its asset path.
99 | The path may be relative to the currently compiling asset, or may be absolute (with a leading slash).
100 |
101 | Throws an exception if the asset it not found.")
102 | (uris
103 | [this paths]
104 | "Used to obtain multiple URIs for any number of assets, each identified by a path.
105 |
106 | paths is a seq of asset path strings (eitehr relative to the current asset, or absolute).
107 |
108 | *Added in 0.1.21*"))
109 |
110 | (defn- create-twixt-helper
111 | [asset context]
112 | (reify TwixtHelper
113 | (uri [_ path]
114 | (twixt/get-asset-uri context (complete-path asset path)))
115 | (uris [_ paths]
116 | (->> paths
117 | (map (partial complete-path asset))
118 | (mapcat (partial twixt/get-asset-uris context))))))
119 |
120 | (defn register-jade
121 | "Updates the Twixt options with support for compiling Jade into HTML. Pretty printing of the output HTML is enabled
122 | in development mode, but disabled in production (for efficiency)."
123 | [options]
124 | (-> options
125 | (assoc-in [:content-types "jade"] "text/jade")
126 | (assoc-in [:content-transformers "text/jade"] jade-compiler)
127 | (assoc-in [:twixt-template :jade :helpers "twixt"] create-twixt-helper)))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/js_minification.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.js-minification
2 | "Provides support for JavaScript minification using the Google Closure compiler."
3 | (:require [clojure.java.io :as io]
4 | [io.aviso.twixt.utils :as utils]
5 | [io.aviso.twixt.schemas :refer [AssetHandler]]
6 | [io.aviso.tracker :as t]
7 | [clojure.string :as str]
8 | [schema.core :as s])
9 | (:import (com.google.javascript.jscomp CompilerOptions ClosureCodingConvention DiagnosticGroups CheckLevel
10 | CompilationLevel)
11 | [io.aviso.twixt.shims CompilerShim]))
12 |
13 |
14 |
15 | (defn- minimize-javascript-asset
16 | [{file-path :resource-path :as asset} compilation-level]
17 | (t/timer
18 | #(format "Minimized `%s' (%,d bytes) in %.2f ms"
19 | file-path (:size asset) %)
20 | (t/track
21 | #(format "Minimizing `%s' using Google Closure with compilation level %s" file-path compilation-level)
22 | (let [options ^CompilerOptions (doto (CompilerOptions.)
23 | (.setCodingConvention (ClosureCodingConvention.))
24 | (.setOutputCharset "utf-8")
25 | (.setWarningLevel DiagnosticGroups/CHECK_VARIABLES CheckLevel/WARNING))
26 | [failures output] (CompilerShim/runCompiler options
27 | compilation-level
28 | file-path
29 | (-> asset :content io/input-stream))]
30 | (if failures
31 | (throw (ex-info (str "JavaScript minimization failed: "
32 | (str/join "; " (seq failures)))
33 | asset))
34 | (utils/create-compiled-asset asset "text/javascript" output nil))))))
35 |
36 | (def ^:private optimization-levels {:simple CompilationLevel/SIMPLE_OPTIMIZATIONS
37 | :whitespace CompilationLevel/WHITESPACE_ONLY
38 | :advanced CompilationLevel/ADVANCED_OPTIMIZATIONS})
39 |
40 | (s/defn wrap-with-javascript-minimizations :- AssetHandler
41 | "Identifies JavaScript assets and, if not aggregating, passes them through the Google Closure compiler."
42 | [asset-handler :- AssetHandler
43 | {:keys [development-mode js-optimizations]}]
44 | (let [js-optimizations' (if (= :default js-optimizations)
45 | (if development-mode :none :simple)
46 | js-optimizations)
47 | level (optimization-levels js-optimizations')]
48 | (if (nil? level)
49 | asset-handler
50 | (fn [asset-path {:keys [for-aggregation] :as context}]
51 | (let [{:keys [content-type] :as asset} (asset-handler asset-path context)]
52 | (if (and (= "text/javascript" content-type)
53 | (not for-aggregation))
54 | (minimize-javascript-asset asset level)
55 | asset))))))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/less.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.less
2 | "Provides asset pipeline middleware for compiling Less source files to CSS."
3 | (:require [clojure.string :as str]
4 | [io.aviso.tracker :as t]
5 | [io.aviso.twixt.utils :as utils])
6 | (:import [com.github.sommeri.less4j LessSource LessSource$FileNotFound Less4jException LessCompiler$Problem LessCompiler LessCompiler$CompilationResult LessCompiler$Configuration LessCompiler$SourceMapConfiguration]
7 | [com.github.sommeri.less4j.core DefaultLessCompiler]))
8 |
9 | ;; Putting this logic inside the (proxy) call causes some really awful Clojure compiler problems.
10 | ;; This shim seems to defuse that.
11 | (defn- find-relative
12 | [asset-resolver asset relative-path context]
13 | (->
14 | asset
15 | :asset-path
16 | (utils/compute-relative-path relative-path)
17 | (asset-resolver context)))
18 |
19 | (defn- ^LessSource create-less-source
20 | [asset-resolver dependencies asset context]
21 | ;; Whenever a LessSource is created, associated the asset as a dependency; this includes the primary source
22 | ;; and all imported sources.
23 | (swap! dependencies utils/add-asset-as-dependency asset)
24 | (proxy [LessSource] []
25 | (relativeSource [filename]
26 | (if-let [rel (find-relative asset-resolver asset filename context)]
27 | (create-less-source asset-resolver dependencies rel context)
28 | (throw (new LessSource$FileNotFound))))
29 |
30 | (getName [] (-> asset :asset-path utils/path->name))
31 |
32 | (toString [] (:resource-path asset))
33 |
34 | (getContent []
35 | (->
36 | asset
37 | :content
38 | utils/as-string
39 | (.replace "\r\n" "\n")))
40 |
41 | (getBytes [] (:content asset))))
42 |
43 | (defn- problem-to-string [^LessCompiler$Problem problem]
44 | (let [source (-> problem .getSource .toString)
45 | line (-> problem .getLine)
46 | character (-> problem .getCharacter)
47 | message (-> problem .getMessage)]
48 | (str source
49 | ":" line
50 | ":" character
51 | ": " message)))
52 |
53 | (defn- format-less-exception [^Less4jException e]
54 | (let [problems (->> e .getErrors (map problem-to-string))]
55 | (str
56 | "Less compilation "
57 | (if (= 1 (count problems)) "error" "errors")
58 | ":\n"
59 | (str/join "\n" problems))))
60 |
61 |
62 | (defn- compile-less
63 | [^LessCompiler less-compiler asset {:keys [asset-resolver] :as context}]
64 | (let [path (:resource-path asset)]
65 | (t/timer
66 | #(format "Compiled `%s' to CSS in %.2f ms" path %)
67 | (t/track
68 | #(format "Compiling `%s' from Less to CSS" path)
69 | (try
70 | (let [dependencies (atom {})
71 | root-source (create-less-source asset-resolver dependencies asset context)
72 | ^LessCompiler$Configuration options (LessCompiler$Configuration.)
73 | _ (doto ^LessCompiler$SourceMapConfiguration (.getSourceMapConfiguration options)
74 | (.setLinkSourceMap false)
75 | (.setIncludeSourcesContent true))
76 | ;; A bit of work to trick the Less compiler into writing the right thing into the output CSS.
77 | ^LessCompiler$CompilationResult output (.compile less-compiler root-source options)
78 | complete-css (str
79 | (.getCss output)
80 | "\n/*# sourceMappingURL="
81 | (utils/path->name path)
82 | "@source.map */\n")
83 | source-map (.getSourceMap output)]
84 | (->
85 | asset
86 | (utils/create-compiled-asset "text/css" complete-css @dependencies)
87 | (utils/add-attachment "source.map" "application/json" (utils/as-bytes source-map))))
88 | (catch Less4jException e
89 | (throw (RuntimeException. (format-less-exception e) e))))))))
90 |
91 | (defn register-less
92 | "Updates the Twixt options with support for compiling Less into CSS."
93 | [options]
94 | (-> options
95 | (assoc-in [:content-types "less"] "text/less")
96 | (assoc-in [:content-transformers "text/less"] (partial compile-less (DefaultLessCompiler.)))))
97 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/memory_cache.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.memory-cache
2 | "Provides asset pipeline middleware implementing an in-memory cache for assets.
3 | This cache is used to bypass the normal loading, compiling, and transforming steps."
4 | (:require [io.aviso.twixt.schemas :refer [Asset AssetHandler]]
5 | [io.aviso.toolchest.macros :refer [cond-let]]
6 | [schema.core :as s])
7 | (:import [java.util.concurrent.locks ReentrantLock Lock]))
8 |
9 | (s/defn wrap-with-transforming-cache :- AssetHandler
10 | "A cache that operates in terms of a transformation of assets: the canonical example
11 | is GZip compression, where a certain assets can be compressed to form new assets.
12 |
13 | Assets that are obtained for aggregation are _never_ cached; it is assumed that they are
14 | intermediate results and only the final aggregated asset needs to be cached.
15 |
16 | This cache assumes that another cache is present that can quickly obtain the
17 | non-transformed version of the asset.
18 |
19 | asset-handler
20 | : The downstream asset handler, from which the untransformed asset is obtained.
21 |
22 | store-in-cache?
23 | : Function that is passed an Asset and determines if it can be transformed and stored in the cache.
24 |
25 | asset-tranformer
26 | : Function that is passed the untransformed Asset and returns the transformed Asset to be stored
27 | in the cache."
28 | {:added "0.1.20"}
29 | [asset-handler :- AssetHandler
30 | store-in-cache? :- (s/=> s/Bool Asset)
31 | asset-transformer :- (s/=> Asset Asset)]
32 | (let [cache (atom {})]
33 | (fn [asset-path {:keys [for-aggregation] :as context}]
34 | (cond-let
35 | ;; We assume this is cheap because of caching
36 | [prime-asset (asset-handler asset-path context)]
37 |
38 | for-aggregation
39 | prime-asset
40 |
41 | (nil? prime-asset)
42 | nil
43 |
44 | (not (store-in-cache? prime-asset))
45 | prime-asset
46 |
47 | [{:keys [transformed prime-checksum]} (get cache asset-path)]
48 |
49 | (= prime-checksum (:checksum prime-asset))
50 | transformed
51 |
52 | :else
53 | (let [transformed (asset-transformer prime-asset)]
54 | (swap! cache assoc asset-path {:transformed transformed
55 | :prime-checksum prime-checksum})
56 | transformed)))))
57 |
58 | (defmacro ^:private with-lock
59 | [lock & body]
60 | `(let [^Lock lock# ~lock]
61 | (try
62 | (.lock lock#)
63 | ~@body
64 | (finally
65 | (.unlock lock#)))))
66 |
67 | (defn- update-cache
68 | [cache asset-handler asset-path context]
69 | (let [cached-asset (asset-handler asset-path context)]
70 | (swap! cache update-in [asset-path]
71 | assoc :cached-asset cached-asset
72 | :cached-at (System/currentTimeMillis))
73 |
74 | cached-asset))
75 |
76 | (s/defn wrap-with-memory-cache :- AssetHandler
77 | "Wraps another asset handler in a simple in-memory cache.
78 |
79 | This is useful for both compiled and raw assets, as it does two things.
80 |
81 | On first access to an asset, a cache entry is created. Subsequent accesses
82 | to the cache entry within the check interval will be served from the cache.
83 |
84 | After the check interval, the delegate asset handler is used to see if the asset has changed,
85 | replacing it in the cache."
86 | {:added "0.1.20"}
87 | [asset-handler :- AssetHandler
88 | check-interval-ms :- s/Num]
89 | (let [cache (atom {})
90 | cache-lock (ReentrantLock.)]
91 | (fn [asset-path context]
92 | (trampoline
93 | (fn reentry-point []
94 | (cond-let
95 | [now (System/currentTimeMillis)
96 | {:keys [cached-asset cached-at] :as cache-entry} (get @cache asset-path)]
97 |
98 | (and cached-asset
99 | (< now (+ cached-at check-interval-ms)))
100 | cached-asset
101 |
102 | (nil? cache-entry)
103 | (let [lock (ReentrantLock.)]
104 | (with-lock cache-lock
105 | ;; There can be a race condition where two threads are trying to first
106 | ;; access the same asset. This detects that; the first thread will have
107 | ;; added a cache entry with just the :lock key, then released the cache
108 | ;; lock while obtaining the asset. The second thread will reach this point,
109 | ;; see that there's something in the cache, and loop back
110 | ;; to fall through the normal path, and the per-asset lock.
111 | (if (contains? @cache asset-path)
112 | ;; This releases the cache-lock and falls through the normal
113 | ;; processing. Depending on the timing, either the asset will just be in the cache
114 | ;; and ready to go, or this thread will block while the other thread
115 | ;; is obtaining the asset.
116 | reentry-point
117 | (do
118 | ;; Do this now, with the cache-lock locked
119 | (swap! cache assoc-in [asset-path :lock] lock)
120 | ;; And do this via trampoline, after the cache-lock is unlocked.
121 | #(with-lock lock
122 | (update-cache cache asset-handler asset-path context))))))
123 |
124 | :else
125 | (with-lock (:lock cache-entry)
126 | (update-cache cache asset-handler asset-path context))))))))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/rhino.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.rhino
2 | "Code to execute Rhino for purposes such as compiling CoffeeScript to JavaScript."
3 | ;; Liberally borrowing from Dieter!
4 | (:import
5 | [org.mozilla.javascript Context Scriptable Function])
6 | (:require
7 | [clojure.java.io :as io]
8 | [io.aviso.tracker :as t]))
9 |
10 |
11 | (defn- load-script [^Context context scope file]
12 | (with-open [content-reader (-> file io/resource io/reader)]
13 | (t/track
14 | #(str "Loading JavaScript from " file)
15 | (.evaluateReader context scope content-reader file 1 nil))))
16 |
17 | (defn- invoke-function [^Context context ^Scriptable scope ^String function-name arguments]
18 | (let [^Function js-fn (.get scope function-name scope)]
19 | (.call js-fn context scope nil (into-array arguments))))
20 |
21 | (defn invoke-javascript
22 | "Invokes a JavaScript function, returning the result.
23 |
24 | script-paths
25 | : JavaScript files to load, as seq of classpath resource paths
26 |
27 | javascript-fn-name
28 | : name of JavaScript function to execute
29 |
30 | arguments
31 | : additional arguments to pass to the function
32 |
33 | Returns the JavaScript result; typically this will be a JavaScript object, and the properties
34 | of it can be accessed via the methods of `java.util.Map`."
35 | [script-paths javascript-fn-name & arguments]
36 | (let [context (Context/enter)]
37 | (try
38 | ;; Apparently, CoffeeScript can blow away a 64K limit pretty easily.
39 | (.setOptimizationLevel context -1)
40 |
41 | (let [scope (.initStandardObjects context)]
42 |
43 | ;; Dieter maintains a pool of configured Context instances so that we don't
44 | ;; have to re-load the scripts on each compilation. That would be nice.
45 | (doseq [file script-paths]
46 | (load-script context scope file))
47 |
48 | (invoke-function context scope javascript-fn-name arguments))
49 |
50 | (finally (Context/exit)))))
51 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/ring.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.ring
2 | "Support needed to use Twixt inside a Ring handler pipeline."
3 | (:require [clojure.java.io :as io]
4 | [io.aviso.tracker :as t]
5 | [io.aviso.twixt [asset :as asset]]
6 | [ring.util.response :as r])
7 | (:import [java.util Calendar TimeZone]))
8 |
9 | (def ^:private far-future
10 | (let [cal (doto (Calendar/getInstance (TimeZone/getTimeZone "UTC"))
11 | (.add Calendar/YEAR 10))]
12 | (.getTime cal)))
13 |
14 | (defn- asset->ring-response
15 | [asset attachment-name]
16 | (let [content-source (if (some? attachment-name)
17 | (get-in asset [:attachments attachment-name])
18 | asset)]
19 | ;; content-source may be nil if the URL includes an attachment but the attachment doesn't exist.
20 | (if content-source
21 | ;; First the standard stuff ...
22 | (cond->
23 | (->
24 | content-source
25 | :content
26 | io/input-stream
27 | r/response
28 | (r/header "Content-Length" (:size content-source))
29 | ;; Because any change to the content will create a new checksum and a new URL, a change to the content
30 | ;; is really an entirely new resource. The current resource is therefore immutable and can have a far-future expires
31 | ;; header.
32 | (r/header "Expires" far-future)
33 | (r/content-type (:content-type content-source)))
34 |
35 | ;; The optional extras ...
36 | ;; :compressed will never be true for an attachment (that may change in the future).
37 | (:compressed content-source) (r/header "Content-Encoding" "gzip")))))
38 |
39 |
40 | (defn- match? [^String path-prefix ^String path]
41 | (and
42 | (.startsWith path path-prefix)
43 | (not (or (= path path-prefix)
44 | (.endsWith path "/")))))
45 |
46 | (defn- split-asset-path-and-attachment-name
47 | [^String path]
48 | (let [atx (.indexOf path "@")]
49 | (if (< atx 0)
50 | [path nil]
51 | [(.substring path 0 atx)
52 | (.substring path (inc atx))])))
53 |
54 | (defn- parse-path
55 | "Parses the complete request path into a checksum, compressed-flag, asset path and (optional) attachment name."
56 | [^String path-prefix ^String path]
57 | (let [suffix (.substring path (.length path-prefix))
58 | slashx (.indexOf suffix "/")
59 | full-checksum (.substring suffix 0 slashx)
60 | compressed? (.startsWith full-checksum "z")
61 | checksum (if compressed? (.substring full-checksum 1) full-checksum)
62 | [asset-path attachment-name] (-> suffix (.substring (inc slashx)) split-asset-path-and-attachment-name)]
63 | [checksum
64 | compressed?
65 | asset-path
66 | attachment-name]))
67 |
68 | (defn- asset->redirect-response
69 | [status path-prefix asset]
70 | {:status status
71 | :headers {"Location" (asset/asset->request-path path-prefix asset)}
72 | :body ""})
73 |
74 | (def ^:private asset->301-response (partial asset->redirect-response 301))
75 |
76 | (defn- create-asset-response
77 | [path-prefix requested-checksum asset attachment-name]
78 | (cond
79 | (nil? asset) nil
80 | (= requested-checksum (:checksum asset)) (asset->ring-response asset attachment-name)
81 | :else (asset->301-response path-prefix asset)))
82 |
83 | (defn twixt-handler
84 | "A Ring request handler that identifies requests targetted for Twixt assets. Returns a Ring response map
85 | if the request is for an existing asset, otherwise returns nil.
86 |
87 | The path may indicate an attachment to the asset (such as the source.map for a compiled JavaScript file).
88 | The attachment is indicated as a suffix: the `@` symbol and the name of the attachment.
89 |
90 | Asset URLs always include the intended asset's checksum; if the actual asset checksum does not match, then
91 | a 301 (moved permanently) response is sent with the correct asset URL."
92 | [request]
93 | (let [path-prefix (-> request :twixt :path-prefix)
94 | path (:uri request)]
95 | (when (match? path-prefix path)
96 | (t/track
97 | #(format "Handling asset request `%s'" path)
98 | (let [[requested-checksum compressed? asset-path attachment-name] (parse-path path-prefix path)
99 | context (:twixt request)
100 | ;; When actually servicing an asset request, we have to trust the data in the URL
101 | ;; that determines whether to server the normal or gzip'ed resource.
102 | context' (assoc context :gzip-enabled compressed?)
103 | asset-pipeline (:asset-pipeline context)]
104 | (create-asset-response path-prefix
105 | requested-checksum
106 | (asset-pipeline asset-path context')
107 | attachment-name))))))
108 |
109 | ;;; It's really difficult to start with a path, convert it to a resource, and figure out if it is a folder
110 | ;;; or a file on the classpath. Instead, we just assume that anything that doesn't look like a path that
111 | ;;; ends with a file extension can be ignored.
112 |
113 | (defn- handle-asset-redirect
114 | [^String uri context]
115 | ;; This may be too specific, may need to find a better way to differentiate between a "folder" and a file.
116 | ;; This just checks to see if the path ends with something that looks like an extension. We just have to
117 | ;; assume that none of the folders on classpath look like that! This is also a bit restrictive; it assumes
118 | ;; the extension consists of word characters, or the dash.
119 | (if (re-find #"\.[\w-]+$" uri)
120 | (let [{:keys [asset-pipeline path-prefix]} context
121 | asset-path (.substring uri 1)]
122 | (if-let [asset (asset-pipeline asset-path context)]
123 |
124 | (asset->redirect-response 302 path-prefix asset)))))
125 |
126 | (defn wrap-with-asset-redirector
127 | "In some cases, it is not possible for the client to know what the full asset URI will be, such as when the
128 | URL is composed on the client (in which case, the asset checksum will not be known).
129 | The redirector accepts any request path that maps to an asset and returns a redirect to the asset's true URL.
130 | Non-matching requests are passed through to the provided handler.
131 |
132 | For a file under `META-INF/assets`, such as `META-INF/assets/myapp/icon.png`, the redirector will match
133 | the URI `/myapp/icon.png` and send a redirect to `/assets/123abc/myapp/icon.png`.
134 |
135 | This middleware is __not__ applied by default."
136 | [handler]
137 | (fn [{uri :uri
138 | context :twixt
139 | :as request}]
140 | (or
141 | (handle-asset-redirect uri context)
142 | (handler request))))
143 |
144 | (defn wrap-with-twixt
145 | "Invokes the twixt-handler and delegates to the provided Ring handler if twixt-handler returns nil.
146 |
147 | This assumes that the resulting handler will then be wrapped with the twixt setup.
148 |
149 | In most cases, you will want to use the `wrap-with-twixt` function in the `io.aviso.twixt.startup` namespace."
150 | [handler]
151 | (fn [request]
152 | (or (twixt-handler request)
153 | (handler request))))
154 |
155 | (defn wrap-with-twixt-setup
156 | "Wraps a Ring handler with another Ring handler that provides the `:twixt` key in the request object.
157 |
158 | The `:twixt` key is the default asset pipeline context, which is needed by [[get-asset-uri]] in order to resolve asset paths
159 | to an actual asset.
160 | It also contains the keys `:asset-pipeline` (the pipeline used to resolve assets),
161 | `:stack-frame-filter` (which is used by the HTML exception report) and `:development-mode`.
162 |
163 | This provides the information needed by the actual Twixt handler, as well as anything else downstream that needs to
164 | generate Twixt asset URIs."
165 | [handler twixt-options asset-pipeline]
166 | (let [context (-> twixt-options
167 | ;; Pass down only what is needed to generate asset URIs, or to produce the HTML exception report.
168 | (select-keys [:path-prefix :stack-frame-filter :development-mode])
169 | (assoc :asset-pipeline asset-pipeline)
170 | (merge (:twixt-template twixt-options)))]
171 | (fn [request]
172 | (handler (assoc request :twixt context)))))
173 |
--------------------------------------------------------------------------------
/src/io/aviso/twixt/schemas.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.schemas
2 | "Defines schemas for the main types and functions."
3 | {:added "0.1.17"}
4 | (:require [schema.core :as s]))
5 |
6 | (def Content
7 | "Defines the content for an Asset; this is any value compatible with clojure.java.io."
8 | s/Any)
9 |
10 | (def AssetPath
11 | "Path of the asset under the root folder (which is typically `/META-INF/assets/`)."
12 | s/Str)
13 |
14 | (def ResourcePath
15 | "Full path of the underlying resource (on the classpath)."
16 | s/Str)
17 |
18 | (def ContentType
19 | "The MIME type of the content, as determined form the path's extension."
20 | s/Str)
21 |
22 | (def Size
23 | "Size of the content, in bytes."
24 | s/Num)
25 |
26 | (def Checksum
27 | "Computed Adler32 checksum of the content, as a string of hex characters."
28 | s/Str)
29 |
30 | (def ModifiedAt
31 | "Instant at which an asset was last modified; this may not be accurate for assets
32 | stored within a JAR file, but will be adequate for determining if a file's content
33 | has changed."
34 | s/Inst)
35 |
36 | (def AttachmentName
37 | "A unique logical name that represents an attachment to an asset.
38 | Attachments are a limited form of an Asset typically generated as a side-effect
39 | of creating/compiling the asset; the canonical example is a JavaScript source map."
40 | s/Str)
41 |
42 | (s/defschema Dependency
43 | {:asset-path AssetPath
44 | :checksum Checksum
45 | :modified-at ModifiedAt})
46 |
47 | (s/defschema DependencyMap
48 | {ResourcePath Dependency})
49 |
50 | (s/defschema Attachment
51 | {:content Content
52 | :size Size
53 | :content-type ContentType})
54 |
55 | (s/defschema AttachmentMap
56 | {AttachmentName Attachment})
57 |
58 | (s/defschema Asset
59 | "A server-side resource that may be exposed to a client (such as a web browser).
60 | An Asset may be transformed from raw content, for example to transpile a language
61 | to JavaScript. Other transformations include compression."
62 | {:asset-path AssetPath
63 | :resource-path ResourcePath
64 | :content-type ContentType
65 | :size Size
66 | :checksum Checksum
67 | :modified-at ModifiedAt
68 | :content Content
69 | (s/optional-key :compiled) s/Bool
70 | (s/optional-key :aggregate-asset-paths) [AssetPath]
71 | :dependencies DependencyMap
72 | (s/optional-key :attachments) AttachmentMap
73 | ;; Various plugins and extensions will add their own keys, so we need to not be picky:
74 | s/Any s/Any
75 | })
76 |
77 | (s/defschema TwixtContext
78 | "Defines the minimal values provided in the Twixt context (the :twixt key of the Ring
79 | request map)."
80 | ;; :asset-pipeline will always be present when requests are processing; there's
81 | ;; just a bit of chicken-or-the-egg at startup time.
82 | {(s/optional-key :asset-pipeline) s/Any ; should be AssetHandler
83 | :path-prefix s/Str
84 | :development-mode s/Bool
85 | (s/optional-key :gzip-enabled) (s/maybe s/Bool) ; often set explicitly to false for aggregation
86 | ;; Other plugins are likely to add thier own data to the context
87 | ;; (via the :twixt-template key of the Twixt options).
88 | s/Any s/Any})
89 |
90 | (s/defschema AssetHandler
91 | "An asset handler is passed as asset path and the Twixt context and, maybe, returns
92 | an Asset."
93 | (s/=> (s/maybe Asset) AssetPath TwixtContext))
94 |
95 | (def AssetURI
96 | "A URI that allows an external client to access the client content. AssetURIs typically
97 | include the checksum in the URI path, forming an immutable web resource."
98 | s/Str)
99 |
100 | (s/defschema AssetExport
101 | "Either just an asset path (a String), or two strings: an Asset to export, and an alias
102 | to export it as."
103 | (s/either AssetPath
104 | [(s/one AssetPath 'asset-path) (s/one AssetPath 'output-alias)]))
105 |
106 | (s/defschema ExportsConfiguration
107 | "Defines the details of how assets are exported to the file system.
108 |
109 | :interval-ms
110 | : The time between checks for performing exports.
111 | : Checks only occur during requests, so an external ping process may be needed to keep checks running.
112 |
113 | :output-dir
114 | : The root directory to output files to. The directory will be created as necessary.
115 |
116 | :assets
117 | : The assets to export.
118 | : Keys are the assets to export."
119 | {:interval-ms s/Num
120 | :output-dir s/Str
121 | :output-uri s/Str
122 | :assets [AssetExport]})
--------------------------------------------------------------------------------
/src/io/aviso/twixt/stacks.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.stacks
2 | "Stacks are a way of combining several related files together to form a single aggregate virtual file.
3 | In development, the individual files are used, but in production they are aggregated together so that
4 | the entire stack can be obtained in a single request."
5 | {:since "0.1.13"}
6 | (:require [io.aviso.tracker :as t]
7 | [io.aviso.twixt.utils :as utils]
8 | [clojure.java.io :as io]
9 | [clojure.edn :as edn])
10 | (:import (java.io PushbackReader ByteArrayOutputStream)))
11 |
12 | (def stack-mime-type
13 | "Defines a MIME type for a Twixt stack, a kind of aggregate resource that combines
14 | multiple other resources."
15 | "application/vnd.twixt-stack+edn")
16 |
17 | (defn- read-stack
18 | [asset]
19 | (-> asset :content io/reader PushbackReader. edn/read))
20 |
21 | (defn- update-aggregate-asset-paths
22 | [asset-paths component-asset]
23 | (if-let [aggregate-paths (-> component-asset :aggregate-asset-paths seq)]
24 | (concat asset-paths aggregate-paths)
25 | (conj asset-paths (:asset-path component-asset))))
26 |
27 | (defn- include-component
28 | [{:keys [asset-pipeline] :as context} {:keys [asset-path] :as asset} component-path content-stream]
29 | ;; This may only be correct for relative, not absolute, paths:
30 | (let [complete-path (utils/compute-relative-path asset-path component-path)
31 | component-asset (asset-pipeline complete-path context)]
32 |
33 | (if-not (some? component-asset)
34 | (throw (ex-info (format "Could not locate resource `%s' (a component of `%s')."
35 | complete-path
36 | asset-path)
37 | context)))
38 |
39 | (io/copy (:content component-asset) content-stream)
40 |
41 | ;; Now add the component asset as a dependency by itself, or
42 | ;; add the component asset's dependencies
43 |
44 | (-> asset
45 | (update-in [:dependencies] utils/add-asset-as-dependency component-asset)
46 | (update-in [:aggregate-asset-paths] (fnil update-aggregate-asset-paths []) component-asset))))
47 |
48 | (defn- aggregate-stack
49 | [{:keys [resource-path] :as asset} context]
50 | (t/track
51 | #(format "Reading stack `%s' and aggregating contents." resource-path)
52 | ;; Make sure that the aggregated asset is not compressed
53 | (let [context' (assoc context :for-aggregation true)
54 | stack (read-stack asset)]
55 |
56 | (assert (-> stack :content-type some?))
57 | (assert (-> stack :components empty? not))
58 |
59 | (with-open [content-stream (ByteArrayOutputStream. 5000)]
60 | (-> (reduce (fn [asset component-path] (include-component context' asset component-path content-stream)) asset (:components stack))
61 | (assoc-in [:dependencies resource-path] (utils/extract-dependency asset))
62 | (utils/replace-asset-content (:content-type stack) (.toByteArray content-stream))
63 | (assoc :compiled true))))))
64 |
65 | (defn register-stacks
66 | "Updates the Twixt options with support for stacks."
67 | [options]
68 | (-> options
69 | (assoc-in [:content-types "stack"] stack-mime-type)
70 | (assoc-in [:content-transformers stack-mime-type] aggregate-stack)))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/startup.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.startup
2 | "Breaks out the default logic for initializing Twixt's handlers and middleware."
3 | (:require
4 | [io.aviso.twixt :as t]
5 | [io.aviso.twixt
6 | [coffee-script :as cs]
7 | [compress :as compress]
8 | [exceptions :as te]
9 | [export :as export]
10 | [jade :as jade]
11 | [less :as less]
12 | [ring :as ring]
13 | [stacks :as stacks]]))
14 |
15 | (defn wrap-with-twixt
16 | "The default way to setup Twixt, with exception reporting.
17 | This (currently) enables support for CoffeeScript, Less, and Jade, and Stacks.
18 |
19 | The provided Ring request handler is wrapped in the following stack (outermost to innermost):
20 |
21 | - twixt setup (adds `:twixt` key to the request)
22 | - exception reporting
23 | - compression analyzer (does the client support GZip encoding?)
24 | - asset export logic (exports certain assets to file system when changed
25 | - asset request handling
26 | - the provided handler
27 |
28 | With just a handler, uses the default Twixt options and production mode.
29 |
30 | The two argument version is used to set development-mode, but use default options.
31 |
32 | Otherwise, provide the handler, alternate options and true or false for development mode.
33 | The alternate options are merged with defaults and override them."
34 | ([handler]
35 | (wrap-with-twixt handler false))
36 | ([handler development-mode]
37 | (wrap-with-twixt handler t/default-options development-mode))
38 | ([handler opts development-mode]
39 | (let [twixt-options (-> (merge t/default-options opts)
40 | (assoc :development-mode development-mode)
41 | te/register-exception-reporting
42 | cs/register-coffee-script
43 | jade/register-jade
44 | less/register-less
45 | stacks/register-stacks)
46 | asset-pipeline (t/default-asset-pipeline twixt-options)]
47 | (->
48 | handler
49 | ring/wrap-with-twixt
50 | (export/wrap-with-exporter (:exports twixt-options))
51 | te/wrap-with-exception-reporting
52 | compress/wrap-with-compression-analyzer
53 | (ring/wrap-with-twixt-setup twixt-options asset-pipeline)))))
--------------------------------------------------------------------------------
/src/io/aviso/twixt/utils.clj:
--------------------------------------------------------------------------------
1 | (ns io.aviso.twixt.utils
2 | "Some re-usable utilities.
3 |
4 | Many of these are useful when creating new compilers or translators for Twixt."
5 | (:require [clojure.java.io :as io]
6 | [clojure.string :as str]
7 | [schema.core :as s]
8 | [io.aviso.twixt.schemas :refer :all])
9 | (:import [java.io CharArrayWriter ByteArrayOutputStream File]
10 | [java.util.zip Adler32]
11 | [java.net URL URI]
12 | [java.util Date]))
13 |
14 | (defn ^String as-string
15 | "Converts a source (compatible with clojure.java.io/IOFactory) into a String using the provided encoding.
16 |
17 | The source is typically a byte array, or a File.
18 |
19 | The default charset is UTF-8."
20 | ([source]
21 | (as-string source "UTF-8"))
22 | ([source charset]
23 | (with-open [reader (io/reader source :encoding charset)
24 | writer (CharArrayWriter. 1000)]
25 | (io/copy reader writer)
26 | (.toString writer))))
27 |
28 | (defn as-bytes [^String string]
29 | "Converts a string to a byte array. The string should be UTF-8 encoded."
30 | (.getBytes string "UTF-8"))
31 |
32 | (s/defn compute-checksum :- Checksum
33 | "Returns a hex string of the Adler32 checksum of the content."
34 | [^bytes content]
35 | (->
36 | (doto (Adler32.)
37 | (.update content))
38 | .getValue
39 | Long/toHexString))
40 |
41 | (s/defn replace-asset-content :- Asset
42 | "Modifies an Asset new content.
43 | This updates the :size and :checksum properties as well."
44 | [asset :- Asset
45 | content-type :- ContentType
46 | ^bytes content-bytes]
47 | (assoc asset
48 | :content-type content-type
49 | :content content-bytes
50 | :size (alength content-bytes)
51 | :checksum (compute-checksum content-bytes)))
52 |
53 | (s/defn extract-dependency :- Dependency
54 | "Extracts from the asset the keys needed to track dependencies (used by caching logic)."
55 | [asset :- Asset]
56 | (select-keys asset [:checksum :modified-at :asset-path]))
57 |
58 | (s/defn add-asset-as-dependency :- DependencyMap
59 | "Adds the dependencies of the Asset to a dependency map."
60 | [dependencies :- DependencyMap
61 | asset :- Asset]
62 | (merge dependencies (:dependencies asset)))
63 |
64 | (s/defn create-compiled-asset :- Asset
65 | "Used to transform an Asset after it has been compiled from one form to another.
66 | Dependencies is a map of resource path to source asset details, used to check cache validity.
67 |
68 | The source asset's dependencies are merged into any provided dependencies to form the :dependencies entry of the output Asset."
69 | [source-asset :- Asset
70 | content-type :- s/Str
71 | content :- s/Str
72 | dependencies :- (s/maybe DependencyMap)]
73 | (let [merged-dependencies (add-asset-as-dependency (or dependencies {}) source-asset)]
74 | (->
75 | source-asset
76 | (replace-asset-content content-type (as-bytes content))
77 | (assoc :compiled true :dependencies merged-dependencies))))
78 |
79 | (s/defn add-attachment :- Asset
80 | "Adds an attachment to an asset."
81 | {:since "0.1.13"}
82 | [asset :- Asset
83 | name :- AttachmentName
84 | content-type :- ContentType
85 | ^bytes content]
86 | (assoc-in asset [:attachments name] {:content-type content-type
87 | :content content
88 | :size (alength content)}))
89 |
90 | (defn read-content
91 | "Reads the content of a provided source (compatible with `clojure.java.io/input-stream`) as a byte array
92 |
93 | The content is usually a URI or URL."
94 | [source]
95 | (assert source "Unable to read content from nil.")
96 | (with-open [bos (ByteArrayOutputStream.)
97 | in (io/input-stream source)]
98 | (io/copy in bos)
99 | (.toByteArray bos)))
100 |
101 | (defn compute-relative-path
102 | [^String start ^String relative]
103 | ;; Convert the start path into a stack of just the folders
104 | (loop [path-terms (-> (.split start "/") reverse rest)
105 | terms (-> relative (.split "/"))]
106 | (let [[term & remaining] terms]
107 | (cond
108 | (empty? terms) (->> path-terms reverse (str/join "/"))
109 | (or (= term ".") (= term "")) (recur path-terms remaining)
110 | (= term "..") (if (empty? path-terms)
111 | ;; You could rewrite this with reduce, but then generating this exception would be more difficult:
112 | (throw (IllegalArgumentException. (format "Relative path `%s' for `%s' would go above root." relative start)))
113 | (recur (rest path-terms) remaining))
114 | :else (recur (cons term path-terms) remaining)))))
115 |
116 | (defn- url-to-file
117 | [^URL url]
118 | (-> url .toURI File.))
119 |
120 | (defn- jar-to-file
121 | "For a URL that points to a file inside a jar, this returns the JAR file itself."
122 | [^URL url]
123 | (-> url
124 | .getPath
125 | (str/split #"!")
126 | first
127 | URI.
128 | File.))
129 |
130 | ;; Not the same as io/file!
131 | (defn- ^File as-file
132 | "Locates a file from which a last modified date can be extracted."
133 | [^URL url]
134 | (cond
135 | (= "jar" (.getProtocol url)) (jar-to-file url)
136 | :else (url-to-file url)))
137 |
138 | (s/defn modified-at :- Date
139 | "Extracts a last-modified Date"
140 | [url :- URL]
141 | (if-let [^long time-modified (some-> url as-file .lastModified)]
142 | (Date. time-modified)))
143 |
144 | (defn nil-check
145 | {:no-doc true}
146 | [value message]
147 | (or
148 | value
149 | (throw (NullPointerException. message))))
150 |
151 | (defn path->name
152 | "Converts a path to just the file name, the last term in the path."
153 | [^String path]
154 | (let [slashx (.lastIndexOf path "/")]
155 | (if (< slashx 0)
156 | path
157 | (.substring path (inc slashx)))))
158 |
159 |
--------------------------------------------------------------------------------