├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASING.md ├── bin ├── calcdeps.js ├── configure.js ├── distribute.sh ├── gendocs.sh ├── name.js ├── release.sh └── version.sh ├── bower.json ├── doc ├── api.md ├── documentation │ ├── caching.md │ ├── events.md │ ├── features │ │ └── multipart.md │ ├── prefetching.md │ ├── resources.md │ ├── responses.md │ ├── start.md │ └── versioning.md ├── download.md └── index.md ├── package.json ├── src ├── api.js ├── client │ ├── array │ │ ├── array.js │ │ └── array_test.js │ ├── async │ │ └── async.js │ ├── base.js │ ├── base_test.js │ ├── bootloader.js │ ├── cache │ │ ├── cache.js │ │ └── cache_test.js │ ├── config.js │ ├── config_test.js │ ├── debug │ │ └── debug.js │ ├── dom │ │ ├── classlist.js │ │ ├── dataset.js │ │ ├── dom.js │ │ └── dom_test.js │ ├── history │ │ ├── history.js │ │ └── history_test.js │ ├── main.js │ ├── nav │ │ ├── nav.js │ │ ├── nav_test.js │ │ ├── request.js │ │ ├── request_test.js │ │ ├── response.js │ │ └── response_test.js │ ├── net │ │ ├── connect.js │ │ ├── connect_test.js │ │ ├── resource.js │ │ ├── resource_test.js │ │ ├── script.js │ │ ├── script_test.js │ │ ├── style.js │ │ ├── style_test.js │ │ └── xhr.js │ ├── pubsub │ │ ├── pubsub.js │ │ └── pubsub_test.js │ ├── state.js │ ├── string │ │ ├── string.js │ │ └── string_test.js │ ├── stub.js │ ├── tasks │ │ ├── tasks.js │ │ └── tasks_test.js │ ├── testing │ │ ├── dom.js │ │ └── runner.html │ ├── tracing │ │ └── tracing.js │ └── url │ │ ├── url.js │ │ └── url_test.js ├── license.js ├── server │ ├── demo │ │ ├── app.py │ │ ├── static │ │ │ ├── app-chunked.js │ │ │ ├── app-demo.css │ │ │ ├── app-demo.js │ │ │ ├── app.css │ │ │ └── app.js │ │ └── templates │ │ │ ├── base.tmpl │ │ │ ├── chunked.tmpl │ │ │ ├── demo.tmpl │ │ │ ├── index.tmpl │ │ │ ├── index_ajax.tmpl │ │ │ ├── missing.tmpl │ │ │ ├── other.tmpl │ │ │ ├── spec.tmpl │ │ │ └── truncated.tmpl │ └── python │ │ └── spf.py └── wrapper.js ├── third-party ├── phantomjs │ ├── LICENSE.BSD │ └── examples │ │ ├── run-jasmine.js │ │ └── run-jasmine2.js └── tracing-framework │ ├── LICENSE │ └── shims │ └── wtf-trace-closure.js └── web ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── api ├── class.mustache ├── file.mustache ├── function.mustache ├── index.mustache └── overview.mustache ├── assets ├── images │ ├── animation-dynamic-340x178.gif │ ├── animation-static-340x178.gif │ ├── banner-728x388.jpg │ ├── bg-1600x585.jpg │ ├── bg-990x320.jpg │ ├── hamburger-black.svg │ ├── hamburger-white.svg │ ├── hex-73x84.gif │ ├── logo-black-48x48.png │ ├── logo-white-150x150.png │ ├── logo-white-280x280.png │ └── logo-white-48x48.png ├── scripts │ └── main.js └── styles │ └── main.css ├── config.yml ├── data └── sitenav.yml ├── includes ├── analytics.liquid ├── apitoc.md ├── meta.liquid ├── nav.liquid ├── nextprev.liquid ├── scripts.liquid ├── styles.liquid └── toc.liquid ├── layouts ├── api.liquid ├── base.liquid ├── default.liquid ├── documentation.liquid ├── download.liquid └── home.liquid └── plugins ├── mdlinks.rb ├── pagetoc.rb ├── sitenav.rb ├── spfjson.rb └── staticmd5.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | # Consistent coding style across different editors. 3 | 4 | # Top-most file 5 | root = true 6 | 7 | # Global styles: 8 | # - indent 2 spaces 9 | # - add final new line 10 | # - trim trailing whitespace 11 | [*] 12 | charset = utf-8 13 | end_of_line = lf 14 | indent_size = 2 15 | indent_style = space 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | 19 | # Markdown and Mustache (when rendering Markdown): 20 | # - indents must be 4 spaces 21 | # - trailing whitespace is significant 22 | [*.{md,mustache}] 23 | indent_size = 4 24 | trim_trailing_whitespace = false 25 | 26 | # Makefile: 27 | # - indents must be 1 tab 28 | [Makefile] 29 | indent_size = tab 30 | indent_style = tab 31 | 32 | # Python 33 | # - indents must be 4 spaces 34 | [*.py] 35 | indent_size = 4 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution files 2 | /dist 3 | 4 | # Build output and files 5 | /build 6 | /build.ninja 7 | /npm-debug.log 8 | /web/_site 9 | /web/_source 10 | 11 | # External libraries 12 | /bower_components 13 | /node_modules 14 | /vendor 15 | /web/assets/vendor 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Build output and files 2 | /build 3 | /build.ninja 4 | /npm-debug.log 5 | /web/_site 6 | /web/_source 7 | 8 | # External libraries 9 | /bower_components 10 | /node_modules 11 | /vendor 12 | /web/assets/vendor 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | - 0.10 3 | install: 4 | - npm install 5 | before_script: 6 | - npm run build 7 | - npm run lint 8 | script: 9 | - npm test 10 | sudo: false 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | SPF is an open source project. It is licensed using the 4 | [MIT License][]. We appreciate pull requests; here are our 5 | guidelines: 6 | 7 | 1. [File a bug][] (if there isn't one already). If your patch 8 | is going to be large it might be a good idea to get the 9 | discussion started early. We are happy to discuss it in a 10 | new issue beforehand, and you can always email 11 | about future work. 12 | 13 | 2. Due to legal reasons, all contributors must sign a 14 | contributor license agreement, either for an [individual][] 15 | or [corporation][], before a patch can be accepted. 16 | 17 | 3. Please use [Google JavaScript Style][]. 18 | 19 | 4. We ask that you squash all the commits together before 20 | pushing and that your commit message references the bug. 21 | 22 | 23 | 24 | [MIT License]: http://opensource.org/licenses/MIT 25 | [File a bug]: https://github.com/youtube/spfjs/issues 26 | [individual]: https://cla.developers.google.com/about/google-individual 27 | [corporation]: https://cla.developers.google.com/about/google-corporate 28 | [Google JavaScript Style]: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2012-2017 Google Inc. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | MAKEFLAGS = -j 1 7 | NPM = $(shell command -v npm || echo _missing_npm_) 8 | 9 | 10 | # Always execute targets. 11 | .PHONY: default all tests demo lint fix clean reset 12 | .PHONY: spf spf-debug spf-trace 13 | .PHONY: boot boot-debug boot-trace 14 | .PHONY: deprecated 15 | 16 | # Require npm and show deprecation warning 17 | default all tests demo lint fix dist: deprecated $(NPM) 18 | spf spf-debug spf-trace: deprecated $(NPM) 19 | boot boot-debug boot-trace: deprecated $(NPM) 20 | clean reset: deprecated $(NPM) 21 | 22 | # Deprecation warning. 23 | deprecated: 24 | @echo "Warning: make is deprecated; npm is now required." 25 | @echo 26 | 27 | # Pass off builds to npm. 28 | default: 29 | @echo "Running the following npm command:" 30 | @echo " npm run build" 31 | @echo "Please switch to calling npm directly." 32 | @echo 33 | @$(NPM) install && $(NPM) run build 34 | all: 35 | @echo "Running the following npm command:" 36 | @echo " npm run build-all" 37 | @echo "Please switch to calling npm directly." 38 | @echo 39 | @$(NPM) install && $(NPM) run build-all 40 | spf spf-debug spf-trace: 41 | @echo "Running the following npm command:" 42 | @echo " npm run build-spf" 43 | @echo "Please switch to calling npm directly." 44 | @echo 45 | @$(NPM) install && $(NPM) run build-spf 46 | boot boot-debug boot-trace: 47 | @echo "Running the following npm command:" 48 | @echo " npm run build-boot" 49 | @echo "Please switch to calling npm directly." 50 | @echo 51 | @$(NPM) install && $(NPM) run build-boot 52 | tests: 53 | @echo "Running the following npm command:" 54 | @echo " npm test" 55 | @echo "Please switch to calling npm directly." 56 | @echo 57 | @$(NPM) install && $(NPM) test 58 | demo: 59 | @echo "Running the following npm command:" 60 | @echo " npm start" 61 | @echo "Please switch to calling npm directly." 62 | @echo 63 | @$(NPM) install && $(NPM) start 64 | lint: 65 | @echo "Running the following npm command:" 66 | @echo " npm run lint" 67 | @echo "Please switch to calling npm directly." 68 | @echo 69 | @$(NPM) install && $(NPM) run lint 70 | fix: 71 | @echo "Running the following npm command:" 72 | @echo " npm run fix" 73 | @echo "Please switch to calling npm directly." 74 | @echo 75 | @$(NPM) install && $(NPM) run fix 76 | dist: 77 | @echo "Running the following npm command:" 78 | @echo " npm run dist" 79 | @echo "Please switch to calling npm directly." 80 | @echo 81 | @$(NPM) install && $(NPM) run dist 82 | 83 | # Remove build output and files 84 | clean: 85 | @echo "Running the following npm command:" 86 | @echo " npm run clean" 87 | @echo "Please switch to calling npm directly." 88 | @echo 89 | @$(NPM) install && $(NPM) run clean 90 | # Get back to a newly-cloned state. 91 | reset: clean 92 | @echo "Running the following npm command:" 93 | @echo " npm run reset" 94 | @echo "Please switch to calling npm directly." 95 | @echo 96 | @$(NPM) install && $(NPM) run reset 97 | 98 | # npm is required. 99 | _missing_npm_: 100 | @echo "ERROR: Unable to find npm." 101 | @echo "Please install npm and try again." 102 | @exit 1 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![SPF][]](https://youtube.github.io/spfjs/) 2 | 3 | [![Version][]](https://badge.fury.io/js/spf) 4 | [![Status][]](https://travis-ci.org/youtube/spfjs) 5 | [![InlineDocs][]](https://inch-ci.org/github/youtube/spfjs) 6 | 7 | 8 | Structured Page Fragments — or SPF for short — is a lightweight 9 | JS framework for fast navigation and page updates from YouTube. 10 | 11 | Using progressive enhancement and HTML5, SPF integrates with 12 | your site to enable a faster, more fluid user experience by 13 | updating just the sections of the page that change during 14 | navigation, not the whole page. SPF provides a response format 15 | for sending document fragments, a robust system for script and 16 | style management, an in-memory cache, on-the-fly processing, and 17 | more. 18 | 19 | **Learn more at [youtube.github.io/spfjs][]** 20 | 21 | 22 | ## Overview 23 | 24 | SPF allows you to leverage the benefits of a static initial page 25 | load, while gaining the performance and user experience benefits 26 | of dynamic page loads: 27 | 28 | **User Experience** 29 | 1. Get the fastest possible initial page load. 30 | 2. Keep a responsive persistent interface during navigation. 31 | 32 | **Performance** 33 | 1. Leverage existing techniques for static rendering. 34 | 2. Load small responses and fewer resources each navigation. 35 | 36 | **Development** 37 | 1. Use any server-side language and template system. 38 | 2. Be productive by using the same code for static and dynamic 39 | rendering. 40 | 41 | 42 | ## Download 43 | 44 | Install with [npm][]: 45 | 46 | ```sh 47 | npm install spf 48 | ``` 49 | 50 | Install with [Bower][]: 51 | 52 | ```sh 53 | bower install spf 54 | ``` 55 | 56 | Or, see the [download page][] for options to download the latest 57 | release and link to minified JS from a CDN: 58 | 59 | > [Download SPF](https://youtube.github.io/spfjs/download/) 60 | 61 | 62 | ## Get Started 63 | 64 | The SPF client library is a single ~10K [UMD][] JS file with no 65 | dependencies. It may be asynchronously delay-loaded. All 66 | functions are exposed via the global `spf` object. 67 | 68 | **Enable SPF** 69 | 70 | To add SPF to your site, include the JS file and run `spf.init()`. 71 | 72 | ```html 73 | 76 | ``` 77 | 78 | **Send requests** 79 | 80 | SPF does not change your site's navigation automatically and 81 | instead uses progressive enhancement to enable dynamic 82 | navigation for certain links. Just add a `spf-link` class to an 83 | `` tag to activate SPF. 84 | 85 | Go from static navigation: 86 | 87 | ```html 88 | Go! 89 | ``` 90 | 91 | to dynamic navigation: 92 | 93 | ```html 94 | 95 | Go! 96 | ``` 97 | 98 | **Return responses** 99 | 100 | In static navigation, an entire HTML page is sent. In dynamic 101 | navigation, only fragments are sent, using JSON as transport. 102 | When SPF sends a request to the server, it appends a 103 | configurable identifier to the URL so that your server can 104 | properly handle the request. (By default, this will be 105 | `?spf=navigate`.) 106 | 107 | In the following example, a common layout of upper masthead, 108 | middle content, and lower footer is used. In dynamic 109 | navigation, only the fragment for the middle content is sent, 110 | since the masthead and footer don't change. 111 | 112 | Go from static navigation: 113 | 114 | `GET /destination` 115 | 116 | ```html 117 | 118 | 119 | 120 | 121 | 122 |
...
123 |
124 | 125 |
126 | 127 | 128 | 129 | 130 | ``` 131 | 132 | to dynamic navigation: 133 | 134 | `GET /destination?spf=navigate` 135 | 136 | ```json 137 | { 138 | "head": "", 139 | "body": { 140 | "content": 141 | "", 142 | }, 143 | "foot": "" 144 | } 145 | ``` 146 | 147 | See the [documentation][] for complete information. 148 | 149 | 150 | ## Browser Support 151 | 152 | To use dynamic navigation, SPF requires the HTML5 History API. 153 | This is broadly supported by all current browsers, including 154 | Chrome 5+, Firefox 4+, and IE 10+. See a full browser 155 | compatibility table at [Can I Use][]. Underlying functionality, 156 | such as AJAX-style page updates and script/style loading, is 157 | more broadly supported by IE 8+. 158 | 159 | 160 | ## Get Help 161 | 162 | Send feedback, comments, or questions about SPF to 163 | . 164 | 165 | File bugs or feature requests at [GitHub][]. 166 | 167 | Join our [mailing list][] and follow [@spfjs][] on Twitter for 168 | updates. 169 | 170 | 171 | ## License 172 | 173 | MIT 174 | Copyright 2012-2017 Google, Inc. 175 | 176 | 177 | 178 | [youtube.github.io/spfjs]: https://youtube.github.io/spfjs/ 179 | [npm]: https://www.npmjs.com/ 180 | [Bower]: http://bower.io/ 181 | [download page]: https://youtube.github.io/spfjs/download/ 182 | [UMD]: https://github.com/umdjs/umd 183 | [documentation]: https://youtube.github.io/spfjs/documentation/ 184 | [Can I Use]: http://caniuse.com/#feat=history 185 | [GitHub]: https://github.com/youtube/spfjs/issues 186 | [mailing list]: https://groups.google.com/group/spfjs 187 | [@spfjs]: https://twitter.com/spfjs 188 | 189 | [SPF]: https://youtube.github.io/spfjs/assets/images/banner-728x388.jpg 190 | [Version]: https://badge.fury.io/js/spf.svg 191 | [Status]: https://secure.travis-ci.org/youtube/spfjs.svg?branch=master 192 | [InlineDocs]: https://inch-ci.org/github/youtube/spfjs.svg?branch=master 193 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | When a new version of SPF needs to be released, follow these 4 | steps. You will need admin privileges for the [youtube/spfjs][] 5 | repo. This document will use "vX.Y.Z" to refer to the new 6 | version, where "vX.Y.Z" is a valid [semantic version][]. 7 | 8 | 9 | ## Prepare the new version 10 | 11 | 1. Ensure all code has been tested. Verify changes from the 12 | previous version manually and make sure all unit tests pass. 13 | 14 | 2. Run `bin/version.sh |major|minor|patch` 15 | either replacing `` with a valid 16 | [semantic version][] or specifying one of `major`, `minor`, 17 | or `patch` to increment the version accordingly. This: 18 | 19 | - switches to a new branch 20 | - updates the `version` property in the `package.json` file 21 | - commits the change, titling it "Mark vX.Y.Z for release" 22 | - updates the documenation using `bin/gendocs.sh`. This: 23 | - updates the `release` and `version` properties in the 24 | `web/_config.yml` file to match the output of 25 | `bin/name.js` and `bin/name.js --version` 26 | - updates the `doc/api.md` and `doc/download.md` files, 27 | the sources for the [API][] and [Download][] pages 28 | - commits the change, titling it "Update documentatation for 29 | vX.Y.Z" 30 | 31 | 3. Send a pull request with the two commits. 32 | 33 | 34 | ## Release the new version 35 | 36 | 1. Merge the pull request containing the two commits. 37 | 38 | 2. In a clone of the repo (not of a fork), run `git log` to 39 | locate the hash of the merge commit. 40 | 41 | 3. Run `bin/release.sh `, replacing 42 | `` with the hash of the merge commit. This: 43 | 44 | - switches to a temporary working branch 45 | - builds the release files under the `dist/` folder 46 | - commits the change, titling and tagging it as "vX.Y.Z" 47 | - pushes the tag to the GitHub repo, which allows the built 48 | release output to be accessible via the tagged commit but 49 | not the master branch 50 | - returns to the original branch 51 | 52 | 53 | ## Distribute the new version 54 | 55 | 1. Run `bin/distribute.sh `. This: 56 | 57 | - switches to a temporary working branch 58 | - creates a distribution ZIP archive of the built release 59 | files at `build/spfjs-X.Y.Z-dist.zip` 60 | - pushes the updated source and release files for the npm 61 | package 62 | - returns to the original branch 63 | 64 | 2. If you have ownership of the npm [spfjs][] package, the 65 | `bin/distribute.sh` script will automatically push the 66 | update in the previous step. If not, request a push by 67 | emailing one of the owners listed by running `npm owner ls` 68 | with the subject "npm Update Request: SPF vX.Y.Z". 69 | 70 | 3. Request an update to the [Google Hosted Libraries][] CDN. 71 | Email with the subject 72 | "Hosted Libraries CDN Update Request: SPF vX.Y.Z". 73 | 74 | 75 | ## Document the new version 76 | 77 | 1. Go to the [GitHub Tags][] page and click "Add release notes" 78 | next to the new version. 79 | 80 | 2. Title the release "SPF XY (X.Y.Z)". You can run 81 | `bin/name.js` to generate a title to copy/paste. 82 | 83 | 3. Write the release notes, highlighting new features or 84 | fixed bugs. Auto-link to issues using the `#NUM` syntax. 85 | 86 | 4. Attach the distribution ZIP archive `spfjs-X.Y.Z-dist.zip`. 87 | 88 | 5. Publish the release. 89 | 90 | 6. Push the website. 91 | 92 | 93 | ## Announce the new version 94 | 95 | 1. Post from the [@spfjs][] Twitter account announcing the 96 | new version and linking to the GitHub release page and the 97 | [Download][] page. 98 | 99 | 2. Send an email to announcing the 100 | new version, summarizing the release notes, and linking to 101 | the GitHub release page and the [Download][] page. 102 | 103 | 104 | 105 | [semantic version]: http://semver.org/ 106 | [youtube/spfjs]: https://github.com/youtube/spfjs 107 | [spfjs]: https://www.npmjs.com/package/spf 108 | [Google Hosted Libraries]: https://developers.google.com/speed/libraries/devguide#spf 109 | [GitHub Tags]: https://github.com/youtube/spfjs/tags 110 | [API]: https://youtube.github.io/spfjs/api/ 111 | [Download]: https://youtube.github.io/spfjs/download/ 112 | [@spfjs]: https://twitter.com/spfjs 113 | -------------------------------------------------------------------------------- /bin/distribute.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2014 Google Inc. All rights reserved. 4 | # 5 | # Use of this source code is governed by The MIT License. 6 | # See the LICENSE file for details. 7 | 8 | # Script to distribute a released version of SPF. 9 | # 10 | # Author: nicksay@google.com (Alex Nicksay) 11 | 12 | # The script must be passed a git release tag to distribute. 13 | if [[ $# < 1 ]]; then 14 | echo "Usage: $(basename $0) tag_to_release" 15 | exit 1 16 | fi 17 | 18 | # Make sure node is properly installed. 19 | node=$(command -v node) 20 | npm=$(command -v npm) 21 | if [[ $node == "" || $npm == "" ]]; then 22 | echo "Both node and npm must be installed to distribute." 23 | exit 1 24 | fi 25 | 26 | # Validate the release tag. 27 | tag=$(git describe $1) 28 | if [[ $tag == "" ]]; then 29 | echo "A valid tag is needed for distribution." 30 | exit 1 31 | fi 32 | 33 | # From here on out, exit immediately on any error. 34 | set -o errexit 35 | 36 | # Save the current branch. 37 | branch=$(git symbolic-ref --short HEAD) 38 | 39 | # Check out the tag. 40 | git checkout -q $tag 41 | 42 | # Validate the version. 43 | version=$(bin/name.js --version) 44 | if [[ $version == "" ]]; then 45 | echo "A valid version is needed for distribution." 46 | git checkout -q $branch 47 | exit 1 48 | fi 49 | if [[ $tag != "v$version" ]]; then 50 | echo "The release tag must match the distribution version." 51 | git checkout -q $branch 52 | exit 1 53 | fi 54 | 55 | # Confirm the tag. 56 | while true; do 57 | read -p "Distribute $tag? [y/n] " answer 58 | case $answer in 59 | [Yy]* ) 60 | break;; 61 | [Nn]* ) 62 | git checkout -q $branch; 63 | exit;; 64 | esac 65 | done 66 | 67 | # Create a temp branch, just in case. 68 | git checkout -b distribute-$tag 69 | 70 | # Build a distribution archive for upload to GitHub and CDNs. 71 | echo "Building distribution archive..." 72 | mkdir -p build/spfjs-$version-dist/ 73 | cp dist/* build/spfjs-$version-dist/ 74 | cd build 75 | zip spfjs-$version-dist.zip spfjs-$version-dist/* 76 | cd .. 77 | echo "The archive contents are:" 78 | unzip -l build/spfjs-$version-dist.zip 79 | echo "The distribution archive has been created at:" 80 | echo " build/spfjs-$version-dist.zip" 81 | 82 | # Confirm publishing. 83 | while true; do 84 | echo 85 | echo "WARNING: You cannot undo this next step!" 86 | echo "Once $tag is published to npm, it cannot be changed." 87 | echo 88 | read -p "Publish $tag to npm? [y/n] " answer 89 | case $answer in 90 | [Yy]* ) 91 | break;; 92 | [Nn]* ) 93 | git checkout -q $branch; 94 | exit;; 95 | esac 96 | done 97 | 98 | # Publish to npm. 99 | npm_user=$(npm whoami 2> /dev/null) 100 | npm_publish="false" 101 | if [[ $npm_user == "" ]]; then 102 | echo 'Skipping "npm publish" because npm credentials were not found.' 103 | echo "To get credentials on this machine, run the following:" 104 | echo " npm login" 105 | else 106 | npm_owner=$(npm owner ls | grep "$npm_user") 107 | if [[ $npm_owner == "" ]]; then 108 | echo 'Skipping "npm publish" because npm ownership was not found.' 109 | echo "The current list of npm owners is:" 110 | npm owner ls | sed 's/^/ /' 111 | echo "To get ownership, have an existing owner run the following:" 112 | echo " npm owner add $npm_user" 113 | else 114 | npm_publish="true" 115 | fi 116 | fi 117 | if [[ $npm_publish == "false" ]]; then 118 | echo "To publish this release to npm later, run the following:" 119 | echo " git checkout v$version" 120 | echo " npm publish" 121 | else 122 | npm publish 123 | echo "Published to npm." 124 | fi 125 | 126 | # Return to the original branch. 127 | git checkout $branch 128 | git branch -D distribute-$tag 129 | echo "Distributed $tag." 130 | -------------------------------------------------------------------------------- /bin/gendocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2015 Google Inc. All rights reserved. 4 | # 5 | # Use of this source code is governed by The MIT License. 6 | # See the LICENSE file for details. 7 | 8 | # Script to update generated documentation for SPF. 9 | # 10 | # Used to modify derived docs (like the API) and update version numbers. 11 | # 12 | # Author: nicksay@google.com (Alex Nicksay) 13 | 14 | 15 | # Make sure node is properly installed. 16 | node=$(command -v node) 17 | npm=$(command -v npm) 18 | if [[ $node == "" || $npm == "" ]]; then 19 | echo "Both node and npm must be installed to release." 20 | exit 1 21 | fi 22 | npm_semver=$(npm list --parseable semver) 23 | if [[ $npm_semver == "" ]]; then 24 | echo 'The "semver" package is needed. Run "npm install" and try again.' 25 | exit 1 26 | fi 27 | npm_jsdox=$(npm list --parseable jsdox) 28 | if [[ $npm_jsdox == "" ]]; then 29 | echo 'The "jsdox" package is needed. Run "npm install" and try again.' 30 | exit 1 31 | fi 32 | 33 | 34 | # Get the current verison. 35 | version=$(bin/name.js --version) 36 | release=$(bin/name.js) 37 | echo "Updating documentation for version $version" 38 | 39 | 40 | # Validate the tag. 41 | object=$(git cat-file -t "v$version" 2> /dev/null) 42 | if [[ $object == "tag" ]]; then 43 | tag="v$version" 44 | elif [[ $1 == "--head" ]]; then 45 | tag="HEAD" 46 | else 47 | echo "A valid git tag wasn't found for v$version." 48 | echo "Pass the --head flag to update from the latest commit instead." 49 | exit 1 50 | fi 51 | 52 | 53 | # From here on out, exit immediately on any error. 54 | set -o errexit 55 | 56 | 57 | # Update the API doc. 58 | echo "Updating doc/api.md" 59 | tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'spfjs-gendocs') 60 | tmpfile="$tmpdir/api.js" 61 | script=$(cat < $tmpfile 70 | ./node_modules/jsdox/bin/jsdox $tmpfile \ 71 | --templateDir web/api \ 72 | --index web/includes/apitoc --index-sort none \ 73 | --output doc/ 74 | 75 | echo "Done" 76 | 77 | 78 | # Update the Download doc. 79 | echo "Updating doc/download.md" 80 | pattern='\d+\.\d+\.\d+' 81 | script=$(cat < doc/download.md 89 | 90 | 91 | # Update the website. 92 | echo "Updating web/config.yml" 93 | script=$(cat < web/config.yml 106 | -------------------------------------------------------------------------------- /bin/name.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright 2014 Google Inc. All rights reserved. 4 | // 5 | // Use of this source code is governed by The MIT License. 6 | // See the LICENSE file for details. 7 | 8 | 9 | /** 10 | * @fileoverview Prints the SPF name and version number to standard out. 11 | * 12 | * @author nicksay@google.com (Alex Nicksay) 13 | */ 14 | 15 | 16 | // Library imports. 17 | var $ = { 18 | fs: require('fs'), 19 | minimist: require('minimist'), 20 | path: require('path'), 21 | semver: require('semver'), 22 | util: require('util'), 23 | wordwrap: require('wordwrap') 24 | }; 25 | 26 | 27 | /** 28 | * Command line flags, with long options as keys and short options as values. 29 | * 30 | * @dict 31 | * @const 32 | */ 33 | var FLAGS = { 34 | help: 'h', 35 | name: 'n', 36 | path: 'p', 37 | version: 'v' 38 | }; 39 | 40 | 41 | /** 42 | * Descriptions of command line flags. 43 | * 44 | * @dict 45 | * @const 46 | */ 47 | var DESCRIPTIONS = { 48 | help: 'Show this help message and exit.', 49 | name: 'Print just the name.', 50 | path: 'The path to the "package.json" file to parse.', 51 | version: 'Print just the version number.' 52 | }; 53 | 54 | 55 | /** 56 | * Defaults for command line flags, if applicable. 57 | * 58 | * @dict 59 | * @const 60 | */ 61 | var DEFAULTS = { 62 | path: 'package.json' 63 | }; 64 | 65 | 66 | /** 67 | * Namespace for functions to handle the command line interface. 68 | */ 69 | var cli = {}; 70 | 71 | 72 | /** 73 | * Whether the script is being executed via command line. 74 | * 75 | * @type {boolean} 76 | */ 77 | cli.active = !module.parent; 78 | 79 | 80 | /** 81 | * Parses the command line arguments for flags and values. 82 | * @return {Object} 83 | */ 84 | cli.parse = function() { 85 | return $.minimist(process.argv.slice(2), { 86 | alias: FLAGS, 87 | default: DEFAULTS 88 | }); 89 | }; 90 | 91 | 92 | /** 93 | * Prints the help information for the command line interface. 94 | */ 95 | cli.help = function() { 96 | var program = $.path.basename(process.argv[1]); 97 | console.log($.util.format( 98 | 'Usage: %s [options]', program)); 99 | console.log(''); 100 | wrap = $.wordwrap(8, 78); 101 | console.log('Options:'); 102 | for (var flag in FLAGS) { 103 | console.log($.util.format('--%s, -%s', flag, FLAGS[flag])); 104 | console.log(wrap(DESCRIPTIONS[flag])); 105 | if (flag in DEFAULTS) { 106 | console.log(wrap('Default: ' + DEFAULTS[flag])); 107 | } 108 | } 109 | }; 110 | 111 | 112 | /** 113 | * The main execution function. 114 | */ 115 | function main(opts, args) { 116 | if (cli.active) { 117 | // If this is a command-line invocation, parse the args and opts. If the 118 | // help opt is given, print the help and exit. 119 | opts = cli.parse(); 120 | args = opts._; 121 | if (opts.help) { 122 | cli.help(); 123 | process.exit(); 124 | } 125 | } else { 126 | // Create defaults for options if not provided. 127 | opts = opts || {}; 128 | for (var d in DEFAULTS) { 129 | if (!(d in opts)) { 130 | opts[d] = DEFAULTS[d]; 131 | } 132 | } 133 | } 134 | 135 | // Parse the manifest file. 136 | var manifest = JSON.parse($.fs.readFileSync(opts.path, 'utf8')); 137 | 138 | // Extract and validate the version. 139 | var version = $.semver.valid(manifest.version) || ''; 140 | 141 | // Format the output. 142 | var output; 143 | if (opts.version) { 144 | output = version; 145 | } else { 146 | var num = version.split('.').slice(0, 2).join(''); 147 | var name = $.util.format((num ? '%s %s' : '%s'), 'SPF', num); 148 | if (opts.name) { 149 | output = name; 150 | } else { 151 | output = $.util.format((version ? '%s (v%s)' : '%s'), name, version); 152 | } 153 | } 154 | 155 | // Print the output to stdout, if needed (for the command-line). 156 | if (cli.active) { 157 | console.log(output); 158 | } 159 | 160 | // Return the output (for the module). 161 | return output; 162 | } 163 | 164 | 165 | // Provide a module function. 166 | module.exports = main; 167 | 168 | 169 | // Automatically execute if called directly. 170 | if (cli.active) { 171 | main(); 172 | } 173 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2014 Google Inc. All rights reserved. 4 | # 5 | # Use of this source code is governed by The MIT License. 6 | # See the LICENSE file for details. 7 | 8 | # Script to tag and release a version of SPF. 9 | # 10 | # Author: nicksay@google.com (Alex Nicksay) 11 | 12 | 13 | # The script must be passed a git commit to use as the release. 14 | if [[ $# < 1 ]]; then 15 | echo "Usage: $(basename $0) sha_of_commit_to_release" 16 | exit 1 17 | fi 18 | 19 | # Make sure node is properly installed. 20 | node=$(command -v node) 21 | npm=$(command -v npm) 22 | if [[ $node == "" || $npm == "" ]]; then 23 | echo "Both node and npm must be installed to release." 24 | exit 1 25 | fi 26 | npm_semver=$(npm list --parseable semver) 27 | if [[ $npm_semver == "" ]]; then 28 | echo 'The "semver" package is needed. Run "npm install" and try again.' 29 | exit 1 30 | fi 31 | 32 | # Validate the commit. 33 | commit=$(git rev-parse --quiet --verify $1) 34 | if [[ $commit == "" ]]; then 35 | echo "A valid commit is needed for the release." 36 | exit 1 37 | fi 38 | 39 | # From here on out, exit immediately on any error. 40 | set -o errexit 41 | 42 | # Save the current branch. 43 | branch=$(git symbolic-ref --short HEAD) 44 | 45 | # Check out the commit. 46 | git checkout -q $commit 47 | 48 | # Validate the version. 49 | version=$(bin/name.js --version) 50 | if [[ $version == "" ]]; then 51 | echo "A valid version is needed for the release." 52 | git checkout -q $branch 53 | exit 1 54 | fi 55 | 56 | # Confirm the release. 57 | while true; do 58 | read -p "Release commit $commit as v$version? [y/n] " answer 59 | case $answer in 60 | [Yy]* ) 61 | break;; 62 | [Nn]* ) 63 | git checkout -q $branch; 64 | exit;; 65 | esac 66 | done 67 | 68 | # Create a temp branch, just in case. 69 | git checkout -b release-$commit-$version 70 | 71 | # Build the release files. 72 | echo "Building release files..." 73 | npm run dist 74 | 75 | # Add the files to be released. 76 | git add -f dist/* 77 | 78 | # Sanity check the files in case anything unintended shows up. 79 | git status 80 | while true; do 81 | read -p "Do the files to commit look correct? [y/n] " answer 82 | case $answer in 83 | [Yy]* ) 84 | break;; 85 | [Nn]* ) 86 | git reset HEAD dist/* 87 | git checkout $branch 88 | git branch -D release-$commit-$version 89 | exit;; 90 | esac 91 | done 92 | 93 | tag="v$version" 94 | 95 | # Commit the release files. 96 | git commit -m "$tag" 97 | 98 | # Tag the commit as the release. 99 | git tag -a "$tag" -m "$tag" 100 | 101 | # Confirm release. 102 | while true; do 103 | echo 104 | echo "WARNING: You should not undo this next step!" 105 | echo "Once $tag is pushed, it should not be changed." 106 | echo 107 | read -p "Push $tag to github? [y/n] " answer 108 | case $answer in 109 | [Yy]* ) 110 | break;; 111 | [Nn]* ) 112 | git checkout -q $branch; 113 | exit;; 114 | esac 115 | done 116 | 117 | # Push the tag. 118 | git push --tags 119 | 120 | # Return to the original branch. 121 | git checkout $branch 122 | git branch -D release-$commit-$version 123 | echo "Released $tag." 124 | -------------------------------------------------------------------------------- /bin/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2016 Google Inc. All rights reserved. 4 | # 5 | # Use of this source code is governed by The MIT License. 6 | # See the LICENSE file for details. 7 | 8 | # Script to udpate the version of SPF and generate docs. 9 | # 10 | # Author: nicksay@google.com (Alex Nicksay) 11 | 12 | # The script must be passed a version. 13 | if [[ $# < 1 ]]; then 14 | echo "Usage: $(basename $0) [ | major | minor | patch ]" 15 | exit 1 16 | fi 17 | 18 | # Change to the root directory. 19 | cd "$(dirname $(dirname "$0"))" 20 | 21 | # Make sure node is properly installed. 22 | node=$(command -v node) 23 | npm=$(command -v npm) 24 | if [[ $node == "" || $npm == "" ]]; then 25 | echo "Both node and npm must be installed to update versions." 26 | exit 1 27 | fi 28 | npm_semver=$(npm list --parseable semver) 29 | if [[ $npm_semver == "" ]]; then 30 | echo 'The "semver" package is needed. Run "npm install" and try again.' 31 | exit 1 32 | fi 33 | 34 | # Validate the version. 35 | current=$(bin/name.js --version) 36 | if [[ $1 == "major" || $1 == "minor" || $1 == "patch" ]]; then 37 | version=$(`npm bin`/semver -i $1 $current) 38 | else 39 | version=$(`npm bin`/semver $1) 40 | fi 41 | if [[ $version == "" ]]; then 42 | echo "A valid version is needed." 43 | exit 1 44 | fi 45 | 46 | # Validate there are no pending changes. 47 | if [[ -n $(git status --porcelain) ]]; then 48 | echo "Please commit or revert current changes before proceeding." 49 | exit 1 50 | fi 51 | 52 | # From here on out, exit immediately on any error. 53 | set -o errexit 54 | 55 | # Save the current branch. 56 | branch=$(git symbolic-ref --short HEAD) 57 | 58 | # Create a version branch, just in case. 59 | git checkout -b version-$version 60 | 61 | # Update package.json 62 | echo "Updating package.json" 63 | npm --no-git-tag-version version $version 64 | echo "Updating src/license.js" 65 | cp src/license.js src/license.js.tmp 66 | cat src/license.js.tmp | sed "s/$current/$version/g" > src/license.js 67 | rm src/license.js.tmp 68 | echo "Commiting package.json and src/license.js changes..." 69 | git commit -a -m "Mark v$version for release" 70 | 71 | # Update documentatation 72 | bin/gendocs.sh --head 73 | echo "Commiting documentation changes..." 74 | git commit -a -m "Update documentatation for v$version" 75 | 76 | echo 77 | echo "Version has been updated to v$version" 78 | echo "Please send a pull request with this change." 79 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spf", 3 | "description": "A lightweight JS framework for fast navigation and page updates from YouTube", 4 | "main": "dist/spf.js", 5 | "moduleType": [ 6 | "amd", 7 | "globals", 8 | "node" 9 | ], 10 | "license": "MIT", 11 | "homepage": "https://github.com/youtube/spfjs", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/youtube/spfjs.git" 15 | }, 16 | "ignore": [ 17 | "**/.*", 18 | "bower_components", 19 | "node_modules" 20 | ], 21 | "devDependencies": { 22 | "closure-compiler": "http://dl.google.com/closure-compiler/compiler-20141215.zip", 23 | "jasmine-core": "jasmine#~2.3.4", 24 | "octicons": "~2.1.2", 25 | "spf": "2.1.1", 26 | "web-starter-kit": "~0.4.0", 27 | "webpy": "https://github.com/webpy/webpy/archive/73f1119649ffe54ba26ddaf6a612aaf1dab79b7f.zip" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /doc/documentation/caching.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Caching 3 | description: 4 | Custom response caching for greater application flexibility. 5 | --- 6 | 7 | 8 | When building a complex web application, sometimes you want more 9 | flexibility than standard HTTP based caching in order to best 10 | balance your application logic with performance. SPF provides 11 | a local configurable response cache that you can adjust as 12 | needed. 13 | 14 | 15 | ## When the cache is used 16 | 17 | Before making a request, SPF checks the local cache to see if a 18 | valid cached response is available for the requested URL. If 19 | one is found, then it will be used instead of sending a request 20 | over the network. If not, after the response is received, SPF 21 | will place that response in the local cache for future use. 22 | 23 | By default, SPF uses a caching model that matches the 24 | back-forward cache in browsers: "new" navigation will not use a 25 | previously cached response, whereas "history" navigation will. 26 | This means that your server will receive requests for every link 27 | click, for example, but not for back button uses. However, if 28 | the `cache-unified` config value is set to `true`, then SPF will 29 | use an available cache response for all navigations. This will 30 | mean that your server will not receive requests for link clicks 31 | that are to previously visited pages, just like for back button 32 | presses. 33 | 34 | When [prefetching][] responses, by default, SPF will place a 35 | prefetched response in the local cache as eligible for one "new" 36 | navigation. After that one use, the cached response will only 37 | be eligible for "history" navigation. However, if the 38 | `cache-unified` config value is set to `true`, then the 39 | prefetched response will be available for all navigations like 40 | other cached responses. 41 | 42 | 43 | ## Configuring the cache 44 | 45 | You may configure the cache using parameters to control the 46 | total number and lifetime of entries in the cache. Increasing 47 | these settings will increase the chance of finding a valid, 48 | cached response and also consume more memory in the browser. 49 | Conversely, decreasing these settings will decrease the chance 50 | of finding a valid cached response and also consume less memory 51 | in the browser. A list of the configuration parameters and 52 | their descriptions follows: 53 | 54 | **`cache-lifetime`** 55 | The maximum time in milliseconds a cache entry is considered 56 | valid. Defaults to `600000` (10 minutes). 57 | 58 | **`cache-max`** 59 | The maximum number of total cache entries. Defaults to `50`. 60 | 61 | **`cache-unified`** 62 | Whether to unify all cache responses instead of separating them 63 | by use (e.g. history, prefetching). Defaults to `false`. 64 | 65 | 66 | ## Automatic garbage collection 67 | 68 | The cache performs automatic garbage collection by removing 69 | entries at two times: 70 | 71 | 1. If a requested entry is found but it is expired according to 72 | the configured lifetime, the entry is removed from the cache 73 | instead of being used. 74 | 75 | 2. Each time a new entry is added, the 76 | cache does asynchronous garbage collection. This garbage 77 | collection first removes all expired entries, then if needed, it 78 | removes entries in the cache that exceed the configured maximum 79 | size with a least-recently-used (LRU) policy. 80 | 81 | 82 | ## Manually adjusting the cache 83 | 84 | Sometimes you might need to manually remove an entry from the 85 | cache. For example, a user might take an action on the page 86 | that changes it, and you would like future requests to reflect 87 | that action; by making the next request the server, the response 88 | will stay in sync. 89 | 90 | Each [spfdone][] event specifies a `cacheKey` attribute in the 91 | [response object][]. You can use this `cacheKey` to reference a 92 | specific cache entry when calling API functions that manipulate 93 | the cache. A list of API functions and their descriptions 94 | follow: 95 | 96 | **`spf.cache.remove(key)`** 97 | Removes an entry from the cache. Pass a `cacheKey` from a 98 | response object to reference the entry you wish to remove. See 99 | also the API reference for [spf.cache.remove][]. 100 | 101 | **`spf.cache.clear()`** 102 | Removes all entries from the cache. See also the API reference 103 | for [spf.cache.clear][]. 104 | 105 | 106 | 107 | [prefetching]: ./prefetching.md 108 | [spfdone]: ../events.md#event-descriptions 109 | [response object]: ../../api.md#spf.singleresponse 110 | [spf.cache.remove]: ../../api.md#spf.cache.remove 111 | [spf.cache.clear]: ../../api.md#spf.cache.clear 112 | -------------------------------------------------------------------------------- /doc/documentation/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Events 3 | description: Handle SPF events and the navigation life cycle. 4 | --- 5 | 6 | 7 | SPF is designed to give developers enough flexibility during 8 | navigation to both control application logic and provide UI 9 | updates for things like progress bars. 10 | 11 | 12 | ## Navigation Life Cycle 13 | 14 | A basic event flow diagram follows and a more detailed 15 | explanation is below: 16 | 17 | 18 | Navigation Life Cycle Event Dispatched 19 | + 20 | | 21 | | 22 | +---------------------->[ spfready ] 23 | | 24 | | 25 | +-------------------+ 26 | | | 27 | | | 28 | | +----------------------------+ 29 | | | | | 30 | | api|navigate dom|click history|popstate 31 | | | | | 32 | | | +------+-------+ 33 | | | | +--->[ spfclick ] 34 | | | +-----------+ 35 | | | | +->[ spfhistory ] 36 | | +-------------+------+ 37 | | | 38 | | | 39 | | +-------------------->[ spfrequest ] 40 | | | 41 | | send|request 42 | | | 43 | | +--------------+ 44 | | | | 45 | | | history|pushstate 46 | | | | 47 | | +--------------+ 48 | | | 49 | | | 50 | | receive|response 51 | | | 52 | | +-------------------->[ spfprocess ] 53 | | | 54 | | | 55 | | process|response 56 | | | 57 | | +----------------------->[ spfdone ] 58 | | | 59 | | | 60 | +-------------------+ 61 | | 62 | | 63 | v 64 | 65 | 66 | ## Event Descriptions 67 | 68 | All events in the [API][] are defined as [spf.Event][] objects. 69 | A list of the events and their descriptions follows: 70 | 71 | **`spfclick`** 72 | Fired when handling a click on a valid link (e.g. one with a 73 | valid `spf-link` class or parent element). Use as an early 74 | indication that navigation will happen or to provide element- 75 | level UI feedback. 76 | 77 | **`spfhistory`** 78 | Fired when handling a `popstate` history event, indicating the 79 | user has gone backward or forward; similar to `spfclick`. 80 | 81 | **`spfrequest`** 82 | Fired before a request for navigation is sent. Use to handle 83 | the beginning of navigation and provide global-level UI feedback 84 | (i.e. start a progress bar). This event is fired before a 85 | request is sent for all types of navigation: clicks, 86 | back/forward, and API calls. (Note that this event is fired 87 | even if a response is fetched from cache and no actual network 88 | request is made.) 89 | 90 | **`spfprocess`** 91 | Fired when a response has been received, either from the network 92 | or from cache, before it is processed. Use to update UI 93 | feedback (i.e. advance a progress bar) and dispose event 94 | listeners before content is updated. 95 | 96 | **`spfdone`** 97 | Fired after response processing is done. Use to finalize UI 98 | feedback (i.e. complete a progress bar) and initialize event 99 | listeners after content is updated. 100 | 101 | 102 | ## Callbacks and Cancellations 103 | 104 | If manually starting navigation with [spf.navigate][], then 105 | instead of handling events you may pass callbacks in an object 106 | that conforms to the [spf.RequestOptions][] interface. Almost 107 | all events and callbacks can be canceled by calling 108 | `preventDefault` or returning `false`, respectively. A list of 109 | the events, their corresponding callbacks, and their cancel 110 | action follows: 111 | 112 | | Event | Callback | State | Cancel | 113 | |:-------------|:------------|:------------------------------|:-------| 114 | | `spfclick` | | Link Clicked | Ignore | 115 | | `spfhistory` | | Back/Forward Clicked | Ignore | 116 | | `spfrequest` | `onRequest` | Started; Sending Request | Reload | 117 | | `spfprocess` | `onProcess` | Processing; Response Received | Reload | 118 | | `spfdone` | `onDone` | Done | | 119 | 120 | 121 | 122 | [API]: ../api.md 123 | [spf.Event]: ../api.md#spf.event 124 | [spf.navigate]: ../api.md#spf.navigate 125 | [spf.RequestOptions]: ../api.md#spf.requestoptions 126 | -------------------------------------------------------------------------------- /doc/documentation/features/multipart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multipart Responses 3 | --- 4 | 5 | 6 | > We will be adding documentation and code samples to explain and demonstrate 7 | > this functionality in coming weeks. In the meantime, please 8 | > [get started][start] with SPF. 9 | 10 | 11 | 12 | [start]: ../start.md 13 | -------------------------------------------------------------------------------- /doc/documentation/prefetching.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Prefetching 3 | description: Prefetch responses before they're requested. 4 | --- 5 | 6 | SPF is designed to enable fast navigation, and the best way to 7 | speed up a navigation request is to not make it all. 8 | Prefetching allows you to fetch responses before they are 9 | requested and store them in the [local cache][] until they are 10 | needed. You can also prefetch [scripts and styles][resources] 11 | to prime the browser cache. 12 | 13 | 14 | ## Prefetching requests 15 | 16 | Prefetching a response is done by calling the [spf.prefetch][] 17 | function, which behaves nearly identically to [spf.navigate][] 18 | and accepts the same [callbacks and cancellations][] when passed 19 | an object that conforms to the [spf.RequestOptions][] interface. 20 | A list of callbacks follows: 21 | 22 | | Callback | State | Cancel | 23 | |:------------|:------------------------------|:-------| 24 | | `onRequest` | Started; Sending Prefetch | Abort | 25 | | `onProcess` | Processing; Response Received | Abort | 26 | | `onDone` | Done | | 27 | 28 | When SPF sends the prefetch request to the server, it will 29 | append the configurable identifier to the URL in the same manner 30 | as navigation; by default, this value will be `?spf=prefetch`. 31 | 32 | When the prefetched response has been received, SPF will place 33 | it in the local cache as eligible for one "new" navigation. 34 | After that one use, the cached response will only be eligible 35 | for "history" navigation. However, if the `cache-unified` 36 | config value is set to `true`, then the prefetched response will 37 | be available for all navigations like other cached responses. 38 | For more information, see [when the cache is used][]. 39 | 40 | 41 | ## Prefetching resources 42 | 43 | When SPF processes a prefetched response, it will prefetch any 44 | [resources][] to ensure the browser cache is primed. Fetching 45 | JS and CSS files before they are needed makes future navigation 46 | faster. 47 | 48 | To manually prefetch resources, the following API functions can 49 | be used: 50 | 51 | **`spf.script.prefetch(urls)`** 52 | Prefetches one or more scripts; the scripts will be requested 53 | but not loaded. See also the API reference for 54 | [spf.script.prefetch][]. 55 | 56 | **`spf.style.prefetch(urls)`** 57 | Prefetches one or more stylesheets; the stylesheets will be 58 | requested but not loaded. See also the API reference for 59 | [spf.style.prefetch][]. 60 | 61 | 62 | 63 | [local cache]: ./caching.md 64 | [resources]: ./resources.md 65 | [spf.prefetch]: ../api.md#spf.prefetch 66 | [spf.navigate]: ../api.md#spf.navigate 67 | [callbacks and cancellations]: ./events.md#callbacks-and—cancellations 68 | [spf.RequestOptions]: ../api.md#spf.requestoptions 69 | [when the cache is used]: ./caching.md#when-the-cache-is-used 70 | [spf.script.prefetch]: ../api.md#spf.script.prefetch 71 | [spf.style.prefetch]: ../api.md#spf.style.prefetch 72 | -------------------------------------------------------------------------------- /doc/documentation/resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resources 3 | description: Manage script and style loading. 4 | --- 5 | 6 | 7 | As briefly mentioned in the [Responses overview][responses], 8 | when SPF processes a response, it will install JS and CSS from 9 | the `head` fragment, any of the `body` fragments, and the `foot` 10 | fragment. However, SPF has two methods of handling scripts and 11 | styles: unmanaged and managed. Both are detailed below. 12 | 13 | 14 | ## Unmanaged Resources 15 | 16 | SPF parses each fragment for scripts and styles, extracts them, 17 | and executes them by appending them to the `` of the 18 | document. For example, given the following response: 19 | 20 | ```json 21 | { 22 | "head": " 23 | 24 | 25 | ", 26 | "body": { 27 | "element-id-a": "Lorem ipsum dolor sit amet", 28 | "element-id-b": "consectetur adipisicing elit" 29 | }, 30 | "foot": " 31 | 32 | 33 | " 34 | } 35 | ``` 36 | 37 | Then, when SPF processes this response, it will do the following 38 | steps: 39 | 40 | 1. Append `` to the document 41 | `` to eval the CSS. 42 | 2. Append `` to the 43 | document `` to load the CSS file. 44 | 3. Update the element with DOM id `element-id-a` with the HTML 45 | `Lorem ipsum dolor sit amet`. 46 | 4. Update the element with DOM id `element-id-b` with the HTML 47 | `consectetur adipisicing elit`. 48 | 5. Append `` to the document 49 | `` to load the JS file **and wait for it to complete 50 | before continuing**. 51 | 6. Append `` to the document 52 | `` to eval the JS. 53 | 54 | > **Note:** SPF will wait for script loading or execution to 55 | > complete before processing. This matches browser behavior and 56 | > ensures proper script execution order. (See step 4 above.) 57 | > To not wait for script execution, add the `async` attribute: 58 | > 59 | > ```html 60 | > 61 | > ``` 62 | 63 | As you navigate to and from the page sending this response, 64 | these steps will be repeated each time. 65 | 66 | 67 | ## Managed Resources 68 | 69 | However, a significant benefit of SPF is that only sections of 70 | the page are updated with each navigation instead of the browser 71 | performing a full reload. That means — almost certainly — not 72 | every script and style needs to be executed or loaded during 73 | every navigation. 74 | 75 | Consider the following common pattern where two scripts are 76 | loaded per page: one containing common library code (e.g. 77 | jQuery) and a second containing page-specific code. For 78 | example, a search page and an item page: 79 | 80 | Search Page: 81 | 82 | ```html 83 | 84 | 85 | 86 | 87 | ``` 88 | 89 | Item Page: 90 | 91 | ```html 92 | 93 | 94 | 95 | 96 | ``` 97 | 98 | As a user navigates from the search page to the item page, the 99 | `common-library.js` file does not need to be loaded again, as 100 | it's already in the page. You can instruct SPF to manage this 101 | script by giving it a `name` attribute. Then, when it 102 | encounters the script again, it will not reload it: 103 | 104 | ```html 105 | 106 | ``` 107 | 108 | By applying this to all the scripts, a user can navigate back 109 | and forth between the two pages and only ever load a given file 110 | once: 111 | 112 | Search Page: 113 | 114 | ```html 115 | 116 | 117 | ``` 118 | 119 | Item Page: 120 | 121 | ```html 122 | 123 | 124 | ``` 125 | 126 | Now, given the following navigation flow: 127 | 128 | [ search ]--->[ item ]--->[ search ]--->[ item ] 129 | 130 | Then, when SPF processes the responses for each page in that 131 | flow, it will do the following steps: 132 | 133 | 1. **Navigate to the search page.** 134 | 2. Load the `common-library.js` JS file and wait for it to 135 | complete before continuing. 136 | 3. Load the `search-page.js` JS file and wait for it to 137 | complete before continuing. 138 | 4. **Navigate to the item page.** 139 | 5. _Skip reloading `common-library.js`._ 140 | 6. Load the `item-page.js` JS file and wait for it to complete 141 | before continuing. 142 | 7. **Navigate to the search page.** 143 | 8. _Skip reloading `common-library.js`._ 144 | 9. _Skip reloading `search-page.js`._ 145 | 10. **Navigate to the item page.** 146 | 11. _Skip reloading `common-library.js`._ 147 | 12. _Skip reloading `item-page.js`._ 148 | 149 | Navigation between the two pages now avoids unnecessarily 150 | reloading the scripts. 151 | 152 | > **Note:** See the [Events][events] documentation to properly 153 | > handle initialization and disposal of pages during navigation 154 | > to avoid memory leaks and outdated event listeners. 155 | 156 | > **Note:** See the [Versioning][versioning] documentation to 157 | > automatically switch between script and style versions for seamless 158 | > releases. 159 | 160 | 161 | 162 | [responses]: ./responses.md 163 | [events]: ./events.md 164 | [versioning]: ./versioning.md 165 | -------------------------------------------------------------------------------- /doc/documentation/responses.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Responses 3 | description: 4 | An overview of SPF responses and how they are processed. 5 | --- 6 | 7 | 8 | In dynamic navigation, SPF updates the page with content from 9 | the response. SPF will do this processing in the following 10 | order: 11 | 12 | 1. `title` — Update document title 13 | 2. `url` — Update document URL 14 | 3. `head` — Install early JS and CSS 15 | 4. `attr` — Set element attributes 16 | 5. `body` — Set element content and install JS and CSS 17 | 6. `foot` — Install late JS and CSS 18 | 19 | > **Note:** All fields are optional and the commonly needed 20 | > response values are `title`, `head`, `body`, and `foot`. 21 | 22 | 23 | A response is typically in the following format: 24 | 25 | ```json 26 | { 27 | "title": "Page Title", 28 | "head": 29 | " 30 | 31 | 32 | ...", 33 | "body": { 34 | "DOM ID 1": "HTML Text...", 35 | "DOM ID 2": "..." 36 | }, 37 | "foot": 38 | " 39 | 40 | 41 | ..." 42 | } 43 | ``` 44 | 45 | This pattern follows the general good practice of "styles in the 46 | head, scripts at the end of the body". The "foot" field 47 | represents the "end of the body" section without requiring 48 | developers to create an explicit element. 49 | 50 | To update specific element attributes, the response format is as 51 | follows: 52 | 53 | ```json 54 | { 55 | "attr": { 56 | "DOM ID 1": { 57 | "Name 1": "Value 1", 58 | "Name 2": "Value 2" 59 | }, 60 | "DOM ID 2": { 61 | "...": "..." 62 | } 63 | } 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /doc/documentation/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Get Started 3 | description: 4 | "Use SPF on your site: send requests, return responses." 5 | --- 6 | 7 | 8 | ## Get the code 9 | 10 | Before getting started, first **[download the code][]**. 11 | 12 | 13 | ## _Optional:_ Run the demo 14 | 15 | If you cloned the project from GitHub or downloaded the source 16 | code, you can run the included demo application to see how 17 | everything works together in order to test out the framework: 18 | 19 | ```sh 20 | cd spfjs 21 | npm install 22 | npm start 23 | ``` 24 | 25 | You can then open in your browser and 26 | check out the demo. 27 | 28 | > **Note:** You will need `npm` to install the development 29 | > dependencies and `python` to run the demo application. You 30 | > can check if they are installed by running the following: 31 | > 32 | > ```sh 33 | > npm --version 34 | > python --version 35 | > ``` 36 | > 37 | > If you need to install `npm`, go to and 38 | > click "Install" to get the installer. If you need to install 39 | > `python`, go to and go to 40 | > "Downloads" to get the installer. 41 | 42 | 43 | ## Enable SPF 44 | 45 | To add SPF to your site, you need to include the JS file and run 46 | `spf.init()` to enable the new functionality. 47 | 48 | After [downloading][] the code, copy the `spf.js` file to where 49 | you serve JS files for your site, add the script to your page, 50 | and initialize SPF: 51 | 52 | ```html 53 | 54 | 57 | ``` 58 | 59 | 60 | ## Send requests 61 | 62 | SPF does not change your site's navigation automatically and 63 | instead uses progressive enhancement to enable dynamic 64 | navigation for certain links. Just add a `spf-link` class to an 65 | `` tag to activate SPF. 66 | 67 | Go from static navigation: 68 | 69 | ```html 70 | Go! 71 | ``` 72 | 73 | to dynamic navigation: 74 | 75 | ```html 76 | 77 | Go! 78 | ``` 79 | 80 | 81 | ## Return responses 82 | 83 | In static navigation, an entire HTML page is sent. In dynamic 84 | navigation, only fragments are sent, using JSON as transport. 85 | When SPF sends a request to the server, it appends a 86 | configurable identifier to the URL so that your server can 87 | properly handle the request. (By default, this will be 88 | `?spf=navigate`.) 89 | 90 | In the following example, a common layout of upper masthead, 91 | middle content, and lower footer is used. In dynamic 92 | navigation, only the fragment for the middle content is sent, 93 | since the masthead and footer don't change. 94 | 95 | Go from static navigation: 96 | 97 | `GET /destination` 98 | 99 | ```html 100 | 101 | 102 | 103 | 104 | 105 |
...
106 |
107 | 108 |
109 | 110 | 111 | 112 | 113 | ``` 114 | 115 | to dynamic navigation: 116 | 117 | `GET /destination?spf=navigate` 118 | 119 | ```json 120 | { 121 | "head": "", 122 | "body": { 123 | "content": 124 | "", 125 | }, 126 | "foot": "" 127 | } 128 | ``` 129 | 130 | 131 | 132 | [download the code]: ../download.md 133 | [downloading]: ../download.md 134 | -------------------------------------------------------------------------------- /doc/documentation/versioning.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Versioning 3 | description: Automatically update script and style versions. 4 | --- 5 | 6 | 7 | When using dynamic navigation, SPF transforms your site from 8 | short-lived pages into a long-lived application. When building 9 | a long-lived application, an important concern is how to push 10 | new code -- you don't want old JS attempting to interact with new 11 | HTML or new CSS applying to old HTML. SPF can automatically 12 | update your script and style versions to ensure they stay in 13 | sync with your content. 14 | 15 | > **Note:** Automatic versioning only applies to 16 | > [managed resources][]. 17 | 18 | As an example, consider a user currently on your site who 19 | started with yesterday's code. If you push updated scripts and 20 | content, the user will transition to the new content and needs 21 | the updated scripts as well. 22 | 23 | 24 | ## Use unique URLs for each version 25 | 26 | You can can instruct SPF to 27 | [manage a resource][managed resources] by giving it a `name` 28 | attribute: 29 | 30 | ```html 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | When navigating between pages, a named resource will only be 42 | loaded once. To detect updates, SPF tracks the external URL or 43 | inline text associated with each resource name. If when 44 | processing response a changed URL or text is discovered, SPF 45 | will unload the existing resource and load the new one. 46 | 47 | To guarantee SPF switches between the old and new versions of 48 | your scripts and styles, use a unique URL each time. For 49 | example, for the user currently on your site with yesterday's 50 | code, they might have been served HTML like the following: 51 | 52 | ```html 53 | 54 | 55 | ``` 56 | 57 | Then, when you push updated scripts and content, update the URL 58 | as well: 59 | 60 | ```html 61 | 62 | 63 | ``` 64 | 65 | > **Note:** SPF will automatically detect when the content of 66 | > managed inline scripts and styles are updated by tracking a 67 | > quick hash of the text content. Whitespace is ignored when 68 | > calculating the hash, so indentation and formatting changes 69 | > will not trigger updates. 70 | 71 | 72 | ## Resource events 73 | 74 | If you need to handle resources being unloaded when a new 75 | version is detected, (e.g. to dispose event listeners, etc), SPF 76 | will dispatch events before and after a resource is removed. 77 | As with those detailed in the [Events][] documentation, 78 | these events are defined in the [API][] as [spf.Event][] 79 | objects. The events and their descriptions follow: 80 | 81 | **`spfcssbeforeunload`** 82 | Fired before unloading a managed style resource. Occurs when 83 | an updated style is discovered for a given name. 84 | 85 | **`spfcssunload`** 86 | Fired when unloading a managed style resource. If the style 87 | is being unloaded as part of switching versions, unloading of 88 | the old style occurs after the new style is loaded to avoid 89 | flashes of unstyled content. 90 | 91 | **`spfjssbeforeunload`** 92 | Fired before unloading a managed script resource. Occurs when 93 | an updated script is discovered for a given name. 94 | 95 | **`spfjsunload`** 96 | Fired when unloading a managed script resource. If the script 97 | is being unloaded as part of switching versions, unloading of 98 | the old script occurs after the new script is loaded for 99 | consistency with style loading. 100 | 101 | 102 | 103 | [managed resources]: ./resources.md#managed-resources 104 | [Events]: ./events.md 105 | [API]: ../api.md 106 | [spf.Event]: ../api.md#spf.event 107 | -------------------------------------------------------------------------------- /doc/download.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Download 3 | description: Get the code. 4 | layout: download 5 | --- 6 | 7 | 8 | ## Install with npm or Bower 9 | 10 | Install the [npm][] package: 11 | 12 | ```sh 13 | npm install spf 14 | ``` 15 | 16 | Install the [Bower][] package: 17 | 18 | ```sh 19 | bower install spf 20 | ``` 21 | 22 | 23 | ## Link to a CDN 24 | 25 | You can link to the JS files directly from several popular CDNs: 26 | 27 | - [Google Hosted Libraries][] 28 | `https://ajax.googleapis.com/ajax/libs/spf/2.4.0/spf.js` 29 | - [cdnjs][] 30 | `https://cdnjs.cloudflare.com/ajax/libs/spf/2.4.0/spf.js` 31 | - [jsDelivr][] 32 | `https://cdn.jsdelivr.net/spf/2.4.0/spf.js` 33 | - [OSSCDN][] 34 | `https://oss.maxcdn.com/spf/2.4.0/spf.js` 35 | 36 | 37 | ## Download a release 38 | 39 | Download just the minified JS files: 40 | **[spfjs-2.4.0-dist.zip][spfjs-dist-zip]** 41 | 42 | ```sh 43 | curl -LO https://github.com/youtube/spfjs/releases/download/v2.4.0/spfjs-2.4.0-dist.zip 44 | unzip spfjs-2.4.0-dist.zip 45 | ``` 46 | 47 | Or, download the minified files and complete source code: 48 | **[v2.4.0.zip][spfjs-src-zip]** 49 | 50 | ```sh 51 | curl -LO https://github.com/youtube/spfjs/archive/v2.4.0.zip 52 | unzip v2.4.0.zip 53 | ``` 54 | 55 | 56 | ## Clone with Git 57 | 58 | Clone the project from GitHub and checkout the release: 59 | 60 | ```sh 61 | git clone https://github.com/youtube/spfjs.git 62 | cd spfjs 63 | git checkout v2.4.0 64 | ``` 65 | 66 | 67 | ## Get Started 68 | 69 | After you've grabbed the code, **[get started][]**. 70 | 71 | 72 | 73 | [get started]: ./documentation/start.md 74 | [npm]: https://www.npmjs.com/ 75 | [Bower]: http://bower.io/ 76 | [Google Hosted Libraries]: https://developers.google.com/speed/libraries/devguide#spf 77 | [cdnjs]: https://cdnjs.com/libraries/spf 78 | [jsDelivr]: http://www.jsdelivr.com/#!spf 79 | [OSSCDN]: http://osscdn.com/#/spf 80 | [spfjs-dist-zip]: https://github.com/youtube/spfjs/releases/download/v2.4.0/spfjs-2.4.0-dist.zip 81 | [spfjs-src-zip]: https://github.com/youtube/spfjs/archive/v2.4.0.zip 82 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SPF 3 | layout: home 4 | --- 5 | 6 | 7 | ## A lightweight JS framework for fast navigation and page updates from YouTube 8 | 9 | 10 | Using progressive enhancement and HTML5, SPF integrates with 11 | your site to enable a faster, more fluid user experience by 12 | updating just the sections of the page that change during 13 | navigation, not the whole page. SPF provides a response format 14 | for sending document fragments, a robust system for script and 15 | style management, an in-memory cache, on-the-fly processing, and 16 | more. 17 | 18 | 19 | ## Navigation 20 | 21 | When someone first arrives at your site, content is sent from 22 | the server and rendered normally. This is _static_ navigation. 23 | But when going to the next page, only document fragments are 24 | sent, and the changed sections are updated accordingly. This is 25 | _dynamic_ navigation. 26 | 27 | 1. Static: render everything. 28 | ![Static Navigation][] 29 | 30 | 2. Dynamic: only render new fragments. 31 | ![Dynamic Navigation][] 32 | 33 | 34 | ## Overview 35 | 36 | SPF allows you to leverage the benefits of a static initial page 37 | load, while gaining the performance and user experience benefits 38 | of dynamic page loads: 39 | 40 | > **User Experience** 41 | > 42 | > - Get the fastest possible initial page load. 43 | > - Keep a responsive persistent interface during navigation. 44 | 45 | 46 | 47 | > **Performance** 48 | > 49 | > - Leverage existing techniques for static rendering. 50 | > - Load small responses and fewer resources each navigation. 51 | 52 | 53 | 54 | > **Development** 55 | > 56 | > - Use any server-side language and template system. 57 | > - Be productive by using the same code for static and dynamic 58 | > rendering. 59 | 60 | 61 | ## Features 62 | 63 | > **Script/Style Management** 64 | > 65 | > SPF can manage your script and style loading and unloading to 66 | > ensure smooth updates when versions change during navigation. 67 | 68 | 69 | 70 | > **In-Memory Cache** 71 | > 72 | > SPF can store responses in memory for instant access. This 73 | > makes navigation to previous or future pages extremely fast. 74 | 75 | 76 | 77 | > **On-the-Fly Processing** 78 | > 79 | > SPF supports streaming multipart responses in chunks to enable 80 | > on-the-fly processing. This speeds up navigation by starting 81 | > rendering early. 82 | 83 | 84 | 85 | [Static Navigation]: assets/images/animation-static-340x178.gif 86 | [Dynamic Navigation]: assets/images/animation-dynamic-340x178.gif 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spf", 3 | "version": "2.4.0", 4 | "description": "A lightweight JS framework for fast navigation and page updates from YouTube", 5 | "license": "MIT", 6 | "homepage": "https://github.com/youtube/spfjs", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/youtube/spfjs.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/youtube/spfjs/issues" 13 | }, 14 | "scripts": { 15 | "prepublish": "bower install && bin/configure.js", 16 | "build": "ninja", 17 | "build-all": "ninja all", 18 | "build-spf": "ninja spf spf-debug spf-trace", 19 | "build-boot": "ninja boot boot-debug boot-trace", 20 | "build-dev": "ninja dev", 21 | "build-tests": "ninja tests", 22 | "build-demo": "ninja demo", 23 | "dist": "ninja dist", 24 | "test": "ninja test", 25 | "lint": "ninja lint", 26 | "fix": "ninja fix", 27 | "start": "ninja demo && cd build/demo && python -m app", 28 | "clean": "rm -rf build build.ninja dist npm-debug.log", 29 | "clean-web": "rm -rf web/_site web/assets/vendor", 30 | "clean-install": "rm -rf bower_components node_modules vendor", 31 | "reset": "npm run clean; npm run clean-web; npm run clean-install" 32 | }, 33 | "devDependencies": { 34 | "bower": "^1.3.12", 35 | "closure-linter-wrapper": "^0.2.9", 36 | "glob": "^4.3.1", 37 | "jsdox": "^0.4.7", 38 | "minimist": "^1.1.0", 39 | "ninja-build": "^0.1.5", 40 | "ninja-build-gen": "^0.1.3", 41 | "phantomjs": "^1.9.12", 42 | "semver": "^4.1.0", 43 | "wordwrap": "0.0.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/client/async/async.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Fast asynchronous function execution. 8 | * 9 | * This package provides functions to defer execution on the main thread 10 | * without using setTimeout, though setTimeout is used as a fallback in browsers 11 | * that do not support other methods. Using these methods is advantageous when 12 | * one wants to schedule a callback faster than the setTimeout clamped minimum 13 | * allows (e.g. when doing `setTimeout(fn, 0)`) The clamped minimum for 14 | * setTimeout is often 10ms, though when WebKit browsers are in a background 15 | * tab, setTimeout calls deprioritized to execute with a 1s delay. In these 16 | * cases, this package provides an alternative. 17 | * 18 | * @author nicksay@google.com (Alex Nicksay) 19 | */ 20 | 21 | goog.provide('spf.async'); 22 | 23 | goog.require('spf'); 24 | goog.require('spf.state'); 25 | goog.require('spf.string'); 26 | goog.require('spf.tracing'); 27 | 28 | 29 | /** 30 | * Defers execution of a function to the next slot on the main thread. 31 | * 32 | * @param {!Function} fn The function to defer. 33 | */ 34 | spf.async.defer = function(fn) { 35 | var uid = spf.uid(); 36 | spf.async.defers_[uid] = fn; 37 | if (spf.async.POSTMESSAGE_SUPPORTED_) { 38 | window.postMessage(spf.async.PREFIX_ + uid, '*'); 39 | } else { 40 | window.setTimeout(spf.bind(spf.async.run_, null, uid), 0); 41 | } 42 | }; 43 | 44 | 45 | /** 46 | * Handles a message event and triggers execution function. 47 | * 48 | * @param {Event} evt The click event. 49 | * @private 50 | */ 51 | spf.async.handleMessage_ = function(evt) { 52 | if (evt.data && spf.string.isString(evt.data) && 53 | spf.string.startsWith(evt.data, spf.async.PREFIX_)) { 54 | var uid = evt.data.substring(spf.async.PREFIX_.length); 55 | spf.async.run_(uid); 56 | } 57 | }; 58 | 59 | 60 | /** 61 | * Executes a previously deferred function. 62 | * 63 | * @param {string|number} uid The UID associated with the function. 64 | * @private 65 | */ 66 | spf.async.run_ = function(uid) { 67 | var fn = spf.async.defers_[uid]; 68 | if (fn) { 69 | delete spf.async.defers_[uid]; 70 | fn(); 71 | } 72 | }; 73 | 74 | 75 | /** 76 | * Adds a function as a listener for message events. 77 | * 78 | * @param {!Function} fn The function to add as a listener. 79 | * @private 80 | */ 81 | spf.async.addListener_ = function(fn) { 82 | if (window.addEventListener) { 83 | window.addEventListener('message', fn, false); 84 | } else if (window.attachEvent) { 85 | window.attachEvent('onmessage', fn); 86 | } 87 | }; 88 | 89 | 90 | /** 91 | * Removes a function as a listener for message events. 92 | * 93 | * @param {!Function} fn The function to remove as a listener. 94 | * @private 95 | */ 96 | spf.async.removeListener_ = function(fn) { 97 | if (window.removeEventListener) { 98 | window.removeEventListener('message', fn, false); 99 | } else if (window.detachEvent) { 100 | window.detachEvent('onmessage', fn); 101 | } 102 | }; 103 | 104 | 105 | /** 106 | * Whether the browser supports asynchronous postMessage calls. 107 | * 108 | * @private {boolean} 109 | */ 110 | spf.async.POSTMESSAGE_SUPPORTED_ = (function() { 111 | if (!window.postMessage) { 112 | return false; 113 | } 114 | // Use postMessage where available. But, ensure that postMessage is 115 | // asynchronous; the implementation in IE8 is synchronous, which defeats 116 | // the purpose. To detect this, use a temporary "onmessage" listener. 117 | var supported = true; 118 | var listener = function() { supported = false; }; 119 | // Add the listener, dispatch a message event, and remove the listener. 120 | spf.async.addListener_(listener); 121 | window.postMessage('', '*'); 122 | spf.async.removeListener_(listener); 123 | // Return the status. If the postMessage implementation is correctly 124 | // asynchronous, then the value of the `supported` variable will be 125 | // true, but if the postMessage implementation is synchronous, the 126 | // temporary listener will have executed and set the `supported` 127 | // variable to false. 128 | return supported; 129 | })(); 130 | 131 | 132 | /** 133 | * The prefix to use for message event data to avoid conflicts. 134 | * 135 | * @private {string} 136 | */ 137 | spf.async.PREFIX_ = 'spf:'; 138 | 139 | 140 | /** 141 | * Map of deferred function calls. 142 | * @private {!Object.} 143 | */ 144 | spf.async.defers_ = {}; 145 | 146 | 147 | // Automatic initialization for spf.async.defers_. 148 | // When built for the bootloader, unconditionally set in state. 149 | if (SPF_BOOTLOADER) { 150 | spf.state.set(spf.state.Key.ASYNC_DEFERS, spf.async.defers_); 151 | } else { 152 | if (!spf.state.has(spf.state.Key.ASYNC_DEFERS)) { 153 | spf.state.set(spf.state.Key.ASYNC_DEFERS, spf.async.defers_); 154 | } 155 | spf.async.defers_ = /** @type {!Object.} */ ( 156 | spf.state.get(spf.state.Key.ASYNC_DEFERS)); 157 | } 158 | 159 | // Automatic initialization for spf.state.Key.ASYNC_LISTENER. 160 | // When built for the bootloader, unconditionally set in state. 161 | if (SPF_BOOTLOADER) { 162 | if (spf.async.POSTMESSAGE_SUPPORTED_) { 163 | spf.async.addListener_(spf.async.handleMessage_); 164 | spf.state.set(spf.state.Key.ASYNC_LISTENER, spf.async.handleMessage_); 165 | } 166 | } else { 167 | if (spf.async.POSTMESSAGE_SUPPORTED_) { 168 | if (spf.state.has(spf.state.Key.ASYNC_LISTENER)) { 169 | spf.async.removeListener_(/** @type {function(Event)} */ ( 170 | spf.state.get(spf.state.Key.ASYNC_LISTENER))); 171 | } 172 | spf.async.addListener_(spf.async.handleMessage_); 173 | spf.state.set(spf.state.Key.ASYNC_LISTENER, spf.async.handleMessage_); 174 | } 175 | } 176 | 177 | 178 | if (spf.tracing.ENABLED) { 179 | (function() { 180 | spf.async.defer = spf.tracing.instrument( 181 | spf.async.defer, 'spf.async.defer'); 182 | })(); 183 | } 184 | -------------------------------------------------------------------------------- /src/client/base_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Tests for the base SPF functions. 8 | */ 9 | 10 | goog.require('spf'); 11 | 12 | 13 | describe('spf', function() { 14 | 15 | var foo = 'top'; 16 | var obj = {foo: 'obj'}; 17 | var create = function(arg1, arg2) { 18 | return {foo: this.foo, arg1: arg1, arg2: arg2}; 19 | }; 20 | var add = function(var_args) { 21 | var sum = Number(this) || 0; 22 | for (var i = 0; i < arguments.length; i++) { 23 | sum += arguments[i]; 24 | } 25 | return sum; 26 | }; 27 | 28 | describe('bind', function() { 29 | 30 | it('with this', function() { 31 | var numberLike = {valueOf: function() { return 1; }}; 32 | expect(spf.bind(add, numberLike)()).toEqual(1); 33 | }); 34 | 35 | it('without this', function() { 36 | expect(spf.bind(add, null, 1, 2)()).toEqual(3); 37 | }); 38 | 39 | it('persist this', function() { 40 | var obj1 = {}; 41 | var obj2 = {}; 42 | // Use toBe for exact object matching. 43 | var check = function() { expect(this).toBe(obj1); }; 44 | var fn = spf.bind(check, obj1); 45 | fn.call(); 46 | fn.call(obj1); 47 | }); 48 | 49 | it('static args', function() { 50 | var res = spf.bind(create, obj, 'hot', 'dog')(); 51 | expect(obj.foo).toEqual(res.foo); 52 | expect(res.arg1).toEqual('hot'); 53 | expect(res.arg2).toEqual('dog'); 54 | }); 55 | 56 | it('partial args', function() { 57 | var res = spf.bind(create, obj, 'hot')('dog'); 58 | expect(obj.foo).toEqual(res.foo); 59 | expect(res.arg1).toEqual('hot'); 60 | expect(res.arg2).toEqual('dog'); 61 | }); 62 | 63 | it('dynamic args', function() { 64 | var res = spf.bind(create, obj)('hot', 'dog'); 65 | expect(obj.foo).toEqual(res.foo); 66 | expect(res.arg1).toEqual('hot'); 67 | expect(res.arg2).toEqual('dog'); 68 | }); 69 | 70 | it('double chain', function() { 71 | var res = spf.bind(spf.bind(create, obj, 'hot'), null, 'dog')(); 72 | expect(obj.foo).toEqual(res.foo); 73 | expect(res.arg1).toEqual('hot'); 74 | expect(res.arg2).toEqual('dog'); 75 | }); 76 | 77 | }); 78 | 79 | it('execute', function() { 80 | var err = new Error('fail'); 81 | var foo = { 82 | pass: function() { return 'pass'; }, 83 | fail: function() { throw err; } 84 | }; 85 | spyOn(foo, 'pass').and.callThrough(); 86 | spyOn(foo, 'fail').and.callThrough(); 87 | expect(spf.execute(foo.pass)).toEqual('pass'); 88 | expect(foo.pass).toHaveBeenCalled(); 89 | expect(spf.execute(foo.fail)).toEqual(err); 90 | expect(foo.fail).toHaveBeenCalled(); 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /src/client/bootloader.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview The SPF bootloader (aka bootstrap script loader). 8 | * 9 | * A minimal subset of the SPF API to load scripts, designed to be inlined in 10 | * the document head and extended by the main SPF code. Provides an interface 11 | * loosely similar to $script.js {@link https://github.com/ded/script.js/} but 12 | * with enhancements. 13 | * 14 | * @author nicksay@google.com (Alex Nicksay) 15 | */ 16 | 17 | goog.provide('spf.bootloader'); 18 | 19 | goog.require('spf'); 20 | goog.require('spf.net.script'); 21 | 22 | 23 | // Create the bootloader API by exporting aliased functions. 24 | /** @private {!Object} */ 25 | spf.bootloader.api_ = { 26 | 'script': { 27 | // The bootloader API. 28 | // * Load scripts. 29 | 'load': spf.net.script.load, 30 | 'get': spf.net.script.get, 31 | // * Wait until ready. 32 | 'ready': spf.net.script.ready, 33 | 'done': spf.net.script.done, 34 | // * Load in depedency order. 35 | 'require': spf.net.script.require, 36 | // * Set dependencies and paths. 37 | 'declare': spf.net.script.declare, 38 | 'path': spf.net.script.path 39 | } 40 | }; 41 | // For a production/debug build, isolate access to the API. 42 | // For a development build, mixin the API to the existing namespace. 43 | var global = this; 44 | global['spf'] = global['spf'] || {}; 45 | var api = global['spf']; 46 | for (var fn in spf.bootloader.api_) { 47 | api[fn] = spf.bootloader.api_[fn]; 48 | } 49 | -------------------------------------------------------------------------------- /src/client/config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Functions for handling the SPF config. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | goog.provide('spf.config'); 13 | 14 | goog.require('spf.state'); 15 | 16 | 17 | /** 18 | * Type definition for a SPF config value. 19 | * 20 | * Function type temporarily needed for experimental-html-handler. 21 | * TODO(philharnish): Remove "Function". 22 | * 23 | * @typedef {string|number|boolean|Function|null} 24 | */ 25 | spf.config.Value; 26 | 27 | 28 | /** 29 | * Default configuration values. 30 | * @type {!Object.} 31 | */ 32 | spf.config.defaults = { 33 | 'animation-class': 'spf-animate', 34 | 'animation-duration': 425, 35 | 'cache-lifetime': 10 * 60 * 1000, // 10 minute cache lifetime (ms). 36 | 'cache-max': 50, // 50 items. 37 | 'cache-unified': false, 38 | 'link-class': 'spf-link', 39 | 'nolink-class': 'spf-nolink', 40 | 'navigate-limit': 20, // 20 navigations per session. 41 | 'navigate-lifetime': 24 * 60 * 60 * 1000, // 1 day session lifetime (ms). 42 | 'reload-identifier': null, // Always a param, no '?' needed. 43 | 'request-timeout': 0, // No request timeout. 44 | 'url-identifier': '?spf=__type__' 45 | }; 46 | 47 | 48 | /** 49 | * Initialize the configuration with an optional object. If values are not 50 | * provided, the defaults are used if they exist. 51 | * 52 | * @param {Object.=} opt_config Optional configuration object. 53 | */ 54 | spf.config.init = function(opt_config) { 55 | var config = opt_config || {}; 56 | // Set primary configs; each has a default. 57 | for (var key in spf.config.defaults) { 58 | var value = (key in config) ? config[key] : spf.config.defaults[key]; 59 | spf.config.set(key, value); 60 | } 61 | // Set advanced and experimental configs; none have defaults. 62 | for (var key in config) { 63 | if (!(key in spf.config.defaults)) { 64 | spf.config.set(key, config[key]); 65 | } 66 | } 67 | }; 68 | 69 | 70 | /** 71 | * Checks whether a current configuration value exists. 72 | * 73 | * @param {string} name The configuration name. 74 | * @return {boolean} Whether the configuration value exists. 75 | */ 76 | spf.config.has = function(name) { 77 | return name in spf.config.values; 78 | }; 79 | 80 | 81 | /** 82 | * Gets a current configuration value. 83 | * 84 | * @param {string} name The configuration name. 85 | * @return {spf.config.Value|undefined} The configuration value. 86 | */ 87 | spf.config.get = function(name) { 88 | return spf.config.values[name]; 89 | }; 90 | 91 | 92 | /** 93 | * Sets a current configuration value. 94 | * 95 | * @param {string} name The configuration name. 96 | * @param {spf.config.Value} value The configuration value. 97 | * @return {spf.config.Value} The configuration value. 98 | */ 99 | spf.config.set = function(name, value) { 100 | spf.config.values[name] = value; 101 | return value; 102 | }; 103 | 104 | 105 | /** 106 | * Removes all data from the config. 107 | */ 108 | spf.config.clear = function() { 109 | for (var key in spf.config.values) { 110 | delete spf.config.values[key]; 111 | } 112 | }; 113 | 114 | 115 | /** 116 | * The config storage object. 117 | * @type {!Object.} 118 | */ 119 | spf.config.values = {}; 120 | 121 | 122 | // Automatic initialization for spf.config.values. 123 | if (!spf.state.has(spf.state.Key.CONFIG_VALUES)) { 124 | spf.state.set(spf.state.Key.CONFIG_VALUES, spf.config.values); 125 | } 126 | spf.config.values = /** @type {!Object.} */ ( 127 | spf.state.get(spf.state.Key.CONFIG_VALUES)); 128 | -------------------------------------------------------------------------------- /src/client/config_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Tests for handling the SPF config. 8 | */ 9 | 10 | goog.require('spf.config'); 11 | 12 | 13 | describe('spf.config', function() { 14 | 15 | beforeEach(function() { 16 | spf.config.defaults = {}; 17 | spf.config.values = {}; 18 | }); 19 | 20 | 21 | describe('has', function() { 22 | 23 | it('checks values', function() { 24 | spf.config.values['foo'] = 'foo'; 25 | expect(spf.config.has('foo')).toBe(true); 26 | expect(spf.config.has('bar')).toBe(false); 27 | }); 28 | 29 | }); 30 | 31 | describe('get', function() { 32 | 33 | it('gets values', function() { 34 | spf.config.values['foo'] = 'foo'; 35 | expect(spf.config.get('foo')).toBe('foo'); 36 | expect(spf.config.get('bar')).toBe(undefined); 37 | }); 38 | 39 | }); 40 | 41 | describe('set', function() { 42 | 43 | it('sets values', function() { 44 | var v = spf.config.set('foo', 'foo'); 45 | expect(spf.config.values['foo']).toBe('foo'); 46 | expect(v).toBe('foo'); 47 | expect(spf.config.values['bar']).toBe(undefined); 48 | }); 49 | 50 | }); 51 | 52 | describe('clear', function() { 53 | 54 | it('clears values', function() { 55 | spf.config.set('foo', 'foo'); 56 | expect(spf.config.has('foo')).toBe(true); 57 | spf.config.clear(); 58 | expect(spf.config.has('foo')).toBe(false); 59 | }); 60 | 61 | }); 62 | 63 | describe('init', function() { 64 | 65 | it('uses defaults for values', function() { 66 | spf.config.defaults['foo'] = 'foo'; 67 | spf.config.init(); 68 | expect(spf.config.get('foo')).toBe('foo'); 69 | expect(spf.config.get('bar')).toBe(undefined); 70 | }); 71 | 72 | it('overrides defaults for values', function() { 73 | spf.config.defaults['foo'] = 'foo'; 74 | spf.config.init({'foo': 'surprise!'}); 75 | expect(spf.config.get('foo')).toBe('surprise!'); 76 | expect(spf.config.get('bar')).toBe(undefined); 77 | }); 78 | 79 | it('allows values without defaults', function() { 80 | spf.config.defaults['foo'] = 'foo'; 81 | spf.config.init({'bar': 'bar'}); 82 | expect(spf.config.get('foo')).toBe('foo'); 83 | expect(spf.config.get('bar')).toBe('bar'); 84 | }); 85 | 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /src/client/debug/debug.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Debugging and console logging functions. 8 | * This module is designed to be removed completely by the compiler 9 | * for production builds. 10 | * 11 | * @author nicksay@google.com (Alex Nicksay) 12 | */ 13 | 14 | goog.provide('spf.debug'); 15 | 16 | goog.require('spf'); 17 | 18 | 19 | /** 20 | * Log to the browser console using "debug", the low priority method. 21 | * 22 | * @param {...*} var_args Items to log. 23 | */ 24 | spf.debug.debug = function(var_args) { 25 | if (spf.debug.isLevelEnabled(spf.debug.Level.DEBUG)) { 26 | spf.debug.log(spf.debug.Level.DEBUG, 'spf', arguments); 27 | } 28 | }; 29 | 30 | 31 | /** 32 | * Log to the browser console using "info", the medium priority method. 33 | * 34 | * @param {...*} var_args Items to log. 35 | */ 36 | spf.debug.info = function(var_args) { 37 | if (spf.debug.isLevelEnabled(spf.debug.Level.INFO)) { 38 | spf.debug.log(spf.debug.Level.INFO, 'spf', arguments); 39 | } 40 | }; 41 | 42 | 43 | /** 44 | * Log to the browser console using "warn", the high priority method. 45 | * 46 | * @param {...*} var_args Items to log. 47 | */ 48 | spf.debug.warn = function(var_args) { 49 | if (spf.debug.isLevelEnabled(spf.debug.Level.WARN)) { 50 | spf.debug.log(spf.debug.Level.WARN, 'spf', arguments); 51 | } 52 | }; 53 | 54 | 55 | /** 56 | * Log to the browser console using "error", the critical priority method. 57 | * 58 | * @param {...*} var_args Items to log. 59 | */ 60 | spf.debug.error = function(var_args) { 61 | if (spf.debug.isLevelEnabled(spf.debug.Level.ERROR)) { 62 | spf.debug.log(spf.debug.Level.ERROR, 'spf', arguments); 63 | } 64 | }; 65 | 66 | 67 | /** 68 | * Log to the browser console the specified method. If the method does not 69 | * exist, fallback to using "log" and prefix the message with the intended 70 | * method. Note that in the fallback, all logged items will be converted to 71 | * strings before output for compatibility. 72 | * 73 | * @param {string} method The console method to use when logging. 74 | * @param {string} prefix The string prefix to prepend to the logged items. 75 | * @param {{length: number}} args List of items to log. 76 | */ 77 | spf.debug.log = function(method, prefix, args) { 78 | if (!SPF_DEBUG || !window.console) { 79 | return; 80 | } 81 | args = Array.prototype.slice.call(args); 82 | var current = spf.now(); 83 | var overall = spf.debug.formatDuration(spf.debug.start_, current); 84 | if (spf.debug.split_) { 85 | var split = spf.debug.formatDuration(spf.debug.split_, current); 86 | args.unshift(overall + '/' + split + ':'); 87 | } else { 88 | args.unshift(overall + ':'); 89 | } 90 | if (spf.debug.direct_) { 91 | args.unshift('[' + prefix + ']'); 92 | // Note that passing null for execution context throws an Error in Chrome. 93 | window.console[method].apply(window.console, args); 94 | } else { 95 | args.unshift('[' + prefix + ' - ' + method + ']'); 96 | window.console.log(args.join(' ')); 97 | } 98 | }; 99 | 100 | 101 | /** 102 | * Reset the timer used for logging duration. Call to log split times 103 | * since last reset in addition to overall duration. 104 | */ 105 | spf.debug.reset = function() { 106 | spf.debug.split_ = spf.now(); 107 | }; 108 | 109 | 110 | /** 111 | * Formats two millisecond timestamps into a duration string. 112 | * See {@link spf.now} for timestamp generation. 113 | * 114 | * @param {number} start The starting millisecond timestamp. 115 | * @param {number} end The ending millisecond timestamp. 116 | * @return {string} The formatted duration string. 117 | */ 118 | spf.debug.formatDuration = function(start, end) { 119 | var dur = (end - start) / 1000; 120 | if (dur.toFixed) { 121 | dur = dur.toFixed(3); 122 | } 123 | return dur + 's'; 124 | }; 125 | 126 | 127 | /** 128 | * Checks whether a logging level is enabled for output. 129 | * 130 | * @param {spf.debug.Level} level The logging level. 131 | * @return {boolean} True if the logging level is enabled. 132 | */ 133 | spf.debug.isLevelEnabled = function(level) { 134 | return (spf.debug.levels_[level] >= spf.debug.levels_[spf.debug.OUTPUT]); 135 | }; 136 | 137 | 138 | /** 139 | * The timestamp of when debugging was initialized, for overall duration. 140 | * @private {number} 141 | */ 142 | spf.debug.start_ = spf.now(); 143 | 144 | 145 | /** 146 | * The timestamp of when debugging was reset, for split durations. 147 | * @private {number} 148 | */ 149 | spf.debug.split_ = 0; 150 | 151 | 152 | /** 153 | * Whether to support direct console logging. This mode allows logging of 154 | * objects directly to the console without casting to a string. 155 | * Note: IE does not support direct logging, but also does not support the 156 | * debug method, so this property will be false in IE. 157 | * @private {boolean} 158 | */ 159 | spf.debug.direct_ = !!(window.console && window.console.debug); 160 | 161 | 162 | /** 163 | * A map of logging output levels to corresponding numeric values. 164 | * @private {Object.} 165 | * @const 166 | */ 167 | spf.debug.levels_ = { 168 | 'debug': 1, 169 | 'info': 2, 170 | 'warn': 3, 171 | 'error': 4 172 | }; 173 | 174 | 175 | /** 176 | * The level of logging output, corresponding to browser console logging 177 | * functions: "debug", "info", "warn", "error". 178 | * @enum {string} 179 | */ 180 | spf.debug.Level = { 181 | DEBUG: 'debug', 182 | INFO: 'info', 183 | WARN: 'warn', 184 | ERROR: 'error' 185 | }; 186 | 187 | 188 | /** 189 | * @define {string} OUTPUT is provided to control the level of output 190 | * from debugging code. Valid values correspond to browser console logging 191 | * functions: "debug", "info", "warn", and "error", and can be set by the 192 | * compiler when "--define spf.debug.OUTPUT='warn'" or similar is specified. 193 | */ 194 | spf.debug.OUTPUT = 'debug'; 195 | -------------------------------------------------------------------------------- /src/client/dom/classlist.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Element class manipulation functions. 8 | * See {@link http://www.w3.org/TR/html5/dom.html#classes}. 9 | * 10 | * @author nicksay@google.com (Alex Nicksay) 11 | */ 12 | 13 | goog.provide('spf.dom.classlist'); 14 | 15 | goog.require('spf.array'); 16 | 17 | 18 | /** 19 | * Returns an array of class names on a node. 20 | * 21 | * @param {Node|EventTarget} node DOM node to evaluate. 22 | * @return {{length: number}} Array-like object of class names on the node. 23 | */ 24 | spf.dom.classlist.get = function(node) { 25 | if (node.classList) { 26 | return node.classList; 27 | } else { 28 | return node.className && node.className.match(/\S+/g) || []; 29 | } 30 | }; 31 | 32 | 33 | /** 34 | * Returns true if a node has a class. 35 | * 36 | * @param {Node|EventTarget} node DOM node to test. 37 | * @param {string} cls Class name to test for. 38 | * @return {boolean} Whether node has the class. 39 | */ 40 | spf.dom.classlist.contains = function(node, cls) { 41 | if (!cls) { 42 | return false; 43 | } else if (node.classList) { 44 | return node.classList.contains(cls); 45 | } else { 46 | var classes = spf.dom.classlist.get(node); 47 | return spf.array.some(classes, function(item) { 48 | return item == cls; 49 | }); 50 | } 51 | }; 52 | 53 | 54 | /** 55 | * Adds a class to a node. Does not add multiples. 56 | * 57 | * @param {Node|EventTarget} node DOM node to add class to. 58 | * @param {string} cls Class name to add. 59 | */ 60 | spf.dom.classlist.add = function(node, cls) { 61 | if (cls) { 62 | if (node.classList) { 63 | node.classList.add(cls); 64 | } else if (!spf.dom.classlist.contains(node, cls)) { 65 | node.className += ' ' + cls; 66 | } 67 | } 68 | }; 69 | 70 | 71 | /** 72 | * Removes a class from a node. 73 | * 74 | * @param {Node|EventTarget} node DOM node to remove class from. 75 | * @param {string} cls Class name to remove. 76 | */ 77 | spf.dom.classlist.remove = function(node, cls) { 78 | if (cls) { 79 | if (node.classList) { 80 | node.classList.remove(cls); 81 | } else { 82 | var classes = spf.dom.classlist.get(node); 83 | var newClasses = spf.array.filter(classes, function(item) { 84 | return item != cls; 85 | }); 86 | node.className = newClasses.join(' '); 87 | } 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/client/dom/dataset.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Element dataset manipulation functions. 8 | * See {@link http://www.w3.org/TR/html5/Overview.html#dom-dataset}. 9 | * 10 | * @author nicksay@google.com (Alex Nicksay) 11 | */ 12 | 13 | goog.provide('spf.dom.dataset'); 14 | 15 | 16 | /** 17 | * Gets a custom data attribute from a node. The key should be in 18 | * camelCase format (e.g "keyName" for the "data-key-name" attribute). 19 | * 20 | * @param {Node} node DOM node to get the custom data attribute from. 21 | * @param {string} key Key for the custom data attribute. 22 | * @return {?string} The attribute value, if it exists. 23 | */ 24 | spf.dom.dataset.get = function(node, key) { 25 | if (node.dataset) { 26 | return node.dataset[key]; 27 | } else { 28 | return node.getAttribute('data-' + spf.string.toSelectorCase(key)); 29 | } 30 | }; 31 | 32 | 33 | /** 34 | * Sets a custom data attribute on a node. The key should be in 35 | * camelCase format (e.g "keyName" for the "data-key-name" attribute). 36 | * 37 | * @param {Node} node DOM node to set the custom data attribute on. 38 | * @param {string} key Key for the custom data attribute. 39 | * @param {string} val Value for the custom data attribute. 40 | */ 41 | spf.dom.dataset.set = function(node, key, val) { 42 | if (node.dataset) { 43 | node.dataset[key] = val; 44 | } else { 45 | node.setAttribute('data-' + spf.string.toSelectorCase(key), val); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/client/dom/dom_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Tests for basic DOM manipulation functions. 8 | */ 9 | 10 | goog.require('spf.dom'); 11 | goog.require('spf.string'); 12 | 13 | 14 | describe('spf.dom', function() { 15 | 16 | 17 | describe('setAttributes', function() { 18 | 19 | it('sets "class" correctly', function() { 20 | var el = document.createElement('div'); 21 | spf.dom.setAttributes(el, {'class': 'foo'}); 22 | expect(el.className).toEqual('foo'); 23 | expect(el.getAttribute('class')).toEqual('foo'); 24 | }); 25 | 26 | it('sets "style" correctly', function() { 27 | var el = document.createElement('span'); 28 | spf.dom.setAttributes(el, {'style': 'display: block;'}); 29 | // Note that some browsers add trailing whitespace to the 30 | // style text here, so trim it for the test. 31 | expect(spf.string.trim(el.style.cssText)).toEqual('display: block;'); 32 | expect(spf.string.trim(el.getAttribute('style'))) 33 | .toEqual('display: block;'); 34 | }); 35 | 36 | it('sets "value" correctly', function() { 37 | var el = document.createElement('input'); 38 | spf.dom.setAttributes(el, {'value': 'bar'}); 39 | expect(el.value).toEqual('bar'); 40 | expect(el.getAttribute('value')).toEqual('bar'); 41 | }); 42 | 43 | }); 44 | 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /src/client/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview The primary SPF entry point. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | goog.provide('spf.main'); 13 | 14 | goog.require('spf'); 15 | goog.require('spf.config'); 16 | goog.require('spf.debug'); 17 | goog.require('spf.history'); 18 | goog.require('spf.nav'); 19 | goog.require('spf.net.script'); 20 | goog.require('spf.net.style'); 21 | goog.require('spf.pubsub'); 22 | 23 | 24 | /** 25 | * Initializes SPF. 26 | * 27 | * @param {Object=} opt_config Optional global configuration object. 28 | * @return {boolean} Whether SPF was successfully initialized. If the HTML5 29 | * history modification API is not supported, returns false. 30 | */ 31 | spf.main.init = function(opt_config) { 32 | var enable = spf.main.canInit_(); 33 | spf.debug.info('main.init ', 'enable=', enable); 34 | spf.config.init(opt_config); 35 | if (enable) { 36 | spf.nav.init(); 37 | } 38 | return enable; 39 | }; 40 | 41 | 42 | /** 43 | * Checks to see if SPF can be initialized. 44 | * 45 | * @return {boolean} 46 | * @private 47 | */ 48 | spf.main.canInit_ = function() { 49 | return !!(typeof window.history.pushState == 'function' || 50 | spf.history.getIframe().contentWindow.history.pushState); 51 | }; 52 | 53 | 54 | /** 55 | * Disposes SPF. 56 | */ 57 | spf.main.dispose = function() { 58 | var enable = !!(typeof History != 'undefined' && History.prototype.pushState); 59 | if (enable) { 60 | spf.nav.dispose(); 61 | } 62 | spf.config.clear(); 63 | }; 64 | 65 | 66 | /** 67 | * Discovers existing script and style elements in the document and registers 68 | * them as loaded, once during initial code execution and again when the 69 | * document is ready to catch any resources in the page after SPF is included. 70 | * @private 71 | */ 72 | spf.main.discover_ = function() { 73 | spf.net.script.discover(); 74 | spf.net.style.discover(); 75 | if (document.readyState == 'complete') { 76 | // Since IE 8+ is supported for common library functions such as script 77 | // and style loading, use both standard and legacy event handlers to 78 | // discover existing resources. 79 | if (document.removeEventListener) { 80 | document.removeEventListener( 81 | 'DOMContentLoaded', spf.main.discover_, false); 82 | } else if (document.detachEvent) { 83 | document.detachEvent( 84 | 'onreadystatechange', spf.main.discover_); 85 | } 86 | } 87 | }; 88 | if (document.addEventListener) { 89 | document.addEventListener( 90 | 'DOMContentLoaded', spf.main.discover_, false); 91 | } else if (document.attachEvent) { 92 | document.attachEvent( 93 | 'onreadystatechange', spf.main.discover_); 94 | } 95 | spf.main.discover_(); 96 | 97 | 98 | // Create the API by exporting aliased functions. 99 | // Core API functions are available on the top-level namespace. 100 | // Extra API functions are available on second-level namespaces. 101 | /** @private {!Object} */ 102 | spf.main.api_ = { 103 | 'init': spf.main.init, 104 | 'dispose': spf.main.dispose, 105 | 'navigate': spf.nav.navigate, 106 | 'load': spf.nav.load, 107 | 'prefetch': spf.nav.prefetch, 108 | 'process': spf.nav.process 109 | }; 110 | /** @private {!Object} */ 111 | spf.main.extra_ = { 112 | 'cache': { 113 | // Cache API. 114 | // * Remove one entry. 115 | 'remove': spf.cache.remove, 116 | // * Clear all entries. 117 | 'clear': spf.cache.clear 118 | }, 119 | 'script': { 120 | // The bootloader API. 121 | // * Load scripts. 122 | 'load': spf.net.script.load, 123 | 'get': spf.net.script.get, 124 | // * Wait until ready. 125 | 'ready': spf.net.script.ready, 126 | 'done': spf.net.script.done, 127 | // * Load in depedency order. 128 | 'require': spf.net.script.require, 129 | // * Set dependencies and paths. 130 | 'declare': spf.net.script.declare, 131 | 'path': spf.net.script.path, 132 | // Extended script loading API. 133 | // * Unload scripts. 134 | 'unload': spf.net.script.unload, 135 | // * Ignore ready. 136 | 'ignore': spf.net.script.ignore, 137 | // * Unload in depedency order. 138 | 'unrequire': spf.net.script.unrequire, 139 | // * Prefetch. 140 | 'prefetch': spf.net.script.prefetch 141 | }, 142 | 'style': { 143 | // Style loading API. 144 | // * Load styles. 145 | 'load': spf.net.style.load, 146 | 'get': spf.net.style.get, 147 | // * Unload styles. 148 | 'unload': spf.net.style.unload, 149 | // * Set paths. 150 | 'path': spf.net.style.path, 151 | // * Prefetch. 152 | 'prefetch': spf.net.style.prefetch 153 | } 154 | }; 155 | // For a production/debug build, isolate access to the API. 156 | // For a development build, mixin the API to the existing namespace. 157 | var global = this; 158 | global['spf'] = global['spf'] || {}; 159 | var api = global['spf']; 160 | for (var fn1 in spf.main.api_) { 161 | api[fn1] = spf.main.api_[fn1]; 162 | } 163 | // Use two-stage exporting to allow aliasing the intermediate namespaces 164 | // created by the bootloader (e.g. s = spf.script; s.load(...)). 165 | for (var ns in spf.main.extra_) { 166 | for (var fn2 in spf.main.extra_[ns]) { 167 | api[ns] = api[ns] || {}; 168 | api[ns][fn2] = spf.main.extra_[ns][fn2]; 169 | } 170 | } 171 | 172 | // Signal that the API is ready with custom event. Only supported in IE 9+. 173 | spf.dispatch(spf.EventName.READY); 174 | -------------------------------------------------------------------------------- /src/client/net/connect.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Functions for handling connections (i.e. pre-resolving DNS 8 | * and establishing the TCP AND TLS handshake). 9 | * 10 | * @author nicksay@google.com (Alex Nicksay) 11 | */ 12 | 13 | goog.provide('spf.net.connect'); 14 | 15 | goog.require('spf.array'); 16 | goog.require('spf.net.resource'); 17 | goog.require('spf.tracing'); 18 | 19 | 20 | /** 21 | * Preconnects to a URL. 22 | * Use to both resolve DNS and establish connections before requests are made. 23 | * 24 | * @param {string|Array.} urls One or more URLs to preconnect. 25 | */ 26 | spf.net.connect.preconnect = function(urls) { 27 | // Use an tag to handle the preconnect in a compatible manner. 28 | var type = spf.net.resource.Type.IMG; 29 | // Convert to an array if needed. 30 | urls = spf.array.toArray(urls); 31 | spf.array.each(urls, function(url) { 32 | // When preconnecting, always fetch the image and make the request. 33 | // This is necessary to consistenly establish connections to repeat 34 | // URLs when the keep-alive time is shorter than the interval between 35 | // attempts. 36 | spf.net.resource.prefetch(type, url, true); // Force repeat fetching. 37 | }); 38 | }; 39 | 40 | 41 | if (spf.tracing.ENABLED) { 42 | (function() { 43 | spf.net.connect.preconnect = spf.tracing.instrument( 44 | spf.net.connect.preconnect, 'spf.net.connect.preconnect'); 45 | })(); 46 | } 47 | -------------------------------------------------------------------------------- /src/client/net/connect_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Tests for handling connections. 8 | */ 9 | 10 | goog.require('spf.array'); 11 | goog.require('spf.net.connect'); 12 | goog.require('spf.net.resource'); 13 | 14 | 15 | describe('spf.net.connect', function() { 16 | 17 | var IMG = spf.net.resource.Type.IMG; 18 | 19 | beforeEach(function() { 20 | spyOn(spf.net.resource, 'prefetch'); 21 | }); 22 | 23 | 24 | describe('preconnect', function() { 25 | 26 | it('calls for a single url', function() { 27 | var url = 'url/a'; 28 | var force = true; 29 | spf.net.connect.preconnect(url); 30 | expect(spf.net.resource.prefetch).toHaveBeenCalledWith(IMG, url, force); 31 | }); 32 | 33 | it('calls for multiples urls', function() { 34 | var urls = ['url/b-1', 'url/b-2']; 35 | var force = true; 36 | spf.net.connect.preconnect(urls); 37 | spf.array.each(urls, function(url) { 38 | expect(spf.net.resource.prefetch).toHaveBeenCalledWith( 39 | IMG, url, force); 40 | }); 41 | }); 42 | 43 | }); 44 | 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /src/client/net/style.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Functions for dynamically loading stylesheets. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | goog.provide('spf.net.style'); 13 | 14 | goog.require('spf.array'); 15 | goog.require('spf.net.resource'); 16 | goog.require('spf.string'); 17 | goog.require('spf.tracing'); 18 | 19 | 20 | /** 21 | * Loads a stylesheet asynchronously and defines a name to use for dependency 22 | * management and unloading. See {@link #unload} to remove previously loaded 23 | * stylesheets. 24 | * 25 | * - Subsequent calls to load the same URL will not reload the stylesheet. To 26 | * reload a stylesheet, unload it first with {@link #unload}. To 27 | * unconditionally load a stylesheet, see {@link #get}. 28 | * 29 | * - A name must be specified to identify the same stylesheet at different URLs. 30 | * (For example, "main-A.css" and "main-B.css" are both "main".) When a name 31 | * is specified, all other stylesheets with the same name will be unloaded. 32 | * This allows switching between versions of the same stylesheet at different 33 | * URLs. 34 | * 35 | * - A callback can be specified to execute once the stylesheet has loaded. The 36 | * callback will be executed each time, even if the stylesheet is not 37 | * reloaded. NOTE: Unlike scripts, this callback is best effort and is 38 | * supported in the following browser versions: IE 6, Chrome 19, Firefox 9, 39 | * Safari 6. 40 | * 41 | * @param {string} url URL of the stylesheet to load. 42 | * @param {string} name Name to identify the stylesheet. 43 | * @param {Function=} opt_fn Optional callback function to execute when the 44 | * stylesheet is loaded. 45 | */ 46 | spf.net.style.load = function(url, name, opt_fn) { 47 | var type = spf.net.resource.Type.CSS; 48 | spf.net.resource.load(type, url, name, opt_fn); 49 | }; 50 | 51 | 52 | /** 53 | * Unloads a stylesheet identified by dependency name. See {@link #load}. 54 | * 55 | * @param {string} name The dependency name. 56 | */ 57 | spf.net.style.unload = function(name) { 58 | var type = spf.net.resource.Type.CSS; 59 | spf.net.resource.unload(type, name); 60 | }; 61 | 62 | 63 | /** 64 | * Discovers existing stylesheets in the document and registers them as loaded. 65 | */ 66 | spf.net.style.discover = function() { 67 | var type = spf.net.resource.Type.CSS; 68 | spf.net.resource.discover(type); 69 | }; 70 | 71 | 72 | /** 73 | * Unconditionally loads a stylesheet by dynamically creating an element and 74 | * appending it to the document without regard for whether it has been loaded 75 | * before. A stylesheet directly loaded by this method cannot be unloaded by 76 | * name. Compare to {@link #load}. 77 | * 78 | * @param {string} url URL of the stylesheet to load. 79 | * @param {Function=} opt_fn Function to execute when loaded. 80 | */ 81 | spf.net.style.get = function(url, opt_fn) { 82 | // NOTE: Callback execution depends on onload support and is best effort. 83 | // Chrome 19, Safari 6, Firefox 9, Opera and IE 5.5 support stylesheet onload. 84 | var type = spf.net.resource.Type.CSS; 85 | spf.net.resource.create(type, url, opt_fn); 86 | }; 87 | 88 | 89 | /** 90 | * Prefetchs one or more stylesheets; the stylesheets will be requested but not 91 | * loaded. Use to prime the browser cache and avoid needing to request the 92 | * stylesheet when subsequently loaded. See {@link #load}. 93 | * 94 | * @param {string|Array.} urls One or more stylesheet URLs to prefetch. 95 | */ 96 | spf.net.style.prefetch = function(urls) { 97 | var type = spf.net.resource.Type.CSS; 98 | // Convert to an array if needed. 99 | urls = spf.array.toArray(urls); 100 | spf.array.each(urls, function(url) { 101 | spf.net.resource.prefetch(type, url); 102 | }); 103 | }; 104 | 105 | 106 | /** 107 | * Evaluates style text and defines a name to use for management. 108 | * 109 | * - Subsequent calls to evaluate the same text will not re-evaluate the style. 110 | * To unconditionally evalute a style, see {@link #exec}. 111 | * 112 | * @param {string} text The text of the style. 113 | * @param {string} name Name to identify the style. 114 | * @return {undefined} 115 | */ 116 | spf.net.style.eval = function(text, name) { 117 | var type = spf.net.resource.Type.CSS; 118 | spf.net.resource.eval(type, text, name); 119 | }; 120 | 121 | 122 | /** 123 | * Unconditionally evaluates style text. See {@link #eval}. 124 | * 125 | * @param {string} text The text of the style. 126 | */ 127 | spf.net.style.exec = function(text) { 128 | var type = spf.net.resource.Type.CSS; 129 | spf.net.resource.exec(type, text); 130 | }; 131 | 132 | 133 | /** 134 | * Sets the path prefix or replacement map to use when resolving relative URLs. 135 | * 136 | * Note: The order in which replacements are made is not guaranteed. 137 | * 138 | * @param {string|Object.} paths The paths. 139 | */ 140 | spf.net.style.path = function(paths) { 141 | var type = spf.net.resource.Type.CSS; 142 | spf.net.resource.path(type, paths); 143 | }; 144 | 145 | 146 | if (spf.tracing.ENABLED) { 147 | (function() { 148 | spf.net.style.load = spf.tracing.instrument( 149 | spf.net.style.load, 'spf.net.style.load'); 150 | spf.net.style.unload = spf.tracing.instrument( 151 | spf.net.style.unload, 'spf.net.style.unload'); 152 | spf.net.style.discover = spf.tracing.instrument( 153 | spf.net.style.discover, 'spf.net.style.discover'); 154 | spf.net.style.get = spf.tracing.instrument( 155 | spf.net.style.get, 'spf.net.style.get'); 156 | spf.net.style.prefetch = spf.tracing.instrument( 157 | spf.net.style.prefetch, 'spf.net.style.prefetch'); 158 | spf.net.style.eval = spf.tracing.instrument( 159 | spf.net.style.eval, 'spf.net.style.eval'); 160 | spf.net.style.path = spf.tracing.instrument( 161 | spf.net.style.path, 'spf.net.style.path'); 162 | })(); 163 | } 164 | -------------------------------------------------------------------------------- /src/client/net/style_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Tests for dynamically loading styles. 8 | */ 9 | 10 | goog.require('spf.array'); 11 | goog.require('spf.net.resource'); 12 | goog.require('spf.net.style'); 13 | 14 | 15 | describe('spf.net.style', function() { 16 | 17 | var CSS = spf.net.resource.Type.CSS; 18 | 19 | beforeEach(function() { 20 | spyOn(spf.net.resource, 'load'); 21 | spyOn(spf.net.resource, 'unload'); 22 | spyOn(spf.net.resource, 'create'); 23 | spyOn(spf.net.resource, 'prefetch'); 24 | }); 25 | 26 | 27 | describe('load', function() { 28 | 29 | it('passes a url with name', function() { 30 | var url = 'url-a.css'; 31 | var name = 'a'; 32 | var fn = undefined; 33 | spf.net.style.load(url, name); 34 | expect(spf.net.resource.load).toHaveBeenCalledWith(CSS, url, name, fn); 35 | }); 36 | 37 | it('passes a url with name and callback', function() { 38 | var url = 'url-a.css'; 39 | var name = 'a'; 40 | var fn = function() {}; 41 | spf.net.style.load(url, name, fn); 42 | expect(spf.net.resource.load).toHaveBeenCalledWith(CSS, url, name, fn); 43 | }); 44 | 45 | }); 46 | 47 | 48 | describe('unload', function() { 49 | 50 | it('passes name', function() { 51 | var name = 'a'; 52 | spf.net.style.unload(name); 53 | expect(spf.net.resource.unload).toHaveBeenCalledWith(CSS, name); 54 | }); 55 | 56 | }); 57 | 58 | 59 | describe('get', function() { 60 | 61 | it('passes url', function() { 62 | var url = 'url-a.css'; 63 | var fn = undefined; 64 | spf.net.style.get(url); 65 | expect(spf.net.resource.create).toHaveBeenCalledWith(CSS, url, fn); 66 | }); 67 | 68 | it('passes url with function', function() { 69 | var url = 'url-a.css'; 70 | var fn = function() {}; 71 | spf.net.style.get(url, fn); 72 | expect(spf.net.resource.create).toHaveBeenCalledWith(CSS, url, fn); 73 | }); 74 | 75 | }); 76 | 77 | 78 | describe('prefetch', function() { 79 | 80 | it('calls for a single url', function() { 81 | var url = 'url-a.css'; 82 | spf.net.style.prefetch(url); 83 | expect(spf.net.resource.prefetch).toHaveBeenCalledWith(CSS, url); 84 | }); 85 | 86 | it('calls for multiples urls', function() { 87 | var urls = ['url-a-1.css', 'url-a-2.css']; 88 | spf.net.style.prefetch(urls); 89 | spf.array.each(urls, function(url) { 90 | expect(spf.net.resource.prefetch).toHaveBeenCalledWith(CSS, url); 91 | }); 92 | }); 93 | 94 | }); 95 | 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /src/client/pubsub/pubsub.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Simple publish/subscribe instance used as a "dispatch" 8 | * for centralized notifications. 9 | * 10 | * @author nicksay@google.com (Alex Nicksay) 11 | */ 12 | 13 | goog.provide('spf.pubsub'); 14 | 15 | goog.require('spf'); 16 | goog.require('spf.array'); 17 | goog.require('spf.state'); 18 | 19 | 20 | /** 21 | * Subscribes a function to a topic. The function is invoked in the global 22 | * scope. Subscribing the same function to the same topic multiple 23 | * times will result in multiple function invocations while publishing. 24 | * 25 | * @param {string} topic Topic to subscribe to. Passing an empty string does 26 | * nothing. 27 | * @param {Function|undefined} fn Function to be invoked when a message is 28 | * published to the given topic. Passing `null` or `undefined` 29 | * does nothing. 30 | */ 31 | spf.pubsub.subscribe = function(topic, fn) { 32 | if (topic && fn) { 33 | if (!(topic in spf.pubsub.subscriptions)) { 34 | spf.pubsub.subscriptions[topic] = []; 35 | } 36 | spf.pubsub.subscriptions[topic].push(fn); 37 | } 38 | }; 39 | 40 | 41 | /** 42 | * Unsubscribes a function from a topic. Only deletes the first match found. 43 | * 44 | * @param {string} topic Topic to unsubscribe from. Passing an empty string does 45 | * nothing. 46 | * @param {Function|undefined} fn Function to unsubscribe. Passing `null` 47 | * or `undefined` does nothing. 48 | */ 49 | spf.pubsub.unsubscribe = function(topic, fn) { 50 | if (topic in spf.pubsub.subscriptions && fn) { 51 | spf.array.every(spf.pubsub.subscriptions[topic], function(subFn, i, arr) { 52 | if (subFn == fn) { 53 | arr[i] = null; 54 | return false; 55 | } 56 | return true; 57 | }); 58 | } 59 | }; 60 | 61 | 62 | /** 63 | * Publishes a topic. Calls functions subscribed to the topic in 64 | * the order in which they were added. If any of the functions throws an 65 | * uncaught error, publishing is aborted. 66 | * 67 | * @param {string} topic Topic to publish. Passing an empty string does 68 | * nothing. 69 | */ 70 | spf.pubsub.publish = function(topic) { 71 | spf.pubsub.publish_(topic); 72 | }; 73 | 74 | 75 | /** 76 | * Simulaneously publishes and clears a topic. Calls functions subscribed to 77 | * topic in the order in which they were added, unsubscribing each beforehand. 78 | * If any of the functions throws an uncaught error, publishing is aborted. 79 | * See {#publish} and {#clear}. 80 | * 81 | * @param {string} topic Topic to publish. Passing an empty string does 82 | * nothing. 83 | */ 84 | spf.pubsub.flush = function(topic) { 85 | spf.pubsub.publish_(topic, true); 86 | }; 87 | 88 | 89 | /** 90 | * See {@link #publish} or {@link #flush}. 91 | * 92 | * @param {string} topic Topic to publish. 93 | * @param {boolean=} opt_unsub Whether to unsubscribe functions beforehand. 94 | * @private 95 | */ 96 | spf.pubsub.publish_ = function(topic, opt_unsub) { 97 | if (topic in spf.pubsub.subscriptions) { 98 | spf.array.each(spf.pubsub.subscriptions[topic], function(subFn, i, arr) { 99 | if (opt_unsub) { 100 | arr[i] = null; 101 | } 102 | if (subFn) { 103 | subFn(); 104 | } 105 | }); 106 | } 107 | }; 108 | 109 | 110 | /** 111 | * Renames a topic. All functions subscribed to the old topic will then 112 | * be subscribed to the new topic instead. 113 | * 114 | * @param {string} oldTopic The old name for the topic. Passing an empty string 115 | * does nothing. 116 | * @param {string} newTopic The new name for the topic. Passing an empty string 117 | * does nothing. 118 | */ 119 | spf.pubsub.rename = function(oldTopic, newTopic) { 120 | if (oldTopic && newTopic && oldTopic in spf.pubsub.subscriptions) { 121 | var existing = spf.pubsub.subscriptions[newTopic] || []; 122 | spf.pubsub.subscriptions[newTopic] = 123 | existing.concat(spf.pubsub.subscriptions[oldTopic]); 124 | spf.pubsub.clear(oldTopic); 125 | } 126 | }; 127 | 128 | 129 | /** 130 | * Clears the subscription list for a topic. 131 | * 132 | * @param {string} topic Topic to clear. 133 | */ 134 | spf.pubsub.clear = function(topic) { 135 | delete spf.pubsub.subscriptions[topic]; 136 | }; 137 | 138 | 139 | /** 140 | * Map of subscriptions. 141 | * @type {!Object.} 142 | */ 143 | spf.pubsub.subscriptions = {}; 144 | 145 | 146 | // Automatic initialization for spf.pubsub.subscriptions. 147 | // When built for the bootloader, unconditionally set in state. 148 | if (SPF_BOOTLOADER) { 149 | spf.state.set(spf.state.Key.PUBSUB_SUBS, spf.pubsub.subscriptions); 150 | } else { 151 | if (!spf.state.has(spf.state.Key.PUBSUB_SUBS)) { 152 | spf.state.set(spf.state.Key.PUBSUB_SUBS, spf.pubsub.subscriptions); 153 | } 154 | spf.pubsub.subscriptions = /** @type {!Object.} */ ( 155 | spf.state.get(spf.state.Key.PUBSUB_SUBS)); 156 | } 157 | -------------------------------------------------------------------------------- /src/client/state.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Functions for handling the SPF state. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | goog.provide('spf.state'); 13 | 14 | 15 | /** 16 | * Checks whether a current state value exists. 17 | * 18 | * @param {spf.state.Key} key The state key. 19 | * @return {boolean} Whether the state value exists. 20 | */ 21 | spf.state.has = function(key) { 22 | return key in spf.state.values_; 23 | }; 24 | 25 | 26 | /** 27 | * Gets a current state value. 28 | * 29 | * @param {spf.state.Key} key The state key. 30 | * @return {*} The state value. 31 | */ 32 | spf.state.get = function(key) { 33 | return spf.state.values_[key]; 34 | }; 35 | 36 | 37 | /** 38 | * Sets a current state value. 39 | * 40 | * @param {spf.state.Key} key The state key. 41 | * @param {T} value The state value. 42 | * @return {T} The state value. 43 | * @template T 44 | */ 45 | spf.state.set = function(key, value) { 46 | spf.state.values_[key] = value; 47 | return value; 48 | }; 49 | 50 | 51 | /** 52 | * @enum {string} 53 | */ 54 | spf.state.Key = { 55 | ASYNC_DEFERS: 'async-defers', 56 | ASYNC_LISTENER: 'async-listener', 57 | CACHE_COUNTER: 'cache-counter', 58 | CACHE_MAX: 'cache-max', 59 | CACHE_STORAGE: 'cache-storage', 60 | CONFIG_VALUES: 'config', 61 | HISTORY_CALLBACK: 'history-callback', 62 | HISTORY_ERROR_CALLBACK: 'history-error-callback', 63 | HISTORY_IGNORE_POP: 'history-ignore-pop', 64 | HISTORY_INIT: 'history-init', 65 | HISTORY_LISTENER: 'history-listener', 66 | HISTORY_TIMESTAMP: 'history-timestamp', 67 | HISTORY_URL: 'history-url', 68 | NAV_COUNTER: 'nav-counter', 69 | NAV_INIT: 'nav-init', 70 | NAV_INIT_TIME: 'nav-init-time', 71 | NAV_CLICK_LISTENER: 'nav-listener', 72 | NAV_MOUSEDOWN_LISTENER: 'nav-mousedown-listener', 73 | NAV_SCROLL_LISTENER: 'nav-scroll-listener', 74 | NAV_SCROLL_TEMP_POSITION: 'nav-scroll-position', 75 | NAV_SCROLL_TEMP_URL: 'nav-scroll-url', 76 | NAV_PREFETCHES: 'nav-prefetches', 77 | NAV_PROMOTE: 'nav-promote', 78 | NAV_PROMOTE_TIME: 'nav-promote-time', 79 | NAV_REQUEST: 'nav-request', 80 | PUBSUB_SUBS: 'ps-s', 81 | RESOURCE_NAME: 'rsrc-n', 82 | RESOURCE_PATHS_PREFIX: 'rsrc-p-', 83 | RESOURCE_STATUS: 'rsrc-s', 84 | RESOURCE_URL: 'rsrc-u', 85 | SCRIPT_DEPS: 'js-d', 86 | SCRIPT_URL: 'js-u', 87 | TASKS_UID: 'uid' 88 | }; 89 | 90 | 91 | /** 92 | * Current state values. Globally exported to maintain continuity 93 | * across revisions. 94 | * @private {Object} 95 | */ 96 | spf.state.values_ = window['_spf_state'] || {}; 97 | window['_spf_state'] = spf.state.values_; 98 | -------------------------------------------------------------------------------- /src/client/string/string.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview String manipulation functions. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | goog.provide('spf.string'); 13 | 14 | goog.require('spf'); 15 | 16 | 17 | /** 18 | * Checks whether a string contains a given substring. 19 | * 20 | * @param {string} str The string to test. 21 | * @param {string} substr The substring to test for. 22 | * @return {boolean} True if `str` contains `substr`. 23 | */ 24 | spf.string.contains = function(str, substr) { 25 | return str.indexOf(substr) != -1; 26 | }; 27 | 28 | 29 | /** 30 | * Fast prefix-checker. 31 | * 32 | * @param {string} str The string to check. 33 | * @param {string} prefix A string to look for at the start of `str`. 34 | * @param {number=} opt_offset Offset from index 0 at which to check. 35 | * @return {boolean} True if `str` begins with `prefix`. 36 | */ 37 | spf.string.startsWith = function(str, prefix, opt_offset) { 38 | var idx = opt_offset || 0; 39 | return str.lastIndexOf(prefix, idx) == idx; 40 | }; 41 | 42 | 43 | /** 44 | * Fast suffix-checker. 45 | * 46 | * @param {string} str The string to check. 47 | * @param {string} suffix A string to look for at the end of `str`. 48 | * @return {boolean} True if `str` ends with `suffix`. 49 | */ 50 | spf.string.endsWith = function(str, suffix) { 51 | var l = str.length - suffix.length; 52 | return l >= 0 && str.indexOf(suffix, l) == l; 53 | }; 54 | 55 | 56 | /** 57 | * Simple check for if a value is a string. 58 | * 59 | * @param {?} val Value to test. 60 | * @return {boolean} Whether the value is a string. 61 | */ 62 | spf.string.isString = function(val) { 63 | // When built for the bootloader, optimize for size over complete accuracy. 64 | if (SPF_BOOTLOADER) { 65 | // The return value for typeof will be one of the following: 66 | // * number 67 | // * string 68 | // * boolean 69 | // * function 70 | // * object 71 | // * undefined 72 | // Match "string" to provide an identity test. 73 | // This test will fail if a string object like "new String()" is passed in, 74 | // but for the bootloader, this is an acceptable trade off. 75 | return typeof val == 'string'; 76 | } 77 | return Object.prototype.toString.call(val) == '[object String]'; 78 | }; 79 | 80 | 81 | /** 82 | * Removes leading and trailing whitespace. 83 | * 84 | * @param {string} str The string to trim. 85 | * @return {string} The trimmed string. 86 | */ 87 | spf.string.trim = (function() { 88 | if (String.prototype.trim) { 89 | return function(str) { return str.trim(); }; 90 | } else { 91 | return function(str) { return str.replace(/^\s+|\s+$/g, ''); }; 92 | } 93 | })(); 94 | 95 | 96 | /** 97 | * Partitions a string by dividing it at the first occurance of a separator and 98 | * returning an array of 3 parts: the part before the separator, the separator 99 | * itself, and the part after the separator. If the separator is not found, 100 | * the last two items will be empty strings. 101 | * 102 | * @param {string} str The string to partition. 103 | * @param {string} sep The separator. 104 | * @return {!Array.} The partitioned string result. 105 | */ 106 | spf.string.partition = function(str, sep) { 107 | var arr = str.split(sep); 108 | var nosep = arr.length == 1; 109 | return [arr[0], (nosep ? '' : sep), (nosep ? '' : arr.slice(1).join(sep))]; 110 | }; 111 | 112 | 113 | /** 114 | * String hash function similar to java.lang.String.hashCode(). 115 | * The hash code for a string is computed as 116 | * s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1], 117 | * where s[i] is the ith character of the string and n is the length of 118 | * the string. We mod the result to make it between 0 (inclusive) and 2^32 119 | * (exclusive). 120 | * 121 | * @param {string} str A string. 122 | * @return {number} Hash value for `str`, between 0 (inclusive) and 2^32 123 | * (exclusive). The empty string returns 0. 124 | */ 125 | spf.string.hashcode = function(str) { 126 | str = str || ''; 127 | var result = 0; 128 | for (var i = 0, l = str.length; i < l; ++i) { 129 | result = 31 * result + str.charCodeAt(i); 130 | // Normalize to 4 byte range, 0 ... 2^32. 131 | result %= 0x100000000; 132 | } 133 | return result; 134 | }; 135 | 136 | 137 | /** 138 | * Converts a string from camelCase to selector-case (e.g. from 139 | * "multiPartString" to "multi-part-string"), useful for converting JS 140 | * style and dataset properties to equivalent CSS selectors and HTML keys. 141 | * 142 | * @param {string} str The string in camelCase form. 143 | * @return {string} The string in selector-case form. 144 | */ 145 | spf.string.toSelectorCase = function(str) { 146 | return String(str).replace(/([A-Z])/g, '-$1').toLowerCase(); 147 | }; 148 | -------------------------------------------------------------------------------- /src/client/string/string_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Tests for string manipulation functions. 8 | */ 9 | 10 | goog.require('spf.string'); 11 | 12 | 13 | describe('spf.string', function() { 14 | 15 | it('contains', function() { 16 | // Character matching. 17 | expect(spf.string.contains('/path?query=arg', '?')).toBe(true); 18 | expect(spf.string.contains('/path?query=arg', '#')).toBe(false); 19 | // Substring matching. 20 | expect(spf.string.contains('/path?query=arg', 'query')).toBe(true); 21 | expect(spf.string.contains('/path?query=arg', 'hash')).toBe(false); 22 | // Full matching. 23 | expect(spf.string.contains('attr="value"', 'attr="value"')).toBe(true); 24 | expect(spf.string.contains('attr="value"', 'attr="other"')).toBe(false); 25 | }); 26 | 27 | it('startsWith', function() { 28 | // Character matching. 29 | expect(spf.string.startsWith('?query=arg', '?')).toBe(true); 30 | expect(spf.string.startsWith('?query=arg', '#')).toBe(false); 31 | // Substring matching. 32 | expect(spf.string.startsWith('?query=arg', '?query')).toBe(true); 33 | expect(spf.string.startsWith('?query=arg', '?other')).toBe(false); 34 | // Full matching. 35 | expect(spf.string.startsWith('attr="value"', 'attr="value"')).toBe(true); 36 | expect(spf.string.startsWith('attr="value"', 'attr="other"')).toBe(false); 37 | // Offset matching. 38 | expect(spf.string.startsWith('"use strict";', 'use strict', 1)).toBe(true); 39 | expect(spf.string.startsWith('"use strict";', 'use strict')).toBe(false); 40 | }); 41 | 42 | it('endsWith', function() { 43 | // Character matching. 44 | expect(spf.string.endsWith('file.js', 's')).toBe(true); 45 | expect(spf.string.endsWith('file.js', 't')).toBe(false); 46 | // Substring matching. 47 | expect(spf.string.endsWith('file.js', '.js')).toBe(true); 48 | expect(spf.string.endsWith('file.js', '.txt')).toBe(false); 49 | // Substring matching. 50 | expect(spf.string.endsWith('file.js', 'file.js')).toBe(true); 51 | expect(spf.string.endsWith('file.js', 'file.txt')).toBe(false); 52 | }); 53 | 54 | it('trim', function() { 55 | // No trimming. 56 | expect(spf.string.trim('foo bar')).toEqual('foo bar'); 57 | // Trim leading. 58 | expect(spf.string.trim(' foo bar')).toEqual('foo bar'); 59 | expect(spf.string.trim('\n\nfoo bar')).toEqual('foo bar'); 60 | expect(spf.string.trim('\t\tfoo bar')).toEqual('foo bar'); 61 | expect(spf.string.trim('\r\rfoo bar')).toEqual('foo bar'); 62 | expect(spf.string.trim(' \t \r\n foo bar')).toEqual('foo bar'); 63 | // Trim trailing. 64 | expect(spf.string.trim('foo bar ')).toEqual('foo bar'); 65 | expect(spf.string.trim('foo bar\n\n')).toEqual('foo bar'); 66 | expect(spf.string.trim('foo bar\t\t')).toEqual('foo bar'); 67 | expect(spf.string.trim('foo bar\r\r')).toEqual('foo bar'); 68 | expect(spf.string.trim('foo bar \r\n \t ')).toEqual('foo bar'); 69 | // Trim both. 70 | expect(spf.string.trim(' foo bar ')).toEqual('foo bar'); 71 | expect(spf.string.trim('\n\nfoo bar\n\n')).toEqual('foo bar'); 72 | expect(spf.string.trim('\t\tfoo bar\t\t')).toEqual('foo bar'); 73 | expect(spf.string.trim('\r\rfoo bar\r\r')).toEqual('foo bar'); 74 | expect(spf.string.trim(' \t \r\n foo bar \r\n \t ')).toEqual('foo bar'); 75 | }); 76 | 77 | it('partition', function() { 78 | // No separator. 79 | expect(spf.string.partition('foobar', '|')).toEqual(['foobar', '', '']); 80 | // One separator. 81 | expect(spf.string.partition('foo|bar', '|')).toEqual(['foo', '|', 'bar']); 82 | expect(spf.string.partition('|foobar', '|')).toEqual(['', '|', 'foobar']); 83 | expect(spf.string.partition('foobar|', '|')).toEqual(['foobar', '|', '']); 84 | // Multiple separators. 85 | expect(spf.string.partition('foo|bar|one', '|')).toEqual( 86 | ['foo', '|', 'bar|one']); 87 | expect(spf.string.partition('|foo|bar|one', '|')).toEqual( 88 | ['', '|', 'foo|bar|one']); 89 | expect(spf.string.partition('foo|bar|one|', '|')).toEqual( 90 | ['foo', '|', 'bar|one|']); 91 | }); 92 | 93 | it('hashcode', function() { 94 | expect(function() {spf.string.hashcode(null)}).not.toThrow(); 95 | expect(spf.string.hashcode(null)).toEqual(0); 96 | expect(spf.string.hashcode('')).toEqual(0); 97 | expect(spf.string.hashcode('foo')).toEqual(101574); 98 | expect(spf.string.hashcode('\uAAAAfoo')).toEqual(1301670364); 99 | var repeat = function(n, s) { return (new Array(n + 1)).join(s); }; 100 | expect(spf.string.hashcode(repeat(5, 'a'))).toEqual(92567585); 101 | expect(spf.string.hashcode(repeat(6, 'a'))).toEqual(2869595232); 102 | expect(spf.string.hashcode(repeat(7, 'a'))).toEqual(3058106369); 103 | expect(spf.string.hashcode(repeat(8, 'a'))).toEqual(312017024); 104 | expect(spf.string.hashcode(repeat(1024, 'a'))).toEqual(2929737728); 105 | }); 106 | 107 | it('toSelectorCase', function() { 108 | expect(spf.string.toSelectorCase('OneTwoThree')).toEqual('-one-two-three'); 109 | expect(spf.string.toSelectorCase('oneTwoThree')).toEqual('one-two-three'); 110 | expect(spf.string.toSelectorCase('oneTwo')).toEqual('one-two'); 111 | expect(spf.string.toSelectorCase('one')).toEqual('one'); 112 | expect(spf.string.toSelectorCase('one-two')).toEqual('one-two'); 113 | // String object function name. 114 | expect(spf.string.toSelectorCase('toString')).toEqual('to-string'); 115 | }); 116 | 117 | describe('isString', function() { 118 | 119 | it('evaluates strings', function() { 120 | expect(spf.string.isString('')).toBe(true); 121 | expect(spf.string.isString('foo')).toBe(true); 122 | expect(spf.string.isString(new String())).toBe(true); 123 | expect(spf.string.isString(new String('Foo'))).toBe(true); 124 | }); 125 | 126 | it('evaluates non-strings', function() { 127 | expect(spf.string.isString()).toBe(false); 128 | expect(spf.string.isString(undefined)).toBe(false); 129 | expect(spf.string.isString(null)).toBe(false); 130 | expect(spf.string.isString(50)).toBe(false); 131 | expect(spf.string.isString([])).toBe(false); 132 | expect(spf.string.isString({})).toBe(false); 133 | expect(spf.string.isString({length: 1})).toBe(false); 134 | }); 135 | 136 | }); 137 | 138 | }); 139 | -------------------------------------------------------------------------------- /src/client/stub.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Stubs for functions used in SPF for dependency management. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | 13 | /** @define {boolean} Compiler flag to remove development code. */ 14 | var COMPILED = false; 15 | 16 | 17 | var goog = {}; 18 | 19 | 20 | /** 21 | * @param {string} ns Namespace required in the form "base.package.part". 22 | */ 23 | goog.require = function(ns) {}; 24 | 25 | 26 | /** 27 | * @param {string} ns Namespace provided in the form "base.package.part". 28 | */ 29 | goog.provide = function(ns) { 30 | var parts = ns.split('.'); 31 | var cur = window; 32 | for (var name; parts.length && (name = parts.shift());) { 33 | if (cur[name]) { 34 | cur = cur[name]; 35 | } else { 36 | cur = cur[name] = {}; 37 | } 38 | } 39 | }; 40 | 41 | 42 | /** 43 | * Reference to the global context. In most cases this will be 'window'. 44 | */ 45 | goog.global = this; 46 | 47 | 48 | /** 49 | * Empty function that does nothing. 50 | * 51 | * Used to allow compiler to optimize away functions. 52 | */ 53 | goog.nullFunction = function() {}; 54 | 55 | 56 | /** 57 | * Identity function that returns its first argument. 58 | * 59 | * @param {T=} opt_returnValue The single value that will be returned. 60 | * @param {...*} var_args Optional trailing arguments. These are ignored. 61 | * @return {T} The first argument. 62 | * @template T 63 | */ 64 | goog.identityFunction = function(opt_returnValue, var_args) { 65 | return opt_returnValue; 66 | }; 67 | -------------------------------------------------------------------------------- /src/client/testing/dom.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Helper functions for tests that use the DOM. 8 | * 9 | * @author rviscomi@google.com (Rick Viscomi) 10 | */ 11 | 12 | goog.provide('spf.testing.dom'); 13 | 14 | goog.require('spf.dom'); 15 | 16 | 17 | /** 18 | * Unique identifier for SPF test element tag names. 19 | * 20 | * @const {string} 21 | */ 22 | spf.testing.dom.TAG_NAME = 'spftest'; 23 | 24 | 25 | /** 26 | * Creates a DOM element prepopulated with test data. 27 | * 28 | * @param {string} id The element's unique ID. 29 | * @param {string=} opt_initialHTML Optional inner HTML of the element. 30 | * @param {Object.=} opt_initialAttributes Optional attributes to set 31 | * on the element. 32 | * @return {Element} The newly-created test element. 33 | */ 34 | spf.testing.dom.createElement = function(id, opt_initialHTML, 35 | opt_initialAttributes) { 36 | var element = document.createElement(spf.testing.dom.TAG_NAME); 37 | element.id = id; 38 | element.innerHTML = opt_initialHTML || ''; 39 | if (opt_initialAttributes) { 40 | spf.dom.setAttributes(element, opt_initialAttributes); 41 | } 42 | document.body.appendChild(element); 43 | return element; 44 | }; 45 | 46 | 47 | /** 48 | * Removes all elements with the unique test tag name. 49 | * See {@link #createElement}. 50 | */ 51 | spf.testing.dom.removeAllElements = function() { 52 | var elements = document.getElementsByTagName(spf.testing.dom.TAG_NAME); 53 | // `elements` is a live node list. Removing one of these elements from the DOM 54 | // also removes it from the array. 55 | while (elements.length) { 56 | document.body.removeChild(elements[0]); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/client/testing/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SPF Unit Tests 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/license.js: -------------------------------------------------------------------------------- 1 | /* 2 | SPF 3 | (c) 2012-2017 Google Inc. 4 | https://ajax.googleapis.com/ajax/libs/spf/2.4.0/LICENSE 5 | */ 6 | -------------------------------------------------------------------------------- /src/server/demo/static/app-chunked.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Sample JavaScript for "Chunked Test" area of the SPF demo app. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | 13 | // This file exists only to test various subsystems of the framework. 14 | 15 | var app = app || {}; 16 | 17 | 18 | /** 19 | * The demo app namespace for the chunked page. 20 | * @type {Object} 21 | */ 22 | app.chunked = app.chunked || {}; 23 | 24 | 25 | /** 26 | * Logs to the onscreen page log. 27 | * 28 | * @param {...*} var_args Arguments to log onscreen; they will be converted to 29 | * strings for rendering. 30 | */ 31 | app.chunked.log = function(var_args) { 32 | var args = Array.prototype.slice.call(arguments); 33 | var text = args.join(' ') + '\n'; 34 | var log = document.getElementById('chunked-log'); 35 | log.appendChild(document.createTextNode(text)); 36 | }; 37 | 38 | 39 | /** 40 | * Clears the onscreen page log. 41 | */ 42 | app.chunked.clear = function() { 43 | var log = document.getElementById('chunked-log'); 44 | log.innerHTML = ''; 45 | }; 46 | 47 | 48 | /** 49 | * Formats a chunked test URL. 50 | * @param {string} page The base URL without query params. 51 | * @param {Object=} opt_params The optional map of query params. 52 | * @return {string} The formatted URL. 53 | */ 54 | app.chunked.getRequestUrl = function(page, opt_params) { 55 | var params = {}; 56 | var opt_params = opt_params || {}; 57 | var els = {'chunks-input': 1, 'delay-input': 1}; 58 | for (var id in els) { 59 | var el = document.getElementById(id); 60 | params[el.name] = el.value; 61 | } 62 | for (var k in opt_params) { 63 | params[k] = opt_params[k]; 64 | } 65 | var url = page; 66 | var first = true; 67 | for (var k in params) { 68 | if (first) { 69 | url += '?' + k + '=' + params[k]; 70 | first = false; 71 | } else { 72 | url += '&' + k + '=' + params[k]; 73 | } 74 | } 75 | return url; 76 | }; 77 | 78 | 79 | /** 80 | * Load a regular response sent across a variable number of transfer chunks. 81 | * @param {Object=} opt_params 82 | */ 83 | app.chunked.requestSingle = function(opt_params) { 84 | app.chunked.clear(); 85 | var url = app.chunked.getRequestUrl('/chunked_sample_single', opt_params); 86 | spf.nav.request.send(url, { 87 | onPart: app.chunked.onPart, 88 | onSuccess: app.chunked.onSuccess, 89 | onError: app.chunked.onError 90 | }); 91 | }; 92 | 93 | 94 | /** 95 | * Load a multipart response for 3 partial responses sent across a variable 96 | * number of transfer chunks. 97 | * @param {Object=} opt_params 98 | */ 99 | app.chunked.requestMultipart = function(opt_params) { 100 | app.chunked.clear(); 101 | var url = app.chunked.getRequestUrl('/chunked_sample_multipart', opt_params); 102 | spf.nav.request.send(url, { 103 | onPart: app.chunked.onPart, 104 | onSuccess: app.chunked.onSuccess, 105 | onError: app.chunked.onError 106 | }); 107 | }; 108 | 109 | 110 | /** 111 | * Handle a partial from a multipart response. 112 | * 113 | * @param {string} url The requested URL. 114 | * @param {Object} partial The partial response object from the chunk. 115 | */ 116 | app.chunked.onPart = function(url, partial) { 117 | app.chunked.log('PART RECEIVED', url); 118 | app.chunked.log(JSON.stringify(partial)); 119 | }; 120 | 121 | 122 | /** 123 | * Handle a success. 124 | * 125 | * @param {string} url The requested URL. 126 | * @param {Object} response The response object. 127 | */ 128 | app.chunked.onSuccess = function(url, response) { 129 | app.chunked.log('SUCCESS', url); 130 | app.chunked.log(JSON.stringify(response)); 131 | }; 132 | 133 | 134 | /** 135 | * Handle an error. 136 | * 137 | * @param {string} url The requested URL. 138 | * @param {Error} err The error. 139 | */ 140 | app.chunked.onError = function(url, err) { 141 | app.chunked.log('ERROR', url); 142 | app.chunked.log(err); 143 | }; 144 | 145 | 146 | /** @override **/ 147 | app.shimHandleChunk = spf.nav.request.handleChunkFromXHR_; 148 | /** @private **/ 149 | spf.nav.request.handleChunkFromXHR_ = function() { 150 | app.chunked.log('CHUNK'); 151 | app.shimHandleChunk.apply(null, arguments); 152 | }; 153 | 154 | 155 | /** @override **/ 156 | app.shimHandleCache = spf.nav.request.handleResponseFromCache_; 157 | /** @private **/ 158 | spf.nav.request.handleResponseFromCache_ = function() { 159 | app.chunked.log('CACHE'); 160 | app.shimHandleCache.apply(null, arguments); 161 | }; 162 | -------------------------------------------------------------------------------- /src/server/demo/static/app-demo.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by The MIT License. 5 | See the LICENSE file for details. 6 | */ 7 | 8 | /** 9 | * Stylesheet for "Demo Pages" area of the SPF demo app. 10 | * 11 | * @author nicksay@google.com (Alex Nicksay 12 | */ 13 | 14 | 15 | /* This file only exists to demonstrate external CSS being loaded. */ 16 | #demo-text-external-no { 17 | display: none; 18 | } 19 | #demo-text-external-yes { 20 | display: inline !important; 21 | } 22 | -------------------------------------------------------------------------------- /src/server/demo/static/app-demo.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | /** 7 | * @fileoverview Sample JavaScript for "Demo Pages" area of the SPF demo app. 8 | * 9 | * @author nicksay@google.com (Alex Nicksay) 10 | */ 11 | 12 | 13 | // This file only exists to demonstrate external JS being loaded. 14 | 15 | var app = app || {}; 16 | 17 | /** 18 | * Simple central logging function for the demo app. 19 | * @param {string} msg Message to log. 20 | */ 21 | app.log = function(msg) { 22 | if (window.console) { 23 | window.console.log('[app] ' + msg); 24 | } 25 | }; 26 | 27 | /** 28 | * The namespace for the demo page. 29 | * @type {Object} 30 | */ 31 | app.demo = app.demo || {}; 32 | 33 | // Set a variable to show the external JS is loaded. 34 | /** @type {boolean} */ 35 | app.demo.loaded = true; 36 | 37 | app.log('demo: external javascript'); 38 | -------------------------------------------------------------------------------- /src/server/demo/static/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2012 Google Inc. All rights reserved. 3 | 4 | Use of this source code is governed by The MIT License. 5 | See the LICENSE file for details. 6 | */ 7 | 8 | /** 9 | * Stylesheet for the SPF demo app. 10 | * 11 | * @author nicksay@google.com (Alex Nicksay 12 | */ 13 | 14 | 15 | html, body { 16 | height: 100%; 17 | margin: 0; 18 | padding: 0; 19 | font-family: sans-serif; 20 | line-height: 1.4; 21 | min-width: 45em; 22 | background: #EEE; 23 | } 24 | #masthead { 25 | font-size: 1.25em; 26 | height: 1.2em; 27 | line-height: 1.2; 28 | padding: .4em 1.2em; 29 | background: #FFF; 30 | border-bottom: 1px solid #444; 31 | } 32 | #masthead strong { 33 | font-size: 1.2em; 34 | font-weight: normal; 35 | } 36 | #masthead .info { 37 | float: right; 38 | line-height: 2em; 39 | font-size: .6em; 40 | color: #333; 41 | } 42 | #app-status { 43 | color: #F00; 44 | font-weight: bold; 45 | } 46 | #app-status.enabled { 47 | color: #19B319; 48 | } 49 | #nav { 50 | padding: 0 1em; 51 | background: #FFF; 52 | } 53 | #nav ul, li { 54 | display: inline-block; 55 | list-style: none; 56 | margin: 0; 57 | padding: 0; 58 | text-indent: 0; 59 | } 60 | #nav a { 61 | display: inline-block; 62 | padding: .3em 0.5em; 63 | color: #000; 64 | text-decoration: none; 65 | -webkit-transition: background 300ms linear; 66 | transition: background 300ms linear; 67 | } 68 | #nav a:hover { 69 | text-decoration: underline; 70 | } 71 | #nav.home .nav-home a, 72 | #nav.spec .nav-spec a, 73 | #nav.demo1 .nav-demo1 a, 74 | #nav.demo2 .nav-demo2 a, 75 | #nav.demo3 .nav-demo3 a, 76 | #nav.demo4 .nav-demo4 a, 77 | #nav.demo5 .nav-demo5 a { 78 | background: #EEE; 79 | } 80 | #content { 81 | height: 100%; 82 | position: relative; 83 | } 84 | #content .pane { 85 | position: absolute; 86 | top: 2em; 87 | left: 5%; 88 | width: 80%; 89 | padding: 0 5%; 90 | overflow: hidden; 91 | background: #FFF; 92 | box-shadow: 0 1px 2px rgba(0, 0, 0, .1); 93 | transform: translateZ(0); 94 | } 95 | #footer { 96 | border-top: 1px solid #EEE; 97 | font-size: .8em; 98 | color: #CCC; 99 | padding: .2em .6em; 100 | } 101 | 102 | .spf-animate { 103 | position: relative; 104 | } 105 | .spf-animate .spf-animate-old, 106 | .spf-animate .spf-animate-new { 107 | position: absolute; 108 | top: 0; 109 | left: 0; 110 | width: 100%; 111 | height: 100%; 112 | -webkit-transition: -webkit-transform 400ms ease, opacity 200ms linear; 113 | transition: transform 400ms ease, opacity 200ms linear; 114 | will-change: transform, opacity; 115 | } 116 | /* Transitions fade-out old and fade-in new */ 117 | .spf-animate-start .spf-animate-old, 118 | .spf-animate-end .spf-animate-new { 119 | opacity: 1; 120 | -webkit-transition-delay: 200ms; 121 | transition-delay: 200ms; 122 | } 123 | .spf-animate-start .spf-animate-new, 124 | .spf-animate-end .spf-animate-old { 125 | opacity: 0; 126 | } 127 | /* By default, no movement, just fade */ 128 | .spf-animate-start .spf-animate-old, 129 | .spf-animate-end .spf-animate-new { 130 | -webkit-transform: translate(0%, 0%); 131 | transform: translate(0%, 0%); 132 | } 133 | /* Home/Spec -> Demo, slide in from right */ 134 | .spf-animate-from-home.spf-animate-to-demo.spf-animate-start .spf-animate-new, 135 | .spf-animate-from-spec.spf-animate-to-demo.spf-animate-start .spf-animate-new { 136 | -webkit-transform: translate(150%, 0%); 137 | transform: translate(150%, 0%); 138 | -webkit-transition-delay: 0ms; 139 | transition-delay: 0ms; 140 | } 141 | .spf-animate-from-home.spf-animate-to-demo.spf-animate-end .spf-animate-old, 142 | .spf-animate-from-spec.spf-animate-to-demo.spf-animate-end .spf-animate-old { 143 | -webkit-transform: translate(-150%, 0%); 144 | transform: translate(-150%, 0%); 145 | -webkit-transition-delay: 0ms; 146 | transition-delay: 0ms; 147 | } 148 | /* Demo -> Home/Spec, slide in from left */ 149 | .spf-animate-from-demo.spf-animate-to-home.spf-animate-start .spf-animate-new, 150 | .spf-animate-from-demo.spf-animate-to-spec.spf-animate-start .spf-animate-new { 151 | -webkit-transform: translate(-150%, 0%); 152 | transform: translate(-150%, 0%); 153 | -webkit-transition-delay: 0ms; 154 | transition-delay: 0ms; 155 | } 156 | .spf-animate-from-demo.spf-animate-to-home.spf-animate-end .spf-animate-old, 157 | .spf-animate-from-demo.spf-animate-to-spec.spf-animate-end .spf-animate-old { 158 | -webkit-transform: translate(150%, 0%); 159 | transform: translate(150%, 0%); 160 | -webkit-transition-delay: 0ms; 161 | transition-delay: 0ms; 162 | } 163 | -------------------------------------------------------------------------------- /src/server/demo/templates/base.tmpl: -------------------------------------------------------------------------------- 1 | $def with (content) 2 | 3 | 9 | 10 | 11 | $content.title 12 | 13 | $:content.stylesheet 14 | 15 | 16 |
17 | Structured Page Fragments 18 | 19 | Status: Disabled 20 |      21 | Last refresh: 0s ago 22 | 23 |
24 | 39 |
40 | $:content 41 |
42 | 43 | 44 | 45 | 48 | $:content.javascript 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/server/demo/templates/chunked.tmpl: -------------------------------------------------------------------------------- 1 | $# Copyright 2013 Google Inc. All rights reserved. 2 | $# 3 | $# Use of this source code is governed by The MIT License. 4 | $# See the LICENSE file for details. 5 | 6 | $def _stylesheet(): 7 | 8 | 19 | 20 | $def _attributes(): 21 | { "nav": { "class": "test" } } 22 | 23 | $def _javascript(): 24 | 25 | 26 | 27 | $var title: Chunked Test 28 | $var name: chunked 29 | $var stylesheet: $:_stylesheet() 30 | $var javascript: $:_javascript() 31 | $var attributes: $:_attributes() 32 | 33 |
34 |

