├── .agignore ├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── build-deployment-container.yml │ ├── build-production-container.yml │ └── codeql-analysis.yml ├── .gitignore ├── .gitmodules ├── .jslintrc ├── Dockerfile ├── LICENSE ├── README.md ├── app.build.js ├── app.psgi ├── app ├── fonts ├── inc │ ├── almond.js │ ├── backbone.js │ ├── behave.js │ ├── highlight-default.css │ ├── highlight.js │ ├── jquery.js │ ├── jquery.querystring.js │ ├── less.js │ ├── require.js │ ├── text.js │ ├── tpl.js │ └── underscore.js ├── index.html ├── scripts │ ├── app.js │ ├── collection.js │ ├── model.js │ ├── model │ │ ├── request.js │ │ └── settings.js │ ├── router.js │ ├── settings.js │ ├── store │ │ └── gist.js │ ├── template │ │ ├── navbar.htm │ │ ├── request.htm │ │ ├── settings.htm │ │ └── sidebar.htm │ ├── view.js │ └── view │ │ ├── dragbar.js │ │ ├── list-item.js │ │ ├── navbar.js │ │ ├── request.js │ │ ├── settings.js │ │ ├── sidebar.js │ │ └── viewport.js └── styles │ ├── bootstrap.less │ ├── dragbar.less │ ├── logo.less │ ├── main.less │ ├── request.less │ ├── settings.less │ └── sidebar.less ├── bin ├── docker-build.sh └── docker-run.sh ├── build.sh ├── build ├── app.js ├── fonts ├── index.html └── styles.css ├── env-filter.js ├── inc └── bootstrap ├── package-lock.json ├── package.json └── server.js /.agignore: -------------------------------------------------------------------------------- 1 | build/ 2 | inc/ 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | # We recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | # Don't mess with stuff in inc. 20 | [app/inc/**] 21 | trim_trailing_whitespace = false 22 | insert_final_newline = false 23 | -------------------------------------------------------------------------------- /.github/workflows/build-deployment-container.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build deployment container 3 | on: 4 | push: 5 | branches: 6 | - prod 7 | - staging 8 | workflow_dispatch: 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-22.04 12 | name: Docker Push 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Docker build 16 | run: docker build . -t metacpan/metacpan-explorer:$GITHUB_SHA 17 | - name: Log in to Docker Hub 18 | uses: docker/login-action@v2 19 | with: 20 | username: ${{ secrets.DOCKER_HUB_USER }} 21 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 22 | - name: Push build to Docker hub 23 | run: docker push metacpan/metacpan-explorer:$GITHUB_SHA 24 | -------------------------------------------------------------------------------- /.github/workflows/build-production-container.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build production container 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-22.04 11 | name: Docker Push 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: docker build 15 | run: docker build . -t metacpan/metacpan-explorer:latest 16 | - name: Log in to Docker Hub 17 | uses: docker/login-action@v2 18 | with: 19 | username: ${{ secrets.DOCKER_HUB_USER }} 20 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 21 | - name: Push build to Docker Hub 22 | run: docker push metacpan/metacpan-explorer:latest 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '25 17 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "inc/bootstrap"] 2 | path = app/inc/bootstrap 3 | url = https://github.com/twbs/bootstrap.git 4 | -------------------------------------------------------------------------------- /.jslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "indent": 2, 4 | "predef": [ 5 | "define" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.15.1-alpine 2 | 3 | EXPOSE 8080 4 | 5 | WORKDIR /usr/src/app 6 | 7 | # Bundle app source 8 | COPY . . 9 | 10 | RUN npm install --verbose 11 | 12 | CMD [ "npm", "start" ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is copyright (c) 2010 by Olaf Alders. 2 | 3 | This is free software; you can redistribute it and/or modify it under 4 | the same terms as the Perl 5 programming language system itself. 5 | 6 | Terms of the Perl programming language system itself 7 | 8 | a) the GNU General Public License as published by the Free 9 | Software Foundation; either version 1, or (at your option) any 10 | later version, or 11 | b) the "Artistic License" 12 | 13 | --- The GNU General Public License, Version 1, February 1989 --- 14 | 15 | This software is Copyright (c) 2010 by Olaf Alders. 16 | 17 | This is free software, licensed under: 18 | 19 | The GNU General Public License, Version 1, February 1989 20 | 21 | GNU GENERAL PUBLIC LICENSE 22 | Version 1, February 1989 23 | 24 | Copyright (C) 1989 Free Software Foundation, Inc. 25 | 51 Franklin St, Suite 500, Boston, MA 02110-1335 USA 26 | 27 | Everyone is permitted to copy and distribute verbatim copies 28 | of this license document, but changing it is not allowed. 29 | 30 | Preamble 31 | 32 | The license agreements of most software companies try to keep users 33 | at the mercy of those companies. By contrast, our General Public 34 | License is intended to guarantee your freedom to share and change free 35 | software--to make sure the software is free for all its users. The 36 | General Public License applies to the Free Software Foundation's 37 | software and to any other program whose authors commit to using it. 38 | You can use it for your programs, too. 39 | 40 | When we speak of free software, we are referring to freedom, not 41 | price. Specifically, the General Public License is designed to make 42 | sure that you have the freedom to give away or sell copies of free 43 | software, that you receive source code or can get it if you want it, 44 | that you can change the software or use pieces of it in new free 45 | programs; and that you know you can do these things. 46 | 47 | To protect your rights, we need to make restrictions that forbid 48 | anyone to deny you these rights or to ask you to surrender the rights. 49 | These restrictions translate to certain responsibilities for you if you 50 | distribute copies of the software, or if you modify it. 51 | 52 | For example, if you distribute copies of a such a program, whether 53 | gratis or for a fee, you must give the recipients all the rights that 54 | you have. You must make sure that they, too, receive or can get the 55 | source code. And you must tell them their rights. 56 | 57 | We protect your rights with two steps: (1) copyright the software, and 58 | (2) offer you this license which gives you legal permission to copy, 59 | distribute and/or modify the software. 60 | 61 | Also, for each author's protection and ours, we want to make certain 62 | that everyone understands that there is no warranty for this free 63 | software. If the software is modified by someone else and passed on, we 64 | want its recipients to know that what they have is not the original, so 65 | that any problems introduced by others will not reflect on the original 66 | authors' reputations. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | GNU GENERAL PUBLIC LICENSE 72 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 73 | 74 | 0. This License Agreement applies to any program or other work which 75 | contains a notice placed by the copyright holder saying it may be 76 | distributed under the terms of this General Public License. The 77 | "Program", below, refers to any such program or work, and a "work based 78 | on the Program" means either the Program or any work containing the 79 | Program or a portion of it, either verbatim or with modifications. Each 80 | licensee is addressed as "you". 81 | 82 | 1. You may copy and distribute verbatim copies of the Program's source 83 | code as you receive it, in any medium, provided that you conspicuously and 84 | appropriately publish on each copy an appropriate copyright notice and 85 | disclaimer of warranty; keep intact all the notices that refer to this 86 | General Public License and to the absence of any warranty; and give any 87 | other recipients of the Program a copy of this General Public License 88 | along with the Program. You may charge a fee for the physical act of 89 | transferring a copy. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion of 92 | it, and copy and distribute such modifications under the terms of Paragraph 93 | 1 above, provided that you also do the following: 94 | 95 | a) cause the modified files to carry prominent notices stating that 96 | you changed the files and the date of any change; and 97 | 98 | b) cause the whole of any work that you distribute or publish, that 99 | in whole or in part contains the Program or any part thereof, either 100 | with or without modifications, to be licensed at no charge to all 101 | third parties under the terms of this General Public License (except 102 | that you may choose to grant warranty protection to some or all 103 | third parties, at your option). 104 | 105 | c) If the modified program normally reads commands interactively when 106 | run, you must cause it, when started running for such interactive use 107 | in the simplest and most usual way, to print or display an 108 | announcement including an appropriate copyright notice and a notice 109 | that there is no warranty (or else, saying that you provide a 110 | warranty) and that users may redistribute the program under these 111 | conditions, and telling the user how to view a copy of this General 112 | Public License. 113 | 114 | d) You may charge a fee for the physical act of transferring a 115 | copy, and you may at your option offer warranty protection in 116 | exchange for a fee. 117 | 118 | Mere aggregation of another independent work with the Program (or its 119 | derivative) on a volume of a storage or distribution medium does not bring 120 | the other work under the scope of these terms. 121 | 122 | 3. You may copy and distribute the Program (or a portion or derivative of 123 | it, under Paragraph 2) in object code or executable form under the terms of 124 | Paragraphs 1 and 2 above provided that you also do one of the following: 125 | 126 | a) accompany it with the complete corresponding machine-readable 127 | source code, which must be distributed under the terms of 128 | Paragraphs 1 and 2 above; or, 129 | 130 | b) accompany it with a written offer, valid for at least three 131 | years, to give any third party free (except for a nominal charge 132 | for the cost of distribution) a complete machine-readable copy of the 133 | corresponding source code, to be distributed under the terms of 134 | Paragraphs 1 and 2 above; or, 135 | 136 | c) accompany it with the information you received as to where the 137 | corresponding source code may be obtained. (This alternative is 138 | allowed only for noncommercial distribution and only if you 139 | received the program in object code or executable form alone.) 140 | 141 | Source code for a work means the preferred form of the work for making 142 | modifications to it. For an executable file, complete source code means 143 | all the source code for all modules it contains; but, as a special 144 | exception, it need not include source code for modules which are standard 145 | libraries that accompany the operating system on which the executable 146 | file runs, or for standard header files or definitions files that 147 | accompany that operating system. 148 | 149 | 4. You may not copy, modify, sublicense, distribute or transfer the 150 | Program except as expressly provided under this General Public License. 151 | Any attempt otherwise to copy, modify, sublicense, distribute or transfer 152 | the Program is void, and will automatically terminate your rights to use 153 | the Program under this License. However, parties who have received 154 | copies, or rights to use copies, from you under this General Public 155 | License will not have their licenses terminated so long as such parties 156 | remain in full compliance. 157 | 158 | 5. By copying, distributing or modifying the Program (or any work based 159 | on the Program) you indicate your acceptance of this license to do so, 160 | and all its terms and conditions. 161 | 162 | 6. Each time you redistribute the Program (or any work based on the 163 | Program), the recipient automatically receives a license from the original 164 | licensor to copy, distribute or modify the Program subject to these 165 | terms and conditions. You may not impose any further restrictions on the 166 | recipients' exercise of the rights granted herein. 167 | 168 | 7. The Free Software Foundation may publish revised and/or new versions 169 | of the General Public License from time to time. Such new versions will 170 | be similar in spirit to the present version, but may differ in detail to 171 | address new problems or concerns. 172 | 173 | Each version is given a distinguishing version number. If the Program 174 | specifies a version number of the license which applies to it and "any 175 | later version", you have the option of following the terms and conditions 176 | either of that version or of any later version published by the Free 177 | Software Foundation. If the Program does not specify a version number of 178 | the license, you may choose any version ever published by the Free Software 179 | Foundation. 180 | 181 | 8. If you wish to incorporate parts of the Program into other free 182 | programs whose distribution conditions are different, write to the author 183 | to ask for permission. For software which is copyrighted by the Free 184 | Software Foundation, write to the Free Software Foundation; we sometimes 185 | make exceptions for this. Our decision will be guided by the two goals 186 | of preserving the free status of all derivatives of our free software and 187 | of promoting the sharing and reuse of software generally. 188 | 189 | NO WARRANTY 190 | 191 | 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 192 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 193 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 194 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 195 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 196 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 197 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 198 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 199 | REPAIR OR CORRECTION. 200 | 201 | 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 202 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 203 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 204 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 205 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 206 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 207 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 208 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 209 | POSSIBILITY OF SUCH DAMAGES. 210 | 211 | END OF TERMS AND CONDITIONS 212 | 213 | Appendix: How to Apply These Terms to Your New Programs 214 | 215 | If you develop a new program, and you want it to be of the greatest 216 | possible use to humanity, the best way to achieve this is to make it 217 | free software which everyone can redistribute and change under these 218 | terms. 219 | 220 | To do so, attach the following notices to the program. It is safest to 221 | attach them to the start of each source file to most effectively convey 222 | the exclusion of warranty; and each file should have at least the 223 | "copyright" line and a pointer to where the full notice is found. 224 | 225 | 226 | Copyright (C) 19yy 227 | 228 | This program is free software; you can redistribute it and/or modify 229 | it under the terms of the GNU General Public License as published by 230 | the Free Software Foundation; either version 1, or (at your option) 231 | any later version. 232 | 233 | This program is distributed in the hope that it will be useful, 234 | but WITHOUT ANY WARRANTY; without even the implied warranty of 235 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 236 | GNU General Public License for more details. 237 | 238 | You should have received a copy of the GNU General Public License 239 | along with this program; if not, write to the Free Software 240 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA 241 | 242 | 243 | Also add information on how to contact you by electronic and paper mail. 244 | 245 | If the program is interactive, make it output a short notice like this 246 | when it starts in an interactive mode: 247 | 248 | Gnomovision version 69, Copyright (C) 19xx name of author 249 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 250 | This is free software, and you are welcome to redistribute it 251 | under certain conditions; type `show c' for details. 252 | 253 | The hypothetical commands `show w' and `show c' should show the 254 | appropriate parts of the General Public License. Of course, the 255 | commands you use may be called something other than `show w' and `show 256 | c'; they could even be mouse-clicks or menu items--whatever suits your 257 | program. 258 | 259 | You should also get your employer (if you work as a programmer) or your 260 | school, if any, to sign a "copyright disclaimer" for the program, if 261 | necessary. Here a sample; alter the names: 262 | 263 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 264 | program `Gnomovision' (a program to direct compilers to make passes 265 | at assemblers) written by James Hacker. 266 | 267 | , 1 April 1989 268 | Ty Coon, President of Vice 269 | 270 | That's all there is to it! 271 | 272 | 273 | --- The Artistic License 1.0 --- 274 | 275 | This software is Copyright (c) 2010 by Olaf Alders. 276 | 277 | This is free software, licensed under: 278 | 279 | The Artistic License 1.0 280 | 281 | The Artistic License 282 | 283 | Preamble 284 | 285 | The intent of this document is to state the conditions under which a Package 286 | may be copied, such that the Copyright Holder maintains some semblance of 287 | artistic control over the development of the package, while giving the users of 288 | the package the right to use and distribute the Package in a more-or-less 289 | customary fashion, plus the right to make reasonable modifications. 290 | 291 | Definitions: 292 | 293 | - "Package" refers to the collection of files distributed by the Copyright 294 | Holder, and derivatives of that collection of files created through 295 | textual modification. 296 | - "Standard Version" refers to such a Package if it has not been modified, 297 | or has been modified in accordance with the wishes of the Copyright 298 | Holder. 299 | - "Copyright Holder" is whoever is named in the copyright or copyrights for 300 | the package. 301 | - "You" is you, if you're thinking about copying or distributing this Package. 302 | - "Reasonable copying fee" is whatever you can justify on the basis of media 303 | cost, duplication charges, time of people involved, and so on. (You will 304 | not be required to justify it to the Copyright Holder, but only to the 305 | computing community at large as a market that must bear the fee.) 306 | - "Freely Available" means that no fee is charged for the item itself, though 307 | there may be fees involved in handling the item. It also means that 308 | recipients of the item may redistribute it under the same conditions they 309 | received it. 310 | 311 | 1. You may make and give away verbatim copies of the source form of the 312 | Standard Version of this Package without restriction, provided that you 313 | duplicate all of the original copyright notices and associated disclaimers. 314 | 315 | 2. You may apply bug fixes, portability fixes and other modifications derived 316 | from the Public Domain or from the Copyright Holder. A Package modified in such 317 | a way shall still be considered the Standard Version. 318 | 319 | 3. You may otherwise modify your copy of this Package in any way, provided that 320 | you insert a prominent notice in each changed file stating how and when you 321 | changed that file, and provided that you do at least ONE of the following: 322 | 323 | a) place your modifications in the Public Domain or otherwise make them 324 | Freely Available, such as by posting said modifications to Usenet or an 325 | equivalent medium, or placing the modifications on a major archive site 326 | such as ftp.uu.net, or by allowing the Copyright Holder to include your 327 | modifications in the Standard Version of the Package. 328 | 329 | b) use the modified Package only within your corporation or organization. 330 | 331 | c) rename any non-standard executables so the names do not conflict with 332 | standard executables, which must also be provided, and provide a separate 333 | manual page for each non-standard executable that clearly documents how it 334 | differs from the Standard Version. 335 | 336 | d) make other distribution arrangements with the Copyright Holder. 337 | 338 | 4. You may distribute the programs of this Package in object code or executable 339 | form, provided that you do at least ONE of the following: 340 | 341 | a) distribute a Standard Version of the executables and library files, 342 | together with instructions (in the manual page or equivalent) on where to 343 | get the Standard Version. 344 | 345 | b) accompany the distribution with the machine-readable source of the Package 346 | with your modifications. 347 | 348 | c) accompany any non-standard executables with their corresponding Standard 349 | Version executables, giving the non-standard executables non-standard 350 | names, and clearly documenting the differences in manual pages (or 351 | equivalent), together with instructions on where to get the Standard 352 | Version. 353 | 354 | d) make other distribution arrangements with the Copyright Holder. 355 | 356 | 5. You may charge a reasonable copying fee for any distribution of this 357 | Package. You may charge any fee you choose for support of this Package. You 358 | may not charge a fee for this Package itself. However, you may distribute this 359 | Package in aggregate with other (possibly commercial) programs as part of a 360 | larger (possibly commercial) software distribution provided that you do not 361 | advertise this Package as a product of your own. 362 | 363 | 6. The scripts and library files supplied as input to or produced as output 364 | from the programs of this Package do not automatically fall under the copyright 365 | of this Package, but belong to whomever generated them, and may be sold 366 | commercially, and may be aggregated with this Package. 367 | 368 | 7. C or perl subroutines supplied by you and linked into this Package shall not 369 | be considered part of this Package. 370 | 371 | 8. The name of the Copyright Holder may not be used to endorse or promote 372 | products derived from this software without specific prior written permission. 373 | 374 | 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED 375 | WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF 376 | MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. 377 | 378 | The End 379 | 380 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # metacpan-explorer 2 | 3 | ## Clone 4 | 5 | This repo includes Bootstrap as a submodule, 6 | so after cloning (or pulling) make sure your submodule is up to date: 7 | 8 | ``` 9 | git clone https://github.com/metacpan/metacpan-explorer.git 10 | git submodule init && git submodule update 11 | ``` 12 | 13 | ## Docker Development 14 | 15 | ###Build an image: 16 | 17 | `docker build -t metacpan/metacpan-explorer .` 18 | 19 | ###Run your image: 20 | 21 | `docker run -p 8080:8080 metacpan/metacpan-explorer` 22 | 23 | ###View in your browser: 24 | 25 | http://localhost:8080/ 26 | 27 | 28 | ## Dockerless Development 29 | 30 | ### Rebuilding the static files 31 | 32 | In the project root run 33 | 34 | ./build.sh 35 | 36 | It will install dependencies via npm 37 | and regenerate the static files into the `build` directory. 38 | 39 | The [developer vm](https://github.com/metacpan/metacpan-developer) 40 | has everything you need for this. 41 | 42 | To run it somewhere else you'll need to make sure you have 43 | [node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed. 44 | 45 | ### Running the development server 46 | 47 | You can run `node server.js` (or `npm start`) to launch a dev server. 48 | See the comments in `server.js` for additional instructions. 49 | 50 | ## Adding Examples 51 | 52 | Log in using the credentials from https://github.com/metacpan/metacpan-credentials/blob/master/github and go to https://gist.github.com/. 53 | Create a new **public** gist with the following file structure: 54 | 55 | * endpoint.txt 56 | 57 | Contains the path to the API endpoint (e.g. `/v1/author/_search`) 58 | 59 | * body.json 60 | 61 | Contains the JSON encoded body of the request. Can be `null` if the request has no body. 62 | 63 | Give the gist a useful description and save. The example should then show up on explorer.metacpan.org 64 | -------------------------------------------------------------------------------- /app.build.js: -------------------------------------------------------------------------------- 1 | ({ 2 | baseUrl: "app/scripts", 3 | out: "build/app.js", 4 | name: "../inc/almond", 5 | include: ["app"], 6 | insertRequire: ["app"], 7 | mainConfigFile: "app/scripts/app.js", 8 | // optimize: "none", 9 | wrap: true 10 | }) 11 | -------------------------------------------------------------------------------- /app.psgi: -------------------------------------------------------------------------------- 1 | use Plack::Builder; 2 | use strict; 3 | use warnings; 4 | use Plack::App::Directory; 5 | builder { 6 | mount "/" => builder { 7 | enable "Plack::Middleware::DirIndex", dir_index => 'index.htm'; 8 | mount "/" => Plack::App::Directory->new({ root => "app" })->to_app; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /app/fonts: -------------------------------------------------------------------------------- 1 | inc/bootstrap/fonts -------------------------------------------------------------------------------- /app/inc/almond.js: -------------------------------------------------------------------------------- 1 | /** 2 | * almond 0.2.5 Copyright (c) 2011-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/jrburke/almond for details 5 | */ 6 | //Going sloppy to avoid 'use strict' string cost, but strict practices should 7 | //be followed. 8 | /*jslint sloppy: true */ 9 | /*global setTimeout: false */ 10 | 11 | var requirejs, require, define; 12 | (function (undef) { 13 | var main, req, makeMap, handlers, 14 | defined = {}, 15 | waiting = {}, 16 | config = {}, 17 | defining = {}, 18 | hasOwn = Object.prototype.hasOwnProperty, 19 | aps = [].slice; 20 | 21 | function hasProp(obj, prop) { 22 | return hasOwn.call(obj, prop); 23 | } 24 | 25 | /** 26 | * Given a relative module name, like ./something, normalize it to 27 | * a real name that can be mapped to a path. 28 | * @param {String} name the relative name 29 | * @param {String} baseName a real name that the name arg is relative 30 | * to. 31 | * @returns {String} normalized name 32 | */ 33 | function normalize(name, baseName) { 34 | var nameParts, nameSegment, mapValue, foundMap, 35 | foundI, foundStarMap, starI, i, j, part, 36 | baseParts = baseName && baseName.split("/"), 37 | map = config.map, 38 | starMap = (map && map['*']) || {}; 39 | 40 | //Adjust any relative paths. 41 | if (name && name.charAt(0) === ".") { 42 | //If have a base name, try to normalize against it, 43 | //otherwise, assume it is a top-level require that will 44 | //be relative to baseUrl in the end. 45 | if (baseName) { 46 | //Convert baseName to array, and lop off the last part, 47 | //so that . matches that "directory" and not name of the baseName's 48 | //module. For instance, baseName of "one/two/three", maps to 49 | //"one/two/three.js", but we want the directory, "one/two" for 50 | //this normalization. 51 | baseParts = baseParts.slice(0, baseParts.length - 1); 52 | 53 | name = baseParts.concat(name.split("/")); 54 | 55 | //start trimDots 56 | for (i = 0; i < name.length; i += 1) { 57 | part = name[i]; 58 | if (part === ".") { 59 | name.splice(i, 1); 60 | i -= 1; 61 | } else if (part === "..") { 62 | if (i === 1 && (name[2] === '..' || name[0] === '..')) { 63 | //End of the line. Keep at least one non-dot 64 | //path segment at the front so it can be mapped 65 | //correctly to disk. Otherwise, there is likely 66 | //no path mapping for a path starting with '..'. 67 | //This can still fail, but catches the most reasonable 68 | //uses of .. 69 | break; 70 | } else if (i > 0) { 71 | name.splice(i - 1, 2); 72 | i -= 2; 73 | } 74 | } 75 | } 76 | //end trimDots 77 | 78 | name = name.join("/"); 79 | } else if (name.indexOf('./') === 0) { 80 | // No baseName, so this is ID is resolved relative 81 | // to baseUrl, pull off the leading dot. 82 | name = name.substring(2); 83 | } 84 | } 85 | 86 | //Apply map config if available. 87 | if ((baseParts || starMap) && map) { 88 | nameParts = name.split('/'); 89 | 90 | for (i = nameParts.length; i > 0; i -= 1) { 91 | nameSegment = nameParts.slice(0, i).join("/"); 92 | 93 | if (baseParts) { 94 | //Find the longest baseName segment match in the config. 95 | //So, do joins on the biggest to smallest lengths of baseParts. 96 | for (j = baseParts.length; j > 0; j -= 1) { 97 | mapValue = map[baseParts.slice(0, j).join('/')]; 98 | 99 | //baseName segment has config, find if it has one for 100 | //this name. 101 | if (mapValue) { 102 | mapValue = mapValue[nameSegment]; 103 | if (mapValue) { 104 | //Match, update name to the new value. 105 | foundMap = mapValue; 106 | foundI = i; 107 | break; 108 | } 109 | } 110 | } 111 | } 112 | 113 | if (foundMap) { 114 | break; 115 | } 116 | 117 | //Check for a star map match, but just hold on to it, 118 | //if there is a shorter segment match later in a matching 119 | //config, then favor over this star map. 120 | if (!foundStarMap && starMap && starMap[nameSegment]) { 121 | foundStarMap = starMap[nameSegment]; 122 | starI = i; 123 | } 124 | } 125 | 126 | if (!foundMap && foundStarMap) { 127 | foundMap = foundStarMap; 128 | foundI = starI; 129 | } 130 | 131 | if (foundMap) { 132 | nameParts.splice(0, foundI, foundMap); 133 | name = nameParts.join('/'); 134 | } 135 | } 136 | 137 | return name; 138 | } 139 | 140 | function makeRequire(relName, forceSync) { 141 | return function () { 142 | //A version of a require function that passes a moduleName 143 | //value for items that may need to 144 | //look up paths relative to the moduleName 145 | return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync])); 146 | }; 147 | } 148 | 149 | function makeNormalize(relName) { 150 | return function (name) { 151 | return normalize(name, relName); 152 | }; 153 | } 154 | 155 | function makeLoad(depName) { 156 | return function (value) { 157 | defined[depName] = value; 158 | }; 159 | } 160 | 161 | function callDep(name) { 162 | if (hasProp(waiting, name)) { 163 | var args = waiting[name]; 164 | delete waiting[name]; 165 | defining[name] = true; 166 | main.apply(undef, args); 167 | } 168 | 169 | if (!hasProp(defined, name) && !hasProp(defining, name)) { 170 | throw new Error('No ' + name); 171 | } 172 | return defined[name]; 173 | } 174 | 175 | //Turns a plugin!resource to [plugin, resource] 176 | //with the plugin being undefined if the name 177 | //did not have a plugin prefix. 178 | function splitPrefix(name) { 179 | var prefix, 180 | index = name ? name.indexOf('!') : -1; 181 | if (index > -1) { 182 | prefix = name.substring(0, index); 183 | name = name.substring(index + 1, name.length); 184 | } 185 | return [prefix, name]; 186 | } 187 | 188 | /** 189 | * Makes a name map, normalizing the name, and using a plugin 190 | * for normalization if necessary. Grabs a ref to plugin 191 | * too, as an optimization. 192 | */ 193 | makeMap = function (name, relName) { 194 | var plugin, 195 | parts = splitPrefix(name), 196 | prefix = parts[0]; 197 | 198 | name = parts[1]; 199 | 200 | if (prefix) { 201 | prefix = normalize(prefix, relName); 202 | plugin = callDep(prefix); 203 | } 204 | 205 | //Normalize according 206 | if (prefix) { 207 | if (plugin && plugin.normalize) { 208 | name = plugin.normalize(name, makeNormalize(relName)); 209 | } else { 210 | name = normalize(name, relName); 211 | } 212 | } else { 213 | name = normalize(name, relName); 214 | parts = splitPrefix(name); 215 | prefix = parts[0]; 216 | name = parts[1]; 217 | if (prefix) { 218 | plugin = callDep(prefix); 219 | } 220 | } 221 | 222 | //Using ridiculous property names for space reasons 223 | return { 224 | f: prefix ? prefix + '!' + name : name, //fullName 225 | n: name, 226 | pr: prefix, 227 | p: plugin 228 | }; 229 | }; 230 | 231 | function makeConfig(name) { 232 | return function () { 233 | return (config && config.config && config.config[name]) || {}; 234 | }; 235 | } 236 | 237 | handlers = { 238 | require: function (name) { 239 | return makeRequire(name); 240 | }, 241 | exports: function (name) { 242 | var e = defined[name]; 243 | if (typeof e !== 'undefined') { 244 | return e; 245 | } else { 246 | return (defined[name] = {}); 247 | } 248 | }, 249 | module: function (name) { 250 | return { 251 | id: name, 252 | uri: '', 253 | exports: defined[name], 254 | config: makeConfig(name) 255 | }; 256 | } 257 | }; 258 | 259 | main = function (name, deps, callback, relName) { 260 | var cjsModule, depName, ret, map, i, 261 | args = [], 262 | usingExports; 263 | 264 | //Use name if no relName 265 | relName = relName || name; 266 | 267 | //Call the callback to define the module, if necessary. 268 | if (typeof callback === 'function') { 269 | 270 | //Pull out the defined dependencies and pass the ordered 271 | //values to the callback. 272 | //Default to [require, exports, module] if no deps 273 | deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; 274 | for (i = 0; i < deps.length; i += 1) { 275 | map = makeMap(deps[i], relName); 276 | depName = map.f; 277 | 278 | //Fast path CommonJS standard dependencies. 279 | if (depName === "require") { 280 | args[i] = handlers.require(name); 281 | } else if (depName === "exports") { 282 | //CommonJS module spec 1.1 283 | args[i] = handlers.exports(name); 284 | usingExports = true; 285 | } else if (depName === "module") { 286 | //CommonJS module spec 1.1 287 | cjsModule = args[i] = handlers.module(name); 288 | } else if (hasProp(defined, depName) || 289 | hasProp(waiting, depName) || 290 | hasProp(defining, depName)) { 291 | args[i] = callDep(depName); 292 | } else if (map.p) { 293 | map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); 294 | args[i] = defined[depName]; 295 | } else { 296 | throw new Error(name + ' missing ' + depName); 297 | } 298 | } 299 | 300 | ret = callback.apply(defined[name], args); 301 | 302 | if (name) { 303 | //If setting exports via "module" is in play, 304 | //favor that over return value and exports. After that, 305 | //favor a non-undefined return value over exports use. 306 | if (cjsModule && cjsModule.exports !== undef && 307 | cjsModule.exports !== defined[name]) { 308 | defined[name] = cjsModule.exports; 309 | } else if (ret !== undef || !usingExports) { 310 | //Use the return value from the function. 311 | defined[name] = ret; 312 | } 313 | } 314 | } else if (name) { 315 | //May just be an object definition for the module. Only 316 | //worry about defining if have a module name. 317 | defined[name] = callback; 318 | } 319 | }; 320 | 321 | requirejs = require = req = function (deps, callback, relName, forceSync, alt) { 322 | if (typeof deps === "string") { 323 | if (handlers[deps]) { 324 | //callback in this case is really relName 325 | return handlers[deps](callback); 326 | } 327 | //Just return the module wanted. In this scenario, the 328 | //deps arg is the module name, and second arg (if passed) 329 | //is just the relName. 330 | //Normalize module name, if it contains . or .. 331 | return callDep(makeMap(deps, callback).f); 332 | } else if (!deps.splice) { 333 | //deps is a config object, not an array. 334 | config = deps; 335 | if (callback.splice) { 336 | //callback is an array, which means it is a dependency list. 337 | //Adjust args if there are dependencies 338 | deps = callback; 339 | callback = relName; 340 | relName = null; 341 | } else { 342 | deps = undef; 343 | } 344 | } 345 | 346 | //Support require(['a']) 347 | callback = callback || function () {}; 348 | 349 | //If relName is a function, it is an errback handler, 350 | //so remove it. 351 | if (typeof relName === 'function') { 352 | relName = forceSync; 353 | forceSync = alt; 354 | } 355 | 356 | //Simulate async callback; 357 | if (forceSync) { 358 | main(undef, deps, callback, relName); 359 | } else { 360 | //Using a non-zero value because of concern for what old browsers 361 | //do, and latest browsers "upgrade" to 4 if lower value is used: 362 | //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: 363 | //If want a value immediately, use require('id') instead -- something 364 | //that works in almond on the global level, but not guaranteed and 365 | //unlikely to work in other AMD implementations. 366 | setTimeout(function () { 367 | main(undef, deps, callback, relName); 368 | }, 4); 369 | } 370 | 371 | return req; 372 | }; 373 | 374 | /** 375 | * Just drops the config on the floor, but returns req in case 376 | * the config return value is used. 377 | */ 378 | req.config = function (cfg) { 379 | config = cfg; 380 | if (config.deps) { 381 | req(config.deps, config.callback); 382 | } 383 | return req; 384 | }; 385 | 386 | define = function (name, deps, callback) { 387 | 388 | //This module may not have dependencies 389 | if (!deps.splice) { 390 | //deps is not an array, so probably means 391 | //an object literal or factory function for 392 | //the value. Adjust args. 393 | callback = deps; 394 | deps = []; 395 | } 396 | 397 | if (!hasProp(defined, name) && !hasProp(waiting, name)) { 398 | waiting[name] = [name, deps, callback]; 399 | } 400 | }; 401 | 402 | define.amd = { 403 | jQuery: true 404 | }; 405 | }()); -------------------------------------------------------------------------------- /app/inc/behave.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Behave.js 3 | * 4 | * Copyright 2013, Jacob Kelley - http://jakiestfu.com/ 5 | * Released under the MIT Licence 6 | * http://opensource.org/licenses/MIT 7 | * 8 | * Github: http://github.com/jakiestfu/Behave.js/ 9 | * Version: 1.5 10 | */ 11 | 12 | 13 | (function(undefined){ 14 | 15 | 'use strict'; 16 | 17 | var BehaveHooks = BehaveHooks || (function(){ 18 | var hooks = {}; 19 | 20 | return { 21 | add: function(hookName, fn){ 22 | if(typeof hookName == "object"){ 23 | var i; 24 | for(i=0; i>> 0; 69 | if (typeof func != "function"){ 70 | throw new TypeError(); 71 | } 72 | var res = [], 73 | thisp = arguments[1]; 74 | for (var i = 0; i < len; i++) { 75 | if (i in t) { 76 | var val = t[i]; 77 | if (func.call(thisp, val, i, t)) { 78 | res.push(val); 79 | } 80 | } 81 | } 82 | return res; 83 | }; 84 | } 85 | 86 | var defaults = { 87 | textarea: null, 88 | replaceTab: true, 89 | softTabs: true, 90 | tabSize: 4, 91 | autoOpen: true, 92 | overwrite: true, 93 | autoStrip: true, 94 | autoIndent: true, 95 | fence: false 96 | }, 97 | tab, 98 | newLine, 99 | charSettings = { 100 | 101 | keyMap: [ 102 | { open: "\"", close: "\"", canBreak: false }, 103 | { open: "'", close: "'", canBreak: false }, 104 | { open: "(", close: ")", canBreak: false }, 105 | { open: "[", close: "]", canBreak: true }, 106 | { open: "{", close: "}", canBreak: true } 107 | ] 108 | 109 | }, 110 | utils = { 111 | 112 | _callHook: function(hookName, passData){ 113 | var hooks = BehaveHooks.get(hookName); 114 | passData = typeof passData=="boolean" && passData === false ? false : true; 115 | 116 | if(hooks){ 117 | if(passData){ 118 | var theEditor = defaults.textarea, 119 | textVal = theEditor.value, 120 | caretPos = utils.cursor.get(), 121 | i; 122 | 123 | for(i=0; i -1) { 232 | start = end = len; 233 | } else { 234 | start = -textInputRange.moveStart("character", -len); 235 | start += normalizedValue.slice(0, start).split(newLine).length - 1; 236 | 237 | if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) { 238 | end = len; 239 | } else { 240 | end = -textInputRange.moveEnd("character", -len); 241 | end += normalizedValue.slice(0, end).split(newLine).length - 1; 242 | } 243 | } 244 | } 245 | } 246 | 247 | return start==end ? false : { 248 | start: start, 249 | end: end 250 | }; 251 | } 252 | }, 253 | editor: { 254 | getLines: function(textVal){ 255 | return (textVal).split("\n").length; 256 | }, 257 | get: function(){ 258 | return defaults.textarea.value.replace(/\r/g,''); 259 | }, 260 | set: function(data){ 261 | defaults.textarea.value = data; 262 | } 263 | }, 264 | fenceRange: function(){ 265 | if(typeof defaults.fence == "string"){ 266 | 267 | var data = utils.editor.get(), 268 | pos = utils.cursor.get(), 269 | hacked = 0, 270 | matchedFence = data.indexOf(defaults.fence), 271 | matchCase = 0; 272 | 273 | while(matchedFence>=0){ 274 | matchCase++; 275 | if( pos < (matchedFence+hacked) ){ 276 | break; 277 | } 278 | 279 | hacked += matchedFence+defaults.fence.length; 280 | data = data.substring(matchedFence+defaults.fence.length); 281 | matchedFence = data.indexOf(defaults.fence); 282 | 283 | } 284 | 285 | if( (hacked) < pos && ( (matchedFence+hacked) > pos ) && matchCase%2===0){ 286 | return true; 287 | } 288 | return false; 289 | } else { 290 | return true; 291 | } 292 | }, 293 | isEven: function(_this,i){ 294 | return i%2; 295 | }, 296 | levelsDeep: function(){ 297 | var pos = utils.cursor.get(), 298 | val = utils.editor.get(); 299 | 300 | var left = val.substring(0, pos), 301 | levels = 0, 302 | i, j; 303 | 304 | for(i=0; i=0 ? finalLevels : 0; 331 | }, 332 | deepExtend: function(destination, source) { 333 | for (var property in source) { 334 | if (source[property] && source[property].constructor && 335 | source[property].constructor === Object) { 336 | destination[property] = destination[property] || {}; 337 | utils.deepExtend(destination[property], source[property]); 338 | } else { 339 | destination[property] = source[property]; 340 | } 341 | } 342 | return destination; 343 | }, 344 | addEvent: function addEvent(element, eventName, func) { 345 | if (element.addEventListener){ 346 | element.addEventListener(eventName,func,false); 347 | } else if (element.attachEvent) { 348 | element.attachEvent("on"+eventName, func); 349 | } 350 | }, 351 | removeEvent: function addEvent(element, eventName, func){ 352 | if (element.addEventListener){ 353 | element.removeEventListener(eventName,func,false); 354 | } else if (element.attachEvent) { 355 | element.detachEvent("on"+eventName, func); 356 | } 357 | }, 358 | 359 | preventDefaultEvent: function(e){ 360 | if(e.preventDefault){ 361 | e.preventDefault(); 362 | } else { 363 | e.returnValue = false; 364 | } 365 | } 366 | }, 367 | intercept = { 368 | tabKey: function (e) { 369 | 370 | if(!utils.fenceRange()){ return; } 371 | 372 | if (e.keyCode == 9) { 373 | utils.preventDefaultEvent(e); 374 | 375 | var toReturn = true; 376 | utils._callHook('tab:before'); 377 | 378 | var selection = utils.cursor.selection(), 379 | pos = utils.cursor.get(), 380 | val = utils.editor.get(); 381 | 382 | if(selection){ 383 | 384 | var tempStart = selection.start; 385 | while(tempStart--){ 386 | if(val.charAt(tempStart)=="\n"){ 387 | selection.start = tempStart + 1; 388 | break; 389 | } 390 | } 391 | 392 | var toIndent = val.substring(selection.start, selection.end), 393 | lines = toIndent.split("\n"), 394 | i; 395 | 396 | if(e.shiftKey){ 397 | for(i = 0; i 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; 10 | overflow-x: auto; 11 | padding: 0.5em; 12 | background: #f0f0f0; 13 | -webkit-text-size-adjust: none; 14 | } 15 | 16 | .hljs, 17 | .hljs-subst, 18 | .hljs-tag .hljs-title, 19 | .nginx .hljs-title { 20 | color: black; 21 | } 22 | 23 | .hljs-string, 24 | .hljs-title, 25 | .hljs-constant, 26 | .hljs-parent, 27 | .hljs-tag .hljs-value, 28 | .hljs-rules .hljs-value, 29 | .hljs-preprocessor, 30 | .hljs-pragma, 31 | .haml .hljs-symbol, 32 | .ruby .hljs-symbol, 33 | .ruby .hljs-symbol .hljs-string, 34 | .hljs-template_tag, 35 | .django .hljs-variable, 36 | .smalltalk .hljs-class, 37 | .hljs-addition, 38 | .hljs-flow, 39 | .hljs-stream, 40 | .bash .hljs-variable, 41 | .apache .hljs-tag, 42 | .apache .hljs-cbracket, 43 | .tex .hljs-command, 44 | .tex .hljs-special, 45 | .erlang_repl .hljs-function_or_atom, 46 | .asciidoc .hljs-header, 47 | .markdown .hljs-header, 48 | .coffeescript .hljs-attribute { 49 | color: #800; 50 | } 51 | 52 | .smartquote, 53 | .hljs-comment, 54 | .hljs-annotation, 55 | .hljs-template_comment, 56 | .diff .hljs-header, 57 | .hljs-chunk, 58 | .asciidoc .hljs-blockquote, 59 | .markdown .hljs-blockquote { 60 | color: #888; 61 | } 62 | 63 | .hljs-number, 64 | .hljs-date, 65 | .hljs-regexp, 66 | .hljs-literal, 67 | .hljs-hexcolor, 68 | .smalltalk .hljs-symbol, 69 | .smalltalk .hljs-char, 70 | .go .hljs-constant, 71 | .hljs-change, 72 | .lasso .hljs-variable, 73 | .makefile .hljs-variable, 74 | .asciidoc .hljs-bullet, 75 | .markdown .hljs-bullet, 76 | .asciidoc .hljs-link_url, 77 | .markdown .hljs-link_url { 78 | color: #080; 79 | } 80 | 81 | .hljs-label, 82 | .hljs-javadoc, 83 | .ruby .hljs-string, 84 | .hljs-decorator, 85 | .hljs-filter .hljs-argument, 86 | .hljs-localvars, 87 | .hljs-array, 88 | .hljs-attr_selector, 89 | .hljs-important, 90 | .hljs-pseudo, 91 | .hljs-pi, 92 | .haml .hljs-bullet, 93 | .hljs-doctype, 94 | .hljs-deletion, 95 | .hljs-envvar, 96 | .hljs-shebang, 97 | .apache .hljs-sqbracket, 98 | .nginx .hljs-built_in, 99 | .tex .hljs-formula, 100 | .erlang_repl .hljs-reserved, 101 | .hljs-prompt, 102 | .asciidoc .hljs-link_label, 103 | .markdown .hljs-link_label, 104 | .vhdl .hljs-attribute, 105 | .clojure .hljs-attribute, 106 | .asciidoc .hljs-attribute, 107 | .lasso .hljs-attribute, 108 | .coffeescript .hljs-property, 109 | .hljs-phony { 110 | color: #88f; 111 | } 112 | 113 | .hljs-keyword, 114 | .hljs-id, 115 | .hljs-title, 116 | .hljs-built_in, 117 | .css .hljs-tag, 118 | .hljs-javadoctag, 119 | .hljs-phpdoc, 120 | .hljs-dartdoc, 121 | .hljs-yardoctag, 122 | .smalltalk .hljs-class, 123 | .hljs-winutils, 124 | .bash .hljs-variable, 125 | .apache .hljs-tag, 126 | .hljs-type, 127 | .hljs-typename, 128 | .tex .hljs-command, 129 | .asciidoc .hljs-strong, 130 | .markdown .hljs-strong, 131 | .hljs-request, 132 | .hljs-status { 133 | font-weight: bold; 134 | } 135 | 136 | .asciidoc .hljs-emphasis, 137 | .markdown .hljs-emphasis { 138 | font-style: italic; 139 | } 140 | 141 | .nginx .hljs-built_in { 142 | font-weight: normal; 143 | } 144 | 145 | .coffeescript .javascript, 146 | .javascript .xml, 147 | .lasso .markup, 148 | .tex .hljs-formula, 149 | .xml .javascript, 150 | .xml .vbscript, 151 | .xml .css, 152 | .xml .hljs-cdata { 153 | opacity: 0.5; 154 | } 155 | -------------------------------------------------------------------------------- /app/inc/highlight.js: -------------------------------------------------------------------------------- 1 | // custom built (with only json included) from https://highlightjs.org/download/ 2 | var hljs=new function(){function j(v){return v.replace(/&/gm,"&").replace(//gm,">")}function t(v){return v.nodeName.toLowerCase()}function h(w,x){var v=w&&w.exec(x);return v&&v.index==0}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^lang(uage)?-/,"")});return v.filter(function(x){return i(x)||/no(-?)highlight/.test(x)})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);if(!t(A).match(/br|hr|img|input/)){v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset"}function E(G){F+=""}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=j(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+j(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};var E=function(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})};if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b="\\b("+D.bK.split(" ").join("|")+")\\b"}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?("+F.b+")\\.?":F.b}).concat([D.tE,D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}}}x(y)}function c(T,L,J,R){function v(V,W){for(var U=0;U";V+=aa+'">';return V+Y+Z}function N(){if(!I.k){return j(C)}var U="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(C);while(V){U+=j(C.substr(X,V.index-X));var W=E(I,V);if(W){H+=W[1];U+=w(W[0],j(V[0]))}else{U+=j(V[0])}X=I.lR.lastIndex;V=I.lR.exec(C)}return U+j(C.substr(X))}function F(){if(I.sL&&!f[I.sL]){return j(C)}var U=I.sL?c(I.sL,C,true,S):e(C);if(I.r>0){H+=U.r}if(I.subLanguageMode=="continuous"){S=U.top}return w(U.language,U.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(W,V){var U=W.cN?w(W.cN,"",true):"";if(W.rB){D+=U;C=""}else{if(W.eB){D+=j(V)+U;C=""}else{D+=U;C=V}}I=Object.create(W,{parent:{value:I}})}function G(U,Y){C+=U;if(Y===undefined){D+=Q();return 0}var W=v(Y,I);if(W){D+=Q();P(W,Y);return W.rB?0:Y.length}var X=z(I,Y);if(X){var V=I;if(!(V.rE||V.eE)){C+=Y}D+=Q();do{if(I.cN){D+=""}H+=I.r;I=I.parent}while(I!=X.parent);if(V.eE){D+=j(Y)}C="";if(X.starts){P(X.starts,"")}return V.rE?0:Y.length}if(A(Y,I)){throw new Error('Illegal lexeme "'+Y+'" for mode "'+(I.cN||"")+'"')}C+=Y;return Y.length||1}var M=i(T);if(!M){throw new Error('Unknown language: "'+T+'"')}m(M);var I=R||M;var S;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,"",true)+D}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+=""}}return{r:H,value:D,language:T,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:j(L)}}else{throw O}}}function e(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:j(y)};var w=v;x.forEach(function(z){if(!i(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function g(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"
")}return v}function p(A){var B=r(A);if(/no(-?)highlight/.test(B)){return}var y;if(b.useBR){y=document.createElementNS("http://www.w3.org/1999/xhtml","div");y.innerHTML=A.innerHTML.replace(/\n/g,"").replace(//g,"\n")}else{y=A}var z=y.textContent;var v=B?c(B,z,true):e(z);var x=u(y);if(x.length){var w=document.createElementNS("http://www.w3.org/1999/xhtml","div");w.innerHTML=v.value;v.value=q(x,u(w),z)}v.value=g(v.value);A.innerHTML=v.value;A.className+=" hljs "+(!B&&v.language||"");A.result={language:v.language,re:v.r};if(v.second_best){A.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function d(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function k(){return Object.keys(f)}function i(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=e;this.fixMarkup=g;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=d;this.listLanguages=k;this.getLanguage=i;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/};this.CLCM={cN:"comment",b:"//",e:"$",c:[this.PWM]};this.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[this.PWM]};this.HCM={cN:"comment",b:"#",e:"$",c:[this.PWM]};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.CSSNM={cN:"number",b:this.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0};this.RM={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("json",function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}}); 3 | -------------------------------------------------------------------------------- /app/inc/jquery.querystring.js: -------------------------------------------------------------------------------- 1 | ;(function ($) { 2 | $.extend({ 3 | getParam: function (name) { 4 | function parseParams() { 5 | var params = {}, 6 | e, 7 | a = /\+/g, // Regex for replacing addition symbol with a space 8 | r = /([^&=]+)=?([^&]*)/g, 9 | d = function (s) { return decodeURIComponent(s.replace(a, " ")); }, 10 | q = window.location.search.substring(1); 11 | 12 | while (e = r.exec(q)) 13 | params[d(e[1])] = d(e[2]); 14 | 15 | return params; 16 | } 17 | 18 | if (!this.queryStringParams) 19 | this.queryStringParams = parseParams(); 20 | 21 | return this.queryStringParams[name]; 22 | } 23 | }); 24 | })(jQuery); 25 | -------------------------------------------------------------------------------- /app/inc/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.1.5 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(aa){function I(b){return"[object Function]"===L.call(b)}function J(b){return"[object Array]"===L.call(b)}function y(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(I(n)){if(this.events.error)try{e=i.execCb(c,n,b,e)}catch(d){a=d}else e=i.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",v(this.error= 19 | a)}else e=n;this.exports=e;if(this.map.isDefine&&!this.ignore&&(q[c]=e,l.onResourceLoad))l.onResourceLoad(i,this.map,this.depMaps);x(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=j(a.prefix);this.depMaps.push(d);t(d,"defined",u(this,function(e){var n,d;d=this.map.name;var g=this.map.parentMap?this.map.parentMap.name:null,h= 20 | i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,g,!0)})||""),e=j(a.prefix+"!"+d,this.map.parentMap),t(e,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(p,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else n=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=u(this, 21 | function(a){this.inited=!0;this.error=a;a.requireModules=[b];G(p,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&x(a.map.id)});v(a)}),n.fromText=u(this,function(e,c){var d=a.name,g=j(d),C=O;c&&(e=c);C&&(O=!1);r(g);s(k.config,b)&&(k.config[d]=k.config[b]);try{l.exec(e)}catch(ca){return v(B("fromtexteval","fromText eval for "+b+" failed: "+ca,ca,[b]))}C&&(O=!0);this.depMaps.push(g);i.completeLoad(d);h([d],n)}),e.load(a.name,h,n,k)}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]= 22 | this;this.enabling=this.enabled=!0;y(this.depMaps,u(this,function(a,b){var c,e;if("string"===typeof a){a=j(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;t(a,"defined",u(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&t(a,"error",this.errback)}c=a.id;e=p[c];!s(N,c)&&(e&&!e.enabled)&&i.enable(a,this)}));G(this.pluginMaps,u(this,function(a){var b=m(p,a.id);b&&!b.enabled&&i.enable(a, 23 | this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:k,contextName:b,registry:p,defined:q,urlFetched:U,defQueue:H,Module:Z,makeModuleMap:j,nextTick:l.nextTick,onError:v,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=k.pkgs,c=k.shim,e={paths:!0,config:!0,map:!0};G(a,function(a,b){e[b]? 24 | "map"===b?(k.map||(k.map={}),R(k[b],a,!0,!0)):R(k[b],a,!0):k[b]=a});a.shim&&(G(a.shim,function(a,b){J(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);c[b]=a}),k.shim=c);a.packages&&(y(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ja,"").replace(ea,"")}}),k.pkgs=b);G(p,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=j(b))});if(a.deps||a.callback)i.require(a.deps||[], 25 | a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(aa,arguments));return b||a.exports&&ba(a.exports)}},makeRequire:function(a,f){function d(e,c,h){var g,k;f.enableBuildCallback&&(c&&I(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(I(c))return v(B("requireargs","Invalid require call"),h);if(a&&s(N,e))return N[e](p[a.id]);if(l.get)return l.get(i,e,a,d);g=j(e,a,!1,!0);g=g.id;return!s(q,g)?v(B("notloaded",'Module name "'+g+'" has not been loaded yet for context: '+ 26 | b+(a?"":". Use require([])"))):q[g]}L();i.nextTick(function(){L();k=r(j(null,a));k.skipMap=f.skipMap;k.init(e,c,h,{enabled:!0});D()});return d}f=f||{};R(d,{isBrowser:A,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];if(-1!==f&&(!("."===g||".."===g)||1h.attachEvent.toString().indexOf("[native code"))&&!Y?(O=!0,h.attachEvent("onreadystatechange",b.onScriptLoad)):(h.addEventListener("load",b.onScriptLoad,!1),h.addEventListener("error",b.onScriptError,!1)),h.src=d,K=h,D?x.insertBefore(h,D):x.appendChild(h),K=null,h;if(da)try{importScripts(d),b.completeLoad(c)}catch(j){b.onError(B("importscripts","importScripts failed for "+c+" at "+d,j,[c]))}};A&&M(document.getElementsByTagName("script"),function(b){x||(x= 34 | b.parentNode);if(t=b.getAttribute("data-main"))return r.baseUrl||(E=t.split("/"),Q=E.pop(),fa=E.length?E.join("/")+"/":"./",r.baseUrl=fa,t=Q),t=t.replace(ea,""),r.deps=r.deps?r.deps.concat(t):[t],!0});define=function(b,c,d){var l,h;"string"!==typeof b&&(d=c,c=b,b=null);J(c)||(d=c,c=[]);!c.length&&I(d)&&d.length&&(d.toString().replace(la,"").replace(ma,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(l=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"), 35 | function(b){if("interactive"===b.readyState)return P=b}),l=P;l&&(b||(b=l.getAttribute("data-requiremodule")),h=F[l.getAttribute("data-requirecontext")])}(h?h.defQueue:T).push([b,c,d])};define.amd={jQuery:!0};l.exec=function(b){return eval(b)};l(r)}})(this); -------------------------------------------------------------------------------- /app/inc/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS text 2.0.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/text for details 5 | */ 6 | /*jslint regexp: true */ 7 | /*global require, XMLHttpRequest, ActiveXObject, 8 | define, window, process, Packages, 9 | java, location, Components, FileUtils */ 10 | 11 | define(['module'], function (module) { 12 | 'use strict'; 13 | 14 | var text, fs, Cc, Ci, 15 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 16 | xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, 17 | bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, 18 | hasLocation = typeof location !== 'undefined' && location.href, 19 | defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), 20 | defaultHostName = hasLocation && location.hostname, 21 | defaultPort = hasLocation && (location.port || undefined), 22 | buildMap = [], 23 | masterConfig = (module.config && module.config()) || {}; 24 | 25 | text = { 26 | version: '2.0.6', 27 | 28 | strip: function (content) { 29 | //Strips declarations so that external SVG and XML 30 | //documents can be added to a document without worry. Also, if the string 31 | //is an HTML document, only the part inside the body tag is returned. 32 | if (content) { 33 | content = content.replace(xmlRegExp, ""); 34 | var matches = content.match(bodyRegExp); 35 | if (matches) { 36 | content = matches[1]; 37 | } 38 | } else { 39 | content = ""; 40 | } 41 | return content; 42 | }, 43 | 44 | jsEscape: function (content) { 45 | return content.replace(/(['\\])/g, '\\$1') 46 | .replace(/[\f]/g, "\\f") 47 | .replace(/[\b]/g, "\\b") 48 | .replace(/[\n]/g, "\\n") 49 | .replace(/[\t]/g, "\\t") 50 | .replace(/[\r]/g, "\\r") 51 | .replace(/[\u2028]/g, "\\u2028") 52 | .replace(/[\u2029]/g, "\\u2029"); 53 | }, 54 | 55 | createXhr: masterConfig.createXhr || function () { 56 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 57 | var xhr, i, progId; 58 | if (typeof XMLHttpRequest !== "undefined") { 59 | return new XMLHttpRequest(); 60 | } else if (typeof ActiveXObject !== "undefined") { 61 | for (i = 0; i < 3; i += 1) { 62 | progId = progIds[i]; 63 | try { 64 | xhr = new ActiveXObject(progId); 65 | } catch (e) {} 66 | 67 | if (xhr) { 68 | progIds = [progId]; // so faster next time 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return xhr; 75 | }, 76 | 77 | /** 78 | * Parses a resource name into its component parts. Resource names 79 | * look like: module/name.ext!strip, where the !strip part is 80 | * optional. 81 | * @param {String} name the resource name 82 | * @returns {Object} with properties "moduleName", "ext" and "strip" 83 | * where strip is a boolean. 84 | */ 85 | parseName: function (name) { 86 | var modName, ext, temp, 87 | strip = false, 88 | index = name.indexOf("."), 89 | isRelative = name.indexOf('./') === 0 || 90 | name.indexOf('../') === 0; 91 | 92 | if (index !== -1 && (!isRelative || index > 1)) { 93 | modName = name.substring(0, index); 94 | ext = name.substring(index + 1, name.length); 95 | } else { 96 | modName = name; 97 | } 98 | 99 | temp = ext || modName; 100 | index = temp.indexOf("!"); 101 | if (index !== -1) { 102 | //Pull off the strip arg. 103 | strip = temp.substring(index + 1) === "strip"; 104 | temp = temp.substring(0, index); 105 | if (ext) { 106 | ext = temp; 107 | } else { 108 | modName = temp; 109 | } 110 | } 111 | 112 | return { 113 | moduleName: modName, 114 | ext: ext, 115 | strip: strip 116 | }; 117 | }, 118 | 119 | xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, 120 | 121 | /** 122 | * Is an URL on another domain. Only works for browser use, returns 123 | * false in non-browser environments. Only used to know if an 124 | * optimized .js version of a text resource should be loaded 125 | * instead. 126 | * @param {String} url 127 | * @returns Boolean 128 | */ 129 | useXhr: function (url, protocol, hostname, port) { 130 | var uProtocol, uHostName, uPort, 131 | match = text.xdRegExp.exec(url); 132 | if (!match) { 133 | return true; 134 | } 135 | uProtocol = match[2]; 136 | uHostName = match[3]; 137 | 138 | uHostName = uHostName.split(':'); 139 | uPort = uHostName[1]; 140 | uHostName = uHostName[0]; 141 | 142 | return (!uProtocol || uProtocol === protocol) && 143 | (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && 144 | ((!uPort && !uHostName) || uPort === port); 145 | }, 146 | 147 | finishLoad: function (name, strip, content, onLoad) { 148 | content = strip ? text.strip(content) : content; 149 | if (masterConfig.isBuild) { 150 | buildMap[name] = content; 151 | } 152 | onLoad(content); 153 | }, 154 | 155 | load: function (name, req, onLoad, config) { 156 | //Name has format: some.module.filext!strip 157 | //The strip part is optional. 158 | //if strip is present, then that means only get the string contents 159 | //inside a body tag in an HTML string. For XML/SVG content it means 160 | //removing the declarations so the content can be inserted 161 | //into the current doc without problems. 162 | 163 | // Do not bother with the work if a build and text will 164 | // not be inlined. 165 | if (config.isBuild && !config.inlineText) { 166 | onLoad(); 167 | return; 168 | } 169 | 170 | masterConfig.isBuild = config.isBuild; 171 | 172 | var parsed = text.parseName(name), 173 | nonStripName = parsed.moduleName + 174 | (parsed.ext ? '.' + parsed.ext : ''), 175 | url = req.toUrl(nonStripName), 176 | useXhr = (masterConfig.useXhr) || 177 | text.useXhr; 178 | 179 | //Load the text. Use XHR if possible and in a browser. 180 | if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { 181 | text.get(url, function (content) { 182 | text.finishLoad(name, parsed.strip, content, onLoad); 183 | }, function (err) { 184 | if (onLoad.error) { 185 | onLoad.error(err); 186 | } 187 | }); 188 | } else { 189 | //Need to fetch the resource across domains. Assume 190 | //the resource has been optimized into a JS module. Fetch 191 | //by the module name + extension, but do not include the 192 | //!strip part to avoid file system issues. 193 | req([nonStripName], function (content) { 194 | text.finishLoad(parsed.moduleName + '.' + parsed.ext, 195 | parsed.strip, content, onLoad); 196 | }); 197 | } 198 | }, 199 | 200 | write: function (pluginName, moduleName, write, config) { 201 | if (buildMap.hasOwnProperty(moduleName)) { 202 | var content = text.jsEscape(buildMap[moduleName]); 203 | write.asModule(pluginName + "!" + moduleName, 204 | "define(function () { return '" + 205 | content + 206 | "';});\n"); 207 | } 208 | }, 209 | 210 | writeFile: function (pluginName, moduleName, req, write, config) { 211 | var parsed = text.parseName(moduleName), 212 | extPart = parsed.ext ? '.' + parsed.ext : '', 213 | nonStripName = parsed.moduleName + extPart, 214 | //Use a '.js' file name so that it indicates it is a 215 | //script that can be loaded across domains. 216 | fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; 217 | 218 | //Leverage own load() method to load plugin value, but only 219 | //write out values that do not have the strip argument, 220 | //to avoid any potential issues with ! in file names. 221 | text.load(nonStripName, req, function (value) { 222 | //Use own write() method to construct full module value. 223 | //But need to create shell that translates writeFile's 224 | //write() to the right interface. 225 | var textWrite = function (contents) { 226 | return write(fileName, contents); 227 | }; 228 | textWrite.asModule = function (moduleName, contents) { 229 | return write.asModule(moduleName, fileName, contents); 230 | }; 231 | 232 | text.write(pluginName, nonStripName, textWrite, config); 233 | }, config); 234 | } 235 | }; 236 | 237 | if (masterConfig.env === 'node' || (!masterConfig.env && 238 | typeof process !== "undefined" && 239 | process.versions && 240 | !!process.versions.node)) { 241 | //Using special require.nodeRequire, something added by r.js. 242 | fs = require.nodeRequire('fs'); 243 | 244 | text.get = function (url, callback) { 245 | var file = fs.readFileSync(url, 'utf8'); 246 | //Remove BOM (Byte Mark Order) from utf8 files if it is there. 247 | if (file.indexOf('\uFEFF') === 0) { 248 | file = file.substring(1); 249 | } 250 | callback(file); 251 | }; 252 | } else if (masterConfig.env === 'xhr' || (!masterConfig.env && 253 | text.createXhr())) { 254 | text.get = function (url, callback, errback, headers) { 255 | var xhr = text.createXhr(), header; 256 | xhr.open('GET', url, true); 257 | 258 | //Allow plugins direct access to xhr headers 259 | if (headers) { 260 | for (header in headers) { 261 | if (headers.hasOwnProperty(header)) { 262 | xhr.setRequestHeader(header.toLowerCase(), headers[header]); 263 | } 264 | } 265 | } 266 | 267 | //Allow overrides specified in config 268 | if (masterConfig.onXhr) { 269 | masterConfig.onXhr(xhr, url); 270 | } 271 | 272 | xhr.onreadystatechange = function (evt) { 273 | var status, err; 274 | //Do not explicitly handle errors, those should be 275 | //visible via console output in the browser. 276 | if (xhr.readyState === 4) { 277 | status = xhr.status; 278 | if (status > 399 && status < 600) { 279 | //An http 4xx or 5xx error. Signal an error. 280 | err = new Error(url + ' HTTP status: ' + status); 281 | err.xhr = xhr; 282 | errback(err); 283 | } else { 284 | callback(xhr.responseText); 285 | } 286 | 287 | if (masterConfig.onXhrComplete) { 288 | masterConfig.onXhrComplete(xhr, url); 289 | } 290 | } 291 | }; 292 | xhr.send(null); 293 | }; 294 | } else if (masterConfig.env === 'rhino' || (!masterConfig.env && 295 | typeof Packages !== 'undefined' && typeof java !== 'undefined')) { 296 | //Why Java, why is this so awkward? 297 | text.get = function (url, callback) { 298 | var stringBuffer, line, 299 | encoding = "utf-8", 300 | file = new java.io.File(url), 301 | lineSeparator = java.lang.System.getProperty("line.separator"), 302 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 303 | content = ''; 304 | try { 305 | stringBuffer = new java.lang.StringBuffer(); 306 | line = input.readLine(); 307 | 308 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 309 | // http://www.unicode.org/faq/utf_bom.html 310 | 311 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 312 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 313 | if (line && line.length() && line.charAt(0) === 0xfeff) { 314 | // Eat the BOM, since we've already found the encoding on this file, 315 | // and we plan to concatenating this buffer with others; the BOM should 316 | // only appear at the top of a file. 317 | line = line.substring(1); 318 | } 319 | 320 | stringBuffer.append(line); 321 | 322 | while ((line = input.readLine()) !== null) { 323 | stringBuffer.append(lineSeparator); 324 | stringBuffer.append(line); 325 | } 326 | //Make sure we return a JavaScript string and not a Java string. 327 | content = String(stringBuffer.toString()); //String 328 | } finally { 329 | input.close(); 330 | } 331 | callback(content); 332 | }; 333 | } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && 334 | typeof Components !== 'undefined' && Components.classes && 335 | Components.interfaces)) { 336 | //Avert your gaze! 337 | Cc = Components.classes, 338 | Ci = Components.interfaces; 339 | Components.utils['import']('resource://gre/modules/FileUtils.jsm'); 340 | 341 | text.get = function (url, callback) { 342 | var inStream, convertStream, 343 | readData = {}, 344 | fileObj = new FileUtils.File(url); 345 | 346 | //XPCOM, you so crazy 347 | try { 348 | inStream = Cc['@mozilla.org/network/file-input-stream;1'] 349 | .createInstance(Ci.nsIFileInputStream); 350 | inStream.init(fileObj, 1, 0, false); 351 | 352 | convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] 353 | .createInstance(Ci.nsIConverterInputStream); 354 | convertStream.init(inStream, "utf-8", inStream.available(), 355 | Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); 356 | 357 | convertStream.readString(inStream.available(), readData); 358 | convertStream.close(); 359 | inStream.close(); 360 | callback(readData.value); 361 | } catch (e) { 362 | throw new Error((fileObj && fileObj.path || '') + ': ' + e); 363 | } 364 | }; 365 | } 366 | return text; 367 | }); 368 | -------------------------------------------------------------------------------- /app/inc/tpl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from the official plugin text.js 3 | * 4 | * Uses UnderscoreJS micro-templates : http://documentcloud.github.com/underscore/#template 5 | * @author Julien Cabanès 6 | * @version 0.3 7 | * 8 | * @license RequireJS text 0.24.0 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. 9 | * Available via the MIT or new BSD license. 10 | * see: http://github.com/jrburke/requirejs for details 11 | */ 12 | 13 | define(['module', 'text', 'underscore'], function(module, text, _) { 14 | var settings = module.config() || {}; 15 | 16 | return { 17 | /** 18 | * Dynamically loads your module by using text! to require the content, and then return the results of _.template. 19 | * @param name 20 | * @param req 21 | * @param onLoad 22 | * @param config 23 | */ 24 | load: function(name, req, onLoad, config) { 25 | if (config.isBuild) { 26 | text.load.call(text, name, req, onLoad, config); 27 | return; 28 | } 29 | req([ 'text!' + name ], function(content) { 30 | onLoad(content 31 | ? _.template(content, undefined, settings) 32 | : function() { return ''; }); 33 | }); 34 | }, 35 | /** 36 | * Used by the optimizer to compile your templates. Logs detailed error messages whenever an invalid template is 37 | * encountered. 38 | * @param pluginName 39 | * @param moduleName 40 | * @param write 41 | * @param config 42 | */ 43 | write: function(pluginName, moduleName, write, config) { 44 | text.write.call(text, pluginName, moduleName, { 45 | asModule: function(name, contents) { 46 | contents = contents.substr(contents.indexOf('return') + 7); 47 | contents = contents.substr(0, contents.lastIndexOf(';') - 3); 48 | try { 49 | var template = _.template(eval(contents), undefined, settings); 50 | write.asModule(pluginName + "!" + moduleName, 51 | "define(function() { return " + template.source + " });"); 52 | } 53 | catch (err) { 54 | console.error('~~~~~'); 55 | console.error('FAILED TO COMPILE ' + pluginName + "!" + moduleName); 56 | console.error('Error:\t\t' + String(err)); 57 | console.error('\n'); 58 | if (err && err.source) { 59 | console.error('Error Source:\n'); 60 | console.error(err.source); 61 | console.error('\n\n'); 62 | } 63 | console.error('Original Contents:\n'); 64 | console.error(contents); 65 | console.error('\n\n'); 66 | throw err; 67 | } 68 | } 69 | }, config); 70 | } 71 | }; 72 | }); -------------------------------------------------------------------------------- /app/inc/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.4.4 2 | // =================== 3 | 4 | // > http://underscorejs.org 5 | // > (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. 6 | // > Underscore may be freely distributed under the MIT license. 7 | 8 | // Baseline setup 9 | // -------------- 10 | (function() { 11 | 12 | // Establish the root object, `window` in the browser, or `global` on the server. 13 | var root = this; 14 | 15 | // Save the previous value of the `_` variable. 16 | var previousUnderscore = root._; 17 | 18 | // Establish the object that gets returned to break out of a loop iteration. 19 | var breaker = {}; 20 | 21 | // Save bytes in the minified (but not gzipped) version: 22 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 23 | 24 | // Create quick reference variables for speed access to core prototypes. 25 | var push = ArrayProto.push, 26 | slice = ArrayProto.slice, 27 | concat = ArrayProto.concat, 28 | toString = ObjProto.toString, 29 | hasOwnProperty = ObjProto.hasOwnProperty; 30 | 31 | // All **ECMAScript 5** native function implementations that we hope to use 32 | // are declared here. 33 | var 34 | nativeForEach = ArrayProto.forEach, 35 | nativeMap = ArrayProto.map, 36 | nativeReduce = ArrayProto.reduce, 37 | nativeReduceRight = ArrayProto.reduceRight, 38 | nativeFilter = ArrayProto.filter, 39 | nativeEvery = ArrayProto.every, 40 | nativeSome = ArrayProto.some, 41 | nativeIndexOf = ArrayProto.indexOf, 42 | nativeLastIndexOf = ArrayProto.lastIndexOf, 43 | nativeIsArray = Array.isArray, 44 | nativeKeys = Object.keys, 45 | nativeBind = FuncProto.bind; 46 | 47 | // Create a safe reference to the Underscore object for use below. 48 | var _ = function(obj) { 49 | if (obj instanceof _) return obj; 50 | if (!(this instanceof _)) return new _(obj); 51 | this._wrapped = obj; 52 | }; 53 | 54 | // Export the Underscore object for **Node.js**, with 55 | // backwards-compatibility for the old `require()` API. If we're in 56 | // the browser, add `_` as a global object via a string identifier, 57 | // for Closure Compiler "advanced" mode. 58 | if (typeof exports !== 'undefined') { 59 | if (typeof module !== 'undefined' && module.exports) { 60 | exports = module.exports = _; 61 | } 62 | exports._ = _; 63 | } else { 64 | root._ = _; 65 | } 66 | 67 | // Current version. 68 | _.VERSION = '1.4.4'; 69 | 70 | // Collection Functions 71 | // -------------------- 72 | 73 | // The cornerstone, an `each` implementation, aka `forEach`. 74 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 75 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 76 | var each = _.each = _.forEach = function(obj, iterator, context) { 77 | if (obj == null) return; 78 | if (nativeForEach && obj.forEach === nativeForEach) { 79 | obj.forEach(iterator, context); 80 | } else if (obj.length === +obj.length) { 81 | for (var i = 0, l = obj.length; i < l; i++) { 82 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 83 | } 84 | } else { 85 | for (var key in obj) { 86 | if (_.has(obj, key)) { 87 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 88 | } 89 | } 90 | } 91 | }; 92 | 93 | // Return the results of applying the iterator to each element. 94 | // Delegates to **ECMAScript 5**'s native `map` if available. 95 | _.map = _.collect = function(obj, iterator, context) { 96 | var results = []; 97 | if (obj == null) return results; 98 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 99 | each(obj, function(value, index, list) { 100 | results[results.length] = iterator.call(context, value, index, list); 101 | }); 102 | return results; 103 | }; 104 | 105 | var reduceError = 'Reduce of empty array with no initial value'; 106 | 107 | // **Reduce** builds up a single result from a list of values, aka `inject`, 108 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 109 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 110 | var initial = arguments.length > 2; 111 | if (obj == null) obj = []; 112 | if (nativeReduce && obj.reduce === nativeReduce) { 113 | if (context) iterator = _.bind(iterator, context); 114 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 115 | } 116 | each(obj, function(value, index, list) { 117 | if (!initial) { 118 | memo = value; 119 | initial = true; 120 | } else { 121 | memo = iterator.call(context, memo, value, index, list); 122 | } 123 | }); 124 | if (!initial) throw new TypeError(reduceError); 125 | return memo; 126 | }; 127 | 128 | // The right-associative version of reduce, also known as `foldr`. 129 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 130 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 131 | var initial = arguments.length > 2; 132 | if (obj == null) obj = []; 133 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 134 | if (context) iterator = _.bind(iterator, context); 135 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 136 | } 137 | var length = obj.length; 138 | if (length !== +length) { 139 | var keys = _.keys(obj); 140 | length = keys.length; 141 | } 142 | each(obj, function(value, index, list) { 143 | index = keys ? keys[--length] : --length; 144 | if (!initial) { 145 | memo = obj[index]; 146 | initial = true; 147 | } else { 148 | memo = iterator.call(context, memo, obj[index], index, list); 149 | } 150 | }); 151 | if (!initial) throw new TypeError(reduceError); 152 | return memo; 153 | }; 154 | 155 | // Return the first value which passes a truth test. Aliased as `detect`. 156 | _.find = _.detect = function(obj, iterator, context) { 157 | var result; 158 | any(obj, function(value, index, list) { 159 | if (iterator.call(context, value, index, list)) { 160 | result = value; 161 | return true; 162 | } 163 | }); 164 | return result; 165 | }; 166 | 167 | // Return all the elements that pass a truth test. 168 | // Delegates to **ECMAScript 5**'s native `filter` if available. 169 | // Aliased as `select`. 170 | _.filter = _.select = function(obj, iterator, context) { 171 | var results = []; 172 | if (obj == null) return results; 173 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 174 | each(obj, function(value, index, list) { 175 | if (iterator.call(context, value, index, list)) results[results.length] = value; 176 | }); 177 | return results; 178 | }; 179 | 180 | // Return all the elements for which a truth test fails. 181 | _.reject = function(obj, iterator, context) { 182 | return _.filter(obj, function(value, index, list) { 183 | return !iterator.call(context, value, index, list); 184 | }, context); 185 | }; 186 | 187 | // Determine whether all of the elements match a truth test. 188 | // Delegates to **ECMAScript 5**'s native `every` if available. 189 | // Aliased as `all`. 190 | _.every = _.all = function(obj, iterator, context) { 191 | iterator || (iterator = _.identity); 192 | var result = true; 193 | if (obj == null) return result; 194 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 195 | each(obj, function(value, index, list) { 196 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 197 | }); 198 | return !!result; 199 | }; 200 | 201 | // Determine if at least one element in the object matches a truth test. 202 | // Delegates to **ECMAScript 5**'s native `some` if available. 203 | // Aliased as `any`. 204 | var any = _.some = _.any = function(obj, iterator, context) { 205 | iterator || (iterator = _.identity); 206 | var result = false; 207 | if (obj == null) return result; 208 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 209 | each(obj, function(value, index, list) { 210 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 211 | }); 212 | return !!result; 213 | }; 214 | 215 | // Determine if the array or object contains a given value (using `===`). 216 | // Aliased as `include`. 217 | _.contains = _.include = function(obj, target) { 218 | if (obj == null) return false; 219 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 220 | return any(obj, function(value) { 221 | return value === target; 222 | }); 223 | }; 224 | 225 | // Invoke a method (with arguments) on every item in a collection. 226 | _.invoke = function(obj, method) { 227 | var args = slice.call(arguments, 2); 228 | var isFunc = _.isFunction(method); 229 | return _.map(obj, function(value) { 230 | return (isFunc ? method : value[method]).apply(value, args); 231 | }); 232 | }; 233 | 234 | // Convenience version of a common use case of `map`: fetching a property. 235 | _.pluck = function(obj, key) { 236 | return _.map(obj, function(value){ return value[key]; }); 237 | }; 238 | 239 | // Convenience version of a common use case of `filter`: selecting only objects 240 | // containing specific `key:value` pairs. 241 | _.where = function(obj, attrs, first) { 242 | if (_.isEmpty(attrs)) return first ? null : []; 243 | return _[first ? 'find' : 'filter'](obj, function(value) { 244 | for (var key in attrs) { 245 | if (attrs[key] !== value[key]) return false; 246 | } 247 | return true; 248 | }); 249 | }; 250 | 251 | // Convenience version of a common use case of `find`: getting the first object 252 | // containing specific `key:value` pairs. 253 | _.findWhere = function(obj, attrs) { 254 | return _.where(obj, attrs, true); 255 | }; 256 | 257 | // Return the maximum element or (element-based computation). 258 | // Can't optimize arrays of integers longer than 65,535 elements. 259 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797 260 | _.max = function(obj, iterator, context) { 261 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 262 | return Math.max.apply(Math, obj); 263 | } 264 | if (!iterator && _.isEmpty(obj)) return -Infinity; 265 | var result = {computed : -Infinity, value: -Infinity}; 266 | each(obj, function(value, index, list) { 267 | var computed = iterator ? iterator.call(context, value, index, list) : value; 268 | computed >= result.computed && (result = {value : value, computed : computed}); 269 | }); 270 | return result.value; 271 | }; 272 | 273 | // Return the minimum element (or element-based computation). 274 | _.min = function(obj, iterator, context) { 275 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 276 | return Math.min.apply(Math, obj); 277 | } 278 | if (!iterator && _.isEmpty(obj)) return Infinity; 279 | var result = {computed : Infinity, value: Infinity}; 280 | each(obj, function(value, index, list) { 281 | var computed = iterator ? iterator.call(context, value, index, list) : value; 282 | computed < result.computed && (result = {value : value, computed : computed}); 283 | }); 284 | return result.value; 285 | }; 286 | 287 | // Shuffle an array. 288 | _.shuffle = function(obj) { 289 | var rand; 290 | var index = 0; 291 | var shuffled = []; 292 | each(obj, function(value) { 293 | rand = _.random(index++); 294 | shuffled[index - 1] = shuffled[rand]; 295 | shuffled[rand] = value; 296 | }); 297 | return shuffled; 298 | }; 299 | 300 | // An internal function to generate lookup iterators. 301 | var lookupIterator = function(value) { 302 | return _.isFunction(value) ? value : function(obj){ return obj[value]; }; 303 | }; 304 | 305 | // Sort the object's values by a criterion produced by an iterator. 306 | _.sortBy = function(obj, value, context) { 307 | var iterator = lookupIterator(value); 308 | return _.pluck(_.map(obj, function(value, index, list) { 309 | return { 310 | value : value, 311 | index : index, 312 | criteria : iterator.call(context, value, index, list) 313 | }; 314 | }).sort(function(left, right) { 315 | var a = left.criteria; 316 | var b = right.criteria; 317 | if (a !== b) { 318 | if (a > b || a === void 0) return 1; 319 | if (a < b || b === void 0) return -1; 320 | } 321 | return left.index < right.index ? -1 : 1; 322 | }), 'value'); 323 | }; 324 | 325 | // An internal function used for aggregate "group by" operations. 326 | var group = function(obj, value, context, behavior) { 327 | var result = {}; 328 | var iterator = lookupIterator(value || _.identity); 329 | each(obj, function(value, index) { 330 | var key = iterator.call(context, value, index, obj); 331 | behavior(result, key, value); 332 | }); 333 | return result; 334 | }; 335 | 336 | // Groups the object's values by a criterion. Pass either a string attribute 337 | // to group by, or a function that returns the criterion. 338 | _.groupBy = function(obj, value, context) { 339 | return group(obj, value, context, function(result, key, value) { 340 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value); 341 | }); 342 | }; 343 | 344 | // Counts instances of an object that group by a certain criterion. Pass 345 | // either a string attribute to count by, or a function that returns the 346 | // criterion. 347 | _.countBy = function(obj, value, context) { 348 | return group(obj, value, context, function(result, key) { 349 | if (!_.has(result, key)) result[key] = 0; 350 | result[key]++; 351 | }); 352 | }; 353 | 354 | // Use a comparator function to figure out the smallest index at which 355 | // an object should be inserted so as to maintain order. Uses binary search. 356 | _.sortedIndex = function(array, obj, iterator, context) { 357 | iterator = iterator == null ? _.identity : lookupIterator(iterator); 358 | var value = iterator.call(context, obj); 359 | var low = 0, high = array.length; 360 | while (low < high) { 361 | var mid = (low + high) >>> 1; 362 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 363 | } 364 | return low; 365 | }; 366 | 367 | // Safely convert anything iterable into a real, live array. 368 | _.toArray = function(obj) { 369 | if (!obj) return []; 370 | if (_.isArray(obj)) return slice.call(obj); 371 | if (obj.length === +obj.length) return _.map(obj, _.identity); 372 | return _.values(obj); 373 | }; 374 | 375 | // Return the number of elements in an object. 376 | _.size = function(obj) { 377 | if (obj == null) return 0; 378 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 379 | }; 380 | 381 | // Array Functions 382 | // --------------- 383 | 384 | // Get the first element of an array. Passing **n** will return the first N 385 | // values in the array. Aliased as `head` and `take`. The **guard** check 386 | // allows it to work with `_.map`. 387 | _.first = _.head = _.take = function(array, n, guard) { 388 | if (array == null) return void 0; 389 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 390 | }; 391 | 392 | // Returns everything but the last entry of the array. Especially useful on 393 | // the arguments object. Passing **n** will return all the values in 394 | // the array, excluding the last N. The **guard** check allows it to work with 395 | // `_.map`. 396 | _.initial = function(array, n, guard) { 397 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 398 | }; 399 | 400 | // Get the last element of an array. Passing **n** will return the last N 401 | // values in the array. The **guard** check allows it to work with `_.map`. 402 | _.last = function(array, n, guard) { 403 | if (array == null) return void 0; 404 | if ((n != null) && !guard) { 405 | return slice.call(array, Math.max(array.length - n, 0)); 406 | } else { 407 | return array[array.length - 1]; 408 | } 409 | }; 410 | 411 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 412 | // Especially useful on the arguments object. Passing an **n** will return 413 | // the rest N values in the array. The **guard** 414 | // check allows it to work with `_.map`. 415 | _.rest = _.tail = _.drop = function(array, n, guard) { 416 | return slice.call(array, (n == null) || guard ? 1 : n); 417 | }; 418 | 419 | // Trim out all falsy values from an array. 420 | _.compact = function(array) { 421 | return _.filter(array, _.identity); 422 | }; 423 | 424 | // Internal implementation of a recursive `flatten` function. 425 | var flatten = function(input, shallow, output) { 426 | each(input, function(value) { 427 | if (_.isArray(value)) { 428 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 429 | } else { 430 | output.push(value); 431 | } 432 | }); 433 | return output; 434 | }; 435 | 436 | // Return a completely flattened version of an array. 437 | _.flatten = function(array, shallow) { 438 | return flatten(array, shallow, []); 439 | }; 440 | 441 | // Return a version of the array that does not contain the specified value(s). 442 | _.without = function(array) { 443 | return _.difference(array, slice.call(arguments, 1)); 444 | }; 445 | 446 | // Produce a duplicate-free version of the array. If the array has already 447 | // been sorted, you have the option of using a faster algorithm. 448 | // Aliased as `unique`. 449 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 450 | if (_.isFunction(isSorted)) { 451 | context = iterator; 452 | iterator = isSorted; 453 | isSorted = false; 454 | } 455 | var initial = iterator ? _.map(array, iterator, context) : array; 456 | var results = []; 457 | var seen = []; 458 | each(initial, function(value, index) { 459 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 460 | seen.push(value); 461 | results.push(array[index]); 462 | } 463 | }); 464 | return results; 465 | }; 466 | 467 | // Produce an array that contains the union: each distinct element from all of 468 | // the passed-in arrays. 469 | _.union = function() { 470 | return _.uniq(concat.apply(ArrayProto, arguments)); 471 | }; 472 | 473 | // Produce an array that contains every item shared between all the 474 | // passed-in arrays. 475 | _.intersection = function(array) { 476 | var rest = slice.call(arguments, 1); 477 | return _.filter(_.uniq(array), function(item) { 478 | return _.every(rest, function(other) { 479 | return _.indexOf(other, item) >= 0; 480 | }); 481 | }); 482 | }; 483 | 484 | // Take the difference between one array and a number of other arrays. 485 | // Only the elements present in just the first array will remain. 486 | _.difference = function(array) { 487 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 488 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 489 | }; 490 | 491 | // Zip together multiple lists into a single array -- elements that share 492 | // an index go together. 493 | _.zip = function() { 494 | var args = slice.call(arguments); 495 | var length = _.max(_.pluck(args, 'length')); 496 | var results = new Array(length); 497 | for (var i = 0; i < length; i++) { 498 | results[i] = _.pluck(args, "" + i); 499 | } 500 | return results; 501 | }; 502 | 503 | // Converts lists into objects. Pass either a single array of `[key, value]` 504 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 505 | // the corresponding values. 506 | _.object = function(list, values) { 507 | if (list == null) return {}; 508 | var result = {}; 509 | for (var i = 0, l = list.length; i < l; i++) { 510 | if (values) { 511 | result[list[i]] = values[i]; 512 | } else { 513 | result[list[i][0]] = list[i][1]; 514 | } 515 | } 516 | return result; 517 | }; 518 | 519 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 520 | // we need this function. Return the position of the first occurrence of an 521 | // item in an array, or -1 if the item is not included in the array. 522 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 523 | // If the array is large and already in sort order, pass `true` 524 | // for **isSorted** to use binary search. 525 | _.indexOf = function(array, item, isSorted) { 526 | if (array == null) return -1; 527 | var i = 0, l = array.length; 528 | if (isSorted) { 529 | if (typeof isSorted == 'number') { 530 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); 531 | } else { 532 | i = _.sortedIndex(array, item); 533 | return array[i] === item ? i : -1; 534 | } 535 | } 536 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 537 | for (; i < l; i++) if (array[i] === item) return i; 538 | return -1; 539 | }; 540 | 541 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 542 | _.lastIndexOf = function(array, item, from) { 543 | if (array == null) return -1; 544 | var hasIndex = from != null; 545 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 546 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 547 | } 548 | var i = (hasIndex ? from : array.length); 549 | while (i--) if (array[i] === item) return i; 550 | return -1; 551 | }; 552 | 553 | // Generate an integer Array containing an arithmetic progression. A port of 554 | // the native Python `range()` function. See 555 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 556 | _.range = function(start, stop, step) { 557 | if (arguments.length <= 1) { 558 | stop = start || 0; 559 | start = 0; 560 | } 561 | step = arguments[2] || 1; 562 | 563 | var len = Math.max(Math.ceil((stop - start) / step), 0); 564 | var idx = 0; 565 | var range = new Array(len); 566 | 567 | while(idx < len) { 568 | range[idx++] = start; 569 | start += step; 570 | } 571 | 572 | return range; 573 | }; 574 | 575 | // Function (ahem) Functions 576 | // ------------------ 577 | 578 | // Create a function bound to a given object (assigning `this`, and arguments, 579 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 580 | // available. 581 | _.bind = function(func, context) { 582 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 583 | var args = slice.call(arguments, 2); 584 | return function() { 585 | return func.apply(context, args.concat(slice.call(arguments))); 586 | }; 587 | }; 588 | 589 | // Partially apply a function by creating a version that has had some of its 590 | // arguments pre-filled, without changing its dynamic `this` context. 591 | _.partial = function(func) { 592 | var args = slice.call(arguments, 1); 593 | return function() { 594 | return func.apply(this, args.concat(slice.call(arguments))); 595 | }; 596 | }; 597 | 598 | // Bind all of an object's methods to that object. Useful for ensuring that 599 | // all callbacks defined on an object belong to it. 600 | _.bindAll = function(obj) { 601 | var funcs = slice.call(arguments, 1); 602 | if (funcs.length === 0) funcs = _.functions(obj); 603 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 604 | return obj; 605 | }; 606 | 607 | // Memoize an expensive function by storing its results. 608 | _.memoize = function(func, hasher) { 609 | var memo = {}; 610 | hasher || (hasher = _.identity); 611 | return function() { 612 | var key = hasher.apply(this, arguments); 613 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 614 | }; 615 | }; 616 | 617 | // Delays a function for the given number of milliseconds, and then calls 618 | // it with the arguments supplied. 619 | _.delay = function(func, wait) { 620 | var args = slice.call(arguments, 2); 621 | return setTimeout(function(){ return func.apply(null, args); }, wait); 622 | }; 623 | 624 | // Defers a function, scheduling it to run after the current call stack has 625 | // cleared. 626 | _.defer = function(func) { 627 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 628 | }; 629 | 630 | // Returns a function, that, when invoked, will only be triggered at most once 631 | // during a given window of time. 632 | _.throttle = function(func, wait) { 633 | var context, args, timeout, result; 634 | var previous = 0; 635 | var later = function() { 636 | previous = new Date; 637 | timeout = null; 638 | result = func.apply(context, args); 639 | }; 640 | return function() { 641 | var now = new Date; 642 | var remaining = wait - (now - previous); 643 | context = this; 644 | args = arguments; 645 | if (remaining <= 0) { 646 | clearTimeout(timeout); 647 | timeout = null; 648 | previous = now; 649 | result = func.apply(context, args); 650 | } else if (!timeout) { 651 | timeout = setTimeout(later, remaining); 652 | } 653 | return result; 654 | }; 655 | }; 656 | 657 | // Returns a function, that, as long as it continues to be invoked, will not 658 | // be triggered. The function will be called after it stops being called for 659 | // N milliseconds. If `immediate` is passed, trigger the function on the 660 | // leading edge, instead of the trailing. 661 | _.debounce = function(func, wait, immediate) { 662 | var timeout, result; 663 | return function() { 664 | var context = this, args = arguments; 665 | var later = function() { 666 | timeout = null; 667 | if (!immediate) result = func.apply(context, args); 668 | }; 669 | var callNow = immediate && !timeout; 670 | clearTimeout(timeout); 671 | timeout = setTimeout(later, wait); 672 | if (callNow) result = func.apply(context, args); 673 | return result; 674 | }; 675 | }; 676 | 677 | // Returns a function that will be executed at most one time, no matter how 678 | // often you call it. Useful for lazy initialization. 679 | _.once = function(func) { 680 | var ran = false, memo; 681 | return function() { 682 | if (ran) return memo; 683 | ran = true; 684 | memo = func.apply(this, arguments); 685 | func = null; 686 | return memo; 687 | }; 688 | }; 689 | 690 | // Returns the first function passed as an argument to the second, 691 | // allowing you to adjust arguments, run code before and after, and 692 | // conditionally execute the original function. 693 | _.wrap = function(func, wrapper) { 694 | return function() { 695 | var args = [func]; 696 | push.apply(args, arguments); 697 | return wrapper.apply(this, args); 698 | }; 699 | }; 700 | 701 | // Returns a function that is the composition of a list of functions, each 702 | // consuming the return value of the function that follows. 703 | _.compose = function() { 704 | var funcs = arguments; 705 | return function() { 706 | var args = arguments; 707 | for (var i = funcs.length - 1; i >= 0; i--) { 708 | args = [funcs[i].apply(this, args)]; 709 | } 710 | return args[0]; 711 | }; 712 | }; 713 | 714 | // Returns a function that will only be executed after being called N times. 715 | _.after = function(times, func) { 716 | if (times <= 0) return func(); 717 | return function() { 718 | if (--times < 1) { 719 | return func.apply(this, arguments); 720 | } 721 | }; 722 | }; 723 | 724 | // Object Functions 725 | // ---------------- 726 | 727 | // Retrieve the names of an object's properties. 728 | // Delegates to **ECMAScript 5**'s native `Object.keys` 729 | _.keys = nativeKeys || function(obj) { 730 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 731 | var keys = []; 732 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 733 | return keys; 734 | }; 735 | 736 | // Retrieve the values of an object's properties. 737 | _.values = function(obj) { 738 | var values = []; 739 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); 740 | return values; 741 | }; 742 | 743 | // Convert an object into a list of `[key, value]` pairs. 744 | _.pairs = function(obj) { 745 | var pairs = []; 746 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); 747 | return pairs; 748 | }; 749 | 750 | // Invert the keys and values of an object. The values must be serializable. 751 | _.invert = function(obj) { 752 | var result = {}; 753 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; 754 | return result; 755 | }; 756 | 757 | // Return a sorted list of the function names available on the object. 758 | // Aliased as `methods` 759 | _.functions = _.methods = function(obj) { 760 | var names = []; 761 | for (var key in obj) { 762 | if (_.isFunction(obj[key])) names.push(key); 763 | } 764 | return names.sort(); 765 | }; 766 | 767 | // Extend a given object with all the properties in passed-in object(s). 768 | _.extend = function(obj) { 769 | each(slice.call(arguments, 1), function(source) { 770 | if (source) { 771 | for (var prop in source) { 772 | obj[prop] = source[prop]; 773 | } 774 | } 775 | }); 776 | return obj; 777 | }; 778 | 779 | // Return a copy of the object only containing the whitelisted properties. 780 | _.pick = function(obj) { 781 | var copy = {}; 782 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 783 | each(keys, function(key) { 784 | if (key in obj) copy[key] = obj[key]; 785 | }); 786 | return copy; 787 | }; 788 | 789 | // Return a copy of the object without the blacklisted properties. 790 | _.omit = function(obj) { 791 | var copy = {}; 792 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 793 | for (var key in obj) { 794 | if (!_.contains(keys, key)) copy[key] = obj[key]; 795 | } 796 | return copy; 797 | }; 798 | 799 | // Fill in a given object with default properties. 800 | _.defaults = function(obj) { 801 | each(slice.call(arguments, 1), function(source) { 802 | if (source) { 803 | for (var prop in source) { 804 | if (obj[prop] == null) obj[prop] = source[prop]; 805 | } 806 | } 807 | }); 808 | return obj; 809 | }; 810 | 811 | // Create a (shallow-cloned) duplicate of an object. 812 | _.clone = function(obj) { 813 | if (!_.isObject(obj)) return obj; 814 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 815 | }; 816 | 817 | // Invokes interceptor with the obj, and then returns obj. 818 | // The primary purpose of this method is to "tap into" a method chain, in 819 | // order to perform operations on intermediate results within the chain. 820 | _.tap = function(obj, interceptor) { 821 | interceptor(obj); 822 | return obj; 823 | }; 824 | 825 | // Internal recursive comparison function for `isEqual`. 826 | var eq = function(a, b, aStack, bStack) { 827 | // Identical objects are equal. `0 === -0`, but they aren't identical. 828 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 829 | if (a === b) return a !== 0 || 1 / a == 1 / b; 830 | // A strict comparison is necessary because `null == undefined`. 831 | if (a == null || b == null) return a === b; 832 | // Unwrap any wrapped objects. 833 | if (a instanceof _) a = a._wrapped; 834 | if (b instanceof _) b = b._wrapped; 835 | // Compare `[[Class]]` names. 836 | var className = toString.call(a); 837 | if (className != toString.call(b)) return false; 838 | switch (className) { 839 | // Strings, numbers, dates, and booleans are compared by value. 840 | case '[object String]': 841 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 842 | // equivalent to `new String("5")`. 843 | return a == String(b); 844 | case '[object Number]': 845 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 846 | // other numeric values. 847 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 848 | case '[object Date]': 849 | case '[object Boolean]': 850 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 851 | // millisecond representations. Note that invalid dates with millisecond representations 852 | // of `NaN` are not equivalent. 853 | return +a == +b; 854 | // RegExps are compared by their source patterns and flags. 855 | case '[object RegExp]': 856 | return a.source == b.source && 857 | a.global == b.global && 858 | a.multiline == b.multiline && 859 | a.ignoreCase == b.ignoreCase; 860 | } 861 | if (typeof a != 'object' || typeof b != 'object') return false; 862 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 863 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 864 | var length = aStack.length; 865 | while (length--) { 866 | // Linear search. Performance is inversely proportional to the number of 867 | // unique nested structures. 868 | if (aStack[length] == a) return bStack[length] == b; 869 | } 870 | // Add the first object to the stack of traversed objects. 871 | aStack.push(a); 872 | bStack.push(b); 873 | var size = 0, result = true; 874 | // Recursively compare objects and arrays. 875 | if (className == '[object Array]') { 876 | // Compare array lengths to determine if a deep comparison is necessary. 877 | size = a.length; 878 | result = size == b.length; 879 | if (result) { 880 | // Deep compare the contents, ignoring non-numeric properties. 881 | while (size--) { 882 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 883 | } 884 | } 885 | } else { 886 | // Objects with different constructors are not equivalent, but `Object`s 887 | // from different frames are. 888 | var aCtor = a.constructor, bCtor = b.constructor; 889 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 890 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) { 891 | return false; 892 | } 893 | // Deep compare objects. 894 | for (var key in a) { 895 | if (_.has(a, key)) { 896 | // Count the expected number of properties. 897 | size++; 898 | // Deep compare each member. 899 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 900 | } 901 | } 902 | // Ensure that both objects contain the same number of properties. 903 | if (result) { 904 | for (key in b) { 905 | if (_.has(b, key) && !(size--)) break; 906 | } 907 | result = !size; 908 | } 909 | } 910 | // Remove the first object from the stack of traversed objects. 911 | aStack.pop(); 912 | bStack.pop(); 913 | return result; 914 | }; 915 | 916 | // Perform a deep comparison to check if two objects are equal. 917 | _.isEqual = function(a, b) { 918 | return eq(a, b, [], []); 919 | }; 920 | 921 | // Is a given array, string, or object empty? 922 | // An "empty" object has no enumerable own-properties. 923 | _.isEmpty = function(obj) { 924 | if (obj == null) return true; 925 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 926 | for (var key in obj) if (_.has(obj, key)) return false; 927 | return true; 928 | }; 929 | 930 | // Is a given value a DOM element? 931 | _.isElement = function(obj) { 932 | return !!(obj && obj.nodeType === 1); 933 | }; 934 | 935 | // Is a given value an array? 936 | // Delegates to ECMA5's native Array.isArray 937 | _.isArray = nativeIsArray || function(obj) { 938 | return toString.call(obj) == '[object Array]'; 939 | }; 940 | 941 | // Is a given variable an object? 942 | _.isObject = function(obj) { 943 | return obj === Object(obj); 944 | }; 945 | 946 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 947 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 948 | _['is' + name] = function(obj) { 949 | return toString.call(obj) == '[object ' + name + ']'; 950 | }; 951 | }); 952 | 953 | // Define a fallback version of the method in browsers (ahem, IE), where 954 | // there isn't any inspectable "Arguments" type. 955 | if (!_.isArguments(arguments)) { 956 | _.isArguments = function(obj) { 957 | return !!(obj && _.has(obj, 'callee')); 958 | }; 959 | } 960 | 961 | // Optimize `isFunction` if appropriate. 962 | if (typeof (/./) !== 'function') { 963 | _.isFunction = function(obj) { 964 | return typeof obj === 'function'; 965 | }; 966 | } 967 | 968 | // Is a given object a finite number? 969 | _.isFinite = function(obj) { 970 | return isFinite(obj) && !isNaN(parseFloat(obj)); 971 | }; 972 | 973 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 974 | _.isNaN = function(obj) { 975 | return _.isNumber(obj) && obj != +obj; 976 | }; 977 | 978 | // Is a given value a boolean? 979 | _.isBoolean = function(obj) { 980 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 981 | }; 982 | 983 | // Is a given value equal to null? 984 | _.isNull = function(obj) { 985 | return obj === null; 986 | }; 987 | 988 | // Is a given variable undefined? 989 | _.isUndefined = function(obj) { 990 | return obj === void 0; 991 | }; 992 | 993 | // Shortcut function for checking if an object has a given property directly 994 | // on itself (in other words, not on a prototype). 995 | _.has = function(obj, key) { 996 | return hasOwnProperty.call(obj, key); 997 | }; 998 | 999 | // Utility Functions 1000 | // ----------------- 1001 | 1002 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1003 | // previous owner. Returns a reference to the Underscore object. 1004 | _.noConflict = function() { 1005 | root._ = previousUnderscore; 1006 | return this; 1007 | }; 1008 | 1009 | // Keep the identity function around for default iterators. 1010 | _.identity = function(value) { 1011 | return value; 1012 | }; 1013 | 1014 | // Run a function **n** times. 1015 | _.times = function(n, iterator, context) { 1016 | var accum = Array(n); 1017 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1018 | return accum; 1019 | }; 1020 | 1021 | // Return a random integer between min and max (inclusive). 1022 | _.random = function(min, max) { 1023 | if (max == null) { 1024 | max = min; 1025 | min = 0; 1026 | } 1027 | return min + Math.floor(Math.random() * (max - min + 1)); 1028 | }; 1029 | 1030 | // List of HTML entities for escaping. 1031 | var entityMap = { 1032 | escape: { 1033 | '&': '&', 1034 | '<': '<', 1035 | '>': '>', 1036 | '"': '"', 1037 | "'": ''', 1038 | '/': '/' 1039 | } 1040 | }; 1041 | entityMap.unescape = _.invert(entityMap.escape); 1042 | 1043 | // Regexes containing the keys and values listed immediately above. 1044 | var entityRegexes = { 1045 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1046 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1047 | }; 1048 | 1049 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1050 | _.each(['escape', 'unescape'], function(method) { 1051 | _[method] = function(string) { 1052 | if (string == null) return ''; 1053 | return ('' + string).replace(entityRegexes[method], function(match) { 1054 | return entityMap[method][match]; 1055 | }); 1056 | }; 1057 | }); 1058 | 1059 | // If the value of the named property is a function then invoke it; 1060 | // otherwise, return it. 1061 | _.result = function(object, property) { 1062 | if (object == null) return null; 1063 | var value = object[property]; 1064 | return _.isFunction(value) ? value.call(object) : value; 1065 | }; 1066 | 1067 | // Add your own custom functions to the Underscore object. 1068 | _.mixin = function(obj) { 1069 | each(_.functions(obj), function(name){ 1070 | var func = _[name] = obj[name]; 1071 | _.prototype[name] = function() { 1072 | var args = [this._wrapped]; 1073 | push.apply(args, arguments); 1074 | return result.call(this, func.apply(_, args)); 1075 | }; 1076 | }); 1077 | }; 1078 | 1079 | // Generate a unique integer id (unique within the entire client session). 1080 | // Useful for temporary DOM ids. 1081 | var idCounter = 0; 1082 | _.uniqueId = function(prefix) { 1083 | var id = ++idCounter + ''; 1084 | return prefix ? prefix + id : id; 1085 | }; 1086 | 1087 | // By default, Underscore uses ERB-style template delimiters, change the 1088 | // following template settings to use alternative delimiters. 1089 | _.templateSettings = { 1090 | evaluate : /<%([\s\S]+?)%>/g, 1091 | interpolate : /<%=([\s\S]+?)%>/g, 1092 | escape : /<%-([\s\S]+?)%>/g 1093 | }; 1094 | 1095 | // When customizing `templateSettings`, if you don't want to define an 1096 | // interpolation, evaluation or escaping regex, we need one that is 1097 | // guaranteed not to match. 1098 | var noMatch = /(.)^/; 1099 | 1100 | // Certain characters need to be escaped so that they can be put into a 1101 | // string literal. 1102 | var escapes = { 1103 | "'": "'", 1104 | '\\': '\\', 1105 | '\r': 'r', 1106 | '\n': 'n', 1107 | '\t': 't', 1108 | '\u2028': 'u2028', 1109 | '\u2029': 'u2029' 1110 | }; 1111 | 1112 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1113 | 1114 | // JavaScript micro-templating, similar to John Resig's implementation. 1115 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1116 | // and correctly escapes quotes within interpolated code. 1117 | _.template = function(text, data, settings) { 1118 | var render; 1119 | settings = _.defaults({}, settings, _.templateSettings); 1120 | 1121 | // Combine delimiters into one regular expression via alternation. 1122 | var matcher = new RegExp([ 1123 | (settings.escape || noMatch).source, 1124 | (settings.interpolate || noMatch).source, 1125 | (settings.evaluate || noMatch).source 1126 | ].join('|') + '|$', 'g'); 1127 | 1128 | // Compile the template source, escaping string literals appropriately. 1129 | var index = 0; 1130 | var source = "__p+='"; 1131 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1132 | source += text.slice(index, offset) 1133 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1134 | 1135 | if (escape) { 1136 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1137 | } 1138 | if (interpolate) { 1139 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1140 | } 1141 | if (evaluate) { 1142 | source += "';\n" + evaluate + "\n__p+='"; 1143 | } 1144 | index = offset + match.length; 1145 | return match; 1146 | }); 1147 | source += "';\n"; 1148 | 1149 | // If a variable is not specified, place data values in local scope. 1150 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1151 | 1152 | source = "var __t,__p='',__j=Array.prototype.join," + 1153 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1154 | source + "return __p;\n"; 1155 | 1156 | try { 1157 | render = new Function(settings.variable || 'obj', '_', source); 1158 | } catch (e) { 1159 | e.source = source; 1160 | throw e; 1161 | } 1162 | 1163 | if (data) return render(data, _); 1164 | var template = function(data) { 1165 | return render.call(this, data, _); 1166 | }; 1167 | 1168 | // Provide the compiled function source as a convenience for precompilation. 1169 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1170 | 1171 | return template; 1172 | }; 1173 | 1174 | // Add a "chain" function, which will delegate to the wrapper. 1175 | _.chain = function(obj) { 1176 | return _(obj).chain(); 1177 | }; 1178 | 1179 | // OOP 1180 | // --------------- 1181 | // If Underscore is called as a function, it returns a wrapped object that 1182 | // can be used OO-style. This wrapper holds altered versions of all the 1183 | // underscore functions. Wrapped objects may be chained. 1184 | 1185 | // Helper function to continue chaining intermediate results. 1186 | var result = function(obj) { 1187 | return this._chain ? _(obj).chain() : obj; 1188 | }; 1189 | 1190 | // Add all of the Underscore functions to the wrapper object. 1191 | _.mixin(_); 1192 | 1193 | // Add all mutator Array functions to the wrapper. 1194 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1195 | var method = ArrayProto[name]; 1196 | _.prototype[name] = function() { 1197 | var obj = this._wrapped; 1198 | method.apply(obj, arguments); 1199 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1200 | return result.call(this, obj); 1201 | }; 1202 | }); 1203 | 1204 | // Add all accessor Array functions to the wrapper. 1205 | each(['concat', 'join', 'slice'], function(name) { 1206 | var method = ArrayProto[name]; 1207 | _.prototype[name] = function() { 1208 | return result.call(this, method.apply(this._wrapped, arguments)); 1209 | }; 1210 | }); 1211 | 1212 | _.extend(_.prototype, { 1213 | 1214 | // Start chaining a wrapped Underscore object. 1215 | chain: function() { 1216 | this._chain = true; 1217 | return this; 1218 | }, 1219 | 1220 | // Extracts the result from a wrapped and chained object. 1221 | value: function() { 1222 | return this._wrapped; 1223 | } 1224 | 1225 | }); 1226 | 1227 | }).call(this); -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | MetaCPAN Explorer 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | require.config({ 3 | baseUrl: "scripts", 4 | urlArgs: "bust=" + (new Date()).getTime(), 5 | shim: { 6 | underscore: { 7 | exports: '_' 8 | }, 9 | backbone: { 10 | deps: ["underscore", "jquery"], 11 | exports: "Backbone" 12 | }, 13 | "bootstrap-dropdown": { 14 | deps: ["jquery"], 15 | exports: "jQuery.fn.dropdown" 16 | }, 17 | "bootstrap-modal": { 18 | deps: ["jquery"], 19 | exports: "jQuery.fn.modal" 20 | }, 21 | "bootstrap-typeahead": { 22 | deps: ["jquery"], 23 | exports: "jQuery.fn.typeahead" 24 | }, 25 | "bootstrap-tooltip": { 26 | deps: ["jquery"], 27 | exports: "jQuery.fn.tooltip" 28 | }, 29 | "jquery.querystring": { 30 | deps: ["jquery"], 31 | exports: "jQuery.fn.getParam" 32 | }, 33 | "highlight": { 34 | // https://highlightjs.org/download/ 35 | // We can download a custom build with only JSON support (all we need). 36 | // Version 8.2 of highlight.js uses `var` so when requirejs emeds it in 37 | // a large function call it won't be available on `this`. 38 | // By returning the local scope var we can use the minified custom 39 | // download without needing to wrap or alter the file. 40 | init: function(){ /*global hljs*/ return hljs; }, 41 | exports: "hljs" 42 | } 43 | }, 44 | paths: { 45 | "jquery": "../inc/jquery", 46 | "underscore": "../inc/underscore", 47 | "backbone": "../inc/backbone", 48 | "bootstrap-typeahead": "../inc/bootstrap/js/bootstrap-typeahead", 49 | "bootstrap-dropdown": "../inc/bootstrap/js/bootstrap-dropdown", 50 | "bootstrap-modal": "../inc/bootstrap/js/bootstrap-modal", 51 | "bootstrap-tooltip": "../inc/bootstrap/js/bootstrap-tooltip", 52 | "jquery.querystring": "../inc/jquery.querystring", 53 | "behave": "../inc/behave", 54 | "highlight": "../inc/highlight", 55 | "text": "../inc/text", 56 | "tpl": "../inc/tpl" 57 | } 58 | }); 59 | 60 | define([ 61 | "jquery", 62 | "router", 63 | "view/viewport", 64 | "view/navbar", 65 | "view/request", 66 | "view/sidebar", 67 | "view/settings", 68 | "settings", 69 | "model/request", 70 | "model", 71 | "collection", 72 | "jquery.querystring" 73 | ], 74 | function ( 75 | $, 76 | router, 77 | Viewport, 78 | Navbar, 79 | RequestView, 80 | SidebarView, 81 | SettingsView, 82 | settings, 83 | Request, 84 | Model, 85 | Collection 86 | ) { 87 | $(function(){ 88 | var viewport = new Viewport(); 89 | $(document.body).replaceWith(viewport.render().el); 90 | 91 | var request = new Request({ active: true }); 92 | 93 | var examples = window.e = new Collection([request], { 94 | model: Request, 95 | comparator: "description" 96 | }); 97 | 98 | var settingsView = new SettingsView({ model: settings }); 99 | var sidebar = new SidebarView({ 100 | settingsView: settingsView, 101 | collection: examples 102 | }); 103 | var navbar = new Navbar({ collection: examples }); 104 | 105 | var fetch = examples.fetch({ remove: false }); 106 | 107 | viewport.$el.append( 108 | navbar.render().el, 109 | sidebar.render().el, 110 | settingsView.render().el, 111 | viewport.add(new RequestView({ model: request })).render().el 112 | ); 113 | 114 | examples.bind("change:active", function(model, value) { 115 | if(!value) return; 116 | viewport.removeViews(); 117 | viewport.$el.append(viewport.add(new RequestView({ model: model })).render().el); 118 | }); 119 | 120 | examples.bind("change:id", function(model, id) { 121 | if(!model.isActive()) return; 122 | window.history.pushState(null, null, "/"); 123 | router.navigate("//" + id); 124 | }); 125 | 126 | router.on("route:load", function(id) { 127 | navbar.startLoading(); 128 | fetch.then(function() { 129 | var model = examples.get(id) || examples.newModel(id === "new" ? null : { id: id }); 130 | model.setActive(); 131 | if(id !== "new") 132 | return model.fetch().then(function() { 133 | return model.request({ gist: false }); 134 | }); 135 | }).always(function() { 136 | navbar.endLoading(); 137 | }); 138 | }).start(); 139 | 140 | if($.getParam("url")) { 141 | navbar.startLoading(); 142 | examples.newModel().setActive().set({ 143 | endpoint: $.getParam("url"), 144 | body: $.getParam("content") 145 | }).request().always(function() { 146 | navbar.endLoading(); 147 | }); 148 | } 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /app/scripts/collection.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "underscore", "backbone"], function($, _, Backbone) { 2 | return Backbone.Collection.extend({ 3 | /** 4 | * Sets the `active` attribute of all models but `model` to `false`. 5 | * Triggers the `active` event with the `model` passed as first parameter. 6 | * 7 | * @param {App.Model} model Model that is set to active 8 | * @param {Object} options Options that are passed to `set` such as `{ silent: true }` 9 | * @return {App.Model} returns `model` 10 | */ 11 | setActive: function(model, options) { 12 | _.invoke(this.without(model), "set", "active", false, options); 13 | model.set("active", true, options); 14 | options = options || {}; 15 | return model; 16 | }, 17 | /** 18 | * Get active model of collection 19 | * 20 | * @return {App.Model} Returns a model of one is active. 21 | */ 22 | getActive: function() { 23 | return this.find(function(model) { return model.isActive(); }); 24 | }, 25 | /** 26 | * Calls fetch on all models and returns a $.Deferred object that resolves 27 | * when all models have been fetched. 28 | * 29 | * @return {$.Deferred} 30 | */ 31 | fetchAll: function() { 32 | var self = this; 33 | return $.when.apply($, this.invoke("fetch")).pipe(function(){ return self; }); 34 | }, 35 | sync: function(method, model, options) { 36 | var store = this.store || (this.model && this.model.prototype.store); 37 | return store ? store.sync.apply(store, arguments) : $.when(); 38 | }, 39 | newModel: function(attributes, options) { 40 | var model = new this.model(attributes, options); 41 | this.add(model); 42 | return model; 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /app/scripts/model.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "underscore", "backbone"], function($, _, Backbone) { 2 | return Backbone.Model.extend({ 3 | /** 4 | * Returns a human readable description of the model. It is used 5 | * in the progress bar as subtitle. 6 | * 7 | * TODO: localize, i.e. return object with titles for all languages. 8 | * 9 | * @return {String} 10 | */ 11 | getTitle: function() { 12 | var title = this.get("title"); 13 | return _.isObject(title) ? title[languageSelectGlobal] : title; 14 | }, 15 | 16 | /** 17 | * Calls `setActive(model, options)` on the collection. 18 | * 19 | * @param {options} options 20 | */ 21 | setActive: function(options) { 22 | if(!this.collection){ return this; } 23 | return this.collection.setActive(this, options); 24 | }, 25 | /** 26 | * Returns true if model is active. 27 | * 28 | * @return {Boolean} 29 | */ 30 | isActive: function() { 31 | return !!this.get("active"); 32 | }, 33 | sync: function(method, model, options) { 34 | var store = model.store || model.collection.store; 35 | return store ? store.sync.apply(store, arguments) : $.when(); 36 | } 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /app/scripts/model/request.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "underscore", "model", "store/gist"], function($, _, Model, Store) { 2 | return Model.extend({ 3 | store: new Store(), 4 | defaults: { 5 | endpoint: null, 6 | body: null, 7 | response: null, 8 | active: false 9 | }, 10 | getCurl: function() { 11 | if(!this.get("endpoint")){ return ""; } 12 | var curl = "curl " + (this.get("body") ? "-XPOST " : "") + 13 | "'https://fastapi.metacpan.org" + this.get("endpoint") + "'"; 14 | if(this.get("body")){ 15 | curl += " -d \"$(curl -Ls gist.github.com/" + this.store.config.user + "/" + this.id + "/raw/body.json)\""; 16 | } 17 | return curl; 18 | }, 19 | toJSON: function() { 20 | var json = Model.prototype.toJSON.apply(this, arguments); 21 | _.extend(json, { curl: this.getCurl() }); 22 | return json; 23 | }, 24 | parse: function(res) { 25 | if(!res.files){ return res; } 26 | return _.extend(res, { 27 | body: (res.files["body.json"] && res.files["body.json"].content !== "null" ? 28 | res.files["body.json"].content : null), 29 | endpoint: res.files["endpoint.txt"].content 30 | }); 31 | }, 32 | request: function(options) { 33 | // Notify that the request has been initiated. 34 | this.trigger("pending", true); 35 | 36 | options = options || {}; 37 | var self = this; 38 | var body = this.get("body"); 39 | return $.ajax({ 40 | url: "https://fastapi.metacpan.org" + this.get("endpoint"), 41 | dataType: "text", 42 | type: (body ? "POST" : "GET"), 43 | data: (body || null) 44 | }).then(function(res) { 45 | self.set({ 46 | response: res, 47 | success: true 48 | }); 49 | return self; 50 | }, function(res) { 51 | self.set({ 52 | response: res.responseText, 53 | success: false 54 | }); 55 | return self; 56 | }).always(function(model) { 57 | // Notify that request completed 58 | // ("change:response" won't fire if the response text is the same). 59 | model.trigger("pending", false); 60 | 61 | if(options.gist !== false && model.get("public") !== true){ 62 | model.save(); 63 | } 64 | }); 65 | }, 66 | validate: function(attributes) { 67 | var json = attributes.body; 68 | try { 69 | if( json ){ 70 | JSON.parse(json); 71 | } 72 | } 73 | catch(e) { 74 | return e; 75 | } 76 | } 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /app/scripts/model/settings.js: -------------------------------------------------------------------------------- 1 | define(["backbone"], function(Backbone) { 2 | var lskey = 'metacpan-explorer-settings'; 3 | return Backbone.Model.extend({ 4 | 5 | defaults: { 6 | // Fancy "IDE-like" behavior in editor. 7 | editorFeatures: true, 8 | // Apply syntax highlighting to response body. 9 | highlightResponse: true, 10 | // Wrap lines of response body (alternative is horizontal scrollbar). 11 | wrapLines: true, 12 | // Validate request body as you type. 13 | instantValidation: true 14 | }, 15 | 16 | fetch: function(){ 17 | var attr = {}; 18 | try { 19 | attr = JSON.parse(localStorage[lskey]); 20 | }catch(ignore){} 21 | this.set(attr); 22 | }, 23 | sync: function(){ 24 | localStorage[lskey] = JSON.stringify(this); 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/scripts/router.js: -------------------------------------------------------------------------------- 1 | define(["backbone"], function(Backbone) { 2 | return new (Backbone.Router.extend({ 3 | start: function() { 4 | Backbone.history.start(); 5 | }, 6 | routes: { 7 | ":id": "load" 8 | } 9 | }))(); 10 | }); 11 | -------------------------------------------------------------------------------- /app/scripts/settings.js: -------------------------------------------------------------------------------- 1 | define(["model/settings"], function(Settings) { 2 | var settings = new Settings(); 3 | settings.fetch(); 4 | return settings; 5 | }); 6 | -------------------------------------------------------------------------------- /app/scripts/store/gist.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "underscore", "backbone"], function($, _, Backbone) { 2 | 3 | var Storage = function() { return; }; 4 | _.extend(Storage.prototype, Backbone.Events, { 5 | config: { 6 | user: 'metacpan-user', 7 | token: github_token() 8 | }, 9 | request: function(options) { 10 | //options.url += "?access_token=" + this.config.token; 11 | return $.ajax(options); 12 | }, 13 | find: function(model, options) { 14 | return this.request({ 15 | url: "https://api.github.com/gists/" + model.id, 16 | context: this, 17 | dataType: "json" 18 | }); 19 | }, 20 | findAll: function() { 21 | return $.ajax({ 22 | url: "https://api.github.com/users/" + this.config.user + "/gists", 23 | context: this, 24 | dataType: "json" 25 | }); 26 | }, 27 | create: function(model, options) { 28 | var gist = { 29 | public: false, 30 | files: { 31 | "endpoint.txt": { 32 | content: model.get("endpoint") 33 | }, 34 | //"response.json": { 35 | // content: model.get("response") 36 | //}, 37 | "body.json": { 38 | content: model.get("body") || "null" 39 | } 40 | } 41 | }; 42 | return this.request({ 43 | url: "https://api.github.com/gists" + (model.id ? "/" + model.id : ""), 44 | type: model.id ? "PATCH" : "POST", 45 | context: this, 46 | dataType: "json", 47 | contentType: "application/json", 48 | data: JSON.stringify(gist) 49 | }).then(function(res) { return { id: res.id }; }); 50 | }, 51 | update: function() { return this.create.apply(this, arguments); }, 52 | destroy: function() { throw "destroy not implemented in " + this; }, 53 | sync: function(method, model, options) { 54 | model.trigger("load:start"); 55 | options = options || {}; 56 | var resp; 57 | switch (method) { 58 | case "read": resp = model.id ? this.find(model, options) : this.findAll(model, options); break; 59 | case "create": resp = this.create(model, options); break; 60 | case "update": resp = this.update(model, options); break; 61 | case "delete": resp = this.destroy(model, options); break; 62 | } 63 | resp.always(function() { model.trigger("load:end"); }); 64 | return resp.fail(options.error || $.noop).done(options.success || $.noop); 65 | } 66 | }); 67 | return Storage; 68 | }); 69 | -------------------------------------------------------------------------------- /app/scripts/template/navbar.htm: -------------------------------------------------------------------------------- 1 | 7 | MetaCPAN Explorer 8 | 16 | -------------------------------------------------------------------------------- /app/scripts/template/request.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Invalid JSON 5 |
6 | 7 |
8 |
9 |
10 |
11 | 12 |
13 | 14 |