Chunked Transfer Tests

35 | 36 |
37 | 38 |

39 | Configuration 40 |
41 | 45 |      46 | 50 |

51 | 52 |

53 | Valid Responses 54 |
55 | 56 | 57 |

58 | 59 |

60 | Malformed Responses (No Error) 61 |
62 | 63 | 64 |

65 | 66 |

67 | Invalid Responses (Error) 68 |
69 | 70 | 71 |

72 | 73 |
74 | 75 |
76 | Log 77 |

78 |   
79 |
80 | -------------------------------------------------------------------------------- /src/server/demo/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | $# Copyright 2012 Google Inc. All rights reserved. 2 | $# 3 | $# Use of this source code is governed by The MIT License. 4 | $# See the LICENSE file for details. 5 | 6 | $def _stylesheet(): 7 | 31 | 32 | $def _javascript(): 33 | 37 | 38 | $def _attributes(): 39 | { "nav": { "class": "home" } } 40 | 41 | $var title: Home 42 | $var name: home 43 | $var stylesheet: $:_stylesheet() 44 | $var javascript: $:_javascript() 45 | $var attributes: $:_attributes() 46 | 47 |
48 |

SPF: Structured Page Fragments

49 | 50 |
51 |

52 | SPF is both a protocol for 53 | representing portions of an HTML 54 | document in JSON 55 | format and a framework to automatically replace traditional navigation 56 | with a dynamic AJAX-based 57 | system to request and load only sections of pages. 58 |

59 |

60 | The SPF framework only uses standard URLs—not fragments—so it 61 | is compatible with traditional navigation for browsers that don't support 62 | the dynamic system. Furthermore, the back button and bookmarking both work 63 | as expected without any changes. 64 |

65 |
66 | 67 |
68 | 75 | 83 |
84 | 85 |
86 |

Regular AJAX Updates Too!

87 | 88 |

89 | An added benefit of the SPF framework is that you can use the same 90 | protocol for standard AJAX updates too! This functionality is supported 91 | by all browsers, even if they don't support the dynamic system. Try it 92 | out below! 93 |

94 | 95 | 96 |
    97 |
    98 | 99 |
    100 | -------------------------------------------------------------------------------- /src/server/demo/templates/index_ajax.tmpl: -------------------------------------------------------------------------------- 1 | $# Copyright 2012 Google Inc. All rights reserved. 2 | $# 3 | $# Use of this source code is governed by The MIT License. 4 | $# See the LICENSE file for details. 5 | 6 | $def _stylesheet(): 7 | 12 | 13 | $def _attributes(): 14 | { "home_ajax_out": { "style": "border: 1px solid blue", 15 | "class": "lucky-num", 16 | "title": "You have a lucky number!" } } 17 | 18 | $def _home_ajax_out(): 19 |
  • Your lucky number is $randint(1, 100).
  • 20 | 21 | $var stylesheet: $:_stylesheet() 22 | $var attributes: $:_attributes() 23 | $var home_ajax_out: $:_home_ajax_out() 24 | -------------------------------------------------------------------------------- /src/server/demo/templates/missing.tmpl: -------------------------------------------------------------------------------- 1 | $# Copyright 2013 Google Inc. All rights reserved. 2 | $# 3 | $# Use of this source code is governed by The MIT License. 4 | $# See the LICENSE file for details. 5 | 6 | $def _attributes(): 7 | { "nav": { "class": "missing" } } 8 | 9 | $var title: Missing 10 | $var name: missing 11 | $var stylesheet: 12 | $var javascript: 13 | $var attributes: $:_attributes() 14 | 15 |
    16 |

    Missing

    17 | 18 |

    The page you were looking for could not be found!

    19 |
    20 | -------------------------------------------------------------------------------- /src/server/demo/templates/other.tmpl: -------------------------------------------------------------------------------- 1 | $def with (referer) 2 | 3 | $# Copyright 2013 Google Inc. All rights reserved. 4 | $# 5 | $# Use of this source code is governed by The MIT License. 6 | $# See the LICENSE file for details. 7 | 8 | 9 | $def _attributes(): 10 | { "nav": { "class": "other" } } 11 | 12 | $var title: Other 13 | $var name: other 14 | $var stylesheet: 15 | $var javascript: 16 | $var attributes: $:_attributes() 17 | 18 |
    19 |

    Other

    20 | 21 |

    This page is the target of a HTTP 303 See Other response.

    22 |
    23 |
    Referer:
    24 |
    $referer
    25 |
    Random Number:
    26 |
    $randint(1, 100)
    27 |
    28 |
    29 | -------------------------------------------------------------------------------- /src/server/demo/templates/spec.tmpl: -------------------------------------------------------------------------------- 1 | $# Copyright 2012 Google Inc. All rights reserved. 2 | $# 3 | $# Use of this source code is governed by The MIT License. 4 | $# See the LICENSE file for details. 5 | 6 | $def _stylesheet(): 7 | 10 | 11 | $def _javascript(): 12 | 15 | 16 | $def _attributes(): 17 | { "nav": { "class": "spec" } } 18 | 19 | $var title: Spec 20 | $var name: spec 21 | $var stylesheet: $:_stylesheet() 22 | $var javascript: $:_javascript() 23 | $var attributes: $:_attributes() 24 | 25 |
    26 |

    Spec

    27 | 28 |
    29 |     {
    30 |       "css":  "<style type=\"text/css\"> CSS Text </style>
    31 |                <link rel=\"stylesheet\" type=\"text/css\" href=\" CSS URL \">
    32 |                ",
    33 |       "attr":  {  " DOM ID ":  {  " Name ": " Value ",
    34 |                                   "": "",
    35 |                                },
    36 |                   
    37 |                },
    38 |       "html":  {  " DOM ID ": " HTML Text ",
    39 |                   " DOM ID ": "",
    40 |                   
    41 |                },
    42 |       "js":  "<script type=\"text/javascript\"> JS Text  </script>
    43 |               <script src=\" JS URL \"></script>
    44 |               ",
    45 |       "title":  " Document Title "
    46 |     }
    47 |   
    48 |
    49 | -------------------------------------------------------------------------------- /src/server/demo/templates/truncated.tmpl: -------------------------------------------------------------------------------- 1 | $# Copyright 2014 Google Inc. All rights reserved. 2 | $# 3 | $# Use of this source code is governed by The MIT License. 4 | $# See the LICENSE file for details. 5 | 6 | $def _attributes(): 7 | { "nav": { "class": "truncated" } } 8 | 9 | $var title: Truncated 10 | $var name: truncated 11 | $var stylesheet: 12 | $var javascript: 13 | $var attributes: $:_attributes() 14 | 15 |
    16 |

    Truncated

    17 | 18 |

    This page is artificially truncated when served as an SPF response to 19 | simulate chunking/multipart serving errors.

    20 |
    21 | -------------------------------------------------------------------------------- /src/server/python/spf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | """Utilities to facilitate handling SPF requests and responses.""" 7 | 8 | __author__ = 'nicksay@google.com (Alex Nicksay)' 9 | 10 | 11 | # pylint: disable=too-few-public-methods 12 | class Header(str): 13 | """String subclass representing a HTTP header.""" 14 | 15 | def cgi_env_var(self): 16 | """Formats the header as a CGI environment variable. 17 | 18 | See https://www.python.org/dev/peps/pep-0333/#environ-variables. 19 | """ 20 | return 'HTTP_' + self.replace('-', '_').upper() 21 | 22 | 23 | # pylint: disable=too-few-public-methods 24 | class HeaderIn(object): 25 | """Enum-like class for incoming HTTP header keys.""" 26 | PREVIOUS = Header('X-SPF-Previous') 27 | REFERER = Header('X-SPF-Referer') 28 | REQUEST = Header('X-SPF-Request') 29 | 30 | 31 | # pylint: disable=too-few-public-methods 32 | class HeaderOut(object): 33 | """Enum-like class for outgoing HTTP header keys.""" 34 | RESPONSE_TYPE = Header('X-SPF-Response-Type') 35 | 36 | 37 | # pylint: disable=too-few-public-methods 38 | class UrlIdentifier(object): 39 | """Enum-like class for the URL identifier.""" 40 | PARAM = 'spf' 41 | 42 | 43 | # pylint: disable=too-few-public-methods 44 | class RequestType(object): 45 | """Enum-like class for request types, as sent with the URL identifier.""" 46 | NAVIGATE = 'navigate' 47 | NAVIGATE_BACK = 'navigate-back' 48 | NAVIGATE_FORWARD = 'navigate-forward' 49 | PREFETCH = 'prefetch' 50 | LOAD = 'load' 51 | 52 | 53 | # pylint: disable=too-few-public-methods 54 | class ResponseKey(object): 55 | """Enum-like class for response object keys.""" 56 | TITLE = 'title' 57 | URL = 'url' 58 | HEAD = 'head' 59 | FOOT = 'foot' 60 | ATTR = 'attr' 61 | BODY = 'body' 62 | NAME = 'name' 63 | REDIRECT = 'redirect' 64 | 65 | 66 | # pylint: disable=too-few-public-methods 67 | class ResponseType(object): 68 | """Enum-like class for response types, for the outgoing HTTP header.""" 69 | MULTIPART = 'multipart' 70 | 71 | 72 | # pylint: disable=too-few-public-methods 73 | class MultipartToken(object): 74 | """Enum-like class for the tokens used in multipart respones.""" 75 | BEGIN = '[\r\n' 76 | DELIMITER = ',\r\n' 77 | END = ']\r\n' 78 | 79 | 80 | def hashcode(string): 81 | """A string hash function for compatibility with spf.string.hashCode. 82 | 83 | This function is similar to java.lang.String.hashCode(). 84 | The hash code for a string is computed as 85 | s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1], 86 | where s[i] is the ith character of the string and n is the length of 87 | the string. We mod the result to make it between 0 (inclusive) and 2^32 88 | (exclusive). 89 | 90 | Args: 91 | string: A string. 92 | 93 | Returns: 94 | Integer hash value for the string, between 0 (inclusive) and 2^32 95 | (exclusive). The empty string and None return 0. 96 | """ 97 | if string is None: 98 | string = '' 99 | result = 0 100 | max_value = 2**32 101 | for char in string: 102 | result = (31 * result) + ord(char) 103 | result %= max_value 104 | return result 105 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | (function(){%output%if(typeof define=='function'&&define.amd)define(spf);else if(typeof exports=='object')for(var f in spf)exports[f]=spf[f];})(); 2 | -------------------------------------------------------------------------------- /third-party/phantomjs/LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions are met: 3 | 4 | * Redistributions of source code must retain the above copyright 5 | notice, this list of conditions and the following disclaimer. 6 | * Redistributions in binary form must reproduce the above copyright 7 | notice, this list of conditions and the following disclaimer in the 8 | documentation and/or other materials provided with the distribution. 9 | * Neither the name of the nor the 10 | names of its contributors may be used to endorse or promote products 11 | derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 22 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /third-party/phantomjs/examples/run-jasmine.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | 3 | /** 4 | * Wait until the test condition is true or a timeout occurs. Useful for waiting 5 | * on a server response or for a ui change (fadeIn, etc.) to occur. 6 | * 7 | * @param testFx javascript condition that evaluates to a boolean, 8 | * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or 9 | * as a callback function. 10 | * @param onReady what to do when testFx condition is fulfilled, 11 | * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or 12 | * as a callback function. 13 | * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. 14 | */ 15 | function waitFor(testFx, onReady, timeOutMillis) { 16 | var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timeout is 3s 17 | start = new Date().getTime(), 18 | condition = false, 19 | interval = setInterval(function() { 20 | if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { 21 | // If not time-out yet and condition not yet fulfilled 22 | condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code 23 | } else { 24 | if(!condition) { 25 | // If condition still not fulfilled (timeout but condition is 'false') 26 | console.log("'waitFor()' timeout"); 27 | phantom.exit(1); 28 | } else { 29 | // Condition fulfilled (timeout and/or condition is 'true') 30 | console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); 31 | typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled 32 | clearInterval(interval); //< Stop this interval 33 | } 34 | } 35 | }, 100); //< repeat check every 100ms 36 | }; 37 | 38 | 39 | if (system.args.length !== 2) { 40 | console.log('Usage: run-jasmine.js URL'); 41 | phantom.exit(1); 42 | } 43 | 44 | var page = require('webpage').create(); 45 | 46 | // Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") 47 | page.onConsoleMessage = function(msg) { 48 | console.log(msg); 49 | }; 50 | 51 | page.open(system.args[1], function(status){ 52 | if (status !== "success") { 53 | console.log("Unable to open " + system.args[1]); 54 | phantom.exit(1); 55 | } else { 56 | waitFor(function(){ 57 | return page.evaluate(function(){ 58 | return document.body.querySelector('.symbolSummary .pending') === null 59 | }); 60 | }, function(){ 61 | var exitCode = page.evaluate(function(){ 62 | try { 63 | console.log(''); 64 | console.log(document.body.querySelector('.description').innerText); 65 | var list = document.body.querySelectorAll('.results > #details > .specDetail.failed'); 66 | if (list && list.length > 0) { 67 | console.log(''); 68 | console.log(list.length + ' test(s) FAILED:'); 69 | for (i = 0; i < list.length; ++i) { 70 | var el = list[i], 71 | desc = el.querySelector('.description'), 72 | msg = el.querySelector('.resultMessage.fail'); 73 | console.log(''); 74 | console.log(desc.innerText); 75 | console.log(msg.innerText); 76 | console.log(''); 77 | } 78 | return 1; 79 | } else { 80 | console.log(document.body.querySelector('.alert > .passingAlert.bar').innerText); 81 | return 0; 82 | } 83 | } catch (ex) { 84 | console.log(ex); 85 | return 1; 86 | } 87 | }); 88 | phantom.exit(exitCode); 89 | }); 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /third-party/phantomjs/examples/run-jasmine2.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | 3 | /** 4 | * Wait until the test condition is true or a timeout occurs. Useful for waiting 5 | * on a server response or for a ui change (fadeIn, etc.) to occur. 6 | * 7 | * @param testFx javascript condition that evaluates to a boolean, 8 | * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or 9 | * as a callback function. 10 | * @param onReady what to do when testFx condition is fulfilled, 11 | * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or 12 | * as a callback function. 13 | * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. 14 | */ 15 | function waitFor(testFx, onReady, timeOutMillis) { 16 | var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timeout is 3s 17 | start = new Date().getTime(), 18 | condition = false, 19 | interval = setInterval(function() { 20 | if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { 21 | // If not time-out yet and condition not yet fulfilled 22 | condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code 23 | } else { 24 | if(!condition) { 25 | // If condition still not fulfilled (timeout but condition is 'false') 26 | console.log("'waitFor()' timeout"); 27 | phantom.exit(1); 28 | } else { 29 | // Condition fulfilled (timeout and/or condition is 'true') 30 | console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); 31 | typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled 32 | clearInterval(interval); //< Stop this interval 33 | } 34 | } 35 | }, 100); //< repeat check every 100ms 36 | }; 37 | 38 | /** 39 | * Exits without triggering "Unsafe javascript attempt" output. See 40 | * {@link https://github.com/ariya/phantomjs/issues/12697} 41 | * 42 | * @param page the Page object 43 | * @param code the exit code 44 | */ 45 | function exit(code) { 46 | setTimeout(function(){ phantom.exit(code); }, 0); 47 | phantom.onError = function(){}; 48 | } 49 | 50 | if (system.args.length !== 2) { 51 | console.log('Usage: run-jasmine2.js URL'); 52 | phantom.exit(1); 53 | } 54 | 55 | var page = require('webpage').create(); 56 | 57 | // Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") 58 | page.onConsoleMessage = function(msg) { 59 | console.log(msg); 60 | }; 61 | 62 | page.open(system.args[1], function(status){ 63 | if (status !== "success") { 64 | console.log("Unable to access network"); 65 | phantom.exit(); 66 | } else { 67 | waitFor(function(){ 68 | return page.evaluate(function(){ 69 | return (document.body.querySelector('.symbolSummary .pending') === null && 70 | document.body.querySelector('.duration') !== null); 71 | }); 72 | }, function(){ 73 | var exitCode = page.evaluate(function(){ 74 | console.log(''); 75 | 76 | var title = 'Jasmine'; 77 | var version = document.body.querySelector('.version').innerText; 78 | var duration = document.body.querySelector('.duration').innerText; 79 | var banner = title + " " + version + " " + duration; 80 | console.log(banner); 81 | 82 | var list = document.body.querySelectorAll('.results > .failures > .spec-detail.failed'); 83 | if (list && list.length > 0) { 84 | console.log(''); 85 | console.log(list.length + ' test(s) FAILED:'); 86 | for (i = 0; i < list.length; ++i) { 87 | var el = list[i], 88 | desc = el.querySelector('.description'), 89 | msg = el.querySelector('.messages > .result-message'); 90 | console.log(''); 91 | console.log(desc.innerText); 92 | console.log(msg.innerText); 93 | console.log(''); 94 | } 95 | return 1; 96 | } else { 97 | console.log(document.body.querySelector('.alert > .bar.passed').innerText); 98 | return 0; 99 | } 100 | }); 101 | exit(exitCode); 102 | }); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /third-party/tracing-framework/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012, Google Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /web/Gemfile: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | source 'https://rubygems.org' 7 | 8 | gem 'autoprefixer-rails' 9 | gem 'jekyll' 10 | gem 'rake' 11 | gem 'sass' 12 | -------------------------------------------------------------------------------- /web/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | autoprefixer-rails (4.0.2.1) 5 | execjs 6 | blankslate (2.1.2.4) 7 | celluloid (0.16.0) 8 | timers (~> 4.0.0) 9 | classifier-reborn (2.0.2) 10 | fast-stemmer (~> 1.0) 11 | coffee-script (2.3.0) 12 | coffee-script-source 13 | execjs 14 | coffee-script-source (1.8.0) 15 | colorator (0.1) 16 | execjs (2.2.2) 17 | fast-stemmer (1.0.2) 18 | ffi (1.9.6) 19 | hitimes (1.2.2) 20 | jekyll (2.5.2) 21 | classifier-reborn (~> 2.0) 22 | colorator (~> 0.1) 23 | jekyll-coffeescript (~> 1.0) 24 | jekyll-gist (~> 1.0) 25 | jekyll-paginate (~> 1.0) 26 | jekyll-sass-converter (~> 1.0) 27 | jekyll-watch (~> 1.1) 28 | kramdown (~> 1.3) 29 | liquid (~> 2.6.1) 30 | mercenary (~> 0.3.3) 31 | pygments.rb (~> 0.6.0) 32 | redcarpet (~> 3.1) 33 | safe_yaml (~> 1.0) 34 | toml (~> 0.1.0) 35 | jekyll-coffeescript (1.0.1) 36 | coffee-script (~> 2.2) 37 | jekyll-gist (1.1.0) 38 | jekyll-paginate (1.1.0) 39 | jekyll-sass-converter (1.3.0) 40 | sass (~> 3.2) 41 | jekyll-watch (1.2.0) 42 | listen (~> 2.7) 43 | kramdown (1.5.0) 44 | liquid (2.6.1) 45 | listen (2.8.3) 46 | celluloid (>= 0.15.2) 47 | rb-fsevent (>= 0.9.3) 48 | rb-inotify (>= 0.9) 49 | mercenary (0.3.5) 50 | parslet (1.5.0) 51 | blankslate (~> 2.0) 52 | posix-spawn (0.3.9) 53 | pygments.rb (0.6.0) 54 | posix-spawn (~> 0.3.6) 55 | yajl-ruby (~> 1.1.0) 56 | rake (10.4.2) 57 | rb-fsevent (0.9.4) 58 | rb-inotify (0.9.5) 59 | ffi (>= 0.5.0) 60 | redcarpet (3.2.2) 61 | safe_yaml (1.0.4) 62 | sass (3.4.9) 63 | timers (4.0.1) 64 | hitimes 65 | toml (0.1.2) 66 | parslet (~> 1.5.0) 67 | yajl-ruby (1.1.0) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | autoprefixer-rails 74 | jekyll 75 | rake 76 | sass 77 | -------------------------------------------------------------------------------- /web/api/class.mustache: -------------------------------------------------------------------------------- 1 | ### {{longname}} 2 | 3 | **Class** 4 | {{#description}} 5 | {{{description}}} 6 | 7 | {{/description}} 8 | {{#hasMembers}} 9 | **Attributes** 10 | {{#members}} 11 | `{{name}}: {{#typesString}}{{{typesString}}}{{/typesString}}`{{#description}} 12 | {{{description}}} {{/description}} 13 | {{/members}} 14 | 15 | {{/hasMembers}} 16 | {{#methods}} 17 | {{> function}} 18 | 19 | {{/methods}} 20 | -------------------------------------------------------------------------------- /web/api/file.mustache: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | description: The JS API reference. 4 | layout: api 5 | --- 6 | 7 | 8 | {{#version}}The following API reference is for **{{version}}**.{{/version}} 9 | 10 | {{#modules}} 11 | 12 | * * * 13 | 14 | {{#longname}} 15 | ## {{longname}} 16 | {{/longname}} 17 | 18 | {{#description}} 19 | {{{description}}} 20 | 21 | {{/description}} 22 | {{#functions}} 23 | {{> function}} 24 | 25 | {{/functions}} 26 | {{#classes}} 27 | {{> class}} 28 | 29 | {{/classes}} 30 | {{/modules}} 31 | -------------------------------------------------------------------------------- /web/api/function.mustache: -------------------------------------------------------------------------------- 1 | ### {{longname}} 2 | 3 | **Function** 4 | `{{longname}}({{#paramsString}}{{paramsString}}{{/paramsString}})` 5 | {{#description}} 6 | {{{description}}} 7 | 8 | {{/description}} 9 | {{#deprecated}} 10 | Deprecated: {{{deprecated}}} 11 | 12 | {{/deprecated}} 13 | {{#hasParams}} 14 | **Parameters** 15 | {{#params}} 16 | `{{name}}: {{#typesString}}{{{typesString}}}{{/typesString}}`{{#description}} 17 | {{{description}}} {{/description}} 18 | {{/params}} 19 | 20 | {{/hasParams}} 21 | {{#returns}} 22 | **Returns** 23 | {{#typesString}}`{{{typesString}}}`{{/typesString}}{{#description}} 24 | {{{description}}} {{/description}} 25 | 26 | {{/returns}} 27 | -------------------------------------------------------------------------------- /web/api/index.mustache: -------------------------------------------------------------------------------- 1 | Functions 2 | 3 | {{#functions}} 4 | {{> function}} 5 | {{/functions}} 6 | 7 | 8 | Classes 9 | 10 | {{#classes}} 11 | {{> class}} 12 | {{/classes}} 13 | -------------------------------------------------------------------------------- /web/api/overview.mustache: -------------------------------------------------------------------------------- 1 | * [{{longname}}]({{{target}}}) 2 | -------------------------------------------------------------------------------- /web/assets/images/animation-dynamic-340x178.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/animation-dynamic-340x178.gif -------------------------------------------------------------------------------- /web/assets/images/animation-static-340x178.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/animation-static-340x178.gif -------------------------------------------------------------------------------- /web/assets/images/banner-728x388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/banner-728x388.jpg -------------------------------------------------------------------------------- /web/assets/images/bg-1600x585.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/bg-1600x585.jpg -------------------------------------------------------------------------------- /web/assets/images/bg-990x320.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/bg-990x320.jpg -------------------------------------------------------------------------------- /web/assets/images/hamburger-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/assets/images/hamburger-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/assets/images/hex-73x84.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/hex-73x84.gif -------------------------------------------------------------------------------- /web/assets/images/logo-black-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/logo-black-48x48.png -------------------------------------------------------------------------------- /web/assets/images/logo-white-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/logo-white-150x150.png -------------------------------------------------------------------------------- /web/assets/images/logo-white-280x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/logo-white-280x280.png -------------------------------------------------------------------------------- /web/assets/images/logo-white-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youtube/spfjs/e05f3e491a2947397162d201d3116caced3829f2/web/assets/images/logo-white-48x48.png -------------------------------------------------------------------------------- /web/assets/scripts/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All rights reserved. 2 | // 3 | // Use of this source code is governed by The MIT License. 4 | // See the LICENSE file for details. 5 | 6 | (function () { 7 | 'use strict'; 8 | 9 | var html = document.getElementsByTagName('html')[0]; 10 | var body = document.body; 11 | var nav = document.getElementById('nav'); 12 | var appbar = document.getElementById('app-bar'); 13 | var menu = document.getElementById('menu'); 14 | var content = document.getElementById('content'); 15 | var progress = document.getElementById('progress'); 16 | 17 | var position = -1; 18 | var start = -1; 19 | var timer = -1; 20 | 21 | // Animation states: start time, duration, progress complete, and css class. 22 | var animation = { 23 | // Most progress waiting for response; duration is 3x expected to 24 | // accommodate slow networks and will be short-circuited by next step. 25 | REQUEST: [0, 300, '95%', 'waiting'], 26 | // Finish during short processing time. 27 | PROCESS: [100, 25, '101%', 'waiting'], 28 | // Fade it out slowly. 29 | DONE: [125, 150, '101%', 'done'] 30 | }; 31 | 32 | html.className = html.className.replace('no-js', ''); 33 | if (!('ontouchstart' in window)) { 34 | html.className = html.className + ' no-touch'; 35 | } 36 | 37 | function closeMenu() { 38 | body.classList.remove('open'); 39 | appbar.classList.remove('open'); 40 | nav.classList.remove('open'); 41 | } 42 | 43 | function toggleMenu() { 44 | body.classList.toggle('open'); 45 | appbar.classList.toggle('open'); 46 | nav.classList.toggle('open'); 47 | } 48 | 49 | function setProgress(anim) { 50 | clearTimeout(timer); 51 | var elapsed = (new Date()).getTime() - start; 52 | var scheduled = anim[0]; 53 | var duration = anim[1]; 54 | var percentage = anim[2]; 55 | var classes = anim[3]; 56 | var wait = scheduled - elapsed; 57 | // Since navigation can often be faster than the animation, 58 | // wait for the last scheduled step of the progress bar to complete 59 | // before finishing. 60 | if (classes == 'done' && wait > 0) { 61 | timer = setTimeout(function() { 62 | setProgress(anim); 63 | }, wait); 64 | return; 65 | } 66 | progress.className = ''; 67 | var ps = progress.style; 68 | ps.transitionDuration = ps.webkitTransitionDuration = duration + 'ms'; 69 | ps.width = percentage; 70 | if (classes == 'done') { 71 | // If done, set the class now to start the fade-out and wait until 72 | // the duration is over (i.e. the fade is complete) to reset the bar 73 | // to the beginning. 74 | progress.className = classes; 75 | timer = setTimeout(function() { 76 | ps.width = '0%'; 77 | }, duration); 78 | } else { 79 | // If waiting, set the class after the duration is over (i.e. the 80 | // bar has finished moving) to set the class and start the pulse. 81 | timer = setTimeout(function() { 82 | progress.className = classes; 83 | }, duration); 84 | } 85 | } 86 | 87 | function clearProgress() { 88 | clearTimeout(timer); 89 | progress.className = ''; 90 | var ps = progress.style; 91 | ps.transitionDuration = ps.webkitTransitionDuration = '0ms'; 92 | ps.width = '0%'; 93 | } 94 | 95 | function handleNavClick(event) { 96 | if (event.target.nodeName === 'A' || event.target.nodeName === 'LI') { 97 | closeMenu(); 98 | } 99 | } 100 | 101 | function handleScroll(event) { 102 | var current = body.scrollTop; 103 | if (current >= 80 && position < 80) { 104 | body.className = body.className + ' scrolled'; 105 | } else if (current < 80 && position >= 80) { 106 | body.className = body.className.replace(' scrolled', ''); 107 | } 108 | position = current; 109 | } 110 | 111 | function handleRequest(event) { 112 | start = (new Date()).getTime(); 113 | setProgress(animation.REQUEST); 114 | } 115 | 116 | function handleProcess(event) { 117 | setProgress(animation.PROCESS); 118 | window.scroll(0,0); 119 | } 120 | 121 | function handleDone(event) { 122 | setProgress(animation.DONE); 123 | handleScroll(); 124 | } 125 | 126 | function handleScriptBeforeUnload(event) { 127 | // If this script is going to be replaced with a new version, 128 | // dispose before the new one is loaded. 129 | if (event.detail.name == 'main') { 130 | dispose(); 131 | } 132 | } 133 | 134 | function init() { 135 | content.addEventListener('click', closeMenu); 136 | menu.addEventListener('click', toggleMenu); 137 | nav.addEventListener('click', handleNavClick); 138 | window.addEventListener('scroll', handleScroll); 139 | 140 | spf.init({ 141 | 'cache-unified': true, 142 | 'url-identifier': '.spf.json' 143 | }); 144 | document.addEventListener('spfrequest', handleRequest); 145 | document.addEventListener('spfprocess', handleProcess); 146 | document.addEventListener('spfdone', handleDone); 147 | document.addEventListener('spfjsbeforeunload', handleScriptBeforeUnload); 148 | } 149 | 150 | function dispose() { 151 | content.removeEventListener('click', closeMenu); 152 | menu.removeEventListener('click', toggleMenu); 153 | nav.removeEventListener('click', handleNavClick); 154 | window.removeEventListener('scroll', handleScroll); 155 | 156 | spf.dispose(); 157 | document.removeEventListener('spfprocess', handleRequest); 158 | document.removeEventListener('spfrequest', handleProcess); 159 | document.removeEventListener('spfdone', handleDone); 160 | document.removeEventListener('spfjsbeforeunload', handleScriptBeforeUnload); 161 | 162 | clearProgress(); 163 | } 164 | 165 | init(); 166 | })(); 167 | -------------------------------------------------------------------------------- /web/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | url: http://youtube.github.io 7 | baseurl: /spfjs 8 | 9 | title: SPF 10 | description: A lightweight JS framework for fast navigation and page updates from YouTube 11 | 12 | release: SPF 24 (v2.4.0) 13 | version: 2.4.0 14 | 15 | exclude: 16 | - Gemfile 17 | - Gemfile.lock 18 | - Rakefile 19 | - _graphics 20 | 21 | permalink: pretty 22 | 23 | markdown: redcarpet 24 | redcarpet: 25 | extensions: [tables, with_toc_data] 26 | 27 | analytics: 28 | id: UA-52278317-1 29 | 30 | defaults: 31 | - 32 | scope: 33 | path: "" # An empty path means all files. 34 | values: 35 | layout: default 36 | - 37 | scope: 38 | path: documentation 39 | values: 40 | layout: documentation 41 | -------------------------------------------------------------------------------- /web/data/sitenav.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | # This data file is used by the sitenav plugin to build a site navigation tree. 7 | # 8 | # Required: 9 | # path - the path component of the nav item 10 | # 11 | # Optional: 12 | # children - the list of nav items below this one 13 | # 14 | # Provided automatically: 15 | # url - full path hierachy of the nav item 16 | # 17 | # Note that the paths for a nav item should be those post-processing, so for 18 | # example, an item path for `page.md` would be `page.html` by default, and if 19 | # `permalink: pretty` is set in the config, it would be `page/`. 20 | # 21 | # If a nav item is a directory (e.g. `path: dir/`) and no corresponding index.md 22 | # file exists, a synthetic one will be generated automatically. These generated 23 | # index files have the following extra parameters: 24 | # 25 | # Optional for generated index files: 26 | # title - a title value (defaults to the capitalized directory name) 27 | # description - a description value 28 | 29 | 30 | - path: / 31 | - path: documentation/ 32 | children: 33 | - path: start/ 34 | - path: responses/ 35 | - path: events/ 36 | - path: resources/ 37 | - path: versioning/ 38 | - path: caching/ 39 | - path: prefetching/ 40 | - path: features/ 41 | description: Learn about SPF features to enable high performance. 42 | children: 43 | - path: multipart/ 44 | - path: api/ 45 | - path: download/ 46 | -------------------------------------------------------------------------------- /web/includes/analytics.liquid: -------------------------------------------------------------------------------- 1 | 7 | 13 | -------------------------------------------------------------------------------- /web/includes/apitoc.md: -------------------------------------------------------------------------------- 1 | Functions 2 | 3 | * [spf.init](#spf.init) 4 | * [spf.dispose](#spf.dispose) 5 | * [spf.navigate](#spf.navigate) 6 | * [spf.load](#spf.load) 7 | * [spf.process](#spf.process) 8 | * [spf.prefetch](#spf.prefetch) 9 | * [spf.cache.remove](#spf.cache.remove) 10 | * [spf.cache.clear](#spf.cache.clear) 11 | * [spf.script.load](#spf.script.load) 12 | * [spf.script.unload](#spf.script.unload) 13 | * [spf.script.get](#spf.script.get) 14 | * [spf.script.ready](#spf.script.ready) 15 | * [spf.script.ignore](#spf.script.ignore) 16 | * [spf.script.done](#spf.script.done) 17 | * [spf.script.require](#spf.script.require) 18 | * [spf.script.unrequire](#spf.script.unrequire) 19 | * [spf.script.declare](#spf.script.declare) 20 | * [spf.script.path](#spf.script.path) 21 | * [spf.script.prefetch](#spf.script.prefetch) 22 | * [spf.style.load](#spf.style.load) 23 | * [spf.style.unload](#spf.style.unload) 24 | * [spf.style.get](#spf.style.get) 25 | * [spf.style.path](#spf.style.path) 26 | * [spf.style.prefetch](#spf.style.prefetch) 27 | 28 | 29 | Classes 30 | 31 | * [spf.SingleResponse](#spf.singleresponse) 32 | * [spf.MultipartResponse](#spf.multipartresponse) 33 | * [spf.RequestOptions](#spf.requestoptions) 34 | * [spf.Event](#spf.event) 35 | * [spf.EventDetail](#spf.eventdetail) 36 | * [spf.TaskScheduler](#spf.taskscheduler) 37 | -------------------------------------------------------------------------------- /web/includes/meta.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ site.title }}{% if page.title and page.title != site.title %} - {{ page.title }}{% endif %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /web/includes/nav.liquid: -------------------------------------------------------------------------------- 1 | {% assign sitenav__list = site.data.sitenav %} 2 | 10 | -------------------------------------------------------------------------------- /web/includes/nextprev.liquid: -------------------------------------------------------------------------------- 1 | {% assign nextprev__item = site.data.sitenav_index[include.path] %} 2 | {% if include.top %} 3 | {% assign nextprev__top = include.top %} 4 | {% else %} 5 | {% assign nextprev__top = "/" %} 6 | {% endif %} 7 | {% capture nextprev__top_length %}{{ nextprev__top | size }}{% endcapture %} 8 | {% assign nextprev__prev = nextprev__item.prev %} 9 | {% assign nextprev__next = nextprev__item.next %} 10 | {% capture nextprev__next_prefix %}{{ nextprev__next.url | truncate:nextprev__top_length,'' }}{% endcapture %} 11 | {% capture nextprev__prev_prefix %}{{ nextprev__prev.url | truncate:nextprev__top_length,'' }}{% endcapture %} 12 | 34 | -------------------------------------------------------------------------------- /web/includes/scripts.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /web/includes/styles.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/includes/toc.liquid: -------------------------------------------------------------------------------- 1 | {% if include.top %} 2 | {% assign toc__top = site.data.sitenav_index[include.top] %} 3 | {% assign toc__list = toc__top.children %} 4 | {% if include.title %} 5 |

    6 | {{ toc__top.page.title }} 7 |

    8 | {% endif %} 9 | {% else %} 10 | {% assign toc__list = site.data.sitenav %} 11 | {% endif %} 12 | 24 | -------------------------------------------------------------------------------- /web/layouts/api.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 |
    6 | 7 | 11 | 12 |
    13 | 21 |
    22 | {{ content }} 23 |
    24 |
    25 | 26 |
    27 | 28 | -------------------------------------------------------------------------------- /web/layouts/base.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include meta.liquid %} 5 | 6 | {% include styles.liquid %} 7 | 8 | 9 | 10 | 11 |
    12 |
    13 | 14 |

    SPF

    15 |
    16 | Top 17 |
    18 |
    19 |
    20 | 21 | 27 | 28 |
    29 | 30 | {{ content }} 31 | 32 |
    33 | 34 |
    35 |
    36 |

    37 | © 2014 Google. 38 | Code licensed under MIT. 39 | Documentation licensed under CC BY 4.0. 40 |

    41 |
    42 |
    43 | 44 |
    45 | 46 | 47 | {% include scripts.liquid %} 48 | {% include analytics.liquid %} 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /web/layouts/default.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 |
    6 | 7 | 15 | 16 |
    17 | {{ content}} 18 |
    19 | 20 |
    21 | -------------------------------------------------------------------------------- /web/layouts/documentation.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 |
    6 | 7 | {% assign page__url = page.url | replace:'index.html','' %} 8 | {% assign sitenav_item = site.data.sitenav_index[page__url] %} 9 | 10 | 22 | 23 | {% if page.index %} 24 | 25 |
    26 |
    27 |

    {{ page.title }}

    28 | {% if page.description %} 29 |

    30 | {{ page.description }} 31 |

    32 | {% endif %} 33 |
    34 | {% assign guide__list = sitenav_item.children %} 35 |
      36 | {% for guide__item in guide__list %} 37 |
    1. 38 |
      39 |

      {{ guide__item.page.title }}

      40 | {% if guide__item.page.description %} 41 |

      {{ guide__item.page.description }}

      42 | {% endif %} 43 |
      44 |
      45 | {% if guide__item.children %} 46 | 47 | 54 | {% endif %} 55 |
      56 |
    2. 57 | {% endfor %} 58 |
    59 |
    60 |
    61 | {% include nextprev.liquid top="/documentation/" path=sitenav_item.url %} 62 |
    63 | 64 | {% else %} 65 | 66 |
    67 | 75 |
    76 | {{ content }} 77 |
    78 | {% include nextprev.liquid top="/documentation/" path=sitenav_item.url %} 79 |
    80 | 81 | {% endif %} 82 | 83 | 84 | 97 | 98 |
    99 | -------------------------------------------------------------------------------- /web/layouts/download.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 |
    6 | 7 | {% assign page__url = page.url | replace:'index.html','' %} 8 | 9 | 19 | 20 | 30 | 31 |
    32 | 54 |
    55 | 56 |
    57 |
    58 | {{ content}} 59 |
    60 |
    61 | 62 |
    63 | -------------------------------------------------------------------------------- /web/layouts/home.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base 3 | --- 4 | 5 |
    6 |
    7 |

    SPF Structured Page Fragments

    8 |

    {{ site.release }}

    9 |
    10 |
    11 | 12 |
    13 |
    14 | 15 | {{ content}} 16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 | 38 |
    39 |
    40 | -------------------------------------------------------------------------------- /web/plugins/mdlinks.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | # Jekyll plugin to convert Markdown ".md" links into Jekyll "pretty" permalinks. 7 | # 8 | # Author:: nicksay@google.com (Alex Nicksay) 9 | 10 | 11 | module Jekyll 12 | 13 | module Converters 14 | 15 | class Markdown < Converter 16 | 17 | @@md_link_pattern = /(:\s+|\()([.\w\/]+)\.md($|#|\))/ 18 | 19 | def process_and_convert(content) 20 | processed_content = content.gsub(@@md_link_pattern) do 21 | m = Regexp.last_match 22 | link = "#{m[2]}/" 23 | if link.start_with?('./') 24 | link = link.sub('./', '../') 25 | elsif link.start_with?('../') 26 | link = "../#{link}" 27 | end 28 | 29 | "#{m[1]}#{link}#{m[3]}" 30 | end 31 | setup 32 | @parser.convert(processed_content) 33 | end 34 | 35 | alias_method :_convert, :convert 36 | alias_method :convert, :process_and_convert 37 | 38 | end 39 | 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /web/plugins/pagetoc.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | # Jekyll plugin to build a page table of contents using Redcarpet's TOC data. 7 | # 8 | # Author:: nicksay@google.com (Alex Nicksay) 9 | 10 | 11 | require 'redcarpet' 12 | 13 | 14 | module Jekyll 15 | 16 | 17 | class Page 18 | 19 | def render_and_generate_toc(payload, layouts) 20 | # Generate the page TOC. 21 | if @content 22 | toc_renderer = Redcarpet::Render::HTML_TOC.new 23 | toc = Redcarpet::Markdown.new(toc_renderer, {}).render(@content) 24 | # Work around a Redcarpet bug 25 | toc = toc.gsub('<em>', '') 26 | toc = toc.gsub('</em>', '') 27 | @data['toc'] = toc 28 | end 29 | # Then call the default render method. 30 | _render(payload, layouts) 31 | end 32 | 33 | alias_method :_render, :render 34 | alias_method :render, :render_and_generate_toc 35 | 36 | end 37 | 38 | 39 | end 40 | -------------------------------------------------------------------------------- /web/plugins/sitenav.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | # Jekyll plugin to build a site navigation tree from directory layout. 7 | # 8 | # Author:: nicksay@google.com (Alex Nicksay) 9 | 10 | 11 | module Jekyll 12 | 13 | 14 | class SiteNavIndexPage < Page 15 | 16 | def initialize(site, base, dir, item) 17 | @site = site 18 | @base = base 19 | @dir = dir 20 | @name = 'index.html' 21 | 22 | process(@name) 23 | 24 | @data = {} 25 | @data.default_proc = proc do |hash, key| 26 | site.frontmatter_defaults.find(File.join(dir, name), type, key) 27 | end 28 | if item.has_key?('title') 29 | @data['title'] = item['title'] 30 | else 31 | @data['title'] = dir.split('/').last.capitalize 32 | end 33 | if item.has_key?('description') 34 | @data['description'] = item['description'] 35 | end 36 | @data['index'] = true 37 | end 38 | 39 | end 40 | 41 | 42 | class SiteNavGenerator < Generator 43 | 44 | safe true 45 | priority :normal 46 | 47 | def link(siblings, parent, pages, sitenav_index, prefix, site) 48 | siblings.each_with_index do |item, index| 49 | url = [prefix, item['path']].join('/').gsub('//', '/') 50 | item['url'] = url 51 | # For directories, create empty index pages if needed. 52 | if url.end_with?('/') and not pages.has_key?(url) 53 | index_page = SiteNavIndexPage.new(site, site.source, url, item) 54 | site.pages << index_page 55 | pages[url] = index_page 56 | index_page.data['original_layout'] = index_page.data['layout'] 57 | end 58 | # Link item -> page. 59 | item['page'] = pages[url] 60 | # Link url -> item. 61 | sitenav_index[url] = item 62 | # Link item -> parent, prev/next, children. 63 | item['parent'] = parent 64 | item['prev'] = siblings[index - 1] unless item == siblings.first 65 | item['next'] = siblings[index + 1] unless item == siblings.last 66 | if item.key?('children') 67 | link(item['children'], item, pages, sitenav_index, url, site) 68 | # Update prev/next to link into and out of children. 69 | item['children'].first['prev'] = item 70 | unless item['children'].last.key?('children') 71 | item['children'].last['next'] = item['next'] 72 | end 73 | item['next'] = item['children'].first 74 | end 75 | end 76 | end 77 | 78 | def generate(site) 79 | pages = {} 80 | site.pages.each do |page| 81 | url = page.url.sub('index.html', '') 82 | pages[url] = page 83 | page.data['original_layout'] = page.data['layout'] 84 | end 85 | sitenav_index = {} 86 | link(site.data['sitenav'], nil, pages, sitenav_index, '', site) 87 | site.data['sitenav_index'] = sitenav_index 88 | end 89 | 90 | end 91 | 92 | 93 | end 94 | -------------------------------------------------------------------------------- /web/plugins/spfjson.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | # Jekyll plugin to generate a SPF JSON version of each page. 7 | # 8 | # Author:: nicksay@google.com (Alex Nicksay) 9 | 10 | 11 | require 'json' 12 | 13 | 14 | module Jekyll 15 | 16 | 17 | class SpfJsonPage < Page 18 | 19 | def initialize(site, base, original) 20 | @site = site 21 | @base = base 22 | # Get the @dir value from the original page, not the dir() output. 23 | @dir = original.instance_variable_get(:@dir) 24 | @name = original.name 25 | 26 | process(@name) 27 | 28 | @content = original.content 29 | @data = original.data 30 | end 31 | 32 | def render(layouts, site_payload) 33 | super 34 | html = @output 35 | 36 | title_regex = /(.*)<\/title>/m 37 | head_regex = /<!-- begin spf head -->(.*)<!-- end spf head -->/m 38 | body_regex = /<!-- begin spf body: (\w+) -->(.*)<!-- end spf body: \1 -->/m 39 | foot_regex = /<!-- begin spf foot -->(.*)<!-- end spf foot -->/m 40 | attr_body_class_regex = /<body[^>]* class="([^"]*)"/ 41 | 42 | title = html.match(title_regex)[1] 43 | head = html.match(head_regex)[1] 44 | body = {} 45 | html.scan(body_regex).each do |group| 46 | body[group[0]] = group[1] 47 | end 48 | foot = html.match(foot_regex)[1] 49 | attr_body_class = html.match(attr_body_class_regex)[1] 50 | attrs = { 51 | 'body' => {'class' => attr_body_class} 52 | } 53 | 54 | response = { 55 | 'title' => title, 56 | 'head' => head, 57 | 'body' => body, 58 | 'attr' => attrs, 59 | 'foot' => foot, 60 | } 61 | # Use JSON.pretty_generate instead of response.to_json or JSON.generate 62 | # to reduce diff sizes during updates, since the files are checked in. 63 | @output = JSON.pretty_generate(response) 64 | end 65 | 66 | # Output a .json file. 67 | def output_ext 68 | '.spf.json' 69 | end 70 | 71 | # Masquerade as an .html file. 72 | def html? 73 | true 74 | end 75 | 76 | # Output index.spf.json files, not index.html files. 77 | def destination(dest) 78 | path = site.in_dest_dir(dest, URL.unescape_path(url)) 79 | path = File.join(path, 'index.spf.json') if url =~ /\/$/ 80 | path 81 | end 82 | 83 | end 84 | 85 | 86 | class SpfJsonPageGenerator < Generator 87 | 88 | safe true 89 | priority :low 90 | 91 | def generate(site) 92 | pages = [] 93 | site.pages.each do |page| 94 | pages << SpfJsonPage.new(site, site.source, page) 95 | end 96 | site.pages.concat(pages) 97 | end 98 | 99 | end 100 | 101 | 102 | end 103 | -------------------------------------------------------------------------------- /web/plugins/staticmd5.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All rights reserved. 2 | # 3 | # Use of this source code is governed by The MIT License. 4 | # See the LICENSE file for details. 5 | 6 | # Jekyll plugin to generate a MD5 hashcode for every static file. 7 | # 8 | # Author:: nicksay@google.com (Alex Nicksay) 9 | 10 | 11 | require 'digest' 12 | 13 | 14 | module Jekyll 15 | 16 | class StaticFile 17 | 18 | def md5 19 | Digest::MD5.file(path).hexdigest 20 | end 21 | 22 | def to_liquid 23 | { 24 | "path" => File.join("", relative_path), 25 | "modified_time" => mtime.to_s, 26 | "extname" => File.extname(relative_path), 27 | "md5" => md5 28 | } 29 | end 30 | 31 | end 32 | 33 | 34 | class StaticMD5Generator < Generator 35 | 36 | safe true 37 | priority :low 38 | 39 | def generate(site) 40 | static_files_index = {} 41 | site.static_files.each do |static_file| 42 | path = File.join("", static_file.relative_path) 43 | static_files_index[path] = static_file 44 | end 45 | site.data['static_files_index'] = static_files_index 46 | end 47 | 48 | end 49 | 50 | 51 | module StaticMD5Filter 52 | 53 | def md5_cgi_url(input) 54 | static_file = @context.registers[:site].data['static_files_index'][input] 55 | "#{input}?md5=#{static_file.md5}" 56 | end 57 | 58 | end 59 | 60 | 61 | end 62 | 63 | 64 | Liquid::Template.register_filter(Jekyll::StaticMD5Filter) 65 | --------------------------------------------------------------------------------