15 |       <%= model.response || "" %>
16 |     
17 |
18 |
19 | -------------------------------------------------------------------------------- /app/scripts/template/settings.htm: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /app/scripts/template/sidebar.htm: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /app/scripts/view.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "underscore", "backbone"], function($, _, Backbone) { 2 | return Backbone.View.extend({ 3 | constructor: function(options) { 4 | options = options || {}; 5 | var name = this.name || options.name; 6 | if(!this.template && name) { 7 | var template = $("#template-" + name).html(); 8 | if(template){ 9 | this.template = _.template(template); 10 | } 11 | } 12 | if(this.name && (!this.attributes || !this.attributes.class)) { 13 | this.attributes = _.extend( 14 | this.attributes || {}, 15 | { "class": name } 16 | ); 17 | } 18 | if(options.model){ 19 | this.listenTo(options.model, "change:active", function(model, value) { 20 | this.$el.toggleClass("active", value); 21 | }); 22 | } 23 | Backbone.View.apply(this, arguments); 24 | }, 25 | triggerSelect: function() { 26 | this.trigger("select", this.model); 27 | }, 28 | proxy: function(from, event) { 29 | return from.bind(event, _.bind(this.trigger, this, event)); 30 | }, 31 | add: function() { 32 | this.views = this.views || []; 33 | this.views.push.apply(this.views, arguments); 34 | return arguments.length === 1 ? arguments[0] : arguments; 35 | }, 36 | remove: function() { 37 | var args = arguments; 38 | _.each(this.views || [], function(view) { 39 | view.remove.apply(view, args); 40 | }); 41 | return Backbone.View.prototype.remove.apply(this, arguments); 42 | }, 43 | removeViews: function() { 44 | var views = this.views; 45 | _.invoke(views, "remove"); 46 | this.views = []; 47 | return views; 48 | }, 49 | render: function(options) { 50 | this.removeViews(); 51 | var template = this.options.template || this.template; 52 | options = _.extend({ 53 | model: this.model ? this.model.toJSON() : {}, 54 | collection: this.collection ? this.collection.toJSON() : [] 55 | }, options || {}); 56 | if(template){ 57 | this.$el.html(template(options)); 58 | } 59 | if(this.model){ 60 | this.$el.toggleClass("active", this.model.isActive()); 61 | } 62 | return this; 63 | } 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /app/scripts/view/dragbar.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jquery", 3 | "underscore", 4 | "backbone", 5 | ], function($, _, Backbone) { 6 | var lskey = 'metacpan-explorer-dragbar', 7 | cancel_event = function () { return false; }; 8 | 9 | return Backbone.View.extend({ 10 | events: { 11 | // The move/up events are handled in the start/stop methods. 12 | 'mousedown': 'start' 13 | }, 14 | 15 | initialize: function (options) { 16 | this.dragging = false; 17 | this.$body = $('body'); 18 | this.$container = options.container; 19 | this.$left = options.left; 20 | this.$right = options.right; 21 | this.fetch(); 22 | this._bound_move = _.bind(this.move, this); 23 | this._bound_stop = _.bind(this.stop, this); 24 | }, 25 | 26 | fetch: function () { 27 | try { 28 | this.position = localStorage[lskey] || 50; 29 | } 30 | catch (e) { 31 | this.position = 50; 32 | } 33 | }, 34 | 35 | render: function () { 36 | this.setPosition(this.position); 37 | return this; 38 | }, 39 | 40 | start: function (e) { 41 | // Only with left-button. 42 | if(e.which !== 1){ return; } 43 | 44 | this.dragging = true; 45 | 46 | // Cache this here as it seems unlikely to change while dragging. 47 | // Subtract one to help center the bar under the cursor. 48 | this.offsetLeft = this.$container.offset().left - 1; 49 | this.totalWidth = this.$container.width(); 50 | 51 | this.$body 52 | // Don't let the browser think we're trying to select text. 53 | .addClass('dragging') 54 | .on('selectstart.dragbar', cancel_event) 55 | // Listen to mouse move/up on whole body so that dragging ends 56 | // even if the mouse moves off the bar. 57 | // Only listen to body mouse events when dragging. 58 | .on('mousemove.dragbar', this._bound_move) 59 | .on('mouseup.dragbar', this._bound_stop); 60 | }, 61 | 62 | move: function (e) { 63 | if(!this.dragging){ return; } 64 | 65 | // Convert position to percentage of width 66 | // so it can easily be used as width for surrounding elements. 67 | var pos = ((e.pageX - this.offsetLeft) / this.totalWidth) * 100; 68 | 69 | // Don't let either box get too small. 70 | if( pos >= 10 && pos <= 90 ){ 71 | this.setPosition(pos); 72 | } 73 | }, 74 | 75 | stop: function () { 76 | if(!this.dragging){ return; } 77 | 78 | this.dragging = false; 79 | 80 | this.$body 81 | .off('.dragbar') 82 | .removeClass('dragging'); 83 | 84 | this.save(); 85 | }, 86 | 87 | save: function () { 88 | localStorage[lskey] = this.position; 89 | }, 90 | 91 | setPosition: function (pos) { 92 | this.position = pos; 93 | this.$el.css('left', pos + '%'); 94 | this.$left.css('width', pos + '%'); 95 | this.$right.css('width', (100 - pos) + '%'); 96 | } 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /app/scripts/view/list-item.js: -------------------------------------------------------------------------------- 1 | define(["underscore", "view"], function(_, View) { 2 | return View.extend({ 3 | tagName: "li", 4 | name: "list-item", 5 | template: _.template('<%- model.description || model.id %>'), 6 | events: { 7 | "click a": function() { this.model.setActive(); } 8 | } 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/scripts/view/navbar.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "underscore", 3 | "view", 4 | "tpl!template/navbar.htm", 5 | "bootstrap-typeahead", 6 | "bootstrap-tooltip" 7 | ], function(_, View, template) { 8 | return View.extend({ 9 | loading: 0, 10 | loadingInterval: null, 11 | template: template, 12 | name: "navbar", 13 | attributes: { 14 | "class": "navbar navbar-fixed-top" 15 | }, 16 | events: { 17 | "submit form" : function() { 18 | this.collection.getActive().set({ 19 | endpoint: this.$endpoint.val() 20 | }).request(); 21 | return false; 22 | } 23 | }, 24 | initialize: function() { 25 | this.listenTo(this.collection, "load:start", this.startLoading); 26 | this.listenTo(this.collection, "load:end", this.endLoading); 27 | this.listenTo(this.collection, "change:active", this.render); 28 | this.listenTo(this.collection, "change:endpoint", this.render); 29 | }, 30 | startLoading: function() { 31 | if(!this.loading) { 32 | this.loadingInterval = window.setInterval(_.bind(this.animateLogo, this), 2000); 33 | _.defer(_.bind(this.animateLogo, this)); 34 | } 35 | this.loading++; 36 | }, 37 | endLoading: function() { 38 | this.loading--; 39 | if(!this.loading){ 40 | window.clearInterval(this.loadingInterval); 41 | } 42 | }, 43 | animateLogo: function() { 44 | var ll = this.$(".ll"), 45 | lr = this.$(".lr"), 46 | ur = this.$(".ur"), 47 | ul = this.$(".ul"); 48 | ll.toggleClass("ll ul"); 49 | lr.toggleClass("lr ll"); 50 | ur.toggleClass("ur lr"); 51 | ul.toggleClass("ul ur"); 52 | }, 53 | render: function(model) { 54 | model = model || this.collection.getActive(); 55 | View.prototype.render.call(this, { model: model ? model.toJSON() : {} }); 56 | this.$endpoint = this.$("input").typeahead({ 57 | source: [ 58 | "/v1/file", 59 | "/v1/author", 60 | "/v1/release", 61 | "/v1/distribution", 62 | "/v1/module", 63 | "/v1/favorite", 64 | "/v1/rating" 65 | ] 66 | }); 67 | this.$("button").tooltip({ placement: "bottom", trigger: "hover", container: "body" }); 68 | return this; 69 | } 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /app/scripts/view/request.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "underscore", 3 | "view", 4 | "view/dragbar", 5 | "settings", 6 | "behave", 7 | "highlight", 8 | "tpl!template/request.htm", 9 | "bootstrap-dropdown" 10 | ], function(_, View, DragBar, settings, Behave, hljs, template) { 11 | /*jslint unparam: true*/ // A lot of event callbacks in here. 12 | return View.extend({ 13 | name: "request", 14 | template: template, 15 | 16 | events: { 17 | "keydown textarea": function(e) { 18 | // Shift + Enter to send request. 19 | // NOTE: Our copy of behave has an edit on line 441 to disable behave's 20 | // enterKey functionality when shift is pressed. 21 | // Behave offers hooks for keydown as well as enter:before but they both 22 | // fire after behave's enterKey function has started, so I think it's 23 | // too late... I think we're stuck with the edit. 24 | if((e.keyCode || e.which) === 13 && e.shiftKey === true) { 25 | this.model.request(); 26 | return false; 27 | } 28 | }, 29 | "keyup textarea": "updateBody" 30 | }, 31 | 32 | updateBody: function() { 33 | var json = this.$body.val(); 34 | this.model.set("body", json); 35 | }, 36 | validateBody: function () { 37 | this.setValid(this.model.isValid()); 38 | }, 39 | onChangeBody: function(m, body) { 40 | // Only update the html if the text is different, 41 | // since doing so can move the cursor in some browsers. 42 | if( body !== this.$body.val() ){ 43 | this.$body.val(body); 44 | } 45 | if( settings.get('instantValidation') ){ 46 | this.validateBody(); 47 | } 48 | }, 49 | 50 | initialize: function() { 51 | this.listenTo(this.model, { 52 | // Use special pending event not only for the start, but also 53 | // to ensure we get the event even if the response doesn't *change*. 54 | "change:response": this.updateResponse, 55 | "change:body": this.onChangeBody, 56 | "pending": this.updatePendingIndicator 57 | }); 58 | this.listenTo(settings, { 59 | 'change:editorFeatures': this.onChangeEditorFeatures, 60 | 'change:highlightResponse': this.updateResponse, 61 | 'change:wrapLines': this.onChangeWrapLines, 62 | 'change:instantValidation': this.onChangeInstantValidation 63 | }); 64 | }, 65 | 66 | updatePendingIndicator: function (pending) { 67 | this.$resbox.toggleClass('pending', pending); 68 | }, 69 | updateResponse: function() { 70 | var res = _.escape(this.model.get("response")); 71 | this.$response.html(res); 72 | if( settings.get('highlightResponse') ){ 73 | hljs.highlightBlock(this.$response.get(0)); 74 | } 75 | }, 76 | 77 | render: function() { 78 | View.prototype.render.apply(this, arguments); 79 | this.$label = this.$(".editor .label").hide(); 80 | this.$body = this.$("textarea"); 81 | this.$resbox = this.$('.response'); 82 | this.$response = this.$('pre code'); 83 | 84 | this.dragbar = (new DragBar({ 85 | container: this.$('.request-inner'), 86 | left: this.$('.editor'), 87 | right: this.$resbox, 88 | el: this.$('.dragbar') 89 | })).render(); 90 | 91 | this.setEditorFeatures(settings.get('editorFeatures')); 92 | this.setWrapLines(settings.get('wrapLines')); 93 | 94 | this.updateResponse(); 95 | return this; 96 | }, 97 | 98 | onChangeEditorFeatures: function(m, val, o){ this.setEditorFeatures(val); }, 99 | setEditorFeatures: function(enabled) { 100 | if( this.behave ){ 101 | this.behave.destroy(); 102 | this.behave = null; 103 | } 104 | if( enabled ){ 105 | this.behave = new Behave({ 106 | textarea: this.$body.get(0), 107 | tabSize: 2 108 | }); 109 | } 110 | }, 111 | 112 | onChangeInstantValidation: function(m, val, o){ 113 | if( val ){ 114 | // Initiate validation when enabled. 115 | this.validateBody(); 116 | } 117 | else { 118 | // Update the display if checking is disabled. 119 | this.setValid(true); 120 | } 121 | }, 122 | setValid: function(valid) { 123 | if( valid ){ 124 | this.$label.hide(); 125 | } 126 | else { 127 | this.$label.show(); 128 | } 129 | }, 130 | 131 | onChangeWrapLines: function(m, val, o) { this.setWrapLines(val); }, 132 | setWrapLines: function(wrap) { 133 | this.$response.toggleClass('wrap', wrap); 134 | } 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /app/scripts/view/settings.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jquery", 3 | "underscore", 4 | "backbone", 5 | "tpl!template/settings.htm", 6 | "bootstrap-modal" 7 | ], function($, _, Backbone, template){ 8 | 9 | var _helpers = { 10 | //e: _.escape, 11 | 12 | checkbox: function(name) { 13 | return ''; 14 | }, 15 | 16 | item: function() { 17 | return [ 18 | '
  • ' 21 | ].join(''); 22 | } 23 | }; 24 | 25 | var _bind_helpers = function(context) { 26 | return _.reduce(_helpers, function(memo, func, name) { 27 | memo[name] = _.bind(func, context); 28 | return memo; 29 | }, {}); 30 | }; 31 | 32 | return Backbone.View.extend({ 33 | tagName: 'div', 34 | id: 'settings', 35 | template: template, 36 | 37 | events: { 38 | "change input[type=checkbox]": "toggleCheckbox", 39 | "click .cancel": "hide", 40 | "click .save": "save" 41 | }, 42 | 43 | initialize: function() { 44 | this.helpers = _bind_helpers(this); 45 | }, 46 | 47 | bsmodal: function(arg) { 48 | this.$modal.modal(arg); 49 | }, 50 | 51 | render: function(options) { 52 | this.changes = {}; 53 | this.$el.html(this.template(_.extend({ 54 | model: this.model.toJSON() 55 | }, this.helpers, options))); 56 | this.$modal = $('#settings .modal'); 57 | this.$toggle = $('.settings-toggle'); 58 | return this; 59 | }, 60 | 61 | toggleCheckbox: function(e) { 62 | var $el = $(e.target), 63 | name = $el.attr('name') || $el.attr('id'); 64 | this.changes[name] = $el.prop('checked'); 65 | }, 66 | 67 | save: function(){ 68 | this.model.save(this.changes); 69 | this.hide(); 70 | }, 71 | 72 | hide: function() { 73 | this.$toggle.removeClass('open'); 74 | this.bsmodal('hide'); 75 | }, 76 | show: function() { 77 | // Reset form elements to current settings. 78 | this.render(); 79 | this.$toggle.addClass('open'); 80 | this.bsmodal(); 81 | } 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /app/scripts/view/sidebar.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jquery", 3 | "view", 4 | "view/list-item", 5 | "tpl!template/sidebar.htm" 6 | ], function($, View, ItemView, template) { 7 | return View.extend({ 8 | name: "sidebar", 9 | template: template, 10 | events: { 11 | "click .settings-toggle": "toggleSettings", 12 | "click .input": function(e) { 13 | $(e.target).focus().select(); 14 | } 15 | }, 16 | initialize: function(options) { 17 | this.listenTo(this.collection, "sync", this.render); 18 | this.listenTo(this.collection, "change:active", this.updateCurl); 19 | this.settingsView = options.settingsView; 20 | }, 21 | updateCurl: function(model, value) { 22 | this.$("input").val(value ? model.getCurl() : ""); 23 | }, 24 | render: function() { 25 | var self = this; 26 | var model = this.collection.getActive(); 27 | View.prototype.render.call(this, { 28 | model: model ? model.toJSON() : null 29 | }); 30 | var $nav = this.$("ul.nav .examples"); 31 | this.collection.each(function(item) { 32 | if(!item.id){ 33 | return; 34 | } 35 | $nav.after(self.add(new ItemView({ model: item })).render().el); 36 | }); 37 | return this; 38 | }, 39 | toggleSettings: function(e) { 40 | e.preventDefault(); 41 | this.settingsView.show(); 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /app/scripts/view/viewport.js: -------------------------------------------------------------------------------- 1 | define(["view", "view/navbar"], function(View, Navbar) { 2 | return View.extend({ 3 | tagName: "body", 4 | name: "viewport" 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /app/styles/bootstrap.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.0 3 | * 4 | * Copyright 2013 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world by @mdo and @fat. 9 | */ 10 | 11 | // Core variables and mixins 12 | @import "../inc/bootstrap/less/variables.less"; 13 | @import "../inc/bootstrap/less/mixins.less"; 14 | 15 | // Reset 16 | @import "../inc/bootstrap/less/normalize.less"; 17 | //@import "../inc/bootstrap/less/print.less"; 18 | 19 | // Core CSS 20 | @import "../inc/bootstrap/less/scaffolding.less"; 21 | @import "../inc/bootstrap/less/type.less"; 22 | @import "../inc/bootstrap/less/code.less"; 23 | @import "../inc/bootstrap/less/grid.less"; 24 | 25 | //@import "../inc/bootstrap/less/tables.less"; 26 | @import "../inc/bootstrap/less/forms.less"; 27 | @import "../inc/bootstrap/less/buttons.less"; 28 | 29 | // Components: common 30 | //@import "../inc/bootstrap/less/component-animations.less"; 31 | @import "../inc/bootstrap/less/glyphicons.less"; 32 | @import "../inc/bootstrap/less/dropdowns.less"; 33 | @import "../inc/bootstrap/less/list-group.less"; 34 | //@import "../inc/bootstrap/less/panels.less"; 35 | //@import "../inc/bootstrap/less/wells.less"; 36 | @import "../inc/bootstrap/less/close.less"; 37 | 38 | // Components: Nav 39 | @import "../inc/bootstrap/less/navs.less"; 40 | @import "../inc/bootstrap/less/navbar.less"; 41 | @import "../inc/bootstrap/less/button-groups.less"; 42 | //@import "../inc/bootstrap/less/breadcrumbs.less"; 43 | //@import "../inc/bootstrap/less/pagination.less"; 44 | //@import "../inc/bootstrap/less/pager.less"; 45 | 46 | // Components: Popovers 47 | @import "../inc/bootstrap/less/modals.less"; 48 | @import "../inc/bootstrap/less/tooltip.less"; 49 | //@import "../inc/bootstrap/less/popovers.less"; 50 | 51 | // Components: Misc 52 | //@import "../inc/bootstrap/less/alerts.less"; 53 | //@import "../inc/bootstrap/less/thumbnails.less"; 54 | //@import "../inc/bootstrap/less/media.less"; 55 | @import "../inc/bootstrap/less/labels.less"; 56 | //@import "../inc/bootstrap/less/badges.less"; 57 | //@import "../inc/bootstrap/less/progress-bars.less"; 58 | //@import "../inc/bootstrap/less/accordion.less"; 59 | //@import "../inc/bootstrap/less/carousel.less"; 60 | //@import "../inc/bootstrap/less/jumbotron.less"; 61 | 62 | // Utility classes 63 | @import "../inc/bootstrap/less/utilities.less"; // Has to be last to override when necessary 64 | //@import "../inc/bootstrap/less/responsive-utilities.less"; 65 | -------------------------------------------------------------------------------- /app/styles/dragbar.less: -------------------------------------------------------------------------------- 1 | .request { 2 | // Position the outer .dragbar with the percentage so its between the two 3 | // areas, then relative position the child .handle so that it can be large 4 | // enough to grab, but overall the draggable element seems "in the middle". 5 | // Leave the style as a single pixel bar; Make the handle wider so its easier 6 | // to grab but leave it transparent so the single pixel bar shows through. 7 | .dragbar { 8 | background: transparent; 9 | left: 50%; 10 | position: absolute; 11 | width: 1px; 12 | z-index: 10; 13 | 14 | &, .handle { 15 | border: 0; 16 | bottom: 0; 17 | height: 100%; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | .handle { 23 | // ...but don't use background:transparent as that makes IE act like it's 24 | // smaller than it is... so fake it with a translucent white. 25 | background: #fff; 26 | opacity: 0; 27 | cursor: col-resize; 28 | left: -5px; 29 | position: relative; 30 | width: 9px; 31 | } 32 | } 33 | } 34 | 35 | .user-select (@val) { 36 | -moz-user-select: @val; 37 | -moz-user-select: -moz-@val; 38 | -khtml-user-select: @val; 39 | -webkit-user-select: @val; 40 | -ms-user-select: @val; 41 | user-select: @val; 42 | } 43 | // Don't select text while dragging. 44 | body.dragging * { 45 | .user-select(none); 46 | } 47 | -------------------------------------------------------------------------------- /app/styles/logo.less: -------------------------------------------------------------------------------- 1 | div.logo { 2 | position: relative; 3 | margin: 15px 20px 15px 0; 4 | float: left; 5 | 6 | div { 7 | position: absolute; 8 | display: inline; 9 | width: 9px; 10 | height: 9px; 11 | background-color: @brand-primary; 12 | border-radius: 9px; 13 | .box-shadow(inset 1px 1px 2px rgba(0,0,0,.2)); 14 | .transition(~"-webkit-transform ease 1s"); 15 | .transition(~"transform ease 1s"); 16 | } 17 | 18 | .ul { 19 | .translate(0px, 0px); 20 | } 21 | .ur { 22 | .translate(12px, 0px); 23 | } 24 | .ll { 25 | .translate(0px, 12px); 26 | } 27 | .lr { 28 | .translate(12px, 12px); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/styles/main.less: -------------------------------------------------------------------------------- 1 | @import "bootstrap.less"; 2 | 3 | // Make .btn-primary the same color as the dots in the logo. 4 | @brand-primary: #da2037; 5 | @glyphicons-font-path: "/fonts"; 6 | 7 | html, body { 8 | height: 100%; 9 | } 10 | 11 | @import "request.less"; 12 | @import "dragbar.less"; 13 | @import "sidebar.less"; 14 | @import "settings.less"; 15 | 16 | @import "logo.less"; 17 | 18 | // Force import as less to enusre the contents are embedded in build file. 19 | @import (less) "../inc/highlight-default.css"; 20 | 21 | textarea, 22 | input[type="text"], 23 | input[type="password"], 24 | input[type="datetime"], 25 | input[type="datetime-local"], 26 | input[type="date"], 27 | input[type="month"], 28 | input[type="time"], 29 | input[type="week"], 30 | input[type="number"], 31 | input[type="email"], 32 | input[type="url"], 33 | input[type="search"], 34 | input[type="tel"], 35 | input[type="color"] { 36 | // Focus state 37 | &:focus { 38 | border-color: @input-border; 39 | outline: 0; 40 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); 41 | } 42 | } 43 | 44 | * { 45 | -webkit-tap-highlight-color: rgba(0,0,0,0); 46 | -webkit-overflow-scrolling: touch; 47 | } 48 | 49 | .navbar-fixed-top { 50 | .box-shadow(0 1px 3px rgba(0,0,0,.2)); 51 | } 52 | 53 | .modal { 54 | .modal-header { 55 | // Make header bg different than body but don't lose the round (top) corners. 56 | background-color: #eee; 57 | .border-top-radius(inherit); 58 | } 59 | } 60 | 61 | .fitted-icon() { 62 | display: inline-block; 63 | height: @font-size-base; 64 | line-height: @font-size-base; 65 | margin: 0; 66 | padding: 0; 67 | text-decoration: none !important; 68 | width: @font-size-base; 69 | } 70 | -------------------------------------------------------------------------------- /app/styles/request.less: -------------------------------------------------------------------------------- 1 | div.request { 2 | position: absolute; 3 | padding-left: 200px; 4 | top: 0px; 5 | height: 100%; 6 | width: 100%; 7 | 8 | div.request-inner { 9 | position: relative; 10 | height: 100%; 11 | } 12 | 13 | div.editor, 14 | div.response { 15 | position: absolute; 16 | top: 0; 17 | bottom: 0; 18 | top: (@navbar-height - 1); 19 | width: 50%; 20 | } 21 | 22 | div.editor { 23 | .label { 24 | position: absolute; 25 | right: 10px; 26 | top: 10px; 27 | } 28 | 29 | textarea { 30 | margin: 0; 31 | height: 100%; 32 | display: block; 33 | position: absolute; 34 | bottom: 0; 35 | top: 0; 36 | tab-size: 2; 37 | font-family: @font-family-monospace; 38 | border-radius: 0; 39 | resize: none; 40 | border-left-width: 0; 41 | font-size: 0.8em; 42 | line-height: 1.4; 43 | color: #000; 44 | 45 | // Redo the placeholder style with increased specificity. 46 | // (It looks fine most of the time but in my vm IE it looks black.) 47 | .placeholder(); 48 | } 49 | } 50 | } 51 | 52 | div.response { 53 | right: 0px; 54 | 55 | pre { 56 | position: absolute; 57 | bottom: 0px; 58 | top: 0px; 59 | width: 100%; 60 | tab-size: 2; 61 | margin: 0; 62 | overflow: auto; 63 | border-radius: 0; 64 | border-left-width: 0; 65 | word-wrap: normal; 66 | font-size: 0.8em; 67 | line-height: 1.4; 68 | 69 | code { 70 | background: inherit; 71 | font: inherit; 72 | margin: 0; 73 | padding: 0; 74 | white-space: pre; 75 | 76 | &.wrap { 77 | white-space: pre-wrap; 78 | } 79 | } 80 | 81 | .hljs-attribute { 82 | color: #550; 83 | } 84 | } 85 | 86 | } 87 | 88 | .keyframes-transform(@name, @from, @to) { 89 | @-moz-keyframes @name { 90 | from { -moz-transform: @from; } 91 | to { -moz-transform: @to; } 92 | } 93 | @-webkit-keyframes @name { 94 | from { -webkit-transform: @from; } 95 | to { -webkit-transform: @to; } 96 | } 97 | @-o-keyframes @name { 98 | from { -o-transform: @from; } 99 | to { -o-transform: @to; } 100 | } 101 | @keyframes @name { 102 | from { transform: @from; } 103 | to { transform: @to; } 104 | } 105 | } 106 | .keyframes-transform(spin, rotate(0deg), rotate(360deg)); 107 | 108 | .animation(@args) { 109 | -moz-animation: @args; 110 | -webkit-animation: @args; 111 | -o-animation: @args; 112 | animation: @args; 113 | } 114 | 115 | .response { 116 | .indicator { 117 | .animation(spin 2.5s 0s infinite linear normal); 118 | .fitted-icon(); 119 | color: #888; 120 | display: none; 121 | padding: 1px; 122 | position: absolute; 123 | right: 50%; 124 | top: 25%; 125 | width: auto; 126 | z-index: 10; 127 | } 128 | &.pending { 129 | .indicator { 130 | display: block; 131 | } 132 | pre code { 133 | opacity: 0.5; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/styles/settings.less: -------------------------------------------------------------------------------- 1 | .settings-toggle { 2 | .glyphicon { 3 | border: none !important; 4 | color: inherit; 5 | .fitted-icon(); 6 | transform: rotate(0deg); 7 | transition: transform 1.5s; 8 | } 9 | 10 | &.open { 11 | .glyphicon { 12 | transform: rotate(180deg); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/styles/sidebar.less: -------------------------------------------------------------------------------- 1 | div.sidebar { 2 | height: 100%; 3 | z-index: 1; 4 | position: relative; 5 | width: 200px; 6 | border-top: 1px solid rgba(0, 0, 0, 0.15); 7 | text-shadow: 0 1px 0 #fff; 8 | background-color: #f5f5f5; 9 | box-shadow: inset -1px 0 0 #e5e5e5; 10 | overflow-y: auto; 11 | 12 | .nav { 13 | padding-top: (@navbar-height - 1 + 10); 14 | 15 | > li { 16 | &.input { 17 | padding: 0 15px; 18 | 19 | input { 20 | cursor: pointer; 21 | } 22 | } 23 | 24 | /* Nav: first level */ 25 | 26 | > a { 27 | display: block; 28 | color: #666; 29 | padding: 4px 15px; 30 | 31 | &:hover, 32 | &:focus { 33 | text-decoration: none; 34 | border-right: 1px solid #d5d5d5; 35 | } 36 | } 37 | } 38 | > .active { 39 | > a, 40 | &:hover > a, 41 | &:focus > a { 42 | cursor: default; 43 | font-weight: 500; 44 | color: #b94a48; 45 | background-color: transparent; 46 | border-right: 1px solid #b94a48; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bin/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | docker build -t metacpan/metacpan-explorer:latest . 6 | -------------------------------------------------------------------------------- /bin/docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | docker run --rm -it -v "$PWD:/usr/src/app" -p 8080:8080 metacpan/metacpan-explorer:latest 6 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm install 4 | export PATH=`npm bin || echo node_modules/.bin`:$PATH 5 | 6 | npm run build 7 | -------------------------------------------------------------------------------- /build/fonts: -------------------------------------------------------------------------------- 1 | ../app/fonts -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MetaCPAN Explorer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /env-filter.js: -------------------------------------------------------------------------------- 1 | var text = ""; 2 | process.stdin.resume(); 3 | process.stdin.on("data", function(data){ text += data; }); 4 | process.stdin.on("end", function(){ 5 | var bust = (new Date()).getTime().toString(36); 6 | process.stdout.write( 7 | text 8 | .replace(/\{\{bust\}\}/g, bust) 9 | .replace(/(?:)?/g, function(_, env, content){ 10 | // Enable the contents of the 'build' sections and remove the 'dev' sections. 11 | return env === 'build' ? content : ''; 12 | }) 13 | // Remove any remaining blank lines. 14 | .replace(/\n{2,}/g, "\n") 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /inc/bootstrap: -------------------------------------------------------------------------------- 1 | The contents of `inc` were moved to `app/inc` including this submodule. 2 | Run `git submodule update --init` and ignore this directory. 3 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "amdefine": { 6 | "version": "1.0.1", 7 | "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", 8 | "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", 9 | "dev": true 10 | }, 11 | "clean-css": { 12 | "version": "4.2.1", 13 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", 14 | "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", 15 | "dev": true, 16 | "requires": { 17 | "source-map": "~0.6.0" 18 | } 19 | }, 20 | "colors": { 21 | "version": "1.3.3", 22 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", 23 | "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", 24 | "optional": true 25 | }, 26 | "commander": { 27 | "version": "2.8.1", 28 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", 29 | "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", 30 | "dev": true, 31 | "requires": { 32 | "graceful-readlink": ">= 1.0.0" 33 | } 34 | }, 35 | "copy-anything": { 36 | "version": "2.0.6", 37 | "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", 38 | "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", 39 | "dev": true, 40 | "requires": { 41 | "is-what": "^3.14.1" 42 | } 43 | }, 44 | "debug": { 45 | "version": "3.2.7", 46 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 47 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 48 | "dev": true, 49 | "optional": true, 50 | "requires": { 51 | "ms": "^2.1.1" 52 | } 53 | }, 54 | "errno": { 55 | "version": "0.1.8", 56 | "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", 57 | "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", 58 | "dev": true, 59 | "optional": true, 60 | "requires": { 61 | "prr": "~1.0.1" 62 | } 63 | }, 64 | "graceful-fs": { 65 | "version": "4.2.11", 66 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 67 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 68 | "dev": true, 69 | "optional": true 70 | }, 71 | "graceful-readlink": { 72 | "version": "1.0.1", 73 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 74 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", 75 | "dev": true 76 | }, 77 | "iconv-lite": { 78 | "version": "0.6.3", 79 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 80 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 81 | "dev": true, 82 | "optional": true, 83 | "requires": { 84 | "safer-buffer": ">= 2.1.2 < 3.0.0" 85 | } 86 | }, 87 | "image-size": { 88 | "version": "0.5.5", 89 | "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", 90 | "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", 91 | "dev": true, 92 | "optional": true 93 | }, 94 | "is-what": { 95 | "version": "3.14.1", 96 | "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", 97 | "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", 98 | "dev": true 99 | }, 100 | "less": { 101 | "version": "4.1.3", 102 | "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", 103 | "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", 104 | "dev": true, 105 | "requires": { 106 | "copy-anything": "^2.0.1", 107 | "errno": "^0.1.1", 108 | "graceful-fs": "^4.1.2", 109 | "image-size": "~0.5.0", 110 | "make-dir": "^2.1.0", 111 | "mime": "^1.4.1", 112 | "needle": "^3.1.0", 113 | "parse-node-version": "^1.0.1", 114 | "source-map": "~0.6.0", 115 | "tslib": "^2.3.0" 116 | } 117 | }, 118 | "less-plugin-clean-css": { 119 | "version": "1.5.1", 120 | "resolved": "https://registry.npmjs.org/less-plugin-clean-css/-/less-plugin-clean-css-1.5.1.tgz", 121 | "integrity": "sha1-zFeveqM5iVflbezr5jy2DCNClwM=", 122 | "dev": true, 123 | "requires": { 124 | "clean-css": "^3.0.1" 125 | }, 126 | "dependencies": { 127 | "clean-css": { 128 | "version": "3.4.28", 129 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", 130 | "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", 131 | "dev": true, 132 | "requires": { 133 | "commander": "2.8.x", 134 | "source-map": "0.4.x" 135 | } 136 | }, 137 | "source-map": { 138 | "version": "0.4.4", 139 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", 140 | "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", 141 | "dev": true, 142 | "requires": { 143 | "amdefine": ">=0.0.4" 144 | } 145 | } 146 | } 147 | }, 148 | "make-dir": { 149 | "version": "2.1.0", 150 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", 151 | "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", 152 | "dev": true, 153 | "optional": true, 154 | "requires": { 155 | "pify": "^4.0.1", 156 | "semver": "^5.6.0" 157 | } 158 | }, 159 | "mime": { 160 | "version": "1.6.0", 161 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 162 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 163 | "optional": true 164 | }, 165 | "minimist": { 166 | "version": "0.0.10", 167 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", 168 | "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", 169 | "optional": true 170 | }, 171 | "ms": { 172 | "version": "2.1.3", 173 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 174 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 175 | "dev": true, 176 | "optional": true 177 | }, 178 | "needle": { 179 | "version": "3.2.0", 180 | "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", 181 | "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", 182 | "dev": true, 183 | "optional": true, 184 | "requires": { 185 | "debug": "^3.2.6", 186 | "iconv-lite": "^0.6.3", 187 | "sax": "^1.2.4" 188 | } 189 | }, 190 | "node-static": { 191 | "version": "0.7.11", 192 | "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", 193 | "integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==", 194 | "optional": true, 195 | "requires": { 196 | "colors": ">=0.6.0", 197 | "mime": "^1.2.9", 198 | "optimist": ">=0.3.4" 199 | } 200 | }, 201 | "optimist": { 202 | "version": "0.6.1", 203 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", 204 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", 205 | "optional": true, 206 | "requires": { 207 | "minimist": "~0.0.1", 208 | "wordwrap": "~0.0.2" 209 | } 210 | }, 211 | "parse-node-version": { 212 | "version": "1.0.1", 213 | "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", 214 | "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", 215 | "dev": true 216 | }, 217 | "pify": { 218 | "version": "4.0.1", 219 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 220 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", 221 | "dev": true, 222 | "optional": true 223 | }, 224 | "prr": { 225 | "version": "1.0.1", 226 | "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", 227 | "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", 228 | "dev": true, 229 | "optional": true 230 | }, 231 | "requirejs": { 232 | "version": "2.3.6", 233 | "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", 234 | "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", 235 | "dev": true 236 | }, 237 | "safer-buffer": { 238 | "version": "2.1.2", 239 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 240 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 241 | "dev": true, 242 | "optional": true 243 | }, 244 | "sax": { 245 | "version": "1.2.4", 246 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 247 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", 248 | "dev": true, 249 | "optional": true 250 | }, 251 | "semver": { 252 | "version": "5.7.1", 253 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 254 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 255 | "dev": true, 256 | "optional": true 257 | }, 258 | "source-map": { 259 | "version": "0.6.1", 260 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 261 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 262 | "dev": true 263 | }, 264 | "tslib": { 265 | "version": "2.6.0", 266 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", 267 | "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", 268 | "dev": true 269 | }, 270 | "wordwrap": { 271 | "version": "0.0.3", 272 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", 273 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", 274 | "optional": true 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "Define repository to silence npm warning.", 3 | "repository": { 4 | "type": "git", 5 | "url": "http://github.com/metacpan/metacpan-explorer" 6 | }, 7 | "devDependencies": { 8 | "clean-css": ">=2.2", 9 | "less": ">=4.1.3", 10 | "less-plugin-clean-css": ">=1.5.1", 11 | "requirejs": ">=2.1.15" 12 | }, 13 | "optionalDependencies": { 14 | "node-static": "x" 15 | }, 16 | "scripts": { 17 | "js": "r.js -o app.build.js", 18 | "css": "lessc --clean-css app/styles/main.less > build/styles.css", 19 | "html": "node env-filter.js < app/index.html > build/index.html", 20 | "build": "npm run js && npm run css && npm run html" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // NOTE: You should get the github token from the private conf repo 4 | // and export it as GITHUB_TOKEN before running this server. 5 | 6 | var fs = require('fs'); 7 | var Static = require('node-static'); 8 | 9 | var startServer = function(dir, port) { 10 | console.log("Serving", dir, 'on port', port); 11 | 12 | var httpd = new Static.Server(dir, { 13 | headers: { 14 | "Cache-Control": "no-cache, must-revalidate" 15 | } 16 | }); 17 | 18 | require('http').createServer(function (request, response) { 19 | request.addListener('end', function () { 20 | 21 | if (request.url === '/github.js') { 22 | response.writeHead(200, {'Content-Type': 'text/javascript'}); 23 | response.end('function github_token () { return ' + JSON.stringify(process.env.GITHUB_TOKEN || 'token') + '; }'); 24 | } 25 | else { 26 | httpd.serve(request, response); 27 | } 28 | 29 | }).resume(); 30 | }).listen(port); 31 | }; 32 | 33 | var port = process.env.PORT || 8080; 34 | 35 | // 0: node, 1: server.js, 2: first arg 36 | var dir = process.argv[2] || 'app'; 37 | 38 | fs.stat(dir, function(err, stats){ 39 | if( err || !stats.isDirectory() ){ 40 | throw(dir + " is not a directory"); 41 | } 42 | startServer(dir, port); 43 | }); 44 | --------------------------------------------------------------------------------