diff --git a/.babelrc b/.babelrc index 0a6ab463b18f..4e36829201d6 100644 --- a/.babelrc +++ b/.babelrc @@ -9,6 +9,7 @@ "es6.blockScoping", "es6.classes", "es6.constants", + "es6.destructuring", "es6.tailCall", "es6.modules", "es6.parameters", diff --git a/.eslintrc b/.eslintrc index 4ecde3079f52..03389868b718 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,12 +1,14 @@ { "parser": "babel-eslint", + "rulePaths": ["build-system/eslint-rules/"], + "plugins": ["eslint-plugin-google-camelcase"], "ecmaFeatures": { "modules": true, "arrowFunctions": true, "blockBindings": true, "forOf": false, - "destructuring": false, - "spread": false + "destructuring": true, + "spread": true }, "env": { "es6": true, @@ -25,7 +27,9 @@ "assert": false, "sinon": true, "sandbox": true, - "context": false + "context": false, + "global": false, + "describes": true }, "rules": { "array-bracket-spacing": [2, "never"], @@ -35,6 +39,7 @@ "computed-property-spacing": [2, "never"], "curly": 2, "dot-location": [2, "property"], + "enforce-private-props": 2, "eol-last": 2, "google-camelcase/google-camelcase": 2, "indent": [2, 2, { "SwitchCase": 1 }], @@ -45,11 +50,16 @@ "ignorePattern": "" }], "no-alert": 2, + "no-array-destructuring": 2, "no-debugger": 2, "no-div-regex": 2, + "no-dupe-keys": 2, + "no-es2015-number-props": 2, "no-eval": 2, + "no-export-side-effect": 2, "no-extend-native": 2, "no-extra-bind": 2, + "no-for-of-statement": 2, "no-implicit-coercion": [2, { "boolean": false }], "no-implied-eval": 2, "no-iterator": 2, @@ -61,6 +71,7 @@ "no-self-compare": 2, "no-sequences": 2, "no-spaced-func": 2, + "no-spread": 2, "no-throw-literal": 2, "no-trailing-spaces": 2, "no-unused-expressions": 0, diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md similarity index 100% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..880c96c587c1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +Please pick a meaningful title for your pull request using sentence case. + +Do not overuse punctuation in the title like `(chore):`. If it is helpful feel free to start with a project name, though, like `ProjectX: Implement some feature`. + +# Title instructions above. + +Enter a succinct description of what is achieved by the PR. Ideally describe why the change is being made. + +Bullet points like + +- Implements aspect X +- Leaves out feature Y because of A +- Improves performance by B + +really help with making this more readable. + +Fixes/Implements/Related-to #1 (enter issue number, except in rare cases where none exists). \ No newline at end of file diff --git a/.gitignore b/.gitignore index a289aef497bd..d945a49f53f2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,14 @@ c dist dist.3p dist.tools -examples.build examples.min node_modules npm-debug.log .idea .tm_properties .settings +.vscode +typings +typings.json build-system/runner/TESTS-TestSuites.xml +/test/manual/amp-ad.adtech.html diff --git a/.travis.yml b/.travis.yml index 1dc6d34dfbf9..8d90fa0c9c38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,30 +18,14 @@ addons: - protobuf-compiler - python-protobuf before_install: - - export CHROME_BIN=chromium-browser + - export CHROME_BIN=google-chrome - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start + - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + - sudo dpkg -i google-chrome*.deb before_script: - - npm install -g gulp - pip install --user protobuf -script: - - gulp lint - - gulp build --fortesting - - gulp check-types - - gulp dist --fortesting - - gulp presubmit - # dep-check needs to occur after build since we rely on build to generate - # the css files into js files. - - gulp dep-check - # Unit tests with Travis' default chromium - - gulp test --compiled --fortesting - # Integration tests with all saucelabs browsers - - gulp test --saucelabs --integration --compiled --fortesting - # All unit tests with an old chrome (best we can do right now to pass tests - # and not start relying on new features). - # Disabled because it regressed. Better to run the other saucelabs tests. - # - gulp test --saucelabs --oldchrome - - gulp validator +script: node build-system/pr-check.js branches: only: - master @@ -51,3 +35,8 @@ branches: env: global: - NPM_CONFIG_PROGRESS="false" + +cache: + yarn: true + directories: + - node_modules diff --git a/3p/3p.js b/3p/3p.js index b57f7559fe04..1642fd49ebc0 100644 --- a/3p/3p.js +++ b/3p/3p.js @@ -24,6 +24,7 @@ import {dev, user} from '../src/log'; import {isArray} from '../src/types'; +import {map} from '../src/utils/object'; import {rethrowAsync} from '../src/log'; @@ -35,7 +36,7 @@ let ThirdPartyFunctionDef; * @const {!Object} * @visibleForTesting */ -export const registrations = {}; +export const registrations = map(); /** @type {number} */ let syncScriptLoads = 0; @@ -45,7 +46,7 @@ let syncScriptLoads = 0; * @param {ThirdPartyFunctionDef} draw Function that draws the 3p integration. */ export function register(id, draw) { - dev.assert(!registrations[id], 'Double registration %s', id); + dev().assert(!registrations[id], 'Double registration %s', id); registrations[id] = draw; } @@ -57,7 +58,7 @@ export function register(id, draw) { */ export function run(id, win, data) { const fn = registrations[id]; - user.assert(fn, 'Unknown 3p: ' + id); + user().assert(fn, 'Unknown 3p: ' + id); fn(win, data); } @@ -83,12 +84,19 @@ export function writeScript(win, url, opt_cb) { * Asynchronously load the given script URL. * @param {!Window} win * @param {string} url - * @param {function()} cb + * @param {function()=} opt_cb + * @param {function()=} opt_errorCb */ -export function loadScript(win, url, cb) { +export function loadScript(win, url, opt_cb, opt_errorCb) { + /** @const {!Element} */ const s = win.document.createElement('script'); s.src = url; - s.onload = cb; + if (opt_cb) { + s.onload = opt_cb; + } + if (opt_errorCb) { + s.onerror = opt_errorCb; + } win.document.body.appendChild(s); } @@ -151,23 +159,6 @@ export function validateSrcContains(string, src) { } } -/** - * Throws a non-interrupting exception if data contains a field not supported - * by this embed type. - * @param {!Object} data - * @param {!Array} allowedFields - */ -export function checkData(data, allowedFields) { - // Throw in a timeout, because we do not want to interrupt execution, - // because that would make each removal an instant backward incompatible - // change. - try { - validateData(data, allowedFields); - } catch (e) { - rethrowAsync(e); - } -} - /** * Utility function to perform a potentially asynchronous task * exactly once for all frames of a given type and the provide the respective @@ -209,15 +200,34 @@ export function computeInMasterFrame(global, taskId, work, cb) { } /** - * Throws an exception if data does not contains a mandatory field. + * Validates given data. Throws an exception if the data does not + * contains a mandatory field. If called with the optional param + * opt_optionalFields, it also validates that the data contains no fields other + * than mandatory and optional fields. + * + * Mandatory fields also accept a string Array as an item. All items in that + * array are considered as alternatives to each other. So the validation checks + * that the data contains exactly one of those alternatives. + * * @param {!Object} data - * @param {!Array} mandatoryFields + * @param {!Array>} mandatoryFields + * @param {Array=} opt_optionalFields */ -export function validateDataExists(data, mandatoryFields) { +export function validateData(data, mandatoryFields, opt_optionalFields) { + let allowedFields = opt_optionalFields || []; for (let i = 0; i < mandatoryFields.length; i++) { const field = mandatoryFields[i]; - user.assert(data[field], - 'Missing attribute for %s: %s.', data.type, field); + if (Array.isArray(field)) { + validateExactlyOne(data, field); + allowedFields = allowedFields.concat(field); + } else { + user().assert(data[field], + 'Missing attribute for %s: %s.', data.type, field); + allowedFields.push(field); + } + } + if (opt_optionalFields) { + validateAllowedFields(data, allowedFields); } } @@ -227,7 +237,7 @@ export function validateDataExists(data, mandatoryFields) { * @param {!Object} data * @param {!Array} alternativeFields */ -export function validateExactlyOne(data, alternativeFields) { +function validateExactlyOne(data, alternativeFields) { let countFileds = 0; for (let i = 0; i < alternativeFields.length; i++) { @@ -237,19 +247,19 @@ export function validateExactlyOne(data, alternativeFields) { } } - user.assert(countFileds === 1, + user().assert(countFileds === 1, '%s must contain exactly one of attributes: %s.', data.type, alternativeFields.join(', ')); } /** - * Throws an exception if data contains a field not supported + * Throws a non-interrupting exception if data contains a field not supported * by this embed type. * @param {!Object} data * @param {!Array} allowedFields */ -export function validateData(data, allowedFields) { +function validateAllowedFields(data, allowedFields) { const defaultAvailableFields = { width: true, height: true, @@ -260,13 +270,38 @@ export function validateData(data, allowedFields) { location: true, mode: true, consentNotificationId: true, + ampSlotIndex: true, }; + for (const field in data) { - if (!data.hasOwnProperty(field) || - field in defaultAvailableFields) { + if (!data.hasOwnProperty(field) || field in defaultAvailableFields) { continue; } - user.assert(allowedFields.indexOf(field) != -1, - 'Unknown attribute for %s: %s.', data.type, field); + if (allowedFields.indexOf(field) < 0) { + // Throw in a timeout, because we do not want to interrupt execution, + // because that would make each removal an instant backward incompatible + // change. + rethrowAsync(new Error(`Unknown attribute for ${data.type}: ${field}.`)); + } } } + +/** @private {!Object} */ +let experimentToggles = {}; + +/** + * Returns true if an experiment is enabled. + * @param {string} experimentId + * @return {boolean} + */ +export function isExperimentOn(experimentId) { + return !!experimentToggles[experimentId]; +} + +/** + * Set experiment toggles. + * @param {!Object} toggles + */ +export function setExperimentToggles(toggles) { + experimentToggles = toggles; +} diff --git a/3p/README.md b/3p/README.md index a224247cc546..d08f395e66f9 100644 --- a/3p/README.md +++ b/3p/README.md @@ -19,7 +19,6 @@ Examples: Youtube, Vimeo videos; Tweets, Instagrams; comment systems; polls; qui - If you can make it not-iframe-based that is much better. (See e.g. the pinterest embed). We will always ask to do this first. E.g. adding a CORS endpoint to your server might make this possible. - Must play well within [AMP's sizing framework](https://github.com/ampproject/amphtml/blob/master/spec/amp-html-layout.md). - All JS on container page must be open source and bundled with AMP. -- Direct iframe embeds not using our 3p iframe mechanism (used e.g. for ads) are preferred. - JavaScript loaded into iframe should be reasonable with respect to functionality. - Use the `sandbox` attribute on iframe if possible. - Provide unit and integration tests. @@ -30,7 +29,7 @@ Examples: Youtube, Vimeo videos; Tweets, Instagrams; comment systems; polls; qui - We welcome pull requests by all ad networks for inclusion into AMP. - All ads and all sub resources must be served from HTTPS. - Must play well within [AMP's sizing framework](https://github.com/ampproject/amphtml/blob/master/spec/amp-html-layout.md). -- Direct iframe embeds not using our 3p iframe mechanism (used by most ads) are preferred. +- For display ads support, always implement amp-ad and instruct your client to use your amp-ad implementation instead of using amp-iframe. Althought amp-iframe will render the ad, ad clicks will break and viewability information is not available. - Providing an optional image only zero-iframe embed is appreciated. - Support viewability and other metrics/instrumentation as supplied by AMP (via postMessage API) - Try to keep overall iframe count at one per ad. Explain why more are needed. @@ -60,4 +59,4 @@ Review the [ads/README](../ads/README.md) for further details on ad integration. - JavaScript can not be involved with the initiation of font loading. - Font loading gets controlled (but not initiated) by [``](https://github.com/ampproject/amphtml/issues/648). - AMP by default does not allow inclusion of external stylesheets, but it is happy to whitelist URL prefixes of font providers for font inclusion via link tags. These link tags and their fonts must be served via HTTPS. -- If a font provider does referrer based "security" it needs to whitelist the AMP proxy origins before being included in the link tag whitelist. AMP proxy sends the appropriate referrer header such as "https://cdn.ampproject.org". +- If a font provider does referrer based "security" it needs to whitelist the AMP proxy origins before being included in the link tag whitelist. AMP proxy sends the appropriate referrer header such as "https://cdn.ampproject.org" and "https://amp.cloudflare.com". diff --git a/3p/ampcontext-lib.js b/3p/ampcontext-lib.js new file mode 100644 index 000000000000..f5a9ef25c873 --- /dev/null +++ b/3p/ampcontext-lib.js @@ -0,0 +1,33 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {AmpContext} from './ampcontext.js'; +import {initLogConstructor} from '../src/log'; +initLogConstructor(); + + +/** + * If window.context does not exist, we must instantiate a replacement and + * assign it to window.context, to provide the creative with all the required + * functionality. + */ +try { + const windowContextCreated = new Event('amp-windowContextCreated'); + window.context = new AmpContext(window); + // Allows for pre-existence, consider validating correct window.context lib instance? + window.dispatchEvent(windowContextCreated); +} catch (err) { + // do nothing with error +} diff --git a/3p/ampcontext.js b/3p/ampcontext.js new file mode 100644 index 000000000000..a8be388e2355 --- /dev/null +++ b/3p/ampcontext.js @@ -0,0 +1,160 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import './polyfills'; +import {dev} from '../src/log'; +import {IframeMessagingClient} from './iframe-messaging-client'; +import {MessageType} from '../src/3p-frame'; +import {tryParseJson} from '../src/json'; + +export class AmpContext { + + /** + * @param {Window} win The window that the instance is built inside. + */ + constructor(win) { + this.win_ = win; + + this.findAndSetMetadata_(); + this.client_ = new IframeMessagingClient(win); + this.client_.setHostWindow(this.getHostWindow_()); + this.client_.setSentinel(this.sentinel); + } + + /** + * Send message to runtime to start sending page visibility messages. + * @param {function(Object)} callback Function to call every time we receive + * a page visibility message. + * @returns {function()} that when called stops triggering the callback + * every time we receive a page visibility message. + */ + observePageVisibility(callback) { + return this.client_.makeRequest( + MessageType.SEND_EMBED_STATE, + MessageType.EMBED_STATE, + callback); + }; + + /** + * Send message to runtime to start sending intersection messages. + * @param {function(Object)} callback Function to call every time we receive + * an intersection message. + * @returns {function()} that when called stops triggering the callback + * every time we receive an intersection message. + */ + observeIntersection(callback) { + return this.client_.makeRequest( + MessageType.SEND_INTERSECTIONS, + MessageType.INTERSECTION, + callback); + }; + + /** + * Send message to runtime requesting to resize ad to height and width. + * This is not guaranteed to succeed. All this does is make the request. + * @param {int} width The new width for the ad we are requesting. + * @param {int} height The new height for the ad we are requesting. + */ + requestResize(width, height) { + this.client_.sendMessage(MessageType.EMBED_SIZE, {width, height}); + }; + + /** + * Allows a creative to set the callback function for when the resize + * request returns a success. The callback should be set before resizeAd + * is ever called. + * @param {function(requestedHeight, requestedWidth)} callback Function + * to call if the resize request succeeds. + */ + onResizeSuccess(callback) { + this.client_.registerCallback(MessageType.EMBED_SIZE_CHANGED, obj => { + callback(obj.requestedHeight, obj.requestedWidth); }); + }; + + /** + * Allows a creative to set the callback function for when the resize + * request is denied. The callback should be set before resizeAd + * is ever called. + * @param {function(requestedHeight, requestedWidth)} callback Function + * to call if the resize request is denied. + */ + onResizeDenied(callback) { + this.client_.registerCallback(MessageType.EMBED_SIZE_DENIED, obj => { + callback(obj.requestedHeight, obj.requestedWidth); + }); + }; + + /** + * Takes the current name on the window, and attaches it to + * the name of the iframe. + * @param {HTMLIframeElement} iframe The iframe we are adding the context to. + */ + addContextToIframe(iframe) { + iframe.name = this.win_.name; + } + + /** + * Parse the metadata attributes from the name and add them to + * the class instance. + * @param {!Object|string} contextData + * @private + */ + setupMetadata_(data) { + data = tryParseJson(data); + if (!data) { + throw new Error('Could not setup metadata.'); + } + const context = data._context; + this.location = context.location; + this.canonicalUrl = context.canonicalUrl; + this.pageViewId = context.pageViewId; + this.sentinel = context.sentinel || context.amp3pSentinel; + this.startTime = context.startTime; + this.referrer = context.referrer; + } + + /** + * Calculate the hostWindow + * @private + */ + getHostWindow_() { + const sentinelMatch = this.sentinel.match(/((\d+)-\d+)/); + dev().assert(sentinelMatch, 'Incorrect sentinel format'); + const depth = Number(sentinelMatch[2]); + const ancestors = []; + for (let win = this.win_; win && win != win.parent; win = win.parent) { + // Add window keeping the top-most one at the front. + ancestors.push(win.parent); + } + return ancestors[(ancestors.length - 1) - depth]; + } + + /** + * Checks to see if there is a window variable assigned with the + * sentinel value, sets it, and returns true if so. + * @private + */ + findAndSetMetadata_() { + // If the context data is set on window, that means we don't need + // to check the name attribute as it has been bypassed. + if (!this.win_.AMP_CONTEXT_DATA) { + this.setupMetadata_(this.win_.name); + } else if (typeof this.win_.AMP_CONTEXT_DATA == 'string') { + this.sentinel = this.win_.AMP_CONTEXT_DATA; + } else { + this.setupMetadata_(this.win_.AMP_CONTEXT_DATA); + } + } +} diff --git a/3p/facebook.js b/3p/facebook.js index c8a7e6ad0c11..83bee00c05d3 100644 --- a/3p/facebook.js +++ b/3p/facebook.js @@ -40,7 +40,7 @@ function getFacebookSdk(global, cb) { */ export function facebook(global, data) { const embedAs = data.embedAs || 'post'; - user.assert(['post', 'video'].indexOf(embedAs) !== -1, + user().assert(['post', 'video'].indexOf(embedAs) !== -1, 'Attribute data-embed-as for value is wrong, should be' + ' "post" or "video" was: %s', embedAs); const fbPost = global.document.createElement('div'); diff --git a/3p/iframe-messaging-client.js b/3p/iframe-messaging-client.js new file mode 100644 index 000000000000..a022a93517fa --- /dev/null +++ b/3p/iframe-messaging-client.js @@ -0,0 +1,129 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {listen} from '../src/event-helper'; +import {map} from '../src/utils/object'; +import {serializeMessage, deserializeMessage} from '../src/3p-frame'; +import {getMode} from '../src/mode'; +import {dev} from '../src/log'; + +export class IframeMessagingClient { + + /** + * @param {!Window} win A window object. + */ + constructor(win) { + /** @private {!Window} */ + this.win_ = win; + /** @private {?string} */ + this.rtvVersion_ = getMode().rtvVersion || null; + /** @private {!Window} */ + this.hostWindow_ = win.parent; + /** @private {?string} */ + this.sentinel_ = null; + /** Map messageType keys to callback functions for when we receive + * that message + * @private {!Object} + */ + this.callbackFor_ = map(); + this.setupEventListener_(); + } + + /** + * Make an event listening request to the host window. + * + * @param {string} requestType The type of the request message. + * @param {string} responseType The type of the response message. + * @param {function(Object)} callback The callback function to call + * when a message with type responseType is received. + */ + makeRequest(requestType, responseType, callback) { + const unlisten = this.registerCallback(responseType, callback); + this.sendMessage(requestType); + return unlisten; + } + + /** + * Register callback function for message with type messageType. + * As it stands right now, only one callback can exist at a time. + * All future calls will overwrite any previously registered + * callbacks. + * @param {string} messageType The type of the message. + * @param {function(Object)} callback The callback function to call + * when a message with type messageType is received. + */ + registerCallback(messageType, callback) { + // NOTE : no validation done here. any callback can be register + // for any callback, and then if that message is received, this + // class *will execute* that callback + this.callbackFor_[messageType] = callback; + return () => { delete this.callbackFor_[messageType]; }; + } + + /** + * Send a postMessage to Host Window + * @param {string} type The type of message to send. + * @param {Object=} opt_payload The payload of message to send. + */ + sendMessage(type, opt_payload) { + this.hostWindow_.postMessage/*OK*/( + serializeMessage( + type, dev().assertString(this.sentinel_), + opt_payload, this.rtvVersion_), + '*'); + } + + /** + * Sets up event listener for post messages of the desired type. + * The actual implementation only uses a single event listener for all of + * the different messages, and simply diverts the message to be handled + * by different callbacks. + * To add new messages to listen for, call registerCallback with the + * messageType to listen for, and the callback function. + * @private + */ + setupEventListener_() { + listen(this.win_, 'message', event => { + // Does it look a message from AMP? + if (event.source != this.hostWindow_) { + return; + } + + const message = deserializeMessage(event.data); + if (!message || message.sentinel != this.sentinel_) { + return; + } + + const callback = this.callbackFor_[message.type]; + if (callback) { + callback(message); + } + }); + } + + /** + * @param {!Window} win + */ + setHostWindow(win) { + this.hostWindow_ = win; + } + + /** + * @param {string} sentinel + */ + setSentinel(sentinel) { + this.sentinel_ = sentinel; + } +} diff --git a/3p/integration.js b/3p/integration.js index a4bbc83fab27..cd1098d6b27f 100644 --- a/3p/integration.js +++ b/3p/integration.js @@ -1,4 +1,4 @@ -/** +/** * Copyright 2015 The AMP HTML Authors. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,119 +23,270 @@ */ import './polyfills'; -import {installEmbedStateListener} from './environment'; +import {installEmbedStateListener, manageWin} from './environment'; +import {nonSensitiveDataPostMessage, listenParent} from './messaging'; +import { + computeInMasterFrame, + nextTick, + register, + run, + setExperimentToggles, +} from './3p'; +import {urls} from '../src/config'; +import {endsWith} from '../src/string'; +import {parseUrl, getSourceUrl, isProxyOrigin} from '../src/url'; +import {dev, initLogConstructor, user} from '../src/log'; +import {getMode} from '../src/mode'; + +// 3P - please keep in alphabetic order +import {facebook} from './facebook'; +import {reddit} from './reddit'; +import {twitter} from './twitter'; + +// 3P Ad Networks - please keep in alphabetic order +import {_ping_} from '../ads/_ping_'; import {a9} from '../ads/a9'; +import {accesstrade} from '../ads/accesstrade'; import {adblade, industrybrains} from '../ads/adblade'; -import {adition} from '../ads/adition'; +import {adbutler} from '../ads/adbutler'; import {adform} from '../ads/adform'; +import {adgeneration} from '../ads/adgeneration'; +import {adition} from '../ads/adition'; import {adman} from '../ads/adman'; import {adreactor} from '../ads/adreactor'; import {adsense} from '../ads/google/adsense'; +import {adsnative} from '../ads/adsnative'; +import {adspirit} from '../ads/adspirit'; +import {adstir} from '../ads/adstir'; import {adtech} from '../ads/adtech'; import {aduptech} from '../ads/aduptech'; -import {plista} from '../ads/plista'; +import {adverline} from '../ads/adverline'; +import {adverticum} from '../ads/adverticum'; +import {advertserve} from '../ads/advertserve'; +import {affiliateb} from '../ads/affiliateb'; +import {amoad} from '../ads/amoad'; +import {appnexus} from '../ads/appnexus'; +import {atomx} from '../ads/atomx'; +import {brainy} from '../ads/brainy'; +import {caajainfeed} from '../ads/caajainfeed'; +import {capirs} from '../ads/capirs'; +import {caprofitx} from '../ads/caprofitx'; +import {chargeads} from '../ads/chargeads'; +import {colombia} from '../ads/colombia'; +import {contentad} from '../ads/contentad'; import {criteo} from '../ads/criteo'; -import {doubleclick} from '../ads/google/doubleclick'; +import {csa} from '../ads/google/csa'; +import {distroscale} from '../ads/distroscale'; +import {ezoic} from '../ads/ezoic'; import {dotandads} from '../ads/dotandads'; -import {endsWith} from '../src/string'; -import {facebook} from './facebook'; +import {doubleclick} from '../ads/google/doubleclick'; +import {eplanning} from '../ads/eplanning'; +import {f1e} from '../ads/f1e'; +import {felmat} from '../ads/felmat'; import {flite} from '../ads/flite'; -import {mantisDisplay, mantisRecommend} from '../ads/mantis'; +import {fusion} from '../ads/fusion'; +import {genieessp} from '../ads/genieessp'; +import {gmossp} from '../ads/gmossp'; +import {holder} from '../ads/holder'; +import {ibillboard} from '../ads/ibillboard'; +import {imobile} from '../ads/imobile'; import {improvedigital} from '../ads/improvedigital'; -import {manageWin} from './environment'; +import {inmobi} from '../ads/inmobi'; +import {ix} from '../ads/ix'; +import {kargo} from '../ads/kargo'; +import {kixer} from '../ads/kixer'; +import {ligatus} from '../ads/ligatus'; +import {loka} from '../ads/loka'; +import {mads} from '../ads/mads'; +import {mantisDisplay, mantisRecommend} from '../ads/mantis'; import {mediaimpact} from '../ads/mediaimpact'; -import {nonSensitiveDataPostMessage, listenParent} from './messaging'; -import {twitter} from './twitter'; -import {yieldmo} from '../ads/yieldmo'; -import {computeInMasterFrame, nextTick, register, run} from './3p'; -import {parseUrl, getSourceUrl} from '../src/url'; -import {appnexus} from '../ads/appnexus'; -import {taboola} from '../ads/taboola'; -import {smartadserver} from '../ads/smartadserver'; -import {sovrn} from '../ads/sovrn'; -import {sortable} from '../ads/sortable'; -import {revcontent} from '../ads/revcontent'; +import {medianet} from '../ads/medianet'; +import {mediavine} from '../ads/mediavine'; +import {meg} from '../ads/meg'; +import {microad} from '../ads/microad'; +import {mixpo} from '../ads/mixpo'; +import {nativo} from '../ads/nativo'; +import {nend} from '../ads/nend'; +import {nokta} from '../ads/nokta'; import {openadstream} from '../ads/openadstream'; import {openx} from '../ads/openx'; -import {triplelift} from '../ads/triplelift'; -import {teads} from '../ads/teads'; +import {plista} from '../ads/plista'; +import {popin} from '../ads/popin'; +import {pubmatic} from '../ads/pubmatic'; +import {pubmine} from '../ads/pubmine'; +import {pulsepoint} from '../ads/pulsepoint'; +import {purch} from '../ads/purch'; +import {revcontent} from '../ads/revcontent'; +import {relap} from '../ads/relap'; import {rubicon} from '../ads/rubicon'; -import {imobile} from '../ads/imobile'; +import {sharethrough} from '../ads/sharethrough'; +import {sklik} from '../ads/sklik'; +import {smartadserver} from '../ads/smartadserver'; +import {smartclip} from '../ads/smartclip'; +import {sortable} from '../ads/sortable'; +import {sovrn} from '../ads/sovrn'; +import {swoop} from '../ads/swoop'; +import {taboola} from '../ads/taboola'; +import {teads} from '../ads/teads'; +import {triplelift} from '../ads/triplelift'; +import {valuecommerce} from '../ads/valuecommerce'; import {webediads} from '../ads/webediads'; -import {pubmatic} from '../ads/pubmatic'; -import {yieldbot} from '../ads/yieldbot'; -import {user} from '../src/log'; -import {gmossp} from '../ads/gmossp'; import {weboramaDisplay} from '../ads/weborama'; -import {adstir} from '../ads/adstir'; -import {colombia} from '../ads/colombia'; -import {sharethrough} from '../ads/sharethrough'; -import {eplanning} from '../ads/eplanning'; -import {microad} from '../ads/microad'; +import {widespace} from '../ads/widespace'; +import {xlift} from '../ads/xlift'; +import {yahoo} from '../ads/yahoo'; import {yahoojp} from '../ads/yahoojp'; -import {chargeads} from '../ads/chargeads'; -import {nend} from '../ads/nend'; +import {yieldbot} from '../ads/yieldbot'; +import {yieldmo} from '../ads/yieldmo'; +import {yieldone} from '../ads/yieldone'; +import {zedo} from '../ads/zedo'; +import {zergnet} from '../ads/zergnet'; +import {zucks} from '../ads/zucks'; /** * Whether the embed type may be used with amp-embed tag. * @const {!Object} */ const AMP_EMBED_ALLOWED = { - taboola: true, + _ping_: true, 'mantis-recommend': true, plista: true, + smartclip: true, + taboola: true, + zergnet: true, }; +// Need to cache iframeName as it will be potentially overwritten by +// masterSelection, as per below. +const iframeName = window.name; +let data = {}; +try { + // TODO(bradfrizzell@): Change the data structure of the attributes + // to make it less terrible. + data = JSON.parse(iframeName).attributes; + window.context = data._context; +} catch (err) { + window.context = {}; + if (!getMode().test) { + dev().info( + 'INTEGRATION', 'Could not parse context from:', iframeName); + } +} + +// This should only be invoked after window.context is set +initLogConstructor(); + +// Experiment toggles +setExperimentToggles(window.context.experimentToggles); +delete window.context.experimentToggles; + +if (getMode().test || getMode().localDev) { + register('_ping_', _ping_); +} + +// Keep the list in alphabetic order register('a9', a9); +register('accesstrade', accesstrade); register('adblade', adblade); -register('adition', adition); +register('adbutler', adbutler); register('adform', adform); +register('adgeneration', adgeneration); +register('adition', adition); register('adman', adman); register('adreactor', adreactor); register('adsense', adsense); +register('adsnative', adsnative); +register('adspirit', adspirit); +register('adstir', adstir); register('adtech', adtech); register('aduptech', aduptech); -register('plista', plista); +register('adverline', adverline); +register('adverticum', adverticum); +register('advertserve', advertserve); +register('affiliateb', affiliateb); +register('amoad', amoad); +register('appnexus', appnexus); +register('atomx', atomx); +register('brainy', brainy); +register('caajainfeed', caajainfeed); +register('capirs', capirs); +register('caprofitx', caprofitx); +register('chargeads', chargeads); +register('colombia', colombia); +register('contentad', contentad); register('criteo', criteo); +register('csa', csa); +register('distroscale', distroscale); +register('dotandads', dotandads); register('doubleclick', doubleclick); -register('appnexus', appnexus); +register('eplanning', eplanning); +register('ezoic', ezoic); +register('f1e', f1e); +register('facebook', facebook); +register('felmat', felmat); register('flite', flite); -register('mantis-display', mantisDisplay); -register('mantis-recommend', mantisRecommend); +register('fusion', fusion); +register('genieessp', genieessp); +register('gmossp', gmossp); +register('holder', holder); +register('ibillboard', ibillboard); +register('imobile', imobile); register('improvedigital', improvedigital); register('industrybrains', industrybrains); -register('taboola', taboola); -register('dotandads', dotandads); -register('yieldmo', yieldmo); -register('_ping_', function(win, data) { - win.document.getElementById('c').textContent = data.ping; -}); -register('twitter', twitter); -register('facebook', facebook); -register('smartadserver', smartadserver); -register('sovrn', sovrn); +register('inmobi', inmobi); +register('ix', ix); +register('kargo', kargo); +register('kixer', kixer); +register('ligatus', ligatus); +register('loka', loka); +register('mads', mads); +register('mantis-display', mantisDisplay); +register('mantis-recommend', mantisRecommend); register('mediaimpact', mediaimpact); -register('revcontent', revcontent); -register('sortable', sortable); +register('medianet', medianet); +register('mediavine', mediavine); +register('meg', meg); +register('microad', microad); +register('mixpo', mixpo); +register('nativo', nativo); +register('nend', nend); +register('nokta', nokta); register('openadstream', openadstream); register('openx', openx); -register('triplelift', triplelift); -register('teads', teads); +register('plista', plista); +register('popin', popin); +register('pubmatic', pubmatic); +register('pubmine', pubmine); +register('pulsepoint', pulsepoint); +register('purch', purch); +register('reddit', reddit); +register('relap', relap); +register('revcontent', revcontent); register('rubicon', rubicon); -register('imobile', imobile); +register('sharethrough', sharethrough); +register('sklik', sklik); +register('smartadserver', smartadserver); +register('smartclip', smartclip); +register('sortable', sortable); +register('sovrn', sovrn); +register('swoop', swoop); +register('taboola', taboola); +register('teads', teads); +register('triplelift', triplelift); +register('twitter', twitter); +register('valuecommerce', valuecommerce); register('webediads', webediads); -register('pubmatic', pubmatic); -register('gmossp', gmossp); register('weborama-display', weboramaDisplay); -register('yieldbot', yieldbot); -register('adstir', adstir); -register('colombia', colombia); -register('sharethrough', sharethrough); -register('eplanning', eplanning); -register('microad', microad); +register('widespace', widespace); +register('xlift' , xlift); +register('yahoo', yahoo); register('yahoojp', yahoojp); -register('chargeads', chargeads); -register('nend', nend); +register('yieldbot', yieldbot); +register('yieldmo', yieldmo); +register('zergnet', zergnet); +register('yieldone', yieldone); +register('zedo', zedo); +register('zucks', zucks); // For backward compat, we always allow these types without the iframe // opting in. @@ -164,14 +315,12 @@ const defaultAllowedTypesInCustomFrame = [ */ export function draw3p(win, data, configCallback) { const type = data.type; - user.assert(win.context.location.originValidated != null, - 'Origin should have been validated'); - user.assert(isTagNameAllowed(data.type, win.context.tagName), + user().assert(isTagNameAllowed(data.type, win.context.tagName), 'Embed type %s not allowed with tag %s', data.type, win.context.tagName); if (configCallback) { configCallback(data, data => { - user.assert(data, + user().assert(data, 'Expected configuration to be passed as first argument'); run(type, win, data); }); @@ -207,6 +356,13 @@ function masterSelection(type) { return master; } +/** + * @return {boolean} Whether this is the master iframe. + */ +function isMaster() { + return window.context.master == window; +} + /** * Draws an embed, optionally synchronously, to the DOM. * @param {function(!Object, function(!Object))} opt_configCallback If provided @@ -223,19 +379,25 @@ window.draw3p = function(opt_configCallback, opt_allowed3pTypes, opt_allowedEmbeddingOrigins) { try { ensureFramed(window); - const data = parseFragment(location.hash); - window.context = data._context; window.context.location = parseUrl(data._context.location.href); validateParentOrigin(window, window.context.location); validateAllowedTypes(window, data.type, opt_allowed3pTypes); if (opt_allowedEmbeddingOrigins) { validateAllowedEmbeddingOrigins(window, opt_allowedEmbeddingOrigins); } - window.context.master = masterSelection(data.type); - window.context.isMaster = window.context.master == window; + // Define master related properties to be lazily read. + Object.defineProperties(window.context, { + master: { + get: () => masterSelection(data.type), + }, + isMaster: { + get: isMaster, + }, + }); window.context.data = data; window.context.noContentAvailable = triggerNoContentAvailable; window.context.requestResize = triggerResizeRequest; + window.context.renderStart = triggerRenderStart; if (data.type === 'facebook' || data.type === 'twitter') { // Only make this available to selected embeds until the @@ -258,16 +420,24 @@ window.draw3p = function(opt_configCallback, opt_allowed3pTypes, window.context.reportRenderedEntityIdentifier = reportRenderedEntityIdentifier; window.context.computeInMasterFrame = computeInMasterFrame; + window.context.addContextToIframe = iframe => { + iframe.name = iframeName; + }; + window.context.getHtml = getHtml; delete data._context; - manageWin(window); installEmbedStateListener(); draw3p(window, data, opt_configCallback); updateVisibilityState(window); - nonSensitiveDataPostMessage('render-start'); + // Subscribe to page visibility updates. + nonSensitiveDataPostMessage('send-embed-state'); + nonSensitiveDataPostMessage('bootstrap-loaded'); } catch (e) { - lightweightErrorReport(e); - throw e; + const c = window.context || {mode: {test: false}}; + if (!c.mode.test) { + lightweightErrorReport(e, c.canary); + throw e; + } } }; @@ -283,6 +453,37 @@ function triggerResizeRequest(width, height) { nonSensitiveDataPostMessage('embed-size', {width, height}); } +/** + * @param {{width, height}=} opt_data + */ +function triggerRenderStart(opt_data) { + nonSensitiveDataPostMessage('render-start', opt_data); +} + +/** + * Id for getHtml postMessage. + * @type {!number} + */ +let currentMessageId = 0; + +/** + * See readme for window.context.getHtml + * @param {!string} selector - CSS selector of the node to take content from + * @param {!Array} attributes - tag attributes to be left in the stringified HTML + * @param {!Function} callback + */ +function getHtml(selector, attributes, callback) { + const messageId = currentMessageId++; + nonSensitiveDataPostMessage('get-html', {selector, attributes, messageId}); + + const unlisten = listenParent(window, 'get-html-result', data => { + if (data.messageId === messageId) { + callback(data.content); + unlisten(); + } + }); +} + /** * Registers a callback for intersections of this iframe with the current * viewport. @@ -352,7 +553,7 @@ function onResizeDenied(observerCallback) { * @param {string} entityId See comment above for content. */ function reportRenderedEntityIdentifier(entityId) { - user.assert(typeof entityId == 'string', + user().assert(typeof entityId == 'string', 'entityId should be a string %s', entityId); nonSensitiveDataPostMessage('entity-id', { id: entityId, @@ -362,8 +563,7 @@ function reportRenderedEntityIdentifier(entityId) { /** * Throws if the current frame's parent origin is not equal to * the claimed origin. - * For browsers that don't support ancestorOrigins it adds - * `originValidated = false` to the location object. + * Only check for browsers that support ancestorOrigins * @param {!Window} window * @param {!Location} parentLocation * @visibleForTesting @@ -374,13 +574,11 @@ export function validateParentOrigin(window, parentLocation) { // ancestorOrigins. In that case we proceed but mark the origin // as non-validated. if (!ancestors || !ancestors.length) { - parentLocation.originValidated = false; return; } - user.assert(ancestors[0] == parentLocation.origin, + user().assert(ancestors[0] == parentLocation.origin, 'Parent origin mismatch: %s, %s', ancestors[0], parentLocation.origin); - parentLocation.originValidated = true; } /** @@ -388,14 +586,16 @@ export function validateParentOrigin(window, parentLocation) { * @param {!Window} window * @param {string} type 3p type * @param {!Array|undefined} allowedTypes May be undefined. - * @visiblefortesting + * @visibleForTesting */ export function validateAllowedTypes(window, type, allowedTypes) { + const thirdPartyHost = parseUrl(urls.thirdParty).hostname; + // Everything allowed in default iframe. - if (window.location.hostname == '3p.ampproject.net') { + if (window.location.hostname == thirdPartyHost) { return; } - if (/^d-\d+\.ampproject\.net$/.test(window.location.hostname)) { + if (urls.thirdPartyFrameRegex.test(window.location.hostname)) { return; } if (window.location.hostname == 'ads.localhost') { @@ -404,7 +604,7 @@ export function validateAllowedTypes(window, type, allowedTypes) { if (defaultAllowedTypesInCustomFrame.indexOf(type) != -1) { return; } - user.assert(allowedTypes && allowedTypes.indexOf(type) != -1, + user().assert(allowedTypes && allowedTypes.indexOf(type) != -1, 'Non-whitelisted 3p type for custom iframe: ' + type); } @@ -412,7 +612,7 @@ export function validateAllowedTypes(window, type, allowedTypes) { * Check that parent host name was whitelisted. * @param {!Window} window * @param {!Array} allowedHostnames Suffixes of allowed host names. - * @visiblefortesting + * @visibleForTesting */ export function validateAllowedEmbeddingOrigins(window, allowedHostnames) { if (!window.document.referrer) { @@ -423,8 +623,7 @@ export function validateAllowedEmbeddingOrigins(window, allowedHostnames) { // nothing. const ancestor = ancestors ? ancestors[0] : window.document.referrer; let hostname = parseUrl(ancestor).hostname; - const onDefault = hostname == 'cdn.ampproject.org'; - if (onDefault) { + if (isProxyOrigin(ancestor)) { // If we are on the cache domain, parse the source hostname from // the referrer. The referrer is used because it should be // trustable. @@ -447,7 +646,7 @@ export function validateAllowedEmbeddingOrigins(window, allowedHostnames) { /** * Throws if this window is a top level window. * @param {!Window} window - * @visiblefortesting + * @visibleForTesting */ export function ensureFramed(window) { if (window == window.parent) { @@ -458,18 +657,22 @@ export function ensureFramed(window) { /** * Expects the fragment to contain JSON. * @param {string} fragment Value of location.fragment - * @return {!JSONType} + * @return {?JSONType} * @visibleForTesting */ export function parseFragment(fragment) { - let json = fragment.substr(1); - // Some browser, notably Firefox produce an encoded version of the fragment - // while most don't. Since we know how the string should start, this is easy - // to detect. - if (json.indexOf('{%22') == 0) { - json = decodeURIComponent(json); + try { + let json = fragment.substr(1); + // Some browser, notably Firefox produce an encoded version of the fragment + // while most don't. Since we know how the string should start, this is easy + // to detect. + if (json.indexOf('{%22') == 0) { + json = decodeURIComponent(json); + } + return /** @type {!JSONType} */ (json ? JSON.parse(json) : {}); + } catch (err) { + return null; } - return /** @type {!JSONType} */ (json ? JSON.parse(json) : {}); } /** @@ -495,10 +698,13 @@ export function isTagNameAllowed(type, tagName) { * too many deps for this small JS binary. * * @param {!Error} e + * @param {boolean} isCanary */ -function lightweightErrorReport(e) { - new Image().src = 'https://amp-error-reporting.appspot.com/r' + +function lightweightErrorReport(e, isCanary) { + new Image().src = urls.errorReporting + '?3p=1&v=' + encodeURIComponent('$internalRuntimeVersion$') + '&m=' + encodeURIComponent(e.message) + - '&r=' + encodeURIComponent(document.referrer); + '&ca=' + (isCanary ? 1 : 0) + + '&r=' + encodeURIComponent(document.referrer) + + '&s=' + encodeURIComponent(e.stack || ''); } diff --git a/3p/messaging.js b/3p/messaging.js index 04c2223f8558..9783a25af229 100644 --- a/3p/messaging.js +++ b/3p/messaging.js @@ -25,7 +25,7 @@ export function nonSensitiveDataPostMessage(type, opt_object) { } const object = opt_object || {}; object.type = type; - object.sentinel = window.context.amp3pSentinel; + object.sentinel = window.context.sentinel || window.context.amp3pSentinel; window.parent./*OK*/postMessage(object, window.context.location.origin); } @@ -67,6 +67,8 @@ function startListening(win) { if (win.AMP_LISTENING) { return; } + win.context.sentinel = win.context.sentinel || + win.context.amp3pSentinel; win.AMP_LISTENING = true; win.addEventListener('message', function(event) { // Cheap operations first, so we don't parse JSON unless we have to. @@ -79,7 +81,7 @@ function startListening(win) { // Parse JSON only once per message. const data = /** @type {!Object} */ ( JSON.parse(event.data.substr(4))); - if (data.sentinel != win.context.amp3pSentinel) { + if (win.context.sentinel && data.sentinel != win.context.sentinel) { return; } // Don't let other message handlers interpret our events. diff --git a/3p/nameframe.max.html b/3p/nameframe.max.html new file mode 100644 index 000000000000..49aed4323388 --- /dev/null +++ b/3p/nameframe.max.html @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/3p/polyfills.js b/3p/polyfills.js index deec963d37f0..f2cab54a79d0 100644 --- a/3p/polyfills.js +++ b/3p/polyfills.js @@ -21,3 +21,8 @@ // This list should not get longer without a very good reason. import '../third_party/babel/custom-babel-helpers'; +import {install as installMathSign} from '../src/polyfills/math-sign'; +import {install as installObjectAssign} from '../src/polyfills/object-assign'; + +installMathSign(self); +installObjectAssign(self); diff --git a/3p/reddit.js b/3p/reddit.js new file mode 100644 index 000000000000..99b16bece4be --- /dev/null +++ b/3p/reddit.js @@ -0,0 +1,85 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript} from './3p'; + +/** + * Get the correct script for the container. + * @param {!Window} global + * @param {string} scriptSource The source of the script, different for post and comment embeds. + */ +function getContainerScript(global, scriptSource) { + loadScript(global, scriptSource, () => {}); +} + +/** + * Embedly looks for a blockquote with a '-card' suffixed class. + * @param {!Window} global + * @return {!Element} blockquote + */ +function getPostContainer(global) { + const blockquote = global.document.createElement('blockquote'); + blockquote.classList.add('reddit-card'); + blockquote.setAttribute('data-card-created', Math.floor(Date.now() / 1000)); + return blockquote; +} + +/** + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getCommentContainer(global, data) { + const div = global.document.createElement('div'); + div.classList.add('reddit-embed'); + div.setAttribute('data-embed-media', 'www.redditmedia.com'); + // 'uuid' and 'created' are provided by the embed script, but don't seem + // to actually be needed. Account for them, but let them default to undefined. + div.setAttribute('data-embed-uuid', data.uuid); + div.setAttribute('data-embed-created', data.embedcreated); + div.setAttribute('data-embed-parent', data.embedparent || 'false'); + div.setAttribute('data-embed-live', data.embedlive || 'false'); + + return div; +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function reddit(global, data) { + const embedtype = data.embedtype || 'post'; + + let container; + let scriptSource = ''; + + // Post and comment embeds are handled totally differently. + if (embedtype === 'post') { + container = getPostContainer(global); + scriptSource = 'https://embed.redditmedia.com/widgets/platform.js'; + } else if (embedtype === 'comment') { + container = getCommentContainer(global, data); + scriptSource = 'https://www.redditstatic.com/comment-embed.js'; + } + + const link = global.document.createElement('a'); + link.href = data.src; + + container.appendChild(link); + global.document.getElementById('c').appendChild(container); + + getContainerScript(global, scriptSource); +} diff --git a/3p/twitter.js b/3p/twitter.js index 4ca40e5d5122..147607e63b02 100644 --- a/3p/twitter.js +++ b/3p/twitter.js @@ -17,6 +17,7 @@ // TODO(malteubl) Move somewhere else since this is not an ad. import {loadScript} from './3p'; +import {setStyles} from '../src/style'; /** * Produces the Twitter API object for the passed in callback. If the current @@ -50,10 +51,12 @@ function getTwttr(global, cb) { export function twitter(global, data) { const tweet = global.document.createElement('div'); tweet.id = 'tweet'; - tweet.style.width = '100%'; - tweet.style.display = 'flex'; - tweet.style.alignItems = 'center'; - tweet.style.justifyContent = 'center'; + setStyles(tweet, { + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }); global.document.getElementById('c').appendChild(tweet); getTwttr(global, function(twttr) { // Dimensions are given by the parent frame. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 606a195bad1e..807d5b5f9ac0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,10 @@ limitations under the License. ## Contributing to AMP HTML +The AMP HTML project strongly encourages technical [contributions](https://www.ampproject.org/contribute/)! + +We hope you'll become an ongoing participant in our open source community but we also welcome one-off contributions for the issues you're particularly passionate about. + ### Filing Issues **Suggestions** @@ -28,14 +32,32 @@ As with all pieces of software, you may end up running into bugs. Please submit The best bug reports include a detailed way to predictably reproduce the issue, and possibly even a working example that demonstrates the issue. -### Contributing Code +### Ongoing Participation + +We actively encourage ongoing participation by community members. -The AMP HTML project accepts and greatly appreciates contributions. The project follows the [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) model for accepting contributions. +* **Discussions** for implementations, designs, etc. often happen in the [amphtml-discuss Google Group](https://groups.google.com/forum/#!forum/amphtml-discuss) and the [amphtml Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877). +* **Weekly status updates** from individual community members are posted as GitHub issues labeled [Type: Weekly Status](https://github.com/ampproject/amphtml/issues?q=label%3A%22Type%3A+Weekly+Status%22). If you have a weekly status update related to your work on AMP that you'd like to share with the community please add it as a comment on the relevant Weekly Status issue. +* **Weekly design reviews** are held as video conferences via Google Hangouts on Wednesdays at [1pm Pacific](https://www.google.com/?#q=1pm+pacific+in+local+time). Design reviews are used to discuss/refine engineering designs after an initial draft of the design has been created and shared with the community. The design reviews are meant as an *optional* venue for concentrated feedback and discussion. **Going through this design review is *not* required to make a contribution to AMP.** + * We use GitHub issues labeled [Type: Design Review](https://github.com/ampproject/amphtml/issues?q=label%3A%22Type%3A+Design+Review%22) to track design reviews. The Design Review issue for a given week will have a link to the design docs being discussed that week as well as a link to the Hangout. + * When you attend a design review please read through the design docs before the review starts. + * If you have an engineering design you would like to discuss at a design review: + * Create a software design document in a shared Google Document open to public comments. A short design doc is fine as long as it covers your design in sufficient detail to allow for a review by other members of the community. Take a look at [Design docs - A design doc](https://medium.com/@cramforce/design-docs-a-design-doc-a152f4484c6b) for tips on putting together a good design doc. Some examples: + * [Phone call tracking in AMP](https://docs.google.com/document/d/1UDMYv0f2R9CvMUSBQhxjtkSnC4984t9dJeqwm_8WiAM/edit) + * [New AMP Boilerplate](https://docs.google.com/document/d/1gZFaKvcDffceJNaI3bYfuYPtYU5u2y6UhE5wBPTsJ9w/edit) + * Perform a design pre-review with at least one [core committer](https://github.com/ampproject/amphtml/blob/master/GOVERNANCE.md); you can request a pre-review in the [#design-review Slack channel](https://amphtml.slack.com/messages/design-review/). It is fine to request a pre-review before your design doc is complete. + * When your design is ready to be discussed at a design review add a comment on the appropriate Design Review GitHub issue. Post a link to the design doc and a brief summary by **1pm Pacific Monday** on the week of your design review. + +### Contributing Code + +The AMP HTML project accepts and greatly appreciates contributions. The project follows the [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) model for accepting contributions. When contributing code, please also include appropriate tests as part of the pull request, and follow the same comment and coding style as the rest of the project. Take a look through the existing code for examples of the testing and style practices the project follows. A key feature of the AMP HTML project is performance - all pull requests will be analyzed for any performance impact, and the project greatly appreciates ways it can get even faster. Please include any measured performance impact with substantial pull requests. +* We follow [Google's JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html). + **Google Individual Contributor License** Code contributors to the AMP HTML project must sign a Contributor License Agreement, either for an [individual](https://developers.google.com/open-source/cla/individual) or [corporation](https://developers.google.com/open-source/cla/corporate). The CLA is meant to protect contributors, users of the AMP HTML runtime, and Google in issues of intellectual property. @@ -43,10 +65,11 @@ Code contributors to the AMP HTML project must sign a Contributor License Agreem ### Contributing Features All pull requests for new features must go through the following process: -* Intent-to-implement GitHub issue started for discussion +* Please familiarize yourself with the [AMP Design Principles](DESIGN_PRINCIPLES.md) +* Start an Intent-to-implement GitHub issue for discussion of the new feature. * LGTM from Tech Lead and one other core committer is required * Development occurs on a separate branch of a separate fork, noted in the intent-to-implement issue -* A pull request is created, referencing the issue. Once the PR is ready, please add the "NEEDS REVIEW" label. +* A pull request is created, referencing the issue. * AMP HTML developers will provide feedback on pull requests, looking at code quality, style, tests, performance, and directional alignment with the goals of the project. That feedback should be discussed and incorporated * LGTM from Tech Lead and one other core committer, who confirm engineering quality and direction. @@ -54,7 +77,7 @@ All pull requests for new features must go through the following process: A key feature of the AMP HTML project is its extensibility - it is meant to support “Extended Components” that provide first-class support for additional rich features. The project currently accepts pull requests to include these types of extended components. -Because Extended Components may have significant impact on AMP HTML performance, security, and usage, Extended Component contributions will be very carefully analyzed and scrutinized. +Because Extended Components may have significant impact on AMP HTML performance, security, and usage, Extended Component contributions will be very carefully analyzed and scrutinized. In particular we strive to design the overall component set, so that a large number of use cases can be composed from them. Instead of creating a new component it may thus be a better solution to combine existing components to a similar effect. diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 65a2c35885c6..16c7edb242da 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -2,9 +2,12 @@ # Name Avi Mehta +David Sedano Dima Voytenko Erwin Mombay +Greg Grothaus Jake Moening +Johannes Henkel Jordan Adler Justin Ridgewell Kent Brewster diff --git a/DESIGN_PRINCIPLES.md b/DESIGN_PRINCIPLES.md new file mode 100644 index 000000000000..f16e06e515f9 --- /dev/null +++ b/DESIGN_PRINCIPLES.md @@ -0,0 +1,40 @@ +# AMP Design Principles + +These design principles are meant to guide the ongoing design and development of AMP. They should help us make internally consistent decisions. + + +## User Experience > Developer Experience > Ease of Implementation. + +When in doubt, do what’s best for the end user experience, even if it means that it’s harder for the page creator to build or for the library developer to implement. + + +## Don’t design for a hypothetical faster future browser. + +We’ve chosen to build AMP as a library in the spirit of the [extensible web manifesto](https://github.com/extensibleweb/manifesto/blob/master/README.md) to be able to fix the web of today, not the web of tomorrow. + +AMP should be fast in today's browsers. When certain optimizations aren't possible with today's platform, AMP developers should participate in standards development to get these added to the web platform. + + +## Don’t break the web. + +Ensure that if AMP has outages or problems it doesn’t hurt the rest of the web. That means if the Google AMP Cache, the URL API or the library fails it should be possible for websites and consumption apps to gracefully degrade. If something works with an AMP cache it should also work without a cache. + + +## Solve problems on the right layer. + +E.g. don’t integrate things on the client side, just because that is easier, when the user experience would be better with a server side integration. + + +## Only do things if they can be made fast. + +Don’t introduce components or features to AMP that can’t reliably run at 60fps or hinder the instant load experience on today’s most common mobile devices. + + +## Prioritise things that improve the user experience – but compromise when needed. + +Some things can be made fast and are still a terrible user experience. AMPs should deliver a fantastic user experience and speed is just one part of that. Only compromise when lack of support for something would stop AMP from being widely used and deployed. + + +## No whitelists. + +We won’t give any special treatment to specific sites, domains or origins except where needed for security or performance reasons. diff --git a/DEVELOPING.md b/DEVELOPING.md index 09e2398bb24b..c2ef66118ded 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -24,9 +24,9 @@ We discuss implementation issues on [amphtml-discuss@googlegroups.com](https://g For more immediate feedback, [sign up for our Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877). -### Starter issues +### Great First Issues -We're curating a [list of GitHub "starter issues"](https://github.com/ampproject/amphtml/issues?q=is%3Aopen+is%3Aissue+label%3Astarter) of small to medium complexity that are great to jump into development on AMP. +We're curating a [list of GitHub "great first issues"](https://github.com/ampproject/amphtml/labels/Great%20First%20Issues) of small to medium complexity that are great to jump into development on AMP. If you have any questions, feel free to ask on the issue or join us on [Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877)! @@ -34,37 +34,42 @@ If you have any questions, feel free to ask on the issue or join us on [Slack](h 1. Install [NodeJS](https://nodejs.org). 2. In the repo directory, run `npm i` command to install the required npm packages. -3. run `sudo npm i -g gulp` command to install gulp in your local bin folder ('/usr/local/bin/' on Mac). -4. `edit /etc/hosts` and map `ads.localhost` and `iframe.localhost` to `127.0.0.1`. +3. Run `npm i -g gulp` command to install gulp system-wide (on Mac or Linux you may need to prefix this with `sudo`, depending on how Node was installed). +4. Edit your hosts file (`/etc/hosts` on Mac or Linux, `%SystemRoot%\System32\drivers\etc\hosts` on Windows) and map `ads.localhost` and `iframe.localhost` to `127.0.0.1`.
   127.0.0.1               ads.localhost iframe.localhost
 
### Build & Test -| Command | Description | -| ----------------------------- | --------------------------------------------------------------------- | -| **`gulp`** | Runs "watch" and "serve". Use this for standard local dev. | -| `gulp dist` | Builds production binaries. | -| `gulp dist --fortesting` | Indicates the production binaries are used for local testing. Without this ads, tweets and similar use cases are expected to break locally when using minified sources.| -| `gulp lint` | Validates against Google Closure Linter. | -| `gulp lint --watch` | Watches for changes in files, Validates against Google Closure Linter.| -| `gulp lint --fix` | Fixes simple lint warnings/errors automatically. | -| `gulp build` | Builds the AMP library. | -| `gulp clean` | Removes build output. | -| `gulp css` | Recompile css to build directory. | -| `gulp extensions` | Build AMP Extensions. | -| `gulp watch` | Watches for changes in files, re-build. | -| `gulp test` | Runs tests in Chrome. | -| `gulp test --verbose` | Runs tests in Chrome with logging enabled. | -| `gulp test --watch` | Watches for changes in files, runs corresponding test(s) in Chrome. | -| `gulp test --watch --verbose` | Same as "watch" with logging enabled. | -| `gulp test --saucelabs` | Runs test on saucelabs (requires [setup](#saucelabs)). | -| `gulp test --safari` | Runs tests in Safari. | -| `gulp test --firefox` | Runs tests in Firefox. | -| `gulp test --files=` | Runs specific test files. | -| `gulp serve` | Serves content in repo root dir over http://localhost:8000/. Examples live in http://localhost:8000/examples.build/ | - +| Command | Description | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------- | +| **`gulp`**[[1]](#footnote-1) | Runs "watch" and "serve". Use this for standard local dev. | +| `gulp dist`[[1]](#footnote-1) | Builds production binaries. | +| `gulp dist --fortesting`[[1]](#footnote-1) | Indicates the production binaries are used for local testing. Without this ads, tweets and similar use cases are expected to break locally when using minified sources. | +| `gulp lint` | Validates against Google Closure Linter. | +| `gulp lint --watch` | Watches for changes in files, Validates against Google Closure Linter.| +| `gulp lint --fix` | Fixes simple lint warnings/errors automatically. | +| `gulp build`[[1]](#footnote-1) | Builds the AMP library. | +| `gulp build --fortesting`[[1]](#footnote-1) | Builds the AMP library and will read the AMP_TESTING_HOST environment variable to write out an override AMP_CONFIG. | +| `gulp build --css-only`[[1]](#footnote-1) | Builds only the embedded css into js files for the AMP library. | +| `gulp clean` | Removes build output. | +| `gulp css` | Recompile css to build directory. | +| `gulp extensions` | Build AMP Extensions. | +| `gulp watch`[[1]](#footnote-1) | Watches for changes in files, re-build. | +| `gulp test`[[1]](#footnote-1) | Runs tests in Chrome. | +| `gulp test --verbose`[[1]](#footnote-1) | Runs tests in Chrome with logging enabled. | +| `gulp test --nobuild` | Runs tests without re-build. | +| `gulp test --watch`[[1]](#footnote-1) | Watches for changes in files, runs corresponding test(s) in Chrome. | +| `gulp test --watch --verbose`[[1]](#footnote-1) | Same as "watch" with logging enabled. | +| `gulp test --saucelabs`[[1]](#footnote-1) | Runs test on saucelabs (requires [setup](#saucelabs)). | +| `gulp test --safari`[[1]](#footnote-1) | Runs tests in Safari. | +| `gulp test --firefox`[[1]](#footnote-1) | Runs tests in Firefox. | +| `gulp test --files=`[[1]](#footnote-1) | Runs specific test files. | +| `gulp serve` | Serves content in repo root dir over http://localhost:8000/. Examples live in http://localhost:8000/examples/ | +| `npm run ava`[[1]](#footnote-1) | Run node tests for tasks and offline/node code using [ava](https://github.com/avajs/ava). | + +[1] On Windows, this command must be run as administrator. #### Saucelabs @@ -79,19 +84,21 @@ Also for local testing, download [saucelabs connect](https://docs.saucelabs.com/ If your pull request contains JS or CSS changes and it does not change the build system, it will be automatically built and tested on [Travis](https://travis-ci.org/ampproject/amphtml/builds). After the travis run completes, the result will be logged to your PR. -If a test flaked on a pull request you can ask a project owner to restart the tests for you. +If a test flaked on a pull request you can ask a project owner to restart the tests for you. Use [`this.retries(x)`](https://mochajs.org/#retry-tests) as the last resort. ### Manual testing +The below assume you ran `gulp` in a terminal. + #### Examples -The content in the `examples` directory can be reached at: http://localhost:8000/examples.build/ +The content in the `examples` directory can be reached at: http://localhost:8000/examples/ -For each example there are 3 files: +For each example there are 3 modes: -- Original name: This points to prod. This file would not reflect your local changes. -- `.max.html` points to your local unminified AMP. You want to use this during normal dev. -- `.min.html` points to a local minified AMP. This is closer to the prod setup. Only available after running `gulp dist`. +- `/examples/abc.html` points to prod. This file would not reflect your local changes. +- `/examples/abc.max.html` points to your local unminified AMP. You want to use this during normal dev. +- `/examples/abc.min.html` points to a local minified AMP. This is closer to the prod setup. Only available after running `gulp dist --fortesting`. #### Document proxy @@ -105,8 +112,43 @@ You can access is with the local JS at - normal sources: http://localhost:8000/max/output.jsbin.com/pegizoq/quiet - minified: http://localhost:8000/min/output.jsbin.com/pegizoq/quiet +When accessing `min` urls make sure you run `gulp dist` with the `--fortesting` +flag so that we do not strip out the localhost code paths. (We do some +code elimination to trim down the file size for the file we deploy to production) + If the origin resource is on HTTPS, the URLs are http://localhost:8000/max/s/output.jsbin.com/pegizoq/quiet and http://localhost:8000/min/s/output.jsbin.com/pegizoq/quiet + +#### A4A envelope + +AMP ships with a local A4A envelope for testing local and production AMP documents with the local JS version. + +A4A can be run either of these two modes: + +1. Friendly iframe mode: http://localhost:8000/a4a/... +2. 3p iframe mode: http://localhost:8000/a4a-3p/... + +The following forms are supported: + +- local document: http://localhost:8000/a4a[-3p]/examples/animations.amp.max.html +- proxied document with normal sources: http://localhost:8000/a4a[-3p]/max/output.jsbin.com/pegizoq/quiet +- proxied document with minified sources: http://localhost:8000/a4a[-3p]/min/output.jsbin.com/pegizoq/quiet + +When accessing `min` urls make sure you run `gulp dist` with the `--fortesting` +flag so that we do not strip out the localhost code paths. (We do some +code elimination to trim down the file size for the file we deploy to production) + +If the origin resource is on HTTPS, the URLs are http://localhost:8000/a4a[-3p]/max/s/output.jsbin.com/pegizoq/quiet and http://localhost:8000/a4a[-3p]/min/s/output.jsbin.com/pegizoq/quiet + +Notice that all documents are assumed to have a "fake" signature. Thus, this functionality is only available in the +`localDev` mode. + +Additionally, the following query parameters can be provided: + +- `width` - the width of the `amp-ad` (default "300") +- `height` - the height of the `amp-ad` (default "250") + + #### Chrome extension For testing documents on arbitrary URLs with your current local version of the AMP runtime we created a [Chrome extension](testing/local-amp-chrome-extension/README.md). @@ -115,6 +157,13 @@ For testing documents on arbitrary URLs with your current local version of the A For deploying and testing local AMP builds on [HEROKU](https://www.heroku.com/) , please follow the steps outlined in this [document](https://docs.google.com/document/d/1LOr8SEBEpLkqnFjzTNIZGi2VA8AC8_aKmDVux6co63U/edit?usp=sharing). +Meantime, you can also use our automatic build on Heroku [link](http://amphtml-nightly.herokuapp.com/), which is normally built with latest head on master branch (please allow delay). The first time load is normally slow due to Heroku's free account throttling policy. + +To correctly get ads and third party working when testing on hosted services +you will need set the `AMP_TESTING_HOST` environment variable. (On heroku this +is done through +`heroku config:set AMP_TESTING_HOST=my-heroku-subdomain.herokuapp.com`) + ## Repository Layout
   3p/             - Implementation of third party sandbox iframes.
@@ -131,8 +180,6 @@ For deploying and testing local AMP builds on [HEROKU](https://www.heroku.com/)
                     This is what gets deployed to 3p.ampproject.net.
   docs/           - documentation
   examples/       - example AMP HTML files and corresponding assets
-  examples.build/ - (generated) Same as examples with files pointing to the
-                    local AMP.
   extensions/     - plugins which extend the AMP HTML runtime's core set of tags
   spec/           - The AMP HTML Specification files
   src/            - source code for the AMP runtime
@@ -150,8 +197,10 @@ In particular, we try to maintain "it might not be perfect but isn't broken"-sup
 
 ## Eng docs
 
+- [Design Principles](DESIGN_PRINCIPLES.md)
 - [Life of an AMP *](https://docs.google.com/document/d/1WdNj3qNFDmtI--c2PqyRYrPrxSg2a-93z5iX0SzoQS0/edit#)
 - [AMP Layout system](spec/amp-html-layout.md)
+- [Building an AMP Extension](https://docs.google.com/document/d/19o7eDta6oqPGF4RQ17LvZ9CHVQN53whN-mCIeIMM8Qk/edit#)
 
 We also recommend scanning the [spec](spec/). The non-element part should help understand some of the design aspects.
 
diff --git a/OWNERS.yaml b/OWNERS.yaml
new file mode 100644
index 000000000000..c2cd8c87b81e
--- /dev/null
+++ b/OWNERS.yaml
@@ -0,0 +1 @@
+- cramforce
diff --git a/Procfile b/Procfile
index 1889883f9ba3..48d242edbd33 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-web: gulp serve
+web: gulp serve --host=0.0.0.0
diff --git a/README.md b/README.md
index d9e7d7f6c77e..a5f9cc06b967 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,6 @@ limitations under the License.
 -->
 
 [![Build Status](https://travis-ci.org/ampproject/amphtml.svg?branch=master)](https://travis-ci.org/ampproject/amphtml)
-[![Issue Stats](http://issuestats.com/github/ampproject/amphtml/badge/pr)](http://issuestats.com/github/ampproject/amphtml)
-[![Issue Stats](http://issuestats.com/github/ampproject/amphtml/badge/issue)](http://issuestats.com/github/ampproject/amphtml)
 
 # AMP HTML ⚡
 
diff --git a/TICKEVENTS.md b/TICKEVENTS.md
index 1a0cfe71c232..f6baaa31cbe0 100644
--- a/TICKEVENTS.md
+++ b/TICKEVENTS.md
@@ -25,8 +25,11 @@ As an example if we executed `perf.tick('label')` we assume we have a counterpar
 | Name                | id                | Description                        |
 ----------------------|-------------------|------------------------------------|
 | Install Styles      | `is`              | Set when the styles are installed. |
+| End Install Styles  | `e_is`            | Set when the styles are done installing. |
 | Window load event   | `ol`              | Window load event fired.           |
 | Prerender Complete  | `pc`              | The runtime completes prerending a single document. |
 | Frames per second   | `fps`             | Tick to measure fps.               |
 | Frames per second during ad load | `fal`| Tick to measure fps when at least one ad is on the page. |
 | First Viewport Complete | `fc`          | The first viewport is finished rendering. |
+| Make Body Visible | `mbv` | Make Body Visible Executes. |
+| On First Visible | `ofv` | The first time the page has been turned visible. |
diff --git a/ads/OWNER.yaml b/ads/OWNER.yaml
new file mode 100644
index 000000000000..dfaedfc4035a
--- /dev/null
+++ b/ads/OWNER.yaml
@@ -0,0 +1,2 @@
+- lannka
+- zhouyx
diff --git a/ads/README.md b/ads/README.md
index 02f26dfc754b..c7cda705e648 100644
--- a/ads/README.md
+++ b/ads/README.md
@@ -1,9 +1,27 @@
 # Integrating ad networks into AMP
 
-See also our [ad integration guidelines](../3p/README.md#ads) and [3rd party ads integration guidelines](./integration-guide.md)
+**Table of content**
+
+- [Overview](#overview)
+- [Constraints](#constraints)
+- [The iframe sandbox](#the-iframe-sandbox)
+    - [Available information](#available-information)
+    - [Available APIs](#available-apis)
+    - [Exceptions to available APIs and information](#exceptions-to-available-apis-and-information)
+    - [Ad viewability](#ad-viewability)
+    - [Ad resizing](#ad-resizing)
+    - [Support for multi-size ad requests](#support-for-multi-size-ad-requests)
+    - [Optimizing ad performance](#optimizing-ad-performance)
+    - [Ad markup](#ad-markup)
+    - [1st party cookies](#1st-party-cookies)
+- [Developer guidelines for a pull request](#developer-guidelines-for-a-pull-request)
+    - [Files to change](#files-to-change)
+    - [Verify your examples](#verify-your-examples)
+    - [Tests](#tests)
+    - [Other tips](#other-tips)
 
 ## Overview
-Ads are just another external resource and must play within the same constraints placed on all resources in AMP. We aim to support a large subset of existing ads with little or no changes to how the integrations work. Our long term goal is to further improve the impact of ads on the user experience through changes across the entire vertical client side stack.
+Ads are just another external resource and must play within the same constraints placed on all resources in AMP. We aim to support a large subset of existing ads with little or no changes to how the integrations work. Our long term goal is to further improve the impact of ads on the user experience through changes across the entire vertical client side stack. Although technically feasible, do not use amp-iframe to render display ads. Using amp-iframe for display ads breaks ad clicks and prevents recording viewability information. If you are an ad technology provider looking to integrate with AMP HTML, please also check the [general 3P inclusion guidelines](../3p/README.md#ads) and [ad service integration guidelines](./_integration-guide.md).
 
 ## Constraints
 A summary of constraints placed on external resources such as ads in AMP HTML:
@@ -21,9 +39,9 @@ Reasons include:
 
 ## The iframe sandbox
 
-The ad itself is hosted within a document that has an origin different from the primary page.
+The ad itself is hosted within a document that has an origin different from the primary page. The iframe by default loads a [bootstrap HTML](../3p/frame.max.html), which provides a container `div` to hold your content together with a set of APIs. Note that the container `div` (with `id="c"`) is absolute positioned and takes the whole space of the iframe, so you will want to append your content as a child of the container (don't append to `body`).  
 
-### Information available to the ad
+### Available information
 We will provide the following information to the ad:
 
 - `window.context.referrer` contains the origin of the referrer value of the primary document if available.
@@ -33,21 +51,25 @@ We will provide the following information to the ad:
   In browsers that support `location.ancestorOrigins` you can trust that the `origin` of the
   location is actually correct (So rogue pages cannot claim they represent an origin they do not actually represent).
 - `window.context.canonicalUrl` contains the canonical URL of the primary document as defined by its `link rel=canonical` tag.
+- `window.context.sourceUrl` contains the source URL of the original AMP document. See details [here](../spec/amp-var-substitutions.md#source-url).
 - `window.context.clientId` contains a unique id that is persistently the same for a given user and AMP origin site in their current browser until local data is deleted or the value expires (expiration is currently set to 1 year).
   - Ad networks must register their cid scope in the variable clientIdScope in [_config.js](./_config.js).
   - Only available on pages that load `amp-analytics`. The clientId will be null if `amp-analytics` was not loaded on the given page.
 - `window.context.pageViewId` contains a relatively low entropy id that is the same for all ads shown on a page.
 - [ad viewability](#ad-viewability)
 - `window.context.startTime` contains the time at which processing of the amp-ad element started.
+- `window.context.container` contains the ad container extension name if the current ad slot has one as its DOM ancestor. An valid ad container is one of the following AMP extensions: `amp-sticky-ad`, `amp-fx-flying-carpet`, `amp-lightbox`. As they provide non-trivial user experience, ad networks might want to use this info to select their serving strategies.
 
 More information can be provided in a similar fashion if needed (Please file an issue).
 
-### Methods available to the ad.
+### Available APIs
 
-- `window.context.noContentAvailable` is a function that the ad system can call if the ad slot was not filled. The container page will then react by showing fallback content or collapsing the ad if allowed by AMP resizing rules.
-- `window.context.reportRenderedEntityIdentifier` MUST be called by ads, when they know information about which creative was rendered into a particular ad frame and should contain information to allow identifying the creative. Consider including a small string identifying the ad network. This is used by AMP for reporting purposes. The value MUST NOT contain user data or personal identifiable information.
+- `window.context.renderStart(opt_data)` is a method to inform AMP runtime when the ad starts rendering. The ad will then become visible to user. The optional param `opt_data` is an object of form `{width, height}` to request an [ad resize](#ad-resizing) if the size of the returned ad doesn't match the ad slot. To enable this method, add a line `renderStartImplemented=true` to the corresponding ad config in [_config.js](./_config.js).
+- `window.context.noContentAvailable()` is a method to inform AMP runtime that the ad slot cannot be filled. The ad slot will then display the fallback content if provided, otherwise try to collapse.
+- `window.context.reportRenderedEntityIdentifier()` MUST be called by ads, when they know information about which creative was rendered into a particular ad frame and should contain information to allow identifying the creative. Consider including a small string identifying the ad network. This is used by AMP for reporting purposes. The value MUST NOT contain user data or personal identifiable information.
+- `window.context.getHtml (selector, attrs, callback)` is a method that retrieves specified node's content from the parent window which cannot be accessed directly because of security restrictions caused by AMP rules and iframe's usage. `selector` is a CSS selector of the node to take content from. `attrs` takes an array of tag attributes to be left in the stringified HTML representation (for instance, ['id', 'class']). All not specified attributes will be cut off from the result string. `callback` takes a function to be called when the content is ready. `getHtml` invokes callback with the only argument of type string.
 
-### Exceptions to iframe sandbox methods and information
+### Exceptions to available APIs and information
 Depending on the ad server / provider some methods of rendering ads involve a second iframe inside the AMP iframe. In these cases, the iframe sandbox methods and information will be unavailable to the ad. We are working on a creative level API that will enable this information to be accessible in such iframed cases and this README will be updated when that is available. Refer to the documentation for the relevant ad servers / providers (e.g., [doubleclick.md](./google/doubleclick.md)) for more details on how to handle such cases.
 
 ### Ad viewability
@@ -58,8 +80,6 @@ Ads can call the special API `window.context.observeIntersection(changesCallback
 
 The API allows specifying a callback that fires with change records when AMP observes that an ad becomes visible and then while it is visible, changes are reported as they happen.
 
-When a listener is registered, it will be called 2x in short order. Once with the position of the ad when its iframe was created and then again with the current position.
-
 Example usage:
 
 ```javascript
@@ -129,6 +149,20 @@ Here are some factors that affect whether the resize will be executed:
 - Whether the resize is requested for a currently active ad;
 - Whether the resize is requested for an ad below the viewport or above the viewport.
 
+
+### Support for multi-size ad requests
+Allowing more than a single ad size to fill a slot improves ad server competition. Increased competition gives the publisher better monetization for the same slot, therefore increasing overall revenue earned by the publisher.
+In order to support multi-size ad requests, AMP accepts an optional `data` param to `window.context.renderStart` (details in [Available APIs](#available-apis) section) which will automatically invoke request resize with the width and height passed.
+In case the resize is not successful, AMP will horizontally and vertically center align the creative within the space initially reserved for the creative.
+
+#### Example
+```javascript
+// Use the optional param to specify the width and height to request resize.
+window.context.renderStart({width: 200, height: 100});
+```
+
+Note that if the creative needs to resize on user interaction, the creative can continue to do that by calling the `window.context.requestResize(width, height)` API. Details in [Ad Resizing](#ad-resizing).
+
 ### Optimizing ad performance
 
 #### JS reuse across iframes
@@ -160,7 +194,7 @@ and another for DoubleClick:
       type="doubleclick"
       json="{…}">
   
- ````
+```
 
 For ad networks that support loading via a single script tag, this form is supported:
 
@@ -177,8 +211,51 @@ Technically the `` tag loads an iframe to a generic bootstrap URL that k
 
 ### 1st party cookies
 
-Access to a publishers 1st party cookies may be achieved through a custom ad bootstrap
-
-file. See ["Running ads from a custom domain"](../builtins/amp-ad.md) in the ad documentation for details.
+Access to a publishers 1st party cookies may be achieved through a custom ad bootstrap file. See ["Running ads from a custom domain"](../extensions/amp-ad/amp-ad.md#running-ads-from-a-custom-domain) in the ad documentation for details.
 
 If the publisher would like to add custom JavaScript in the `remote.html` file that wants to read or write to the publisher owned cookies, then the publisher needs to ensure that the `remote.html` file is hosted on a sub-domain of the publisher URL. e.g. if the publisher hosts a webpage on https://nytimes.com, then the remote file should be hosted on something similar to https://sub-domain.nytimes.com for the custom JavaScript to have the abiity to read or write cookies for nytimes.com.
+
+## Developer guidelines for a pull request
+Please read through [DEVELOPING.md](../DEVELOPING.md) before contributing to this code repository.
+
+### Files to change
+
+If you're adding support for a new 3P ad service, changes to the following files are expected:
+
+- `/ads/yournetwork.js` - implement the main logic here. This is the code that will be invoked in the 3P iframe once loaded.
+- `/ads/yournetwork.md` - have your service documented for the publishers to read.
+- `/ads/_config.js` - add service specific configuration here.
+- `/3p/integration.js` - register your service here.
+- `/extensions/amp-ad/amp-ad.md` - add a link that points to your publisher doc.
+- `/examples/ads.amp.html` - add publisher examples here. Since real ad isn't guaranteed to fill, a consistently displayed fake ad is highly recommended here to help AMP developers confidently identify new bugs.
+
+### Verify your examples
+
+To verify the examples that you have put in `/examples/ads.amp.html`, you will need to start a local gulp web server by running command `gulp`. Then visit `http://localhost:8000/examples/ads.amp.max.html?type=yournetwork` in your browser to make sure the examples load ads.
+
+Please consider having the example consistently load a fake ad (with ad targeting disabled). Not only it will be a more confident example for publishers to follow, but also for us to catch any regression bug during our releases.
+
+It's encouraged to have multiple examples to cover different use cases.
+
+Please verify your ad is fully functioning, for example, by clicking on an ad. We have seen bugs reported for ads not being clickable, which was due to incorrectly appended content divs.
+
+### Tests
+
+Please make sure your changes pass the tests:
+
+```
+gulp test --watch --nobuild --files=test/functional/{test-ads-config.js,test-integration.js}
+
+```
+
+If you have non-trivial logic in `/ads/yournetwork.js`, adding a unit test at `/test/functional/ads/test-yournetwork.js` is highly recommended.
+
+### Lint and type-check
+
+To speed up the review process, please run `gulp lint` and `gulp check-types`, then fix errors, if any, before sending out the PR.
+
+### Other tips
+
+- Please consider implementing the `render-start` and `no-content-available` APIs (see [Available APIs](#available-apis)), which helps AMP to provide user a much better ad loading experience.
+- [CLA](../CONTRIBUTIONG.md#contributing-code): for anyone who has trouble to pass the automatic CLA check in a pull request, try to follow the guidelines provided by the CLA Bot. Common mistakes are 1) used a different email address in git commit; 2) didn't provide the exact company name in the PR thread.
+
diff --git a/ads/_a4a-config.js b/ads/_a4a-config.js
new file mode 100644
index 000000000000..caa9d132e85e
--- /dev/null
+++ b/ads/_a4a-config.js
@@ -0,0 +1,81 @@
+/**
+ * Copyright 2016 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+  adsenseIsA4AEnabled,
+} from '../extensions/amp-ad-network-adsense-impl/0.1/adsense-a4a-config';
+import {
+  doubleclickIsA4AEnabled,
+} from
+'../extensions/amp-ad-network-doubleclick-impl/0.1/doubleclick-a4a-config';
+import {
+  fakeIsA4AEnabled,
+} from
+'../extensions/amp-ad-network-fake-impl/0.1/fake-a4a-config';
+import {
+  tripleliftIsA4AEnabled,
+} from
+'../extensions/amp-ad-network-triplelift-impl/0.1/triplelift-a4a-config';
+import {
+  cloudflareIsA4AEnabled,
+} from
+'../extensions/amp-ad-network-cloudflare-impl/0.1/cloudflare-a4a-config';
+import {getMode} from '../src/mode';
+import {map} from '../src/utils/object';
+
+/**
+ * Registry for A4A (AMP Ads for AMPHTML pages) "is supported" predicates.
+ * If an ad network, {@code ${NETWORK}}, is registered in this object, then the
+ * {@code } implementation will look up its predicate
+ * here. If there is a predicate and it and returns {@code true}, then
+ * {@code amp-ad} will attempt to render the ad via the A4A pathway (fetch
+ * ad creative via early XHR CORS request; verify that it is validated AMP;
+ * and then render directly in the host page by splicing into the host DOM).
+ * Otherwise, it will attempt to render the ad via the existing "3p iframe"
+ * pathway (delay load into a cross-domain iframe).
+ *
+ * @type {!Object}
+ */
+export const a4aRegistry = map({
+  'adsense': adsenseIsA4AEnabled,
+  'doubleclick': doubleclickIsA4AEnabled,
+  'triplelift': tripleliftIsA4AEnabled,
+  'cloudflare': cloudflareIsA4AEnabled,
+  // TODO: Add new ad network implementation "is enabled" functions here.  Note:
+  // if you add a function here that requires a new "import", above, you'll
+  // probably also need to add a whitelist exception to
+  // build-system/dep-check-config.js in the "filesMatching: 'ads/**/*.js' rule.
+});
+
+// Note: the 'fake' ad network implementation is only for local testing.
+// Normally, ad networks should add their *IsA4AEnabled callback directly
+// to the a4aRegistry, above.  Ad network implementations should NOT use
+// getMode() in this file.  If they need to check getMode() state, they
+// should do so inside their *IsA4AEnabled callback.
+if (getMode().localDev || getMode().test) {
+  a4aRegistry['fake'] = fakeIsA4AEnabled;
+}
+
+/**
+ * An object mapping signing server names to their corresponding URLs.
+ * @type {!Object}
+ */
+export const signingServerURLs = {
+  'google': 'https://cdn.ampproject.org/amp-ad-verifying-keyset.json',
+  'google-dev': 'https://cdn.ampproject.org/amp-ad-verifying-keyset-dev.json',
+  'cloudflare': 'https://amp.cloudflare.com/amp-ad-verifying-keyset.json',
+  'cloudflare-dev': 'https://amp.cloudflare.com/amp-ad-verifying-keyset-dev.json',
+};
diff --git a/ads/_config.js b/ads/_config.js
index e480d9d1cecc..6c5c4d9497bb 100644
--- a/ads/_config.js
+++ b/ads/_config.js
@@ -15,170 +15,578 @@
  */
 
 /**
- * URLs to prefetch for a given ad type.
+ * The config of each ad network.
+ * Please keep the list alphabetic order.
  *
- * This MUST be kept in sync with actual implementation.
+ * yourNetworkName: {  // This is the "type" attribute of 
  *
- * @const {!Object)>}
- */
-export const adPrefetch = {
-  doubleclick: [
-    'https://www.googletagservices.com/tag/js/gpt.js',
-    'https://securepubads.g.doubleclick.net/static/glade.js',
-  ],
-  a9: 'https://c.amazon-adsystem.com/aax2/assoc.js',
-  adblade: 'https://web.adblade.com/js/ads/async/show.js',
-  adsense: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
-  aduptech: 'https://s.d.adup-tech.com/jsapi',
-  criteo: 'https://static.criteo.net/js/ld/publishertag.js',
-  dotandads: 'https://amp.ad.dotandad.com/dotandadsAmp.js',
-  industrybrains: 'https://web.industrybrains.com/js/ads/async/show.js',
-  mediaimpact: 'https://ec-ns.sascdn.com/diff/251/divscripte/amp.js',
-  openx: 'https://www.googletagservices.com/tag/js/gpt.js',
-  smartadserver: 'https://ec-ns.sascdn.com/diff/js/smart.js',
-  'mantis-display': 'https://assets.mantisadnetwork.com/mantodea.min.js',
-  'mantis-recommend': 'https://assets.mantisadnetwork.com/recommend.min.js',
-  sovrn: 'https://ap.lijit.com/www/sovrn_amp/sovrn_ads.js',
-  yieldmo: 'https://static.yieldmo.com/ym.amp1.js',
-  revcontent: 'https://labs-cdn.revcontent.com/build/amphtml/revcontent.amp.min.js',
-  teads: 'https://cdn.teads.tv/media/format/v3/teads-format.min.js',
-  imobile: 'https://spamp.i-mobile.co.jp/script/amp.js',
-  pubmatic: 'https://ads.pubmatic.com/AdServer/js/amp.js',
-  sortable: 'https://www.googletagservices.com/tag/js/gpt.js',
-  gmossp: 'https://cdn.gmossp-sp.jp/ads/amp.js',
-  'weborama-display': [
-    'https://cstatic.weborama.fr/js/advertiserv2/adperf_launch_1.0.0_scrambled.js',
-    'https://cstatic.weborama.fr/js/advertiserv2/adperf_core_1.0.0_scrambled.js',
-  ],
-  yieldbot: 'https://cdn.yldbt.com/js/yieldbot.intent.js',
-  adstir: 'https://js.ad-stir.com/js/adstir_async.js',
-  colombia: 'https://static.clmbtech.com/ad/commons/js/colombia-amp.js',
-  eplanning: 'https://us.img.e-planning.net/layers/epl-amp.js',
-  appnexus: 'https://acdn.adnxs.com/ast/ast.js',
-  microad: 'https://j.microad.net/js/camp.js',
-  yahoojp: [
-    'https://s.yimg.jp/images/listing/tool/yads/ydn/amp/amp.js',
-    'https://yads.c.yimg.jp/js/yads.js',
-  ],
-  nend: 'https://js1.nend.net/js/amp.js',
-};
-
-/**
- * URLs to connect to for a given ad type.
+ *   // List of URLs for prefetch
+ *   prefetch: string|array
  *
- * This MUST be kept in sync with actual implementation.
+ *   // List of hosts for preconnect
+ *   preconnect: string|array
  *
- * @const {!Object)>}
- */
-export const adPreconnect = {
-  adblade: [
-    'https://staticd.cdn.adblade.com',
-    'https://static.adblade.com',
-  ],
-  industrybrains: [
-    'https://staticd.cdn.industrybrains.com',
-    'https://static.industrybrains.com',
-  ],
-  adition: 'https://imagesrv.adition.com',
-  adform: 'https://track.adform.net',
-  adreactor: 'https://adserver.adreactor.com',
-  adsense: 'https://googleads.g.doubleclick.net',
-  aduptech: 'https://s.d.adup-tech.com',
-  taboola: [
-    'https://cdn.taboola.com',
-    'https://trc.taboola.com',
-    'https://images.taboola.com',
-  ],
-  teads: [
-    'https://cdn.teads.tv',
-    'https://cdn2.teads.tv',
-    'https://a.teads.tv',
-    'https://t.teads.tv',
-  ],
-  criteo: [
-    'https://cas.criteo.com',
-  ],
-  doubleclick: [
-    'https://partner.googleadservices.com',
-    'https://securepubads.g.doubleclick.net',
-    'https://tpc.googlesyndication.com',
-  ],
-  'mantis-display': [
-    'https://mantodea.mantisadnetwork.com',
-    'https://res.cloudinary.com',
-    'https://resize.mantisadnetwork.com',
-  ],
-  'mantis-recommend': [
-    'https://mantodea.mantisadnetwork.com',
-    'https://resize.mantisadnetwork.com',
-  ],
-  dotandads: 'https://bal.ad.dotandad.com',
-  improvedigital: 'https://ad.360yield.com/',
-  openx: [
-    'https://partner.googleadservices.com',
-    'https://securepubads.g.doubleclick.net',
-    'https://tpc.googlesyndication.com',
-  ],
-  yieldmo: [
-    'https://static.yieldmo.com',
-    'https://s.yieldmo.com',
-    'https://ads.yieldmo.com',
-  ],
-  triplelift: [
-    'https://ib.3lift.com',
-    'https://dynamic.3lift.com',
-    'https://img.3lift.com',
-    'https://eb2.3lift.com',
-  ],
-  revcontent: [
-    'https://trends.revcontent.com',
-    'https://cdn.revcontent.com',
-    'https://img.revcontent.com',
-  ],
-  rubicon: [
-    'https://ads.rubiconproject.com',
-    'https://optimized-by.rubiconproject.com',
-  ],
-  sortable: [
-    'https://tags-cdn.deployads.com',
-    'https://partner.googleadservices.com',
-    'https://securepubads.g.doubleclick.net',
-    'https://tpc.googlesyndication.com',
-  ],
-  imobile: 'https://spad.i-mobile.co.jp',
-  webediads: [
-    'https://eu1.wbdds.com',
-  ],
-  gmossp: 'https://cdn.gmossp-sp.jp',
-  yieldbot: 'https://i.yldbt.com',
-  adstir: 'https://ad.ad-stir.com',
-  appnexus: 'https://ib.adnxs.com',
-  microad: [
-    'https://s-rtb.send.microad.jp',
-    'https://cache.send.microad.jp',
-  ],
-  yahoojp: [
-    'https://s.yimg.jp',
-    'https://yads.yahoo.co.jp',
-  ],
-  chargeads: [
-    'https://www.chargeplatform.com',
-  ],
-  nend: [
-    'https://js1.nend.net',
-    'https://output.nend.net',
-    'https://img1.nend.net',
-  ],
-};
-
-/**
- * The externalCidScope used to provide CIDs to ads of the given type.
+ *   // The externalCidScope used to provide CIDs to ads
+ *   clientIdScope: string
  *
- * @const {!Object}
+ *   // Whether render-start API has been implemented
+ *   // We highly recommend all networks to implement the API,
+ *   // see details in the README.md
+ *   renderStartImplemented: boolean
+ * }
  */
-export const clientIdScope = {
-  // Add a mapping like
-  // adNetworkType: 'cidScope' here.
-  adsense: 'AMP_ECID_GOOGLE',
-  doubleclick: 'AMP_ECID_GOOGLE',
+export const adConfig = {
+  _ping_: {
+    renderStartImplemented: true,
+  },
+
+  a9: {
+    prefetch: 'https://c.amazon-adsystem.com/aax2/assoc.js',
+  },
+
+  accesstrade: {
+    prefetch: 'https://h.accesstrade.net/js/amp/amp.js',
+  },
+
+  adblade: {
+    prefetch: 'https://web.adblade.com/js/ads/async/show.js',
+    preconnect: [
+      'https://staticd.cdn.adblade.com',
+      'https://static.adblade.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  adbutler: {
+    prefetch: 'https://servedbyadbutler.com/app.js',
+  },
+
+  adform: {},
+
+  adgeneration: {
+    prefetch: 'https://i.socdm.com/sdk/js/adg-script-loader.js',
+  },
+
+  adition: {},
+
+  adman: {},
+
+  adreactor: {},
+
+  adsense: {
+    prefetch: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
+    preconnect: 'https://googleads.g.doubleclick.net',
+    clientIdScope: 'AMP_ECID_GOOGLE',
+  },
+
+  adsnative: {
+    prefetch: 'https://static.adsnative.com/static/js/render.v1.js',
+    preconnect: 'https://api.adsnative.com',
+  },
+
+  adspirit: {},
+
+  adstir: {
+    prefetch: 'https://js.ad-stir.com/js/adstir_async.js',
+    preconnect: 'https://ad.ad-stir.com',
+  },
+
+  adtech: {
+    prefetch: 'https://s.aolcdn.com/os/ads/adsWrapper3.js',
+    preconnect: [
+      'https://mads.at.atwola.com',
+      'https://aka-cdn.adtechus.com',
+    ],
+  },
+
+  aduptech: {
+    prefetch: 'https://s.d.adup-tech.com/jsapi',
+    preconnect: [
+      'https://d.adup-tech.com',
+      'https://m.adup-tech.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  adverline: {
+    prefetch: 'https://ads.adverline.com/richmedias/amp.js',
+    preconnect: [
+      'https://adnext.fr',
+    ],
+    renderStartImplemented: true,
+  },
+
+  adverticum: {},
+
+  advertserve: {
+    renderStartImplemented: true,
+  },
+
+  affiliateb: {
+    prefetch: 'https://track.affiliate-b.com/amp/a.js',
+    renderStartImplemented: true,
+  },
+
+  amoad: {
+    prefetch: [
+      'https://j.amoad.com/js/a.js',
+      'https://j.amoad.com/js/n.js',
+    ],
+    preconnect: [
+      'https://d.amoad.com',
+      'https://i.amoad.com',
+      'https://m.amoad.com',
+      'https://v.amoad.com',
+    ],
+  },
+
+  appnexus: {
+    prefetch: 'https://acdn.adnxs.com/ast/ast.js',
+    preconnect: 'https://ib.adnxs.com',
+  },
+
+  atomx: {
+    prefetch: 'https://s.ato.mx/p.js',
+  },
+
+  brainy: {},
+
+  caajainfeed: {
+    prefetch: [
+      'https://cdn.amanad.adtdp.com/sdk/ajaamp-v1.0.js',
+    ],
+    preconnect: [
+      'https://ad.amanad.adtdp.com',
+    ],
+  },
+
+  capirs: {
+    renderStartImplemented: true,
+  },
+
+  caprofitx: {
+    prefetch: [
+      'https://cdn.caprofitx.com/pfx.min.js',
+      'https://cdn.caprofitx.com/tags/amp/profitx_amp.js',
+    ],
+    preconnect: 'https://ad.caprofitx.adtdp.com',
+  },
+
+  chargeads: {},
+
+  colombia: {
+    prefetch: 'https://static.clmbtech.com/ad/commons/js/colombia-amp.js',
+  },
+
+  contentad: {},
+
+  criteo: {
+    prefetch: 'https://static.criteo.net/js/ld/publishertag.js',
+    preconnect: 'https://cas.criteo.com',
+  },
+
+  csa: {
+    prefetch: 'https://www.google.com/adsense/search/ads.js',
+  },
+
+  distroscale: {
+    preconnect: [
+      'https://c.jsrdn.com',
+      'https://s.jsrdn.com',
+      'https://i.jsrdn.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  dotandads: {
+    prefetch: 'https://amp.ad.dotandad.com/dotandadsAmp.js',
+    preconnect: 'https://bal.ad.dotandad.com',
+  },
+
+  doubleclick: {
+    prefetch: [
+      'https://www.googletagservices.com/tag/js/gpt.js',
+      'https://securepubads.g.doubleclick.net/static/glade.js',
+    ],
+    preconnect: [
+      'https://partner.googleadservices.com',
+      'https://tpc.googlesyndication.com',
+    ],
+    clientIdScope: 'AMP_ECID_GOOGLE',
+    renderStartImplemented: true,
+  },
+
+  eplanning: {
+    prefetch: 'https://us.img.e-planning.net/layers/epl-amp.js',
+  },
+
+  ezoic: {
+    prefetch: [
+      'https://www.googletagservices.com/tag/js/gpt.js',
+      'https://g.ezoic.net/ezoic/ampad.js',
+    ],
+  },
+
+  f1e: {
+    prefetch: 'https://img.ak.impact-ad.jp/util/f1e_amp.min.js',
+  },
+
+  fake: {},
+
+  felmat: {
+    prefetch: 'https://t.felmat.net/js/fmamp.js',
+    renderStartImplemented: true,
+  },
+
+  flite: {},
+
+  fusion: {
+    prefetch: 'https://assets.adtomafusion.net/fusion/latest/fusion-amp.min.js',
+  },
+
+  genieessp: {
+    prefetch: 'https://js.gsspcln.jp/l/amp.js',
+  },
+
+  gmossp: {
+    prefetch: 'https://cdn.gmossp-sp.jp/ads/amp.js',
+  },
+
+  holder: {
+    prefetch: 'https://i.holder.com.ua/js2/holder/ajax/ampv1.js',
+    preconnect: 'https://h.holder.com.ua',
+    renderStartImplemented: true,
+  },
+
+  ibillboard: {},
+
+  imobile: {
+    prefetch: 'https://spamp.i-mobile.co.jp/script/amp.js',
+    preconnect: 'https://spad.i-mobile.co.jp',
+  },
+
+  improvedigital: {},
+
+  industrybrains: {
+    prefetch: 'https://web.industrybrains.com/js/ads/async/show.js',
+    preconnect: [
+      'https://staticd.cdn.industrybrains.com',
+      'https://static.industrybrains.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  inmobi: {
+    prefetch: 'https://cf.cdn.inmobi.com/ad/inmobi.secure.js',
+    renderStartImplemented: true,
+  },
+
+  ix: {
+    prefetch: [
+      'https://js-sec.indexww.com/indexJTag.js',
+      'https://js-sec.indexww.com/apl/apl6.js',
+    ],
+    preconnect: 'https://as-sec.casalemedia.com',
+  },
+
+  kargo: {},
+
+  kixer: {
+    prefetch: 'https://cdn.kixer.com/ad/load.js',
+    renderStartImplemented: true,
+  },
+
+  ligatus: {
+    prefetch: 'https://ssl.ligatus.com/render/ligrend.js',
+    renderStartImplemented: true,
+  },
+
+  loka: {
+    prefetch: 'https://loka-cdn.akamaized.net/scene/amp.js',
+    preconnect: [
+      'https://scene-front.lokaplatform.com',
+      'https://loka-materials.akamaized.net',
+    ],
+    renderStartImplemented: true,
+  },
+
+  mads: {
+    prefetch: 'https://eu2.madsone.com/js/tags.js',
+  },
+
+  'mantis-display': {
+    prefetch: 'https://assets.mantisadnetwork.com/mantodea.min.js',
+    preconnect: [
+      'https://mantodea.mantisadnetwork.com',
+      'https://res.cloudinary.com',
+      'https://resize.mantisadnetwork.com',
+    ],
+  },
+
+  'mantis-recommend': {
+    prefetch: 'https://assets.mantisadnetwork.com/recommend.min.js',
+    preconnect: [
+      'https://mantodea.mantisadnetwork.com',
+      'https://resize.mantisadnetwork.com',
+    ],
+  },
+
+  mediaimpact: {
+    prefetch: 'https://ec-ns.sascdn.com/diff/251/pages/amp_default.js',
+    preconnect: [
+      'https://ww251.smartadserver.com',
+      'https://static.sascdn.com/',
+    ],
+    renderStartImplemented: true,
+  },
+
+  medianet: {
+    preconnect: 'https://contextual.media.net',
+    renderStartImplemented: true,
+  },
+
+  mediavine: {
+    prefetch: 'https://www.googletagservices.com/tag/js/gpt.js',
+    preconnect: [
+      'https://partner.googleadservices.com',
+      'https://securepubads.g.doubleclick.net',
+      'https://tpc.googlesyndication.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  meg: {
+    renderStartImplemented: true,
+  },
+
+  microad: {
+    prefetch: 'https://j.microad.net/js/camp.js',
+    preconnect: [
+      'https://s-rtb.send.microad.jp',
+      'https://s-rtb.send.microadinc.com',
+      'https://cache.send.microad.jp',
+      'https://cache.send.microadinc.com',
+      'https://deb.send.microad.jp',
+    ],
+  },
+
+  mixpo: {
+    prefetch: 'https://cdn.mixpo.com/js/loader.js',
+    preconnect: [
+      'https://player1.mixpo.com',
+      'https://player2.mixpo.com',
+    ],
+  },
+
+  nativo: {
+    prefetch: 'https://s.ntv.io/serve/load.js',
+  },
+
+  nend: {
+    prefetch: 'https://js1.nend.net/js/amp.js',
+    preconnect: [
+      'https://output.nend.net',
+      'https://img1.nend.net',
+    ],
+  },
+
+  nokta: {
+    prefetch: 'https://static.virgul.com/theme/mockups/noktaamp/ampjs.js',
+    renderStartImplemented: true,
+  },
+
+  openadstream: {},
+
+  openx: {
+    prefetch: 'https://www.googletagservices.com/tag/js/gpt.js',
+    preconnect: [
+      'https://partner.googleadservices.com',
+      'https://securepubads.g.doubleclick.net',
+      'https://tpc.googlesyndication.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  plista: {},
+
+  popin: {
+    renderStartImplemented: true,
+  },
+
+  pubmatic: {
+    prefetch: 'https://ads.pubmatic.com/AdServer/js/amp.js',
+  },
+
+  pubmine: {
+    prefetch: [
+      'https://s.pubmine.com/head.js',
+      'https://s.pubmine.com/showad.js',
+    ],
+    preconnect: 'https://delivery.g.switchadhub.com',
+    renderStartImplemented: true,
+  },
+
+  pulsepoint: {
+    prefetch: 'https://ads.contextweb.com/TagPublish/getjs.static.js',
+    preconnect: 'https://tag.contextweb.com',
+  },
+
+  purch: {
+    prefetch: 'https://ramp.purch.com/serve/creative_amp.js',
+    renderStartImplemented: true,
+  },
+
+  relap: {
+    renderStartImplemented: true,
+  },
+
+  revcontent: {
+    prefetch: 'https://labs-cdn.revcontent.com/build/amphtml/revcontent.amp.min.js',
+    preconnect: [
+      'https://trends.revcontent.com',
+      'https://cdn.revcontent.com',
+      'https://img.revcontent.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  rubicon: {},
+
+  sharethrough: {
+    renderStartImplemented: true,
+  },
+
+  sklik: {
+    prefetch: 'https://c.imedia.cz/js/amp.js',
+  },
+
+  smartadserver: {
+    prefetch: 'https://ec-ns.sascdn.com/diff/js/amp.v0.js',
+    preconnect: 'https://static.sascdn.com',
+    renderStartImplemented: true,
+  },
+
+  smartclip: {
+    prefetch: 'https://cdn.smartclip.net/amp/amp.v0.js',
+    preconnect: 'https://des.smartclip.net',
+    renderStartImplemented: true,
+  },
+
+  sortable: {
+    prefetch: 'https://www.googletagservices.com/tag/js/gpt.js',
+    preconnect: [
+      'https://tags-cdn.deployads.com',
+      'https://partner.googleadservices.com',
+      'https://securepubads.g.doubleclick.net',
+      'https://tpc.googlesyndication.com',
+    ],
+  },
+
+  sovrn: {
+    prefetch: 'https://ap.lijit.com/www/sovrn_amp/sovrn_ads.js',
+  },
+
+  swoop: {
+    prefetch: 'https://www.swoop-amp.com/amp.js',
+    preconnect: [
+      'https://www.swpsvc.com',
+      'https://client.swpcld.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  taboola: {},
+
+  teads: {
+    prefetch: 'https://cdn.teads.tv/media/format/v3/teads-format.min.js',
+    preconnect: [
+      'https://cdn2.teads.tv',
+      'https://a.teads.tv',
+      'https://t.teads.tv',
+    ],
+  },
+
+  triplelift: {},
+
+  valuecommerce: {
+    prefetch: 'https://amp.valuecommerce.com/amp_bridge.js',
+    preconnect: [
+      'https://ad.jp.ap.valuecommerce.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  webediads: {
+    prefetch: 'https://eu1.wbdds.com/amp.min.js',
+    preconnect: [
+      'https://goutee.top',
+      'https://mediaathay.org.uk',
+    ],
+    renderStartImplemented: true,
+  },
+
+  'weborama-display': {
+    prefetch: [
+      'https://cstatic.weborama.fr/js/advertiserv2/adperf_launch_1.0.0_scrambled.js',
+      'https://cstatic.weborama.fr/js/advertiserv2/adperf_core_1.0.0_scrambled.js',
+    ],
+  },
+
+  widespace: {},
+
+  xlift: {
+    prefetch: 'https://cdn.x-lift.jp/resources/common/xlift_amp.js',
+    renderStartImplemented: true,
+  },
+
+  yahoo: {
+    prefetch: 'https://s.yimg.com/os/ampad/display.js',
+    preconnect: 'https://us.adserver.yahoo.com',
+  },
+
+  yahoojp: {
+    prefetch: [
+      'https://s.yimg.jp/images/listing/tool/yads/ydn/amp/amp.js',
+      'https://yads.c.yimg.jp/js/yads.js',
+    ],
+    preconnect: 'https://yads.yahoo.co.jp',
+  },
+
+  yieldbot: {
+    prefetch: [
+      'https://cdn.yldbt.com/js/yieldbot.intent.amp.js',
+      'https://msg.yldbt.com/js/ybmsg.html',
+    ],
+    preconnect: 'https://i.yldbt.com',
+  },
+
+  yieldmo: {
+    prefetch: 'https://static.yieldmo.com/ym.amp1.js',
+    preconnect: [
+      'https://s.yieldmo.com',
+      'https://ads.yieldmo.com',
+    ],
+    renderStartImplemented: true,
+  },
+
+  yieldone: {
+    prefetch: 'https://img.ak.impact-ad.jp/ic/pone/commonjs/yone-amp.js',
+  },
+
+  zedo: {
+    prefetch: 'https://ss3.zedo.com/gecko/tag/Gecko.amp.min.js',
+    renderStartImplemented: true,
+  },
+
+  zergnet: {},
+
+  zucks: {
+    preconnect: [
+      'https://j.zucks.net.zimg.jp',
+      'https://sh.zucks.net',
+      'https://k.zucks.net',
+      'https://static.zucks.net.zimg.jp',
+    ],
+  },
+
 };
diff --git a/ads/integration-guide.md b/ads/_integration-guide.md
similarity index 92%
rename from ads/integration-guide.md
rename to ads/_integration-guide.md
index 26bd6c91fe4a..84913acdc85d 100644
--- a/ads/integration-guide.md
+++ b/ads/_integration-guide.md
@@ -87,4 +87,4 @@ Also see approach to using the [intersection observer pattern](https://github.co
 
 *Examples : Taboola, Outbrain*
 
-If you have some piece of JavaScript embeded on the publisher website today but the approach will not work in AMP pages. If you would like to recommend content on an AMP page, we suggest that you use the amp-embed extension to request the content details. Please see the [Taboola](https://github.com/ampproject/amphtml/blob/master/ads/taboola.md) example.
+Useful if you have some piece of JavaScript embeded on the publisher website today but the approach will not work in AMP pages. If you would like to recommend content on an AMP page, we suggest that you use the [`amp-embed` extension](https://www.ampproject.org/docs/reference/components/amp-ad) to request the content details. Please see the [Taboola](https://github.com/ampproject/amphtml/blob/master/ads/taboola.md) example.
diff --git a/ads/_ping_.js b/ads/_ping_.js
new file mode 100644
index 000000000000..403ac0185c1a
--- /dev/null
+++ b/ads/_ping_.js
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2016 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {validateData} from '../3p/3p';
+import {dev} from '../src/log';
+
+/**
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function _ping_(global, data) {
+  validateData(data, [], ['valid', 'adHeight', 'adWidth', 'enableIo', 'url']);
+  global.document.getElementById('c').textContent = data.ping;
+  global.ping = Object.create(null);
+
+  global.context.onResizeSuccess(() => {
+    global.ping.resizeSuccess = true;
+  });
+
+  global.context.onResizeDenied(() => {
+    global.ping.resizeSuccess = false;
+  });
+
+  if (data.ad_container) {
+    dev().assert(
+        global.context.container == data.ad_container, 'wrong container');
+  }
+  if (data.valid && data.valid == 'true') {
+    const img = document.createElement('img');
+    if (data.url) {
+      img.setAttribute('src', data.url);
+      img.setAttribute('width', data.width);
+      img.setAttribute('height', data.height);
+    }
+    let width, height;
+    if (data.adHeight) {
+      img.setAttribute('height', data.adHeight);
+      height = Number(data.adHeight);
+    }
+    if (data.adWidth) {
+      img.setAttribute('width', data.adWidth);
+      width = Number(data.adWidth);
+    }
+    document.body.appendChild(img);
+    if (width || height) {
+      global.context.renderStart({width, height});
+    } else {
+      global.context.renderStart();
+    }
+    if (data.enableIo) {
+      global.context.observeIntersection(function(changes) {
+        changes.forEach(function(c) {
+          dev().info('AMP-AD', 'Intersection: (WxH)' +
+              `${c.intersectionRect.width}x${c.intersectionRect.height}`);
+        });
+        // store changes to global.lastIO for testing purpose
+        global.ping.lastIO = changes[changes.length - 1];
+      });
+    }
+  } else {
+    global.context.noContentAvailable();
+  }
+}
diff --git a/ads/a9.js b/ads/a9.js
index f58ac18780f1..1d2ea8719593 100644
--- a/ads/a9.js
+++ b/ads/a9.js
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-import {writeScript, checkData} from '../3p/3p';
+import {writeScript, validateData} from '../3p/3p';
 
 /**
  * @param {!Window} global
  * @param {!Object} data
  */
 export function a9(global, data) {
-  checkData(data, ['aax_size', 'aax_pubname', 'aax_src']);
+  // TODO: check mandatory fields
+  validateData(data, [], ['aax_size', 'aax_pubname', 'aax_src']);
   /*eslint "google-camelcase/google-camelcase": 0*/
   global.aax_size = data.aax_size;
   global.aax_pubname = data.aax_pubname;
diff --git a/ads/accesstrade.js b/ads/accesstrade.js
new file mode 100644
index 000000000000..efec77e66574
--- /dev/null
+++ b/ads/accesstrade.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2016 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {writeScript, validateData} from '../3p/3p';
+
+/**
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function accesstrade(global, data) {
+  validateData(data, ['atops', 'atrotid']);
+  global.atParams = data;
+  writeScript(global, 'https://h.accesstrade.net/js/amp/amp.js');
+}
diff --git a/ads/accesstrade.md b/ads/accesstrade.md
new file mode 100644
index 000000000000..6f46cfa0334e
--- /dev/null
+++ b/ads/accesstrade.md
@@ -0,0 +1,37 @@
+
+
+# AccessTrade
+
+## Example
+
+```html
+
+
+```
+
+## Configuration
+
+For configuration details and to generate your tags, please contact https://member.accesstrade.net/atv3/contact.html 
+
+Supported parameters:
+
+- data-atops
+- data-atrotid
+
diff --git a/ads/adblade.js b/ads/adblade.js
index a196e79db668..230b7ae76699 100644
--- a/ads/adblade.js
+++ b/ads/adblade.js
@@ -14,15 +14,14 @@
  * limitations under the License.
  */
 
-import {writeScript, checkData, validateDataExists} from '../3p/3p';
+import {writeScript, validateData} from '../3p/3p';
 
 const adbladeFields = ['width', 'height', 'cid'];
 const adbladeHostname = 'web.adblade.com';
 const industrybrainsHostname = 'web.industrybrains.com';
 
 function addAdiantUnit(hostname, global, data) {
-  checkData(data, adbladeFields);
-  validateDataExists(data, adbladeFields);
+  validateData(data, adbladeFields, []);
 
   // create a data element so our script knows what to do
   const ins = global.document.createElement('ins');
@@ -35,6 +34,11 @@ function addAdiantUnit(hostname, global, data) {
   ins.setAttribute('data-tag-type', 1);
   global.document.getElementById('c').appendChild(ins);
 
+  ins.parentNode.addEventListener(
+    'eventAdbladeRenderStart',
+    global.context.renderStart()
+  );
+
   // run our JavaScript code to display the ad unit
   writeScript(global, 'https://' + hostname + '/js/ads/async/show.js');
 }
diff --git a/ads/adblade.md b/ads/adblade.md
index fb858109b8b6..ecd255e44113 100644
--- a/ads/adblade.md
+++ b/ads/adblade.md
@@ -21,8 +21,8 @@ limitations under the License.
 ```html
 
 
 ```
@@ -34,5 +34,5 @@ For semantics of configuration, please see [ad network documentation](https://ww
 Supported parameters:
 
 - data-cid
-- width
-- height
+- data-width
+- data-height
diff --git a/ads/adbutler.js b/ads/adbutler.js
new file mode 100644
index 000000000000..714c811c8fef
--- /dev/null
+++ b/ads/adbutler.js
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {loadScript, validateData} from '../3p/3p';
+
+/**
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function adbutler(global,data) {
+  let placeholderID;
+  validateData(
+    data,
+    ['account', 'zone', 'width', 'height'],
+    ['keyword', 'place']
+  );
+
+  data['place'] = data['place'] || 0;
+
+  placeholderID = 'placement_' + data['zone'] + '_' + data['place'];
+
+  // placeholder div
+  const d = global.document.createElement('div');
+  d.setAttribute('id', placeholderID);
+  global.document.getElementById('c').appendChild(d);
+
+  global.AdButler = global.AdButler || {};
+  global.AdButler.ads = global.AdButler.ads || [];
+
+  global.AdButler.ads.push({
+    handler: function(opt) {
+      global.AdButler.register(
+          data['account'],
+          data['zone'],
+          [data['width'], data['height']],
+          placeholderID,
+          opt
+      );
+    },
+    opt: {
+      place: data['place'],
+      pageKey: global.context.pageViewId,
+      keywords: data['keyword'],
+      domain: 'servedbyadbutler.com',
+      click: 'CLICK_MACRO_PLACEHOLDER',
+    },
+  });
+  loadScript(global, 'https://servedbyadbutler.com/app.js');
+}
diff --git a/ads/adbutler.md b/ads/adbutler.md
new file mode 100644
index 000000000000..3bb57810d784
--- /dev/null
+++ b/ads/adbutler.md
@@ -0,0 +1,42 @@
+
+
+# AdButler
+
+## Example
+
+```html
+
+
+```
+## Configuration
+
+For semantics of configuration, please see [ad network documentation](http://www.adbutlerhelp.com/amp-configuration).
+
+Supported parameters:
+
+Required:
+- width
+- height
+- data-account
+- data-zone
+
+Optional:
+- data-place
+- data-keyword
diff --git a/ads/adform.js b/ads/adform.js
index f3d4e70e84bd..66a6623917f7 100644
--- a/ads/adform.js
+++ b/ads/adform.js
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {writeScript, validateSrcPrefix, validateExactlyOne} from '../3p/3p';
+import {writeScript, validateSrcPrefix, validateData} from '../3p/3p';
 
 // Valid adform ad source hosts
 const hosts = {
@@ -31,8 +31,7 @@ const hosts = {
  * @param {!Object} data
  */
 export function adform(global, data) {
-
-  validateExactlyOne(data, ['src', 'bn', 'mid']);
+  validateData(data, [['src', 'bn', 'mid']]);
   global.Adform = {ampData: data};
   const src = data.src;
   const bn = data.bn;
diff --git a/ads/adgeneration.js b/ads/adgeneration.js
new file mode 100644
index 000000000000..bf139e803230
--- /dev/null
+++ b/ads/adgeneration.js
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2016 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {loadScript, writeScript, validateData} from '../3p/3p';
+
+/**
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function adgeneration(global, data) {
+  validateData(data, ['id'],
+      ['targetid', 'displayid', 'adtype', 'async', 'option']);
+
+  // URL encoding
+  const option = data.option ? encodeQueryValue(data.option) : null;
+
+  const url = 'https://i.socdm.com/sdk/js/adg-script-loader.js?' +
+      'id=' + encodeURIComponent(data.id) +
+      '&width=' + encodeURIComponent(data.width) +
+      '&height=' + encodeURIComponent(data.height) +
+      '&adType=' +
+      (data.adtype ? encodeURIComponent(data.adtype.toUpperCase()) : 'FREE') +
+      '&async=' +
+      (data.async ? encodeURIComponent(data.async.toLowerCase()) : 'false') +
+      '&displayid=' +
+      (data.displayid ? encodeURIComponent(data.displayid) : '1') +
+      '&tagver=2.0.0' +
+      (data.targetid ? '&targetID=' + encodeURIComponent(data.targetid) : '') +
+      (option ? '&' + option : '');
+
+  if (data.async && data.async.toLowerCase() === 'true') {
+    loadScript(global, url);
+  } else {
+    writeScript(global, url);
+  }
+}
+
+/**
+ * URL encoding of query string
+ * @param {!String} str
+ */
+function encodeQueryValue(str) {
+  return str.split('&').map(v => {
+    const key = v.split('=')[0],
+      val = v.split('=')[1];
+    return encodeURIComponent(key) + '=' + encodeURIComponent(val);
+  }).join('&');
+}
diff --git a/ads/adgeneration.md b/ads/adgeneration.md
new file mode 100644
index 000000000000..116accb15942
--- /dev/null
+++ b/ads/adgeneration.md
@@ -0,0 +1,39 @@
+
+
+# Ad Generation
+
+## Example
+
+```html
+
+
+```
+
+## Configuration
+
+For semantics of configuration, please see [ad network documentation](https://github.com/AdGeneration/sdk/wiki).
+
+Supported parameters:
+
+- data-id
+- data-targetid
+- data-displayid
+- data-adtype
+- data-async
+- data-option
diff --git a/ads/adition.js b/ads/adition.js
index 34a827958623..3f94f7048759 100644
--- a/ads/adition.js
+++ b/ads/adition.js
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-import {writeScript, validateDataExists} from '../3p/3p';
+import {writeScript, validateData} from '../3p/3p';
 
 /**
  * @param {!Window} global
  * @param {!Object} data
  */
 export function adition(global, data) {
-  validateDataExists(data, ['version']);
+  validateData(data, ['version']);
   global.data = data;
-  writeScript(global, 'https://imagesrv.adition.com/js/amp/v' + encodeURIComponent(data['version']) + '.js');
+  writeScript(global, 'https://imagesrv.adition.com/js/amp/v'
+      + encodeURIComponent(data['version']) + '.js');
 }
diff --git a/ads/adman.js b/ads/adman.js
index e26a6074a01b..d52e3a40738c 100644
--- a/ads/adman.js
+++ b/ads/adman.js
@@ -14,19 +14,16 @@
  * limitations under the License.
  */
 
-import {checkData, validateDataExists} from '../3p/3p';
+import {validateData} from '../3p/3p';
 
 /**
  * @param {!Window} global
  * @param {!Object} data
  */
 export function adman(global, data) {
-  const script = global.document.createElement('script');
-  const fields = ['ws', 'host', 's'];
-
-  checkData(data, fields);
-  validateDataExists(data, fields);
+  validateData(data, ['ws', 'host', 's'], []);
 
+  const script = global.document.createElement('script');
   script.setAttribute('data-ws', data.ws);
   script.setAttribute('data-h', data.host);
   script.setAttribute('data-s', data.s);
diff --git a/ads/adreactor.js b/ads/adreactor.js
index 66bbdf382bba..99010bd37270 100644
--- a/ads/adreactor.js
+++ b/ads/adreactor.js
@@ -14,20 +14,21 @@
  * limitations under the License.
  */
 
-import {writeScript, checkData} from '../3p/3p';
+import {writeScript, validateData} from '../3p/3p';
 
 /**
  * @param {!Window} global
  * @param {!Object} data
  */
 export function adreactor(global, data) {
-  checkData(data, ['zid', 'pid', 'custom3']);
+  // TODO: check mandatory fields
+  validateData(data, [], ['zid', 'pid', 'custom3']);
   const url = 'https://adserver.adreactor.com' +
       '/servlet/view/banner/javascript/zone?' +
       'zid=' + encodeURIComponent(data.zid) +
       '&pid=' + encodeURIComponent(data.pid) +
       '&custom3=' + encodeURIComponent(data.custom3) +
       '&random=' + Math.floor(89999999 * Math.random() + 10000000) +
-      '&millis=' + new Date().getTime();
+      '&millis=' + Date.now();
   writeScript(global, url);
 }
diff --git a/ads/ads.extern.js b/ads/ads.extern.js
new file mode 100644
index 000000000000..e0a540870a9e
--- /dev/null
+++ b/ads/ads.extern.js
@@ -0,0 +1,254 @@
+/**
+ * Copyright 2016 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// HACK. Define application types used in default AMP externs
+// that are not in the 3p code.
+/** @constructor */
+function BaseElement$$module$src$base_element() {};
+/** @constructor */
+function AmpAdXOriginIframeHandler$$module$extensions$amp_ad$0_1$amp_ad_xorigin_iframe_handler() {};
+/** @constructor */
+function AmpAd3PImpl$$module$extensions$amp_ad$0_1$amp_ad_3p_impl() {};
+/** @constructor */
+function AmpA4A$$module$extensions$amp_a4a$0_1$amp_a4a() {};
+/** @constructor */
+function AmpAdUIHandler$$module$extensions$amp_ad$0_1$amp_ad_ui() {};
+
+// Long list of, uhm, stuff the ads code needs to compile.
+// All unquoted external properties need to be added here.
+data.cid;
+data.bn;
+data.mid;
+data.ws;
+data.s;
+data.sid;
+data.client;
+data.zid;
+data.pid;
+data.custom3;
+window.uAd = {};
+window.uAd.embed;
+data.pageOpts;
+data.adUnits;
+data.clmb_slot;
+data.clmb_position;
+data.clmb_divid;
+data.clmb_section;
+data.epl_si;
+data.epl_isv;
+data.epl_sv;
+data.epl_sec;
+data.epl_ksv;
+data.epl_kvs;
+data.epl_e;
+data.guid;
+data.adslot;
+
+var Criteo;
+Criteo.DisplayAd;
+Criteo.Log.Debug;
+Criteo.CallRTA;
+Criteo.ComputeDFPTargetingForAMP;
+Criteo.PubTag = {};
+Criteo.PubTag.RTA = {};
+Criteo.PubTag.RTA.DefaultCrtgContentName;
+Criteo.PubTag.RTA.DefaultCrtgRtaCookieName
+data.varname;
+data.tagtype;
+data.cookiename;;
+data.networkid;;
+data.zone;
+data.adserver;
+data.slot;
+data.width;
+data.height;
+
+var googletag;
+window.googletag;
+googletag.cmd;
+googletag.cmd.push;
+googletag.pubads;
+googletag.defineSlot
+data.slot;
+
+var _googCsa;
+window._googCsa;
+
+var _inmobi;
+window._inmobi;
+_inmobi.getNewAd;
+data.siteid;
+data.slotid;
+
+var pubads;
+pubads.addService;
+pubads.markAsGladeOptOut;
+pubads.markAsAmp;
+pubads.setCorrelator;
+pubads.markAsGladeControl;
+googletag.enableServices;
+data.slot.setCategoryExclusion;
+pubads.setCookieOptions;
+pubads.setTagForChildDirectedTreatment;
+data.slot.setTargeting;
+data.slot.setAttribute;
+data.optin;
+data.keyvalue;
+var asmi;
+asmi.sas;
+asmi.sas.call;
+asmi.sas.setup;
+data.spot;
+var MicroAd;
+MicroAd.Compass;
+MicroAd.Compass.showAd;
+data.adhost
+data.pos;
+var dfpData;
+dfpData.dfp;
+dfpData.targeting;
+var OX;
+OX._requestArgs;
+var OX_bidder_options;
+OX_bidder_options.bidderType;
+OX_bidder_options.callback;
+var OX_bidder_ads;
+var oxRequest;
+oxRequest.addAdUnit;
+oxRequest.setAdSizes;
+oxRequest.getOrCreateAdUnit;
+data.host;
+data.nc;
+data.dfpSlot;
+data.zone;
+data.sitepage;
+data.auid;
+data.widgetname;
+data.urlprefix;
+var rubicontag;
+rubicontag.setFPV;
+rubicontag.setFPI;
+rubicontag.getSlot;
+rubicontag.getAdServerTargeting;
+data.account;
+rubicontag.addKW;
+rubicontag.setUrl;
+rubicontag.setIntegration;
+data.account;
+data.kw;
+data.visitor;
+data.inventory;
+data.callback;
+var wads;
+wads.init;
+data.wbo_account_id;
+data.wbo_customparameter;
+data.wbo_tracking_element_id;
+data.wbo_host;
+data.wbo_fullhost;
+data.wbo_bid_price;
+data.wbo_price_paid;
+data.wbo_random;
+data.wbo_debug;
+data.wbo_publisherclick;
+data.wbo_disable_unload_event;
+data.wbo_donottrack;
+data.wbo_script_variant;
+data.wbo_is_mobile;
+data.wbo_vars;
+data.wbo_weak_encoding;
+data.psn;
+var yieldbot;
+yieldbot.psn;
+yieldbot.enableAsync;
+yieldbot.defineSlot;
+yieldbot.go;
+data.ybSlot;
+yieldbot.nextPageview;
+yieldbot.getSlotCriteria;
+data.ymid;
+var PostRelease;
+PostRelease.Start;
+PostRelease.checkIsAdVisible;
+var _prx;
+data.delayByTime;
+window.PulsePointHeaderTag;
+data.tagid;
+data.tagtype;
+data.zergid;
+window.zergnetWidgetId;
+data.ankv;
+data.ancat;
+data.annid;
+data.anwid;
+data.antid;
+data.anapiid;
+window.MADSAdrequest = {};
+window.MADSAdrequest.adrequest;
+data.divid;
+/**
+ * @constructor
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+window.EzoicAmpAd = function(global, data) {};
+window.EzoicAmpAd.prototype.createAd;
+data.id;
+data.d;
+data.wid;
+data.url;
+data.customtarget;
+data.dynclickthrough;
+data.viewtracking;
+data.customcss;
+data.enablemraid;
+data.jsplayer;
+var sas;
+sas.callAmpAd;
+data.uuid;
+data.embedcreated;
+data.embedparent
+data.embedlive
+var ZGTag;
+var geckoTag;
+var placement;
+data.superId;
+data.network;
+geckoTag.setAMP;
+geckoTag.addPlacement;
+data.placementId;
+data.channel;
+data.publisher;
+data.dim;
+placement.includeRenderer;
+geckoTag.loadAds;
+geckoTag.placementReady;
+data.plc;
+data.sz;
+data.extra;
+var Fusion;
+Fusion.on;
+Fusion.on.warning;
+Fusion.loadAds;
+var ev;
+ev.msg;
+data.adServer;
+data.mediaZone;
+data.layout;
+data.space;
+
+var Swoop
+Swoop.announcePlace
diff --git a/ads/adsnative.js b/ads/adsnative.js
new file mode 100644
index 000000000000..c68cba77977b
--- /dev/null
+++ b/ads/adsnative.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {writeScript, validateData} from '../3p/3p';
+
+/**
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function adsnative(global, data) {
+  try {
+    validateData(data, ['anapiid'], ['ankv', 'ancat', 'antid']);
+  } catch (e) {
+    validateData(data, ['annid', 'anwid'], ['ankv', 'ancat', 'antid']);
+  }
+
+  // convert string to object
+  let actualkv = undefined;
+  if (data.ankv) {
+    actualkv = {};
+    const arraykv = data.ankv.split(',');
+    for (const k in arraykv) {
+      const kv = arraykv[k].split(':');
+      actualkv[kv.pop()] = kv.pop();
+    }
+  }
+
+  // convert string to array
+  const actualcat = data.ancat ? data.ancat.split(',') : undefined;
+
+  // populate settings
+  global._AdsNativeOpts = {
+    apiKey: data.anapiid,
+    networkKey: data.annid,
+    nativeAdElementId: 'adsnative_ampad',
+    currentPageUrl: global.context.location.href,
+    widgetId: data.anwid,
+    templateKey: data.antid,
+    categories: actualcat,
+    keyValues: actualkv,
+    amp: true,
+  };
+
+  // drop ad placeholder div
+  const ad = global.document.createElement('div');
+  const ampwrapper = global.document.getElementById('c');
+  ad.id = global._AdsNativeOpts.nativeAdElementId;
+  ampwrapper.appendChild(ad);
+
+  // load renderjs
+  writeScript(global, 'https://static.adsnative.com/static/js/render.v1.js');
+}
diff --git a/ads/adsnative.md b/ads/adsnative.md
new file mode 100644
index 000000000000..22c1521eb86b
--- /dev/null
+++ b/ads/adsnative.md
@@ -0,0 +1,46 @@
+
+
+# AdsNative
+
+## Example
+
+```html
+
+```
+
+## Configuration
+
+For configuration, please see [ad network documentation](https://dev.adsnative.com).
+
+Supported parameters:
+
+**Required**
+- width:        required by amp
+- height:       required by amp
+- data-anapiid: the api id may be used instead of network and widget id
+- data-annid:   the network id must be paired with widget id
+- data-anwid:   the widget id must be paired with network id
+
+**Optional**
+- data-anapiid: the api id
+- data-anwid:   the widget id
+- data-antid:   the template id
+- data-ancat:   a comma separated list of categories
+- data-ankv:    a list of key value pairs in the format "key1:value1, key2:value2"
diff --git a/ads/adspirit.js b/ads/adspirit.js
new file mode 100644
index 000000000000..15b3ee01ddd9
--- /dev/null
+++ b/ads/adspirit.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {validateData} from '../3p/3p';
+import {setStyles} from '../src/style';
+
+/**
+  * @param {!Window} global
+  * @param {!Object} data
+  */
+export function adspirit(global, data) {
+  // TODO: check mandatory fields
+  validateData(data, [], ['asmParams', 'asmHost']);
+  const i = global.document.createElement('ins');
+  i.setAttribute('data-asm-params', data['asmParams']);
+  i.setAttribute('data-asm-host', data['asmHost']);
+  i.setAttribute('class', 'asm_async_creative');
+  setStyles(i, {
+    display: 'inline-block',
+    'text-align': 'left',
+  });
+  global.document.getElementById('c').appendChild(i);
+  const s = global.document.createElement('script');
+  s.src = 'https://' + data['asmHost'] + '/adasync.js';
+  global.document.body.appendChild(s);
+}
diff --git a/ads/adspirit.md b/ads/adspirit.md
new file mode 100644
index 000000000000..2d90f599154d
--- /dev/null
+++ b/ads/adspirit.md
@@ -0,0 +1,36 @@
+
+
+# AdSpirit
+
+## Example
+
+```html
+
+
+```
+
+## Configuration
+
+For semantics of configuration, please see ad network documentation at http://help.adspirit.de/help/.
+
+Supported parameters:
+
+- data-asm-params
+- data-asm-host
diff --git a/ads/adstir.js b/ads/adstir.js
index 74b5bab95e6c..248210dad982 100644
--- a/ads/adstir.js
+++ b/ads/adstir.js
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-import {loadScript, checkData} from '../3p/3p';
+import {loadScript, validateData} from '../3p/3p';
 
 /**
  * @param {!Window} global
  * @param {!Object} data
  */
 export function adstir(global, data) {
-  checkData(data, ['appId', 'adSpot']);
+  // TODO: check mandatory fields
+  validateData(data, [], ['appId', 'adSpot']);
 
   const v = '4.0';
 
diff --git a/ads/adtech.js b/ads/adtech.js
index cfd7a0cf61eb..c019335e7daa 100644
--- a/ads/adtech.js
+++ b/ads/adtech.js
@@ -14,15 +14,38 @@
  * limitations under the License.
  */
 
-import {writeScript, validateSrcPrefix, validateSrcContains} from '../3p/3p';
+import {
+  writeScript,
+  validateData,
+  validateSrcPrefix,
+  validateSrcContains,
+} from '../3p/3p';
+
 
-/**
- * @param {!Window} global
- * @param {!Object} data
- */
 export function adtech(global, data) {
-  const src = data.src;
-  validateSrcPrefix('https:', src);
-  validateSrcContains('/addyn/', src);
-  writeScript(global, src);
+  const adsrc = data.src;
+  if (typeof adsrc != 'undefined') {
+    validateSrcPrefix('https:', adsrc);
+    validateSrcContains('/addyn/', adsrc);
+    writeScript(global, adsrc);
+  } else {
+    validateData(data, ['atwmn', 'atwdiv'], [
+      'atwco', 'atwheight', 'atwhtnmat',
+      'atwmoat', 'atwnetid', 'atwothat', 'atwplid',
+      'atwpolar', 'atwsizes', 'atwwidth',
+    ]);
+    global.atwco = data.atwco;
+    global.atwdiv = data.atwdiv;
+    global.atwheight = data.atwheight;
+    global.atwhtnmat = data.atwhtnmat;
+    global.atwmn = data.atwmn;
+    global.atwmoat = data.atwmoat;
+    global.atwnetid = data.atwnetid;
+    global.atwothat = data.atwothat;
+    global.atwplid = data.atwplid;
+    global.atwpolar = data.atwpolar;
+    global.atwsizes = data.atwsizes;
+    global.atwwidth = data.atwwidth;
+    writeScript(global,'https://s.aolcdn.com/os/ads/adsWrapper3.js');
+  }
 }
diff --git a/ads/adtech.md b/ads/adtech.md
index af31ebb76265..5b56194c520b 100644
--- a/ads/adtech.md
+++ b/ads/adtech.md
@@ -20,15 +20,30 @@ limitations under the License.
 
 ```html
 
-
+        type="adtech"
+        data-atwMN="2842475"
+        data-atwDiv="adtech-ad-container"
+        >
 ```
 
 ## Configuration
-
 For semantics of configuration, please see ad network documentation.
 
-Supported parameters:
+### Required Parameters:
+* `data-atwMN` - magic number for the ad spot
+* `data-atwDiv` - div name of the ad spot; can be class or id
+
+### Optional parameters:
+* `data-atwPlId` - placement ID (instead of Magic Number)
+* `data-atwOthAT` - generic var to set key/value pairs to send with the ad call; accepts mulitple values in a semi-colon delimited list
+* `data-atwCo` - override default country code
+* `data-atwHtNmAT` - override ad host name
+* `data-atwNetId` - network ID
+* `data-atwWidth` - ad width (use with atwHeight only if the ad is not 300x250)
+* `data-atwHeight`- ad height (use with atwWidth only if the ad is not 300x250)
+* `data-atwSizes` - this overrides atwWidth/atwHeight; use this to create a comma-separated list of possible ad sizes
+* 'data-atwPolar' - set to "1" to enable Polar.me ad in the ad spot
+
+### Direct URL Call:
+* `src` - Value must start with `https:` and contain `/addyn/`.  This should only be used in cases where a direct ad call is being used rather than a magic number (MN).
 
-- src. Value must start with `https:` and contain `/addyn/`
diff --git a/ads/aduptech.js b/ads/aduptech.js
index 360e6e59c009..6cde94c6a128 100644
--- a/ads/aduptech.js
+++ b/ads/aduptech.js
@@ -1,5 +1,5 @@
 /**
- * Copyright 2016 The AMP HTML Authors. All Rights Reserved.
+ * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,25 +14,39 @@
  * limitations under the License.
  */
 
-import {loadScript, checkData, validateDataExists} from '../3p/3p';
+import {loadScript, validateData} from '../3p/3p';
 
 /**
  * @param {!Window} global
  * @param {!Object} data
  */
 export function aduptech(global, data) {
-  // read data
-  checkData(data, ['placementkey', 'query', 'mincpc', 'adtest']);
+  const elementId = 'aduptech';
 
-  // validate data
-  validateDataExists(data, ['placementkey']);
+  validateData(data, ['placementkey'], ['query', 'mincpc', 'adtest']);
 
   // add id attriubte to given container (required)
-  global.document.getElementById('c').setAttribute('id', 'aduptech');
+  global.document.getElementById('c').setAttribute('id', elementId);
 
-  // load aduptech js api and embed responsive iframe
+  // load aduptech js api
   loadScript(global, 'https://s.d.adup-tech.com/jsapi', () => {
+
+    // force responsive ads for amp
     data.responsive = true;
-    window.uAd.embed('aduptech', data);
+
+    // ads callback => render start
+    //
+    // NOTE: Not using "data.onAds = global.context.renderStart;"
+    //       because the "onAds()" callback returns our API object
+    //       as first parameter which will cause errors
+    data.onAds = () => {
+      global.context.renderStart();
+    };
+
+    // no ads callback => noContentAvailable
+    data.onNoAds = global.context.noContentAvailable;
+
+    // embed iframe
+    global.uAd.embed(elementId, data);
   });
 }
diff --git a/src/visibility.js b/ads/adverline.js
similarity index 69%
rename from src/visibility.js
rename to ads/adverline.js
index 41e41431aff2..fa5dfc0f01eb 100644
--- a/src/visibility.js
+++ b/ads/adverline.js
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 
-import {getElementService} from './element-service';
+import {validateData, writeScript} from '../3p/3p';
 
 /**
- * @param {!Window} win
- * @return {!Promise}
+ * @param {!Window} global
+ * @param {!Object} data
  */
-export function visibilityFor(win) {
-  return getElementService(win, 'visibility', 'amp-analytics');
-};
+export function adverline(global, data) {
+  validateData(data, ['id', 'plc'], ['s', 'section']);
+
+  writeScript(global, 'https://ads.adverline.com/richmedias/amp.js');
+}
diff --git a/ads/adverline.md b/ads/adverline.md
new file mode 100644
index 000000000000..c89405680bd5
--- /dev/null
+++ b/ads/adverline.md
@@ -0,0 +1,40 @@
+
+
+# Adverline
+
+## Examples
+
+### Single ad
+
+```html
+  
+  
+```
+
+## Configuration
+
+Required parameters:
+- id: site ID
+- plc: format ID (unique per page)
+
+Additional parameters:
+- section: tag list, separated by commas
+- s: dynamic sizing, allowed values: fixed, all, small (default), big
diff --git a/ads/adverticum.js b/ads/adverticum.js
new file mode 100644
index 000000000000..4d50d91a9539
--- /dev/null
+++ b/ads/adverticum.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2016 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {writeScript} from '../3p/3p';
+import {validateData} from '../3p/3p';
+import {setStyle} from '../src/style';
+/**
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function adverticum(global, data) {
+  validateData(data, ['goa3zone'], ['costumetargetstring']);
+  const zoneid = 'zone' + data['goa3zone'];
+  const d = global.document.createElement('div');
+
+  d.id = zoneid;
+  d.classList.add('goAdverticum');
+
+  document.getElementById('c').appendChild(d);
+  if (data['costumetargetstring']) {
+    const s = global.document.createTextNode(data['costumetargetstring']);
+    const v = global.document.createElement('var');
+    v.setAttribute('id', 'cT');
+    v.setAttribute('class', 'customtarget');
+    setStyle(v, 'display', 'none');
+    v.appendChild(s);
+    document.getElementById(zoneid).appendChild(v);
+  }
+  writeScript(global, '//ad.adverticum.net/g3.js');
+
+}
diff --git a/ads/adverticum.md b/ads/adverticum.md
new file mode 100644
index 000000000000..51990c6e6fb7
--- /dev/null
+++ b/ads/adverticum.md
@@ -0,0 +1,44 @@
+
+
+#Adverticum
+
+##Example
+
+```html
+
+
+```
+
+##Configuration
+
+The only supported costume parameters are: 
+
+Required parameter:
+
+ - ```data-goa3zone:``` It's value is the zoneID wich can be found at the Adverticum AdServer.
+
+Optional parameter:
+
+ - ```data-costumetargetstring:``` It's value must be Base64Encoded!
+
+##Support and contact
+
+For further information and specific configuration please contact our support team via e-mail:
+support@adverticum.com
\ No newline at end of file
diff --git a/ads/advertserve.js b/ads/advertserve.js
new file mode 100644
index 000000000000..1fcc7e704dc6
--- /dev/null
+++ b/ads/advertserve.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {writeScript, validateData} from '../3p/3p';
+
+/**
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function advertserve(global, data) {
+  validateData(data, [], ['zid', 'pid', 'client']);
+
+  const url = 'https://' + data.client + '.advertserve.com' +
+      '/servlet/view/banner/javascript/zone?amp=true' +
+      '&zid=' + encodeURIComponent(data.zid) +
+      '&pid=' + encodeURIComponent(data.pid) +
+      '&random=' + Math.floor(89999999 * Math.random() + 10000000) +
+      '&millis=' + Date.now();
+
+  writeScript(global, url);
+}
diff --git a/ads/advertserve.md b/ads/advertserve.md
new file mode 100644
index 000000000000..b36e223f6850
--- /dev/null
+++ b/ads/advertserve.md
@@ -0,0 +1,40 @@
+
+
+# AdvertServe
+
+## Example
+
+```html
+
+  
Loading ad.
+
Ad could not be loaded.
+
+``` + +## Configuration + +For semantics of configuration, please see ad network documentation. + +Supported parameters: + +- data-client: client id +- data-pid: publisher id +- data-zid: zone id diff --git a/ads/affiliateb.js b/ads/affiliateb.js new file mode 100644 index 000000000000..d29e21495167 --- /dev/null +++ b/ads/affiliateb.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function affiliateb(global, data) { + validateData(data, ['afb_a', 'afb_p', 'afb_t']); + global.afbParam = data; + writeScript(global, 'https://track.affiliate-b.com/amp/a.js'); +} diff --git a/ads/affiliateb.md b/ads/affiliateb.md new file mode 100644 index 000000000000..b95c944cf993 --- /dev/null +++ b/ads/affiliateb.md @@ -0,0 +1,36 @@ + + +# AffiliateB + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://www.affiliate-b.com/web/contact/form.php + +Supported parameters: + +- data-nend_params diff --git a/ads/alp/handler.js b/ads/alp/handler.js index d009ed429278..5c73db796776 100644 --- a/ads/alp/handler.js +++ b/ads/alp/handler.js @@ -19,8 +19,10 @@ import { parseQueryString, } from '../../src/url'; import {closest, openWindowDialog} from '../../src/dom'; - - +import {dev} from '../../src/log'; +import {urls} from '../../src/config'; +import {isProxyOrigin, isLocalhostOrigin, parseUrl} from '../../src/url'; +import {startsWith} from '../../src/string'; /** * Install a click listener that transforms navigation to the AMP cache @@ -42,9 +44,10 @@ export function installAlpClickHandler(win) { * Filter click event and then transform URL for direct AMP navigation * with impression logging. * @param {!Event} e + * @param {function(string)=} opt_viewerNavigate * @visibleForTesting */ -export function handleClick(e) { +export function handleClick(e, opt_viewerNavigate) { if (e.defaultPrevented) { return; } @@ -61,6 +64,9 @@ export function handleClick(e) { if (!link || !link.eventualUrl) { return; } + if (e.isTrusted === false) { + return; + } // Tag the original href with &=1 and make it a fragment param with // name click. @@ -75,12 +81,19 @@ export function handleClick(e) { const win = link.a.ownerDocument.defaultView; const ancestors = win.location.ancestorOrigins; if (ancestors && ancestors[ancestors.length - 1] == 'http://localhost:8000') { - destination = destination.replace('https://cdn.ampproject.org/c/', + destination = destination.replace( + `${parseUrl(link.eventualUrl).host}/c/`, 'http://localhost:8000/max/'); } - e.preventDefault(); - navigateTo(win, link.a, destination); + if (opt_viewerNavigate) { + // TODO: viewer navigate only support navigating top level window to + // destination. should we try to open a new window here with target=_blank + // here instead of using viewer navigation. + opt_viewerNavigate(destination); + } else { + navigateTo(win, link.a, destination); + } } /** @@ -94,7 +107,7 @@ export function handleClick(e) { * }|undefined} A URL on the AMP Cache. */ function getLinkInfo(e) { - const a = closest(/** @type {!Element} */ (e.target), element => { + const a = closest(dev().assertElement(e.target), element => { return element.tagName == 'A' && element.href; }); if (!a) { @@ -118,7 +131,8 @@ function getEventualUrl(a) { if (!eventualUrl) { return; } - if (!eventualUrl.indexOf('https://cdn.ampproject.org/c/') == 0) { + if (!isProxyOrigin(eventualUrl) || + !startsWith(parseUrl(eventualUrl).pathname, '/c/')) { return; } return eventualUrl; @@ -133,6 +147,13 @@ function getEventualUrl(a) { */ function navigateTo(win, a, url) { const target = (a.target || '_top').toLowerCase(); + const a2aAncestor = getA2AAncestor(win); + if (a2aAncestor) { + a2aAncestor.win./*OK*/postMessage('a2a;' + JSON.stringify({ + url, + }), a2aAncestor.origin); + return; + } openWindowDialog(win, url, target); } @@ -145,13 +166,12 @@ export function warmupStatic(win) { // Preconnect using an image, because that works on all browsers. // The image has a 1 minute cache time to avoid duplicate // preconnects. - new win.Image().src = 'https://cdn.ampproject.org/preconnect.gif'; + new win.Image().src = `${urls.cdn}/preconnect.gif`; // Preload the primary AMP JS that is render blocking. const linkRel = /*OK*/document.createElement('link'); linkRel.rel = 'preload'; linkRel.setAttribute('as', 'script'); - linkRel.href = - 'https://cdn.ampproject.org/rtv/01$internalRuntimeVersion$/v0.js'; + linkRel.href = `${urls.cdn}/v0.js`; getHeadOrFallback(win.document).appendChild(linkRel); } @@ -181,3 +201,51 @@ export function warmupDynamic(e) { function getHeadOrFallback(doc) { return doc.head || doc.documentElement; } + +/** + * Returns info about an ancestor that can perform A2A navigations + * or null if none is present. + * @param {!Window} win + * @return {?{ + * win: !Window, + * origin: string, + * }} + */ +export function getA2AAncestor(win) { + if (!win.location.ancestorOrigins) { + return null; + } + const origins = win.location.ancestorOrigins; + // We expect top, amp cache, ad (can be nested). + if (origins.length < 2) { + return null; + } + const top = origins[origins.length - 1]; + // Not a security property. We just check whether the + // viewer might support A2A. More domains can be added to whitelist + // as needed. + if (top.indexOf('.google.') == -1) { + return null; + } + const amp = origins[origins.length - 2]; + if (!isProxyOrigin(amp) && !isLocalhostOrigin(amp)) { + return null; + } + return { + win: getNthParentWindow(win, origins.length - 1), + origin: amp, + }; +} + +/** + * Returns the Nth parent of the given window. + * @param {!Window} win + * @param {number} distance frames above us. + */ +function getNthParentWindow(win, distance) { + let parent = win; + for (let i = 0; i < distance; i++) { + parent = parent.parent; + } + return parent; +} diff --git a/ads/alp/install-alp.js b/ads/alp/install-alp.js index 83f015050cbd..d9fb90c22508 100644 --- a/ads/alp/install-alp.js +++ b/ads/alp/install-alp.js @@ -19,6 +19,8 @@ import '../../third_party/babel/custom-babel-helpers'; import {installAlpClickHandler, warmupStatic} from './handler'; +import {initLogConstructor} from '../../src/log'; +initLogConstructor(); installAlpClickHandler(window); warmupStatic(window); diff --git a/ads/amoad.js b/ads/amoad.js new file mode 100644 index 000000000000..e6b5e0f7c52f --- /dev/null +++ b/ads/amoad.js @@ -0,0 +1,45 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function amoad(global, data) { + validateData(data, ['sid'], ['adType']); + + let script; + const attrs = {}; + if (data['adType'] === 'native') { + script = 'https://j.amoad.com/js/n.js'; + attrs['class'] = 'amoad_native'; + attrs['data-sid'] = data.sid; + } else { + script = 'https://j.amoad.com/js/a.js'; + attrs['class'] = `amoad_frame sid_${data.sid} container_div sp`; + } + global.amoadOption = {ampData: data}; + + const d = global.document.createElement('div'); + Object.keys(attrs).forEach(k => { + d.setAttribute(k, attrs[k]); + }); + global.document.getElementById('c').appendChild(d); + + loadScript(global, script); +} diff --git a/ads/amoad.md b/ads/amoad.md new file mode 100644 index 000000000000..301790395092 --- /dev/null +++ b/ads/amoad.md @@ -0,0 +1,52 @@ + + +# AMoAd + +## Example + +Banner + +```html + + +``` + +InFeed + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact http://www.amoad.com/form2/ + +Supported parameters: + +- width +- height +- data-sid +- data-ad-type diff --git a/ads/appnexus.js b/ads/appnexus.js index a6fd29a37c83..028ae807ed2b 100644 --- a/ads/appnexus.js +++ b/ads/appnexus.js @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - loadScript, writeScript, validateDataExists, -} -from '../3p/3p'; +import {loadScript, writeScript, validateData} from '../3p/3p'; const APPNEXUS_AST_URL = 'https://acdn.adnxs.com/ast/ast.js'; @@ -29,12 +26,12 @@ export function appnexus(global, data) { const args = []; args.push('size=' + data.width + 'x' + data.height); if (data.tagid) { - validateDataExists(data, ['tagid']); + validateData(data, ['tagid']); args.push('id=' + encodeURIComponent(data.tagid)); writeScript(global, constructTtj(args)); return; } else if (data.member && data.code) { - validateDataExists(data, ['member', 'code']); + validateData(data, ['member', 'code']); args.push('member=' + encodeURIComponent(data.member)); args.push('inv_code=' + encodeURIComponent(data.code)); writeScript(global, constructTtj(args)); @@ -61,7 +58,7 @@ export function appnexus(global, data) { } function appnexusAst(global, data) { - validateDataExists(data, ['adUnits']); + validateData(data, ['adUnits']); let apntag; if (context.isMaster) { // in case we are in the master iframe, we load AST context.master.apntag = context.master.apntag || {}; diff --git a/ads/atomx.js b/ads/atomx.js new file mode 100644 index 000000000000..18ecdfab7c51 --- /dev/null +++ b/ads/atomx.js @@ -0,0 +1,42 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function atomx(global, data) { + const optionals = ['click', 'uv1', 'uv2', 'uv3', 'context']; + + validateData(data, ['id'], optionals); + + const args = [ + 'size=' + data.width + 'x' + data.height, + 'id=' + encodeURIComponent(data.id), + ]; + + for (let i = 0; i < optionals.length; i++) { + const optional = optionals[i]; + if (optional in data) { + args.push(optional + '=' + encodeURIComponent(data[optional])); + } + } + + writeScript(global, 'https://s.ato.mx/p.js#' + args.join('&')); +} + diff --git a/ads/atomx.md b/ads/atomx.md new file mode 100644 index 000000000000..511bdc576eef --- /dev/null +++ b/ads/atomx.md @@ -0,0 +1,39 @@ + + +# Atomx + +## Example + +```html + + +``` + +## Configuration + +For configuration, please see [atomx documentation](https://wiki.atomx.com/tags). + +### Required Parameters: +* `data-id` - placement ID + +### Optional parameters: +* `data-click` - URL to prepend to the click URL to enable tracking +* `data-uv1`, `data-uv2`, `data-uv3` - User Value to pass in to the tag. Can be used to track & report on custom values. Needs to be a whole number between 1 and 4,294,967,295. +* `data-context` - Conversion Callback Context + diff --git a/ads/brainy.js b/ads/brainy.js new file mode 100644 index 000000000000..b121dfc1f4ea --- /dev/null +++ b/ads/brainy.js @@ -0,0 +1,32 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function brainy(global, data) { + + validateData(data, [], ['aid', 'slotId']); + + const url = 'https://proparm.jp/ssp/p/js1' + + '?_aid=' + encodeURIComponent(data['aid']) + + '&_slot=' + encodeURIComponent(data['slotId']); + + writeScript(global, url); +} diff --git a/ads/brainy.md b/ads/brainy.md new file mode 100644 index 000000000000..e8a9b94ab0d2 --- /dev/null +++ b/ads/brainy.md @@ -0,0 +1,36 @@ + + +# brainy + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact http://www.opt.ne.jp/contact_detail/id=8 + +Supported parameters: + +- data-aid +- data-slot-id diff --git a/ads/caajainfeed.js b/ads/caajainfeed.js new file mode 100644 index 000000000000..bf2502c8ec7f --- /dev/null +++ b/ads/caajainfeed.js @@ -0,0 +1,55 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function caajainfeed(global, data) { + + validateData( + data, + [], + [ + 'adSpot', + 'format', + 'test', + 'optout', + 'offset', + 'ipv4', + 'ipv6', + 'networkReachability', + 'osName', + 'osVersion', + 'osLang', + 'osTimezone', + 'deviceVersion', + 'appId', + 'appVersion', + 'kv', + 'uids', + 'template', + 'protocol', + 'fields', + ] + ); + + global.caAjaInfeedConfig = data; + loadScript(global, 'https://cdn.amanad.adtdp.com/sdk/ajaamp-v1.0.js'); + +} diff --git a/ads/caajainfeed.md b/ads/caajainfeed.md new file mode 100644 index 000000000000..da06698fb6a7 --- /dev/null +++ b/ads/caajainfeed.md @@ -0,0 +1,52 @@ + + +# CA A.J.A. Infeed: + +## Example + +```html + + +``` + +## Configuration +For configuration in detail, [please contact us](amb-nad@cyberagent.co.jp). + +### parameters +- (Required) data-ad-spot +- (Option) data-format +- (Option) data-test +- (Option) data-optout +- (Option) data-offset +- (Option) data-ipv4 +- (Option) data-ipv6 +- (Option) data-network-reachability +- (Option) data-os-name +- (Option) data-os-version +- (Option) data-os-lang +- (Option) data-os-timezone +- (Option) data-device-version +- (Option) data-app-id +- (Option) data-app-version +- (Option) data-kv +- (Option) data-uids +- (Option) data-template +- (Option) data-protocol +- (Option) data-fields diff --git a/ads/capirs.js b/ads/capirs.js new file mode 100644 index 000000000000..4c1e11df1bd2 --- /dev/null +++ b/ads/capirs.js @@ -0,0 +1,55 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData} from '../3p/3p'; +import {writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function capirs(global, data) { + validateData(data, ['begunAutoPad', 'begunBlockId']); + + global['begun_callbacks'] = { + lib: { + init: () => { + const block = global.document.createElement('div'); + block.id = 'x-' + Math.round(Math.random() * 1e8).toString(36); + document.body.appendChild(block); + + global['Adf']['banner']['ssp'](block.id, data['params'], { + 'begun-auto-pad': data['begunAutoPad'], + 'begun-block-id': data['begunBlockId'], + }); + }, + }, + block: { + draw: feed => { + const banner = feed['banners']['graph'][0]; + window.context.renderStart({ + width: banner['width'], + height: banner['height'], + }); + const reportId = 'capirs-' + banner['banner_id']; + window.context.reportRenderedEntityIdentifier(reportId); + }, + unexist: window.context.noContentAvailable, + }, + }; + + writeScript(global, '//ssp.rambler.ru/capirs_async.js'); +} diff --git a/ads/capirs.md b/ads/capirs.md new file mode 100644 index 000000000000..1bd1c064a7dc --- /dev/null +++ b/ads/capirs.md @@ -0,0 +1,39 @@ + + +# Rambler&Co + +## Example + +```html + +``` + +## Configuration + +For semantics of configuration, please see Rambler SSP documentation. + +Supported parameters: + +- data-begun-auto-pad +- data-begun-block-id diff --git a/test/functional/test-sha384.js b/ads/caprofitx.js similarity index 61% rename from test/functional/test-sha384.js rename to ads/caprofitx.js index 801844cf8e8f..5516a8d8cad4 100644 --- a/test/functional/test-sha384.js +++ b/ads/caprofitx.js @@ -14,15 +14,15 @@ * limitations under the License. */ -import { - sha384Base64, -} from '../../third_party/closure-library/sha384-generated'; +import {loadScript, validateData} from '../3p/3p'; -describe('sha384', () => { - it('should hash', () => { - expect(sha384Base64('abc')).to.equal( - 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); - expect(sha384Base64('foobar')).to.equal( - 'PJww2fZl501RXIQpYNSkUcg6ASX9Pec5LXs3IxrxDHLqWK7fzfiaV2W_kCr5Ps8G'); - }); -}); +/** + * @param {!Window} global + * @param {!Object} data + */ +export function caprofitx(global, data) { + validateData(data, ['tagid'], []); + + global.caprofitxConfig = data; + loadScript(global, 'https://cdn.caprofitx.com/tags/amp/profitx_amp.js'); +} diff --git a/ads/caprofitx.md b/ads/caprofitx.md new file mode 100644 index 000000000000..1f261db320cb --- /dev/null +++ b/ads/caprofitx.md @@ -0,0 +1,37 @@ + + +# CA ProFit-X + +## Example + +```html + + +``` + +## Configuration +For configuration in detail, [please contact us](ca_profitx_support@cyberagent.co.jp). + +### parameters +- (Required) data-tagid +- (Option) data-placeid diff --git a/ads/colombia.js b/ads/colombia.js index 0f7820075bc0..1f2ec93ffd8b 100644 --- a/ads/colombia.js +++ b/ads/colombia.js @@ -14,14 +14,14 @@ * limitations under the License. */ -import {loadScript, validateDataExists} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function colombia(global, data) { - validateDataExists(data, [ + validateData(data, [ 'clmb_slot', 'clmb_position', 'clmb_section', 'clmb_divid', 'loadingStrategy', ]); @@ -32,7 +32,7 @@ export function colombia(global, data) { clmbsection: data.clmb_section, clmbdivid: data.clmb_divid, }); -// install observation on entering/leaving the view + // install observation on entering/leaving the view global.context.observeIntersection(function(newrequest) { newrequest.forEach(function(d) { if (d.intersectionRect.height > 0) { diff --git a/ads/contentad.js b/ads/contentad.js new file mode 100644 index 000000000000..a0f9ed2c0f9d --- /dev/null +++ b/ads/contentad.js @@ -0,0 +1,53 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function contentad(global, data) { + validateData(data, [], ['id', 'd', 'wid', 'url']); + global.id = data.id; + global.d = data.d; + global.wid = data.wid; + global.url = data.url; + + /* Create div for ad to target */ + const cadDiv = window.document.createElement('div'); + cadDiv.id = 'contentad' + global.wid; + window.document.body.appendChild(cadDiv); + + /* Pass Source URL */ + let sourceUrl = window.context.sourceUrl; + if (data.url) { + const host = window.context.location.host; + const domain = data.url || window.atob(data.d); + sourceUrl = sourceUrl.replace(host, domain); + } + + /* Build API URL */ + const cadApi = 'https://api.content-ad.net/Scripts/widget2.aspx' + + '?id=' + encodeURIComponent(global.id) + + '&d=' + encodeURIComponent(global.d) + + '&wid=' + global.wid + + '&url=' + encodeURIComponent(sourceUrl) + + '&cb=' + Date.now(); + + /* Call Content.ad Widget */ + writeScript(global, cadApi); +} diff --git a/ads/contentad.md b/ads/contentad.md new file mode 100644 index 000000000000..1dfb8a526011 --- /dev/null +++ b/ads/contentad.md @@ -0,0 +1,40 @@ + + +# Content.ad Inline Ad + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see [Content.ad AMP Widget](http://help.content.ad/how-can-i-make-widget-amp-mobile-site/) documentation. + +Supported parameters: + +- data-id : Ad Widget GUID +- data-d : Ad Widget Domain ID +- data-wid : Ad Widget ID diff --git a/ads/criteo.js b/ads/criteo.js index 3dcaaa54516b..416e850eb472 100644 --- a/ads/criteo.js +++ b/ads/criteo.js @@ -14,7 +14,8 @@ * limitations under the License. */ -import {loadScript} from '../3p/3p'; +import {computeInMasterFrame, loadScript} from '../3p/3p'; +import {doubleclick} from '../ads/google/doubleclick'; /* global Criteo: false */ @@ -24,6 +25,47 @@ import {loadScript} from '../3p/3p'; */ export function criteo(global, data) { loadScript(global, 'https://static.criteo.net/js/ld/publishertag.js', () => { - Criteo.DisplayAd({'zoneid': data.zone, 'async': true, 'containerid': 'c'}); + if (data.tagtype === 'rta') { + // Make sure RTA is called only once + computeInMasterFrame(window, 'call-rta', resultCallback => { + const params = { + networkid: data.networkid, + cookiename: + data.cookiename || Criteo.PubTag.RTA.DefaultCrtgRtaCookieName, + varname: + data.varname || Criteo.PubTag.RTA.DefaultCrtgContentName, + }; + Criteo.CallRTA(params); + resultCallback(null); + }, () => {}); + setTargeting(global, data); + } else if (!data.tagtype || data.tagtype === 'passback') { + Criteo.DisplayAd({ + zoneid: data.zone, + containerid: 'c', + integrationmode: 'amp', + }); + } }); } + +/** + * @param {!Window} global + * @param {!Object} data + */ +function setTargeting(global, data) { + if (data.adserver === 'DFP') { + const dblParams = { + slot: data.slot, + targeting: Criteo.ComputeDFPTargetingForAMP( + data.cookiename || Criteo.PubTag.RTA.DefaultCrtgRtaCookieName, + data.varname || Criteo.PubTag.RTA.DefaultCrtgContentName), + width: data.width, + height: data.height, + type: 'criteo', + }; + doubleclick(global, dblParams); + } +} + + diff --git a/ads/criteo.md b/ads/criteo.md index 77b769840c2d..31d4289bb478 100644 --- a/ads/criteo.md +++ b/ads/criteo.md @@ -16,24 +16,51 @@ limitations under the License. # Criteo -## Example +Criteo support for AMP covers Real Time Audience (RTA), Publisher Marketplace (PuMP) and Passback technologies. + +For configuration details and to generate your tags, please refer to [your publisher account](https://publishers.criteo.com) or contact publishers@criteo.com. + +## Example - RTA + +```html + + +``` + +## Example - PuMP and Passback ```html + data-tagtype=“passback” + data-zone=“567890”> ``` -## Ad size +## Configuration The ad size is based on the setup of your Criteo zone. The `width` and `height` attributes of the `amp-ad` tag should match that. +### RTA -## Configuration +Supported parameters: -For configuration details and to generate your tags, please refer to [your publisher account](https://publishers.criteo.com) or contact publishers@criteo.com. +- `data-tagtype`: identifies the used Criteo technology. Must be “rta”. Required. +- `data-adserver`: the name of your adserver. Required. Only “DFP” is supported at this stage. +- `data-slot`: adserver (DFP) slot slot. Required. +- `data-networkid`: your Criteo network id. Required. +- `data-varname`: `crtg_content` variable name to store RTA labels. Optional. +- `data-cookiename`: `crtg_rta` RTA cookie name. Optional. + +### PuMP and Passback Supported parameters: -- `data-zone`: your Criteo zone identifier. +- `data-tagtype`: identifies the used Criteo technology. Must be “passback”. Required. +- `data-zone`: your Criteo zone identifier. Required. + diff --git a/ads/custom.md b/ads/custom.md new file mode 100644 index 000000000000..f8317ae1378a --- /dev/null +++ b/ads/custom.md @@ -0,0 +1,217 @@ + + +# Custom (experimental) + +Custom does not represent a specific network. Rather, it provides a way for +a site to display simple ads on a self-service basis. You must provide +your own ad server to deliver the ads in json format as shown below. + +Each ad must contain a [mustache](https://github.com/ampproject/amphtml/blob/master/extensions/amp-mustache/amp-mustache.md) +template. + +Each ad must contain the URL that will be used to fetch data from the server. + +Usually, there will be multiple ads on a page. The best way of dealing with this +is to give all the ads the same ad server URL and give each ad a different slot id: +this will result in a single call to the ad server. + +An alternative is to use a different URL for each ad, according to some format +understood by the ad server(s) which you are calling. + +## Examples + +### Single ad with no slot specified + +```html + + + + +``` + +### Two ads with different slots + +```html + + + + + + + +``` + +### Ads from different ad servers +```html + + + + + + + + + + + + +``` + +## Supported parameters + +### data-url (mandatory) + +This must be starting with `https://`, and it must be the address of an ad +server returning json in the format defined below. + +### data-slot (optional) + +On the assumption that most pages have multiple ad slots, this is passed to the +ad server to tell it which slot is being fetched. This can be any alphanumeric string. + +If you have only a single ad for a given value of `data-url`, it's OK not to bother with +the slot id. However, do not use two ads for the same `data-url` where one has a slot id +specified and the other does not. + +## Ad server + +The ad server will be called once for each value of `data-url` on the page: for the vast +majority of applications, all your ads will be from a single server so it will be +called only once. + +A parameter like `?ampslots=1,2` will be appended to the URL specified by `data-url` in order +to specify the slots being fetched. See the examples above for details. + +The ad server should return a json object containing a record for each slot in the request, keyed by the +slot id in `data-slot`. The record format is defined by your template. For the examples above, +the record contains three fields: + +* src - string to go into the source parameter of the image to be displayed. This can be a +web reference (in which case it must be `https:` or a `data:` URI including the base64-encoded image. +* href - URL to which the user is to be directed when he clicks on the ad +* info - A string with additional info about the ad that was served, mmaybe for use with analytics + +Here is an example response, assuming two slots named simply 1 and 2: + +```json +{ + "1": { + "src":"https://my-ad-server.com/my-advertisement.gif", + "href":"https://bachtrack.com", + "info":"Info1" + }, + "2": { + "src":"data:image/gif;base64,R0lGODlhyAAiALM...DfD0QAADs=", + "href":"http://onestoparts.com", + "info":"Info2" + } +} +``` +If no slot was specified, the server returns a single template rather than an array. + +```json +{ + "src":"https://my-ad-server.com/my-advertisement.gif", + "href":"https://bachtrack.com", + "info":"Info1" +} +``` + +## Analytics + +To get analytics of how your ads are performing, use the [amp-analyics](https://github.com/ampproject/amphtml/blob/master/extensions/amp-analytics/amp-analytics.md) tag. + +Here is an example of how to make it work with Google Analytics events. Note that the variables can be set either by the code +that displays the page (as in `eventAction`) or in variables passed back by the ad server (as in `eventCategory` and `eventLabel`). + +```html + + + + + + +``` + +## To do + +Add support for json variables in the data-url - and perhaps other variable substitutions in the way amp-list does diff --git a/ads/distroscale.js b/ads/distroscale.js new file mode 100644 index 000000000000..d29a035cde0c --- /dev/null +++ b/ads/distroscale.js @@ -0,0 +1,51 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function distroscale(global, data) { + validateData(data, ['pid'], ['zid', 'tid']); + let src = '//c.jsrdn.com/s/cs.js?p=' + encodeURIComponent(data.pid); + + if (data.zid) { + src += '&z=' + encodeURIComponent(data.zid); + } else { + src += '&z=amp'; + } + + if (data.tid) { + src += '&t=' + encodeURIComponent(data.tid); + } + + let srcUrl = global.context.sourceUrl; + + srcUrl = srcUrl.replace(/#.+/, '').replace(/\?.+/, ''); + + src += '&f=' + encodeURIComponent(srcUrl); + + + global.dsAMPCallbacks = { + renderStart: global.context.renderStart, + noContentAvailable: global.context.noContentAvailable, + }; + loadScript(global, src, () => {}, () => { + global.context.noContentAvailable(); + }); +} diff --git a/ads/distroscale.md b/ads/distroscale.md new file mode 100644 index 000000000000..6260c8141887 --- /dev/null +++ b/ads/distroscale.md @@ -0,0 +1,35 @@ + + +# DistroScale + +## Example + +```html + +``` + +## Configuration + +For semantics of configuration, please [contact DistroScale](http://www.distroscale.com). + +__Required:__ + +- `data-pid` - Partner ID diff --git a/ads/eplanning.js b/ads/eplanning.js index 04c15b33b5b5..04972711af23 100644 --- a/ads/eplanning.js +++ b/ads/eplanning.js @@ -14,14 +14,14 @@ * limitations under the License. */ -import {loadScript, validateDataExists} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function eplanning(global, data) { - validateDataExists(data, [ + validateData(data, [ 'epl_si', 'epl_isv', 'epl_sv', 'epl_sec', 'epl_kvs', 'epl_e', ]); // push the two object into the '_eplanning' global diff --git a/ads/ezoic.js b/ads/ezoic.js new file mode 100644 index 000000000000..9f44b12aa291 --- /dev/null +++ b/ads/ezoic.js @@ -0,0 +1,33 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function ezoic(global, data) { + // TODO: check mandatory fields + validateData(data, [], ['slot','targeting','extras']); + loadScript(global, 'https://g.ezoic.net/ezoic/ampad.js', () => { + loadScript(global, 'https://www.googletagservices.com/tag/js/gpt.js', () => { + global.googletag.cmd.push(() => { + new window.EzoicAmpAd(global,data).createAd(); + }); + }); + }); +} diff --git a/ads/ezoic.md b/ads/ezoic.md new file mode 100644 index 000000000000..a6b05e22b54d --- /dev/null +++ b/ads/ezoic.md @@ -0,0 +1,45 @@ + + +# Ezoic + +## Example + +```html + + +``` + +## Ad size + +The ad size is the size of the ad that should be displayed. Make sure the `width` and `height` attributes of the `amp-ad` tag match the available ad size. + + +## Configuration + +To generate tags, please visit https://svc.ezoic.com/publisher.php?login + +Supported parameters: + +- `data-slot`: the slot name corresponding to the ad position + +Supported via `json` attribute: + +- `targeting` +- `extras` diff --git a/ads/f1e.js b/ads/f1e.js new file mode 100644 index 000000000000..162a5d44ee86 --- /dev/null +++ b/ads/f1e.js @@ -0,0 +1,28 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ + +export function f1e(global, data) { + validateData(data, ['url','target'], []); + global.f1eData = data; + writeScript(global, 'https://img.ak.impact-ad.jp/util/f1e_amp.min.js'); +} diff --git a/ads/f1e.md b/ads/f1e.md new file mode 100644 index 000000000000..95c51151456a --- /dev/null +++ b/ads/f1e.md @@ -0,0 +1,36 @@ + + + +# FlexOneELEPHANT + +## Example + +```html + + +``` + +## Configuration + +Supported parameters: + +- `data-url` - Must start with "https:" +- `data-target` + diff --git a/ads/felmat.js b/ads/felmat.js new file mode 100644 index 000000000000..de96fe5e9b25 --- /dev/null +++ b/ads/felmat.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function felmat(global, data) { + validateData(data, ['host', 'fmt', 'fmk', 'fmp']); + global.fmParam = data; + writeScript(global, 'https://t.' + encodeURIComponent(data.host) + '/js/fmamp.js'); +} diff --git a/ads/felmat.md b/ads/felmat.md new file mode 100644 index 000000000000..3104a24e346a --- /dev/null +++ b/ads/felmat.md @@ -0,0 +1,41 @@ + + +# felmat + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://www.felmat.net/service/inquiry + +Supported parameters: + +- data-host +- data-fmt +- data-fmk +- data-fmp + diff --git a/ads/flite.js b/ads/flite.js index 0c0c58e52036..e10e26432da9 100644 --- a/ads/flite.js +++ b/ads/flite.js @@ -14,14 +14,15 @@ * limitations under the License. */ - import {loadScript, checkData} from '../3p/3p'; + import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function flite(global, data) { - checkData(data, ['guid','mixins']); + // TODO: check mandatory fields + validateData(data, [], ['guid','mixins']); const guid = data.guid, o = global, e = encodeURIComponent, x = 0; let r = '', m, url, dep = ''; o.FLITE = o.FLITE || {}; diff --git a/ads/fusion.js b/ads/fusion.js new file mode 100644 index 000000000000..f5d420586d11 --- /dev/null +++ b/ads/fusion.js @@ -0,0 +1,40 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function fusion(global, data) { + validateData(data, [], ['mediaZone', 'layout', 'adServer', 'space']); + + const container = global.document.getElementById('c'); + const ad = global.document.createElement('div'); + ad.setAttribute('data-fusion-space', data.space); + container.appendChild(ad); + + writeScript(global, 'https://assets.adtomafusion.net/fusion/latest/fusion-amp.min.js', () => { + global.Fusion.apply(container, global.Fusion.loadAds(data)); + + global.Fusion.on.warning.run(ev => { + if (ev.msg === 'Space not present in response.') { + global.context.noContentAvailable(); + } + }); + }); +} diff --git a/ads/fusion.md b/ads/fusion.md new file mode 100644 index 000000000000..577f46f3aa0b --- /dev/null +++ b/ads/fusion.md @@ -0,0 +1,40 @@ + + +# Fusion + +## Example + +```html + + +``` + +## Configuration + +For configuration and implementation details, please contact Fusion support team support@adtoma.com + +Supported parameters: + +- data-ad-server +- data-media-zone +- data-layout +- data-space \ No newline at end of file diff --git a/ads/genieessp.js b/ads/genieessp.js new file mode 100644 index 000000000000..0eb920ac1fd6 --- /dev/null +++ b/ads/genieessp.js @@ -0,0 +1,28 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function genieessp(global, data) { + validateData(data, ['vid', 'zid']); + + global.data = data; + writeScript(global, 'https://js.gsspcln.jp/l/amp.js'); +} diff --git a/ads/genieessp.md b/ads/genieessp.md new file mode 100644 index 000000000000..135bd387a0b1 --- /dev/null +++ b/ads/genieessp.md @@ -0,0 +1,38 @@ + + +# Geniee SSP + +Please visit [our website](https://www.geniee.co.jp/) for more information. + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see our SSP documentation or [contact us](http://en.geniee.co.jp/contact.html). + +Supported parameters: + +- data-vid +- data-zid diff --git a/ads/gmossp.js b/ads/gmossp.js index 7eff7e07c5f2..452ab42b3434 100644 --- a/ads/gmossp.js +++ b/ads/gmossp.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {writeScript, checkData, validateDataExists} from '../3p/3p'; +import {writeScript, validateData} from '../3p/3p'; const gmosspFields = ['width', 'height', 'id']; @@ -23,8 +23,7 @@ const gmosspFields = ['width', 'height', 'id']; * @param {!Object} data */ export function gmossp(global, data) { - checkData(data, gmosspFields); - validateDataExists(data, gmosspFields); + validateData(data, gmosspFields, []); global.gmosspParam = data; writeScript(global, 'https://cdn.gmossp-sp.jp/ads/amp.js'); diff --git a/ads/google/OWNERS.yaml b/ads/google/OWNERS.yaml new file mode 100644 index 000000000000..97a4c1838d4e --- /dev/null +++ b/ads/google/OWNERS.yaml @@ -0,0 +1,3 @@ +- ampproject/a4a +- file-only: + - bobcassels: adsense.js diff --git a/ads/google/a4a/docs/1.png b/ads/google/a4a/docs/1.png new file mode 100644 index 000000000000..f59a3d8751c2 Binary files /dev/null and b/ads/google/a4a/docs/1.png differ diff --git a/ads/google/a4a/docs/2.png b/ads/google/a4a/docs/2.png new file mode 100644 index 000000000000..20497c385e49 Binary files /dev/null and b/ads/google/a4a/docs/2.png differ diff --git a/ads/google/a4a/docs/Network-Impl-Guide.md b/ads/google/a4a/docs/Network-Impl-Guide.md new file mode 100644 index 000000000000..d4142fa377fc --- /dev/null +++ b/ads/google/a4a/docs/Network-Impl-Guide.md @@ -0,0 +1,167 @@ +# Fast Fetch Network Implementation Guide + +*Status: Draft* + +*Authors: [kjwright@google.com](mailto:kjwright@google.com), [bradfrizzell@google.com](mailto:bradfrizzell@google.com)* + +*Last Updated: 1-27-2016* + + +# Objective + +Outline requirements and steps for ad network to implement Fast Fetch for early ad request and support for AMP Ads returned by the ad network to be given preferential rendering. + +# Background + +Relevant design documents: [A4A Readme](./a4a-readme.md), [A4A Format Guide](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md) & [intent to implement](https://github.com/ampproject/amphtml/issues/3133). + +If you haven’t already, please read the A4A Readme to learn about why all networks should implement Fast Fetch. + +# Overview + +Fast Fetch provides preferential treatment to Verified AMP Ads over Legacy Ads, unlike the current 3P rendering flow which treats AMP Ads and Legacy Ads the same. Within Fast Fetch, if an ad fails validation, that ad is wrapped in a cross-domain iframe to sandbox it from the rest of the AMP document. Conversely, an AMP Ad passing validation is written directly into the page. Fast Fetch handles both AMP and non-AMP ads; no additional ad requests are required for ads that fail validation. + +In order to support Fast Fetch, ad networks will be required to implement the following: + +* [XHR CORS](https://www.w3.org/TR/cors/) for the ad request + +* The Javascript to build the ad request must be located within the AMP HTML github repository (example implementations: [AdSense](https://github.com/ampproject/amphtml/tree/master/extensions/amp-ad-network-adsense-impl) & [Doubleclick](https://github.com/ampproject/amphtml/tree/master/extensions/amp-ad-network-doubleclick-impl)). + +# Detailed Design + +![Image of Rendering Flow](./1.png) +Figure 1: Fast Fetch Rendering Flow + + +## Ad server requirements + +### SSL + +All network communication via the AMP HTML runtime (resources or XHR) require SSL. + +### AMP Ad Creative Signature + +In order for the AMP runtime to know that a creative is valid [AMP](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md), and thus receive preferential ad rendering, it must pass a client-side, validation check. The creative must be sent by the ad network to a validation service which verifies the creative conforms to the [specification](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md). If so, it will be rewritten by the validation service and the rewritten creative and a cryptographic signature will be returned to the ad network. The rewritten creative and signature must be included in the response to the AMP runtime from the ad network. extractCreativeAndSignature will then parse out the creative and the signature from the ad response. Lack of, or invalid signature will cause the runtime to treat it as a Legacy Ad, rendering it within a cross domain iframe and using delayed ad rendering. + +### Ad Response Headers + +*See Figure 1 above, Part C* + +Fast Fetch requires that the ad request be sent via [XHR CORS](https://www.w3.org/TR/cors/) as this allows for direct communication with the ad network without the possibility of custom javascript execution (e.g. iframe or JSONP). XHR CORS requires a preflight request where the response needs to indicate if the request is allowed by including the following headers in the response:: + +* "Access-Control-Allow-Origin" with value matching the value of the request "Origin" header only if the origin domain is allowed ("Note that requests from pages hosted on the Google AMP Cache will have a value matching the domain [https://cdn.ampproject.org](https://cdn.ampproject.org)"). + +* "AMP-Access-Control-Allow-Source-Origin" with value matching the value of the request parameter "__amp_source_origin" which is [added](https://github.com/ampproject/amphtml/blob/master/src/service/xhr-impl.js#L103) by the AMP HTML runtime and matches the origin of the request had the page not been served from [Google AMP Cache](https://www.ampproject.org/docs/get_started/about-amp.html) (the originating source of the page). Ad network can use this to prevent access by particular publisher domains where lack of response header will cause the response to be [dropped](https://github.com/ampproject/amphtml/blob/master/src/service/xhr-impl.js#L137) by the AMP HTML runtime. + +* "Access-Control-Allow-Credentials" with value "true" if cookies should be included in the request. + +* "Access-Control-Expose-Headers" with value matching comma separated list any non-standard response headers included in the response. At a minimum, this should include "AMP-Access-Control-Allow-Source-Origin". If other custom headers are not included, they will be dropped by the browser. + +## A4A Extension Implementation + +The [AMP Ad](https://github.com/ampproject/amphtml/blob/master/extensions/amp-ad/amp-ad.md) element differentiates between different ad network implementations via the type attribute, e.g. the following amp-ad will utilize DoubleClick: + + + +To create an ad network implementation, the following steps must be taken: + +Create a new extension within the extensions section in the AMP HTML Github [repository](https://github.com/ampproject/amphtml) whose path and name match the type attribute given for amp ad element as follows: + +![Image of File Hierarchy](./2.png) +Figure 2: A4A Extension File Hierarchy + + +Ad networks that want to add support for Fast Fetch within AMP must add the file hierarchy as seen in Figure 2 to the AMP repository, with `` replaced by their own network. Files must implement all requirements as specified below. Anything not specified, i.e. helper functions etc are at the discretion of the ad network, but must be approved by AMP project members just as any other contributions. + +### `amp-ad-network--impl.js` + +*See Figure 1 Parts B and D* + +Implement class `AmpAdNetworkImpl`. This class must extend [AmpA4A](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/0.1/amp-a4a.js). This class must overwrite the super class methods **getAdUrl()** and **extractCreativeAndSignature()**. + + +``` javascript +getAdUrl() - must construct and return the ad url for ad request. + // @return {string} - the ad url +``` + +``` javascript +extractCreativeAndSignature(responseText, responseHeaders) + // @param {!ArrayBuffer} responseText Response body from the ad request. + // @param {!Headers} responseHeaders Response headers from the ad request + // @return {Object} creativeParts Object must have a .creative and a .signature. +``` + + +Examples of network implementations can be seen for [DoubleClick](https://github.com/ampproject/amphtml/blob/master/extensions/amp-ad-network-doubleclick-impl/0.1/amp-ad-network-doubleclick-impl.js) and [AdSense](https://github.com/ampproject/amphtml/blob/master/extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js). + +Usage of getAdUrl and extractCreativeAndSignature can be seen within the this.adPromise_ promise chain in [amp-a4a.js](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/0.1/amp-a4a.js) + +### `-a4a-config.js` + +*See Figure 1: Part A* + +Must implement and export following function. + +``` javascript +IsA4AEnabled(win, element) + // @param (Window) win Window where AMP runtime is running. + // @param (HTML Element) element ****The amp-ad element. + // @return (boolean) Whether or not A4A should be used in this context. +``` + +Once this file is implemented, [amphtml/ads/_a4a-config.js](https://github.com/ampproject/amphtml/blob/master/ads/_a4a-config.js) must also be updated. Specifically, `IsA4AEnabled()` must be imported, and it must be mapped to the ad network type in the a4aRegistry mapping. + +``` javascript +/**amphtml/ads/_a4a-config.js */ +… +import { + IsA4AEnabled +} from ‘../extensions/amp-ad--impl/0.1/-a4a-config’; +… +export const a4aRegistry = map({ + … + ‘’: IsA4AEnabled, + … +}); +``` + +Example configs for [DoubleClick](https://github.com/ampproject/amphtml/blob/master/extensions/amp-ad-network-doubleclick-impl/0.1/doubleclick-a4a-config.js#L80) and [AdSense](https://github.com/ampproject/amphtml/blob/master/extensions/amp-ad-network-adsense-impl/0.1/adsense-a4a-config.js#L68) + +Usage of DoubleClick and AdSense configs can be seen in [_a4a-config.js](https://github.com/ampproject/amphtml/blob/master/ads/_a4a-config.js) + +### `amp-ad-network--impl-internal.md` + +Documentation for ad network amp-ad type. Please thoroughly document the usage of your implementation. + +Examples can be seen for [DoubleClick](https://github.com/ampproject/amphtml/blob/master/extensions/amp-ad-network-doubleclick-impl/amp-ad-network-doubleclick-impl-internal.md) and [AdSense](https://github.com/ampproject/amphtml/blob/master/extensions/amp-ad-network-adsense-impl/amp-ad-network-adsense-impl-internal.md). + +### `test-amp-ad-network--impl.js` + +Please write thorough testing for your AMP ad network implementation. + +## Ad Network Checklist + +* All Server-AMP communication done with SSL + +* AMP Ads sent to Validation server + +* Validated AMP Ads sent from network to AMP with signature + +* Validated AMP Ads sent from network to AMP with appropriate headers + +* File hierarchy created within amphtml/extensions + +* Custom `amp-ad-network--impl.js` overwrites getAdUrl() + +* Custom `amp-ad-network--impl.js` overwrites extractCreativeAndSignature() + +* `-a4a-config.js` implements IsA4AEnabled() + +* Mapping added for ad network to a4aRegistry map within _a4a-config.js + +* Documentation written in `amp-ad-network--impl-internal.md` + +* Tests written in `test-amp-ad-network--impl.js` + +* Pull request merged to master diff --git a/ads/google/a4a/docs/a4a-readme.md b/ads/google/a4a/docs/a4a-readme.md new file mode 100644 index 000000000000..c8d950d352cb --- /dev/null +++ b/ads/google/a4a/docs/a4a-readme.md @@ -0,0 +1,48 @@ +# A4A - AMP for Ads + +A4A applies AMP’s core philosophy of reliable fast performance and great user experience to ads. + +# AMP Ads + +AMP ads are written in AMP format - [A4A HTML](https://github.com/google/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md) (A variant of AMP HTML) + CSS. This means that ads can no longer have the ability to run arbitrary JavaScript - which is traditionally the number one cause of poor ad performance. Therefore, just like core AMP, the core ads JavaScript use-cases are built right into the AMP Open Source project which guarantees good behavior from ads. + +# Why are AMP ads better than regular ads? + +### Faster +AMP ads are faster because on AMP pages they are requested early while rendering the page and immediately displayed just before the user is about to view the ad. Reduced file size of AMP ads also increases speed. + +### More Aware +On AMP pages, the AMP runtime can coordinate a mobile phone's limited resources to the right component at the right time to give the best user experience. For example, AMP ads with animations are paused when not in the current viewport. + +### Lighter +AMP ads bundle commonly used ad functionality which removes bloat. Once on the page, AMP ads also consume less resources. For example, instead of 10 trackers requesting their own information in regular ads, AMP ads collect all the information once and distribute it to any number of interested trackers. + +### More Engaging +"Users can't tap on ads they can't see". Faster ads lead to higher viewability and therefore higher click through rates, which ultimately leads to higher advertiser conversions. + +### Safer +It's impossible to spread malware through advertising with AMP ads. Not only are visitors safer, but advertiser brand perception cannot be negative. + +### More Flexible +AMP ads are designed to work on both AMP and Non-AMP webpages, including desktop where the ad tagging library supports it. (e.g. GPT) + +# Current status + +The AMP ads format spec has been [released](https://github.com/google/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md) and any creative developer can create AMP ads. In order for ads to get preferred treatment on AMP pages, ad server support is required. Advertisers using DoubleClick or AdSense can already deliverprogrammatic AMP ads to publisher AMP pages. Publishers using DFP (DoubleClick for Publishers) can already deliver their own AMP ads. Advertisers or publishers using other ad providers can implement AMP ads with the help of a signing service like CloudFlare. Cloudflare provides AMP ad verification services, enabling any independent ad provider to deliver faster, lighter, and more engaging ads. + + + +Here is how you can participate. If you are a: + +## Publishers + +If publishers want to serve their direct-sold ad formats they must create the ads in[ A4A format](https://github.com/google/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md) (or use a creative agency), and deliver them using an AMP ad supported ad server. + +## Creative Agencies + +If you are a creative agency, please reach out to us via Github so we can put you in touch with publishers and advertisers who are interested in developing AMP ads. + +## Ad Networks + +Please refer to the [Network Implementation Guide](./Network-Impl-Guide.md) + diff --git a/ads/google/a4a/google-data-reporter.js b/ads/google/a4a/google-data-reporter.js new file mode 100644 index 000000000000..1ba70989498e --- /dev/null +++ b/ads/google/a4a/google-data-reporter.js @@ -0,0 +1,192 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {EXPERIMENT_ATTRIBUTE, QQID_HEADER} from './utils'; +import {BaseLifecycleReporter, GoogleAdLifecycleReporter} from './performance'; +import {getMode} from '../../../src/mode'; +import {isExperimentOn, toggleExperiment} from '../../../src/experiments'; + +import { + parseExperimentIds, + isInManualExperiment, + randomlySelectUnsetPageExperiments, +} from './traffic-experiments'; +import { + ADSENSE_A4A_EXTERNAL_EXPERIMENT_BRANCHES, + ADSENSE_A4A_INTERNAL_EXPERIMENT_BRANCHES, +} from '../../../extensions/amp-ad-network-adsense-impl/0.1/adsense-a4a-config'; +import { + DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES, + DOUBLECLICK_A4A_INTERNAL_EXPERIMENT_BRANCHES, +} from '../../../extensions/amp-ad-network-doubleclick-impl/0.1/doubleclick-a4a-config'; // eslint-disable-line max-len + +/** + * An experiment config for controlling profiling. Profiling has no branches: + * it's either on or off for a given page. The off state is controlled by the + * general traffic-experiments mechanism and is configured via the + * a4aProfilingRate property of the global config(s), + * build-system/global-configs/{canary,prod}-config.js. This object is just + * necessary for the traffic-experiments.js API, which expects a branch list + * for each experiment. We assign all pages to the "control" branch + * arbitrarily. + * + * @const {!Object} + */ +export const PROFILING_BRANCHES = { + a4aProfilingRate: { + control: 'unused', + experiment: 'unused', + }, +}; + +/** + * Set of namespaces that can be set for lifecycle reporters. + * + * @enum {string} + */ +export const ReporterNamespace = { + A4A: 'a4a', + AMP: 'amp', +}; + +/** + * Check whether the element is in an experiment branch that is eligible for + * monitoring. + * + * @param {!AMP.BaseElement} ampElement + * @param {!string} namespace + * @returns {boolean} + */ +function isInReportableBranch(ampElement, namespace) { + // Handle the possibility of multiple eids on the element. + const eids = parseExperimentIds( + ampElement.element.getAttribute(EXPERIMENT_ATTRIBUTE)); + const reportableA4AEids = { + [ADSENSE_A4A_EXTERNAL_EXPERIMENT_BRANCHES.experiment]: 1, + [ADSENSE_A4A_INTERNAL_EXPERIMENT_BRANCHES.experiment]: 1, + [DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES.experiment]: 1, + [DOUBLECLICK_A4A_INTERNAL_EXPERIMENT_BRANCHES.experiment]: 1, + }; + const reportableControlEids = { + [ADSENSE_A4A_EXTERNAL_EXPERIMENT_BRANCHES.control]: 1, + [ADSENSE_A4A_INTERNAL_EXPERIMENT_BRANCHES.control]: 1, + [DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES.control]: 1, + [DOUBLECLICK_A4A_INTERNAL_EXPERIMENT_BRANCHES.control]: 1, + }; + switch (namespace) { + case ReporterNamespace.A4A: + return eids.some(x => { return x in reportableA4AEids; }) || + isInManualExperiment(ampElement.element); + case ReporterNamespace.AMP: + return eids.some(x => { return x in reportableControlEids; }); + default: + return false; + } +} + +/** + * @param {!AMP.BaseElement} ampElement The element on whose lifecycle this + * reporter will be reporting. + * @param {string} namespace + * @param {number|string} slotId A unique numeric identifier in the page for + * the given element's slot. + * @return {!GoogleAdLifecycleReporter|!BaseLifecycleReporter} + */ +export function getLifecycleReporter(ampElement, namespace, slotId) { + // Carve-outs: We only want to enable profiling pingbacks when: + // - The ad is from one of the Google networks (AdSense or Doubleclick). + // - The ad slot is in the A4A-vs-3p amp-ad control branch (either via + // internal, client-side selection or via external, Google Search + // selection). + // - We haven't turned off profiling via the rate controls in + // build-system/global-config/{canary,prod}-config.json + // If any of those fail, we use the `BaseLifecycleReporter`, which is a + // a no-op (sends no pings). + const type = ampElement.element.getAttribute('type'); + const win = ampElement.win; + const experimentName = 'a4aProfilingRate'; + // In local dev mode, neither the canary nor prod config files is available, + // so manually set the profiling rate, for testing/dev. + if (getMode().localDev) { + toggleExperiment(win, experimentName, true, true); + } + randomlySelectUnsetPageExperiments(win, PROFILING_BRANCHES); + if ((type == 'doubleclick' || type == 'adsense') && + isInReportableBranch(ampElement, namespace) && + isExperimentOn(win, experimentName)) { + return new GoogleAdLifecycleReporter(win, ampElement.element, namespace, + Number(slotId)); + } else { + return new BaseLifecycleReporter(); + } +} + +/** + * Creates or reinitializes a lifecycle reporter for Google ad network + * implementations. + * + * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4aElement + * @return {!./performance.GoogleAdLifecycleReporter} + */ +export function googleLifecycleReporterFactory(a4aElement) { + const reporter = + /** @type {!./performance.GoogleAdLifecycleReporter} */ + (getLifecycleReporter(a4aElement, ReporterNamespace.A4A, + a4aElement.element.getAttribute('data-amp-slot-index'))); + reporter.setPingParameters({ + 's': 'AD_SLOT_NAMESPACE', + 'v': '2', + 'c': 'AD_PAGE_CORRELATOR', + 'rls': 'AMP_VERSION', + 'v_h': 'VIEWPORT_HEIGHT', + 's_t': 'SCROLL_TOP', + 'slotId': 'AD_SLOT_ID', + 'stageName': 'AD_SLOT_EVENT_NAME', + 'stageIdx': 'AD_SLOT_EVENT_ID', + 'met.AD_SLOT_NAMESPACE.AD_SLOT_ID': + 'AD_SLOT_EVENT_NAME.AD_SLOT_TIME_TO_EVENT', + 'e.AD_SLOT_ID': a4aElement.element.getAttribute(EXPERIMENT_ATTRIBUTE), + 'adt.AD_SLOT_ID': a4aElement.element.getAttribute('type'), + // Page-level visibility times: `firstVisibleTime.T,.lastVisibleTime.T`. + 'met.AD_SLOT_NAMESPACE': + 'firstVisibleTime.AD_PAGE_FIRST_VISIBLE_TIME' + + ',lastVisibleTime.AD_PAGE_LAST_VISIBLE_TIME', + }); + return reporter; +} + + +/** + * Sets reportable variables from ad response headers. + * + * @param {!../../../src/service/xhr-impl.FetchResponseHeaders} headers + * @param {!./performance.GoogleAdLifecycleReporter} reporter + */ +export function setGoogleLifecycleVarsFromHeaders(headers, reporter) { + // This is duplicated from the amp-a4a.js implementation. It needs to be + // defined there because it's an implementation detail of that module, but + // we want to report it to Google b/c we're interested in how rendering mode + // affects Google ads. However, we can't directly reference a variable + // in extensions/ from here. + const renderingMethodHeader = 'X-AmpAdRender'; + const renderingMethodKey = 'rm.AD_SLOT_ID'; + const qqidKey = 'qqid.AD_SLOT_ID'; + const pingParameters = new Object(null); + pingParameters[qqidKey] = headers.get(QQID_HEADER); + pingParameters[renderingMethodKey] = headers.get(renderingMethodHeader); + reporter.setPingParameters(pingParameters); +} + diff --git a/ads/google/a4a/performance.js b/ads/google/a4a/performance.js new file mode 100644 index 000000000000..a65820e0fc8e --- /dev/null +++ b/ads/google/a4a/performance.js @@ -0,0 +1,253 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {getCorrelator} from './utils'; +import {LIFECYCLE_STAGES} from '../../../extensions/amp-a4a/0.1/amp-a4a'; +import {dev} from '../../../src/log'; +import {serializeQueryString} from '../../../src/url'; +import {getTimingDataSync} from '../../../src/service/variable-source'; +import {urlReplacementsForDoc} from '../../../src/url-replacements'; +import {viewerForDoc} from '../../../src/viewer'; + +/** + * This module provides a fairly crude form of performance monitoring (or + * profiling) for A4A code. It generates individual pings back to Google + * servers at key points in the A4A lifecycle and at a few points in the 3p + * amp-ad lifecycle, for baseline. + * + * This is intended to be a short-term solution, for a rough-and-ready form + * of profiling. In particular, it doesn't use high-resolution timers (when + * they're available) and it doesn't queue pings for network efficiency. A + * better long-term solution is to integrate `src/performance.js` with + * `amp-analytics`. However, we need a short-term solution quickly. This + * module should go away once we have verified that A4A is performing as + * desired. + */ + + +/** + * A NOOP base class for the LifecycleReporter + */ +export class BaseLifecycleReporter { + constructor() { + /** + * @type {!Object} + * @private + */ + this.extraVariables_ = new Object(null); + } + + /** + * A beacon function that will be called at various stages of the lifecycle. + * + * To be overriden by network specific implementations. + * + * @param {string} unusedName A descriptive name for the beacon signal. + */ + sendPing(unusedName) {} + + /** + * Set a URL parameter to be added to the ping data. The parameter's value is + * subject to URL replacement and both parameter name and value are URI + * encoded before being written to the ping. The entry is silently dropped + * if either `parameter` or `value` is falsey, with the exception that the + * `value` may be 0. + * + * @param {string} parameter + * @param {string|number} value + */ + setPingParameter(parameter, value) { + if (parameter == null || parameter === '') { + return; + } + if (value === null || value === undefined || value === '') { return; } + this.extraVariables_[parameter] = String(value); + } + + /** + * Sets a (possibly empty) collection of URL parameter values by invoking + * #setPingParameter on each key/value pair in the input collection. + * + * @param {!Object} parametersToValues + */ + setPingParameters(parametersToValues) { + for (const variable in parametersToValues) { + if (parametersToValues.hasOwnProperty(variable)) { + this.setPingParameter(variable, parametersToValues[variable]); + } + } + } + + /** + * A function to reset the lifecycle reporter. Will be called immediately + * after firing the last beacon signal in unlayoutCallback. Clears all + * variables that have been set via #setPingParameter. + */ + reset() { + this.extraVariables_ = new Object(null); + } +} + +export class GoogleAdLifecycleReporter extends BaseLifecycleReporter { + + /** + * @param {!Window} win Parent window object. + * @param {!Element} element Parent element object. + * @param {string} namespace Namespace for page-level info. (E.g., + * 'amp' vs 'a4a'.) + * @param {number} slotId + */ + constructor(win, element, namespace, slotId) { + super(); + + /** @private {!Window} @const */ + this.win_ = win; + + /** @private {!Element} @const */ + this.element_ = element; + + /** @private {string} @const */ + this.namespace_ = namespace; + + /** @private {number} @const */ + this.slotId_ = slotId; + + /** @private {number} @const */ + this.correlator_ = getCorrelator(win); + + /** @private {string} @const */ + this.slotName_ = this.namespace_ + '.' + this.slotId_; + + // Contortions to convince the type checker that we're type-safe. + let initTime; + const scratch = getTimingDataSync(win, 'navigationStart') || Date.now(); + if (typeof scratch == 'number') { + initTime = scratch; + } else { + initTime = Number(scratch); + } + /** @private {time} @const */ + this.initTime_ = initTime; + + /** @private {!function():number} @const */ + this.getDeltaTime_ = (win.performance && win.performance.now.bind( + win.performance)) || (() => {return Date.now() - this.initTime_;}); + + /** (Not constant b/c this can be overridden for testing.) @private */ + this.pingbackAddress_ = 'https://csi.gstatic.com/csi'; + + /** + * @private {!../../../src/service/url-replacements-impl.UrlReplacements} + * @const + */ + this.urlReplacer_ = urlReplacementsForDoc(element); + + /** @const @private {!../../../src/service/viewer-impl.Viewer} */ + this.viewer_ = viewerForDoc(element); + } + + /** + * Sets the address to which pings will be sent, overriding + * `PINGBACK_ADDRESS`. Intended for testing. + * @param {string} address + * @visibleForTesting + */ + setPingAddress(address) { + this.pingbackAddress_ = address; + } + + /** + * The special variable SLOT_ID will be substituted into either parameter + * names or values with the ID of the ad slot on the page. + * + * @param {string} name Stage name to ping out. Should be one of the ones + * from `LIFECYCLE_STAGES`. If it's an unknown name, it will still be pinged, + * but the stage ID will be set to `9999`. + * @override + */ + sendPing(name) { + const url = this.buildPingAddress_(name); + if (url) { + this.emitPing_(url); + } + } + + /** + * @param {string} name Metric name to send. + * @returns {string} URL to send metrics to. + * @private + */ + buildPingAddress_(name) { + const stageId = LIFECYCLE_STAGES[name] || 9999; + const delta = Math.round(this.getDeltaTime_()); + // Note: extraParams can end up empty if (a) this.extraVariables_ is empty + // or (b) if all values are themselves empty or null. + let extraParams = serializeQueryString(this.extraVariables_); + if (extraParams != '') { + // Note: Using sync URL replacer here, rather than async, for a number + // of reasons: + // - Don't want to block pings waiting for potentially delayed bits + // of information. + // - Don't (currently) need access to any properties that are + // available async only. + // - Don't want to pass through expandStringAsync if there are zero + // extra params, but async would force us to (or to maintain two + // code branches). + // TODO(ampproject/a4a): Change to async if/when there's a need to + // expand async-only parameters. E.g., we'd like to have scroll_y + // offset, but it's not currently available through url-replacement. + // If it becomes available, it's likely to be an async parameter. + extraParams = this.urlReplacer_./*OK*/expandStringSync(extraParams, { + AD_SLOT_NAMESPACE: this.namespace_, + AD_SLOT_ID: this.slotId_, + AD_SLOT_TIME_TO_EVENT: delta, + AD_SLOT_EVENT_NAME: name, + AD_SLOT_EVENT_ID: stageId, + AD_PAGE_CORRELATOR: this.correlator_, + AD_PAGE_VISIBLE: this.viewer_.isVisible() ? 1 : 0, + AD_PAGE_FIRST_VISIBLE_TIME: + Math.round(this.viewer_.getFirstVisibleTime() - this.initTime_), + AD_PAGE_LAST_VISIBLE_TIME: + Math.round(this.viewer_.getLastVisibleTime() - this.initTime_), + }); + } + return extraParams ? `${this.pingbackAddress_}?${extraParams}` : ''; + } + + /** + * Send ping by creating an img element and attaching to the DOM. + * Separate function so that it can be stubbed out for testing. + * + * @param {string} url Address to ping. + * @visibleForTesting + */ + emitPing_(url) { + const pingElement = this.element_.ownerDocument.createElement('img'); + pingElement.setAttribute('src', url); + // Styling is copied directly from amp-pixel's CSS. This is a kludgy way + // to do this -- much better would be to invoke amp-pixel's styling directly + // or to add an additional style selector for these ping pixels. + // However, given that this is a short-term performance system, I'd rather + // not tamper with AMP-wide CSS just to create styling for this + // element. + pingElement.setAttribute('style', + 'position:fixed!important;top:0!important;width:1px!important;' + + 'height:1px!important;overflow:hidden!important;visibility:hidden'); + pingElement.setAttribute('aria-hidden', 'true'); + this.element_.parentNode.insertBefore(pingElement, this.element_); + dev().info('PING', url); + } +} diff --git a/ads/google/a4a/test/test-google-ads-a4a-config.js b/ads/google/a4a/test/test-google-ads-a4a-config.js new file mode 100644 index 000000000000..3e0b63db0aa9 --- /dev/null +++ b/ads/google/a4a/test/test-google-ads-a4a-config.js @@ -0,0 +1,350 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {installDocService} from '../../../../src/service/ampdoc-impl'; +import { + googleAdsIsA4AEnabled, + isInExperiment, + isInManualExperiment, +} from '../traffic-experiments'; +import {toggleExperiment} from '../../../../src/experiments'; +import {installPlatformService} from '../../../../src/service/platform-impl'; +import {installViewerServiceForDoc} from '../../../../src/service/viewer-impl'; +import {resetServiceForTesting} from '../../../../src/service'; +import {documentStateFor} from '../../../../src/service/document-state'; +import * as sinon from 'sinon'; + +const EXP_ID = 'EXP_ID'; +/** @type {!Branches} */ +const EXTERNAL_BRANCHES = { + control: 'EXT_CONTROL', + experiment: 'EXT_EXPERIMENT', +}; +/** @type {!Branches} */ +const INTERNAL_BRANCHES = { + control: 'INT_CONTROL', + experiment: 'INT_EXPERIMENT', +}; + +describe('a4a_config', () => { + let sandbox; + let win; + let rand; + let events; + let element; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + rand = sandbox.stub(Math, 'random'); + win = { + AMP_MODE: { + localDev: true, + }, + location: { + href: 'https://cdn.ampproject.org/fnord', + pathname: '/fnord', + origin: 'https://cdn.ampproject.org', + hash: '', + }, + document: { + nodeType: /* DOCUMENT */ 9, + hidden: false, + cookie: null, + visibilityState: 'visible', + addEventListener: function(type, listener) { + events[type] = listener; + }, + }, + crypto: { + subtle: true, + webkitSubtle: true, + }, + navigator: window.navigator, + }; + win.document.defaultView = win; + const ampdocService = installDocService(win, /* isSingleDoc */ true); + const ampdoc = ampdocService.getAmpDoc(); + events = {}; + documentStateFor(win); + installPlatformService(win); + installViewerServiceForDoc(ampdoc); + element = document.createElement('div'); + document.body.appendChild(element); + toggleExperiment(win, EXP_ID, true, true); + }); + + afterEach(() => { + resetServiceForTesting(win, 'viewer'); + sandbox.restore(); + document.body.removeChild(element); + }); + + it('should attach expt ID and return true when expt is on', () => { + rand.returns(0.75); // Random value to select the 2nd branch + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, + EXTERNAL_BRANCHES, INTERNAL_BRANCHES), + 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.equal( + INTERNAL_BRANCHES.experiment); + }); + + it('should attach control ID and return false when control is on', () => { + rand.returns(0.25); // Random value to select the 1st branch + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), + 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.equal( + INTERNAL_BRANCHES.control); + }); + + it('should not attach ID and return false when selected out', () => { + toggleExperiment(win, EXP_ID, false, true); + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.not.be.ok; + }); + + it('should return false when not on CDN or local dev', () => { + toggleExperiment(win, EXP_ID, false, true); + win.AMP_MODE.localDev = false; + win.location.href = 'http://somewhere.over.the.rainbow.org/'; + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.not.be.ok; + }); + + it('should return false if no crypto is available', () => { + win.crypto = null; + rand.returns(0.75); // Random value to select the 2nd branch + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.not.be.ok; + }); + + it('should return true if only crypto.webkitSubtle is available', () => { + win.crypto.subtle = null; + rand.returns(0.75); // Random value to select the 2nd branch + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + }); + + it('should return true if only crypto.subtle is available', () => { + win.crypto.webkitSubtle = null; + rand.returns(0.75); // Random value to select the 2nd branch + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + }); + + const urlBaseConditions = ['?exp=PARAM', + '?p=blarg&exp=PARAM', + '?p=blarg&exp=PARAM&s=987', + '?p=blarg&exp=zort:123,PARAM,spaz:987&s=987']; + urlBaseConditions.forEach(urlBase => { + + it('should skip url-triggered eid when param is bad', () => { + win.location.search = urlBase.replace('PARAM', 'a4a:spaz'); + toggleExperiment(win, EXP_ID, false, true); + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.not.be.ok; + }); + + it('should skip url-triggered eid when param is empty', () => { + win.location.search = urlBase.replace('PARAM', 'a4a:'); + // Force random client-side selection off. + toggleExperiment(win, EXP_ID, false, true); + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.not.be.ok; + }); + + it('should fall back to client-side eid when param is bad', () => { + win.location.search = urlBase.replace('PARAM', 'a4a:spaz'); + rand.returns(0.75); // Random value to select the 2nd branch + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.equal( + INTERNAL_BRANCHES.experiment); + }); + + it('should fall back to client-side eid when param is empty', () => { + win.location.search = urlBase.replace('PARAM', 'a4a:'); + rand.returns(0.75); // Random value to select the 2nd branch + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.equal( + INTERNAL_BRANCHES.experiment); + }); + + it(`should force experiment param from URL when pattern=${urlBase}`, + () => { + win.location.search = urlBase.replace('PARAM', 'a4a:2'); + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.equal( + EXTERNAL_BRANCHES.experiment); + }); + + it(`should force control param from URL when pattern=${urlBase}`, () => { + win.location.search = urlBase.replace('PARAM', 'a4a:1'); + // Should not register as 'A4A enabled', but should still attach the + // control experiment ID. + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.equal( + EXTERNAL_BRANCHES.control); + }); + + it(`should exclude all experiment IDs when pattern=${urlBase}`, () => { + win.location.search = urlBase.replace('PARAM', 'a4a:0'); + // Should not register as 'A4A enabled', but should still attach the + // control experiment ID. + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.false; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.not.be.ok; + }); + + it(`should attach manual experiment ID when pattern = ${urlBase}`, () => { + win.location.search = urlBase.replace('PARAM', 'a4a:-1'); + // Should not register as 'A4A enabled', but should still attach the + // control experiment ID. + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(isInManualExperiment(element), 'element in manual experiment') + .to.be.true; + // And it shouldn't be in any *other* experiments. + for (const branch in EXTERNAL_BRANCHES) { + expect(isInExperiment(element, EXTERNAL_BRANCHES[branch]), + 'element in ', EXTERNAL_BRANCHES[branch]).to.be.false; + } + for (const branch in EXTERNAL_BRANCHES) { + expect(isInExperiment(element, INTERNAL_BRANCHES[branch]), + 'element in ', EXTERNAL_BRANCHES[branch]).to.be.false; + } + }); + }); +}); + +// These tests are separated because they need to invoke +// installViewerServiceForDoc within the test, rather than in the beforeEach(). +describe('a4a_config hash param parsing', () => { + let sandbox; + let win; + let ampdoc; + let events; + let element; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + win = { + AMP_MODE: { + localDev: true, + }, + location: { + href: 'https://cdn.ampproject.org/fnord', + pathname: '/fnord', + origin: 'https://cdn.ampproject.org', + hash: '', + search: 'somewhere=over&the=rainbow', + }, + document: { + nodeType: /* DOCUMENT */ 9, + hidden: false, + cookie: null, + visibilityState: 'visible', + addEventListener: function(type, listener) { + events[type] = listener; + }, + }, + crypto: { + subtle: true, + webkitSubtle: true, + }, + navigator: window.navigator, + }; + win.document.defaultView = win; + const ampdocService = installDocService(win, /* isSingleDoc */ true); + ampdoc = ampdocService.getAmpDoc(); + events = {}; + installPlatformService(win); + documentStateFor(win); + const attrs = {}; + element = { + nodeType: /* ELEMENT */ 1, + ownerDocument: {defaultView: win}, + getAttribute: name => attrs[name], + setAttribute: (name, value) => attrs[name] = value, + }; + toggleExperiment(win, EXP_ID, true, true); + }); + + afterEach(() => { + resetServiceForTesting(win, 'viewer'); + sandbox.restore(); + }); + + const hashBaseConditions = ['#exp=PARAM', + '#p=blarg&exp=PARAM', + '#p=blarg&exp=PARAM&s=987', + '#p=blarg&exp=zort:123,PARAM,spaz:987&s=987']; + + hashBaseConditions.forEach(hashBase => { + it(`should find viewer param when pattern is ${hashBase}`, () => { + win.location.hash = hashBase.replace('PARAM', 'a4a:-1'); + installViewerServiceForDoc(ampdoc); + // Should not register as 'A4A enabled', but should still attach the + // control experiment ID. + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(isInManualExperiment(element), 'element in manual experiment') + .to.be.true; + // And it shouldn't be in any *other* experiments. + for (const branch in EXTERNAL_BRANCHES) { + expect(isInExperiment(element, EXTERNAL_BRANCHES[branch]), + 'element in ', EXTERNAL_BRANCHES[branch]).to.be.false; + } + for (const branch in EXTERNAL_BRANCHES) { + expect(isInExperiment(element, INTERNAL_BRANCHES[branch]), + 'element in ', EXTERNAL_BRANCHES[branch]).to.be.false; + } + }); + + it(`hash should trump search; pattern=${hashBase}`, () => { + win.location.search = hashBase.replace('PARAM', 'a4a:-1'); + win.location.hash = hashBase.replace('PARAM', 'a4a:2'); + installViewerServiceForDoc(ampdoc); + expect(googleAdsIsA4AEnabled(win, element, EXP_ID, EXTERNAL_BRANCHES, + INTERNAL_BRANCHES), 'googleAdsIsA4AEnabled').to.be.true; + expect(win.document.cookie).to.be.null; + expect(element.getAttribute('data-experiment-id')).to.equal( + EXTERNAL_BRANCHES.experiment); + }); + }); +}); diff --git a/ads/google/a4a/test/test-google-data-reporter.js b/ads/google/a4a/test/test-google-data-reporter.js new file mode 100644 index 000000000000..3d4eef34d93d --- /dev/null +++ b/ads/google/a4a/test/test-google-data-reporter.js @@ -0,0 +1,235 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {createIframePromise} from '../../../../testing/iframe'; +import { + getLifecycleReporter, + setGoogleLifecycleVarsFromHeaders, + googleLifecycleReporterFactory, +} from '../google-data-reporter'; +import { + GoogleAdLifecycleReporter, + BaseLifecycleReporter, +} from '../performance'; +import {EXPERIMENT_ATTRIBUTE, QQID_HEADER} from '../utils'; +import { + ADSENSE_A4A_EXTERNAL_EXPERIMENT_BRANCHES, + ADSENSE_A4A_INTERNAL_EXPERIMENT_BRANCHES, +} from '../../../../extensions/amp-ad-network-adsense-impl/0.1/adsense-a4a-config'; // eslint-disable-line max-len +import { + DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES, + DOUBLECLICK_A4A_INTERNAL_EXPERIMENT_BRANCHES, +} from '../../../../extensions/amp-ad-network-doubleclick-impl/0.1/doubleclick-a4a-config'; // eslint-disable-line max-len + +/** + * Construct a lifecycle reporter for an element with a given eid in one of + * the reporting namespaces. If eid is not specified, creates an element with + * no eid. + * @param {string} namespace + * @param {string} ad type + * @param {string=} opt_eid + * @returns {*} + */ +function buildElementWithEid(namespace, type, opt_eid) { + return createIframePromise(false).then(iframeFixture => { + const win = iframeFixture.win; + const doc = iframeFixture.doc; + const elem = doc.createElement('div'); + elem.setAttribute('type', type); + if (opt_eid) { + elem.setAttribute(EXPERIMENT_ATTRIBUTE, opt_eid); + } + doc.body.appendChild(elem); + const pseudoAmpElement = { + win, + element: elem, + }; + return getLifecycleReporter(pseudoAmpElement, namespace, 0, 0); + }); +} + +describe('#getLifecycleReporter', () => { + const EXPERIMENT_BRANCH_EIDS = [ + ADSENSE_A4A_EXTERNAL_EXPERIMENT_BRANCHES.experiment, + ADSENSE_A4A_INTERNAL_EXPERIMENT_BRANCHES.experiment, + DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES.experiment, + DOUBLECLICK_A4A_INTERNAL_EXPERIMENT_BRANCHES.experiment, + '117152632', + ]; + const CONTROL_BRANCH_EIDS = [ + ADSENSE_A4A_EXTERNAL_EXPERIMENT_BRANCHES.control, + ADSENSE_A4A_INTERNAL_EXPERIMENT_BRANCHES.control, + DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES.control, + DOUBLECLICK_A4A_INTERNAL_EXPERIMENT_BRANCHES.control, + ]; + + ['adsense', 'doubleclick'].forEach(type => { + describe(`type = ${type}`, () => { + EXPERIMENT_BRANCH_EIDS.forEach(eid => { + it(`should return real reporter for a4a eid = ${eid}`, () => { + return buildElementWithEid('a4a', type, eid).then(reporter => { + expect(reporter).to.be.instanceOf(GoogleAdLifecycleReporter); + }); + }); + + it(`should return a null reporter for amp eid = ${eid}`, () => { + return buildElementWithEid('amp', type, eid).then(reporter => { + expect(reporter).to.be.instanceOf(BaseLifecycleReporter); + }); + }); + + it(`should return null reporter for bogus namespace eid = ${eid}`, + () => { + return buildElementWithEid('fnord', type, eid).then(reporter => { + expect(reporter).to.be.instanceOf(BaseLifecycleReporter); + }); + }); + + it(`should return null reporter for non-Google ad, eid = ${eid}`, + () => { + return buildElementWithEid('a4a', 'a9', eid).then(reporter => { + expect(reporter).to.be.instanceOf(BaseLifecycleReporter); + }); + }); + }); + + CONTROL_BRANCH_EIDS.forEach(eid => { + it(`should return null reporter for a4a eid = ${eid}`, () => { + return buildElementWithEid('a4a', type, eid).then(reporter => { + expect(reporter).to.be.instanceOf(BaseLifecycleReporter); + }); + }); + + it(`should return a real reporter for amp eid = ${eid}`, () => { + return buildElementWithEid('amp', type, eid).then(reporter => { + expect(reporter).to.be.instanceOf(GoogleAdLifecycleReporter); + }); + }); + + it(`should return null reporter for bogus namespace eid = ${eid}`, + () => { + return buildElementWithEid('fnord', type, eid).then(reporter => { + expect(reporter).to.be.instanceOf(BaseLifecycleReporter); + }); + }); + + it(`should return null reporter for non-Google ad, eid = ${eid}`, + () => { + return buildElementWithEid('amp', 'a9', eid).then(reporter => { + expect(reporter).to.be.instanceOf(BaseLifecycleReporter); + }); + }); + }); + + for (const namespace in ['a4a', 'amp']) { + it(`should return null reporter for ${namespace} and no eid`, () => { + return buildElementWithEid(namespace, type).then(reporter => { + expect(reporter).to.be.instanceOf(BaseLifecycleReporter); + }); + }); + } + }); + }); + + describes.fakeWin('#setGoogleLifecycleVarsFromHeaders', {amp: true}, env => { + const headerData = {}; + const headerMock = { + get: h => { return h in headerData ? headerData[h] : null; }, + }; + let mockReporter; + beforeEach(() => { + const fakeElt = env.createAmpElement('div'); + env.win.Math = Math; + env.win.document.body.appendChild(fakeElt); + mockReporter = new GoogleAdLifecycleReporter( + env.win, fakeElt, 'test', 37); + mockReporter.setPingAddress('http://localhost:9876/'); + }); + + it('should pick up qqid from headers', () => { + headerData[QQID_HEADER] = 'test_qqid'; + expect(mockReporter.extraVariables_).to.be.empty; + setGoogleLifecycleVarsFromHeaders(headerMock, mockReporter); + mockReporter.sendPing('preAdThrottle'); + const pingElements = env.win.document.querySelectorAll('img'); + expect(pingElements.length).to.equal(1); + const pingUrl = pingElements[0].getAttribute('src'); + expect(pingUrl).to.be.ok; + expect(pingUrl).to.match(/[&?]qqid.37=test_qqid/); + }); + + it('should pick up rendering method from headers', () => { + headerData['X-AmpAdRender'] = 'fnord'; + expect(mockReporter.extraVariables_).to.be.empty; + setGoogleLifecycleVarsFromHeaders(headerMock, mockReporter); + mockReporter.sendPing('preAdThrottle'); + const pingElements = env.win.document.querySelectorAll('img'); + expect(pingElements.length).to.equal(1); + const pingUrl = pingElements[0].getAttribute('src'); + expect(pingUrl).to.be.ok; + expect(pingUrl).to.match(/[&?]rm.37=fnord/); + }); + }); + + describes.sandboxed('#googleLifecycleReporterFactory', {}, () => { + describes.fakeWin('default parameters', {amp: true}, env => { + let mockReporter; + beforeEach(() => { + const fakeElt = env.win.document.createElement('div'); + fakeElt.setAttribute('data-amp-slot-index', '22'); + fakeElt.setAttribute('type', 'doubleclick'); + fakeElt.setAttribute(EXPERIMENT_ATTRIBUTE, + DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES.experiment); + env.win.document.body.appendChild(fakeElt); + env.win.ampAdPageCorrelator = 7777777; + const a4aContainer = { + element: fakeElt, + win: env.win, + }; + mockReporter = googleLifecycleReporterFactory(a4aContainer); + expect(mockReporter).to.be.instanceOf(GoogleAdLifecycleReporter); + mockReporter.setPingAddress('http://localhost:9876/'); + }); + + it('should generate a ping with known parameters', () => { + const viewer = env.win.services.viewer.obj; + viewer.firstVisibleTime_ = viewer.lastVisibleTime_ = Date.now(); + mockReporter.sendPing('renderFriendlyStart'); + const pingElements = env.win.document.querySelectorAll('img'); + expect(pingElements.length).to.equal(1); + const pingUrl = pingElements[0].getAttribute('src'); + expect(pingUrl).to.be.ok; + const expectedParams = [ + 's=a4a', + 'c=7777777', + 'slotId=22', + `rls=${encodeURIComponent('$internalRuntimeVersion$')}`, + 'v_h=[0-9]+', + 's_t=', // SROLL_TOP not defined in test environment. + 'stageName=renderFriendlyStart', + 'stageIdx=6', + 'met.a4a.22=renderFriendlyStart.[0-9]+', + `e.22=${DOUBLECLICK_A4A_EXTERNAL_EXPERIMENT_BRANCHES.experiment}`, + 'adt.22=doubleclick', + 'met.a4a=firstVisibleTime.[0-9]+%2ClastVisibleTime.[0-9]+', + ]; + expectedParams.forEach(p => { + expect(pingUrl, p).to.match(new RegExp(`[?&]${p}(&|$)`)); + }); + }); + }); + }); +}); diff --git a/ads/google/a4a/test/test-performance.js b/ads/google/a4a/test/test-performance.js new file mode 100644 index 000000000000..00b87f231367 --- /dev/null +++ b/ads/google/a4a/test/test-performance.js @@ -0,0 +1,400 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GoogleAdLifecycleReporter, + BaseLifecycleReporter, +} from '../performance'; +import {createIframePromise} from '../../../../testing/iframe'; +import {viewerForDoc} from '../../../../src/viewer'; +import {toArray} from '../../../../src/types'; +import * as sinon from 'sinon'; + +/** + * Verify that `address` matches all of the patterns in `matchlist`. + * + * @param {!string} address + * @param {!Array} matchList + */ +function expectMatchesAll(address, matchList) { + matchList.forEach(m => { + expect(address).to.match(m); + }); +} + +/** + * Verify that `element` has at least one sibling DOM node that is an + * `img` tag whose `src` matches all of the patterns in `matchList`. + * + * @param {!Element} element + * @param {!Array} matchList + */ +function expectHasSiblingImgMatchingAll(element, matchList) { + const imgSiblings = toArray(element.parentElement.querySelectorAll('img')); + expect(imgSiblings).to.not.be.empty; + const result = imgSiblings.some(e => { + const src = e.getAttribute('src'); + return matchList.map(m => m.test(src)).every(x => x); + }); + expect(result, 'No element sibling of ' + element + ' matched all patterns') + .to.be.true; +} + +describe('BaseLifecycleReporter', () => { + describes.fakeWin('', {}, env => { + let doc; + beforeEach(() => { + doc = env.win.document; + }); + + it('should not modify the DOM', () => { + expect(doc.querySelector('img')).not.to.be.ok; + const reporter = new BaseLifecycleReporter(); + reporter.sendPing('foo'); + expect(doc.querySelector('img')).not.to.be.ok; + }); + + it('should store single parameters', () => { + const reporter = new BaseLifecycleReporter(); + expect(reporter.extraVariables_).to.be.empty; + reporter.setPingParameter('x', 3); + reporter.setPingParameter('y', 'kumquat'); + expect(reporter.extraVariables_).to.deep.equal({ + x: '3', + y: 'kumquat', + }); + }); + + it('should ignore null-ish parameter values', () => { + const reporter = new BaseLifecycleReporter(); + expect(reporter.extraVariables_).to.be.empty; + reporter.setPingParameter('x', null); + reporter.setPingParameter('y', ''); + reporter.setPingParameter('z', undefined); + expect(reporter.extraVariables_).to.be.empty; + }); + }); +}); + +describe('GoogleAdLifecycleReporter', () => { + let sandbox; + let emitPingSpy; + let iframe; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + emitPingSpy = sandbox.spy(GoogleAdLifecycleReporter.prototype, 'emitPing_'); + iframe = createIframePromise(false).then(iframeFixture => { + const win = iframeFixture.win; + const doc = iframeFixture.doc; + const viewer = viewerForDoc(doc); + const elem = doc.createElement('div'); + doc.body.appendChild(elem); + const reporter = new GoogleAdLifecycleReporter( + win, elem, 'test_foo', 42); + reporter.setPingAddress('/'); + reporter.setPingParameters({ + 's': 'AD_SLOT_NAMESPACE', + 'rls': 'AMP_VERSION', + 'c': 'AD_PAGE_CORRELATOR', + 'it.AD_SLOT_ID': 'AD_SLOT_TIME_TO_EVENT', + 's_n_id': 'AD_SLOT_EVENT_NAME.AD_SLOT_EVENT_ID', + 'p_v': 'AD_PAGE_VISIBLE', + 'p_v1': 'AD_PAGE_FIRST_VISIBLE_TIME', + 'p_v2': 'AD_PAGE_LAST_VISIBLE_TIME', + }); + return {win, doc, viewer, elem, reporter}; + }); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('#sendPing', () => { + it('should request a single ping and insert into DOM', () => { + return iframe.then(({viewer, elem, reporter}) => { + const iniTime = reporter.initTime_; + sandbox.stub(viewer, 'getFirstVisibleTime', () => iniTime + 11); + sandbox.stub(viewer, 'getLastVisibleTime', () => iniTime + 12); + expect(emitPingSpy).to.not.be.called; + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + const arg = emitPingSpy.getCall(0).args[0]; + const expectations = [ + /[&?]s=test_foo(&|$)/, + // In unit tests, internalRuntimeVersion is not substituted. %24 == + // ASCII encoding of '$'. + /[&?]rls=%24internalRuntimeVersion%24(&|$)/, + /[&?]c=[0-9]+(&|$)/, + /[&?]it.42=[0-9]+(&|$)/, + /[&?]s_n_id=adRequestStart.2(&|$)/, + /[&?]p_v=1(&|$)/, + /[&?]p_v1=11(&|$)/, + /[&?]p_v2=12(&|$)/, + ]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + }); + }); + + it('should request multiple pings and write all to the DOM', () => { + return iframe.then(({unusedWin, unusedDoc, elem, reporter}) => { + const stages = { + adSlotCleared: '-1', + urlBuilt: '1', + adRequestStart: '2', + adRequestEnd: '3', + extractCreativeAndSignature: '4', + adResponseValidateStart: '5', + renderFriendlyStart: '6', + renderCrossDomainStart: '7', + renderFriendlyEnd: '8', + renderCrossDomainEnd: '9', + }; + expect(emitPingSpy).to.not.be.called; + let count = 0; + for (const k in stages) { + reporter.sendPing(k); + ++count; + } + expect(emitPingSpy.callCount).to.equal(count); + count = 0; + for (const k in stages) { + const expectations = [ + /[&?]s=test_foo(&|$)/, + // In unit tests, internalRuntimeVersion is not substituted. %24 == + // ASCII encoding of '$'. + /[&?]rls=%24internalRuntimeVersion%24(&|$)/, + /[&?]c=[0-9]+(&|$)/, + /[&?]it.42=[0-9]+(&|$)/, + RegExp(`[&?]s_n_id=${k}.${stages[k]}(&|$)`), + ]; + const arg = emitPingSpy.getCall(count++).args[0]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + } + }); + }); + + it('should use diff slot IDs, but the same correlator', () => { + return iframe.then(({win, doc, unusedElem, unusedReporter}) => { + const stages = { + adSlotBuilt: '0', + adResponseValidateStart: '5', + renderFriendlyStart: '6', + renderCrossDomainStart: '7', + }; + const nStages = 4; + const allReporters = []; + const nSlots = 20; + for (let i = 0; i < nSlots; ++i) { + const elem = doc.createElement('div'); + elem.setAttribute('id', i); + doc.body.appendChild(elem); + const reporter = new GoogleAdLifecycleReporter(win, elem, 'test_foo', + i + 1); + reporter.setPingAddress('/'); + reporter.setPingParameters({ + 's': 'AD_SLOT_NAMESPACE', + 'c': 'AD_PAGE_CORRELATOR', + 'it.AD_SLOT_ID': 'AD_SLOT_TIME_TO_EVENT', + }); + allReporters.push(reporter); + } + allReporters.forEach(r => { + for (const k in stages) { + r.sendPing(k); + } + }); + expect(emitPingSpy.callCount).to.equal(nSlots * nStages); + const allImgNodes = toArray(doc.querySelectorAll('img')); + expect(allImgNodes.length).to.equal(nSlots * nStages); + let commonCorrelator; + const slotCounts = {}; + allImgNodes.forEach(n => { + const src = n.getAttribute('src'); + expect(src).to.match(/[?&]s=test_foo(&|$)/); + expect(src).to.match(/[?&]c=[0-9]+/); + const corr = /[?&]c=([0-9]+)/.exec(src)[1]; + commonCorrelator = commonCorrelator || corr; + const slotId = /[?&]it.([0-9]+)=[0-9]+(&|$)/.exec(src)[1]; + expect(corr).to.equal(commonCorrelator); + slotCounts[slotId] = slotCounts[slotId] || 0; + ++slotCounts[slotId]; + }); + // SlotId 0 corresponds to unusedReporter, so ignore it. + for (let s = 1; s <= nSlots; ++s) { + expect(slotCounts[s], 'slotCounts[' + s + ']').to.equal(nStages); + } + }); + }); + }); + + describe('#setPingParameter', () => { + it('should pass through static ping variables', () => { + return iframe.then(({unusedWin, unusedDoc, elem, reporter}) => { + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameter('zort', 314159); + reporter.setPingParameter('gack', 'flubble'); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + const arg = emitPingSpy.getCall(0).args[0]; + const expectations = [ + // Be sure that existing ping not deleted by args. + /[&?]s=test_foo/, + /zort=314159/, + /gack=flubble/, + ]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + }); + }); + + it('does not allow empty args', () => { + return iframe.then(({unusedWin, unusedDoc, unusedElem, reporter}) => { + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameter('', ''); + reporter.setPingParameter('foo', ''); + reporter.setPingParameter('bar', null); + reporter.setPingParameter('baz', undefined); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + const arg = emitPingSpy.getCall(0).args[0]; + expect(arg).not.to.match(/\&=?\&/); + expect(arg).not.to.match(/[&?]foo/); + expect(arg).not.to.match(/[&?]bar/); + expect(arg).not.to.match(/[&?]baz/); + }); + }); + + it('does allow value === 0', () => { + return iframe.then(({unusedWin, unusedDoc, elem, reporter}) => { + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameter('foo', 0); + reporter.setPingParameter('bar', 0.0); + reporter.setPingParameter('baz', -0); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + const arg = emitPingSpy.getCall(0).args[0]; + const expectations = [ + /foo=0/, + /bar=0/, + /baz=0/, + ]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + }); + }); + + it('should uri encode extra params', () => { + return iframe.then(({unusedWin, unusedDoc, unusedElem, reporter}) => { + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameter('evil', + ''); + reporter.setPingParameter( + '', 3); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + const arg = emitPingSpy.getCall(0).args[0]; + expect(arg).not.to.have.string( + ''); + expect(arg).to.have.string('&evil=' + encodeURIComponent( + '')); + expect(arg).not.to.have.string( + ''); + expect(arg).to.have.string('&' + encodeURIComponent( + '') + + '=3'); + }); + }); + + it('should expand URL parameters in extra params', () => { + return iframe.then(({unusedWin, unusedDoc, elem, reporter}) => { + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameter('zort', 'RANDOM'); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + const arg = emitPingSpy.getCall(0).args[0]; + const expectations = [ + // Be sure that existing ping not deleted by args. + /[&?]s=test_foo/, + /zort=[0-9.]+/, + ]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + }); + }); + }); + + describe('#setPingParameters', () => { + it('should do nothing on an an empty input', () => { + return iframe.then(({unusedWin, unusedDoc, elem, reporter}) => { + const setPingParameterSpy = sandbox.spy(reporter, 'setPingParameter'); + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameters({}); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + expect(setPingParameterSpy).not.to.be.called; + const arg = emitPingSpy.getCall(0).args[0]; + const expectations = [ + // Be sure that existing ping not deleted by args. + /[&?]s=test_foo/, + ]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + }); + }); + + it('should set a singleton input', () => { + return iframe.then(({unusedWin, unusedDoc, elem, reporter}) => { + const setPingParameterSpy = sandbox.spy(reporter, 'setPingParameter'); + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameters({zort: '12345'}); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + expect(setPingParameterSpy).to.be.calledOnce; + const arg = emitPingSpy.getCall(0).args[0]; + const expectations = [ + // Be sure that existing ping not deleted by args. + /[&?]s=test_foo/, + /zort=12345/, + ]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + }); + }); + + it('should set multiple inputs', () => { + return iframe.then(({unusedWin, unusedDoc, elem, reporter}) => { + const setPingParameterSpy = sandbox.spy(reporter, 'setPingParameter'); + expect(emitPingSpy).to.not.be.called; + reporter.setPingParameters({zort: '12345', gax: 99, flub: 0}); + reporter.sendPing('adRequestStart'); + expect(emitPingSpy).to.be.calledOnce; + expect(setPingParameterSpy).to.be.calledThrice; + const arg = emitPingSpy.getCall(0).args[0]; + const expectations = [ + // Be sure that existing ping not deleted by args. + /[&?]s=test_foo/, + /zort=12345/, + /gax=99/, + /flub=0/, + ]; + expectMatchesAll(arg, expectations); + expectHasSiblingImgMatchingAll(elem, expectations); + }); + }); + }); +}); diff --git a/ads/google/a4a/test/test-traffic-experiments.js b/ads/google/a4a/test/test-traffic-experiments.js new file mode 100644 index 000000000000..adef5b8ff03a --- /dev/null +++ b/ads/google/a4a/test/test-traffic-experiments.js @@ -0,0 +1,357 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import { + RANDOM_NUMBER_GENERATORS, + addExperimentIdToElement, + getPageExperimentBranch, + mergeExperimentIds, + isInExperiment, + randomlySelectUnsetPageExperiments, + validateExperimentIds, +} from '../traffic-experiments'; +import {EXPERIMENT_ATTRIBUTE} from '../utils'; +import {isExperimentOn} from '../../../../src/experiments'; +import {dev} from '../../../../src/log'; +import * as sinon from 'sinon'; + +/** @private @const Tag used in dev log messages */ +const TAG_ = 'test-amp-ad'; + +describe('all-traffic-experiments-tests', () => { + + describe('#randomlySelectUnsetPageExperiments', () => { + let sandbox; + let accurateRandomStub; + let cachedAccuratePrng; + let testExperimentSet; + beforeEach(() => { + const experimentFrequency = 1.0; + testExperimentSet = { + testExperimentId: { + control: 'control_branch_id', + experiment: 'experiment_branch_id', + }, + }; + sandbox = sinon.sandbox.create(); + sandbox.win = { + location: { + hostname: 'test.server.name.com', + }, + AMP_CONFIG: { + testExperimentId: experimentFrequency, + }, + document: { + cookie: null, + querySelector: () => {}, + }, + }; + accurateRandomStub = sandbox.stub().returns(-1); + cachedAccuratePrng = RANDOM_NUMBER_GENERATORS.accuratePrng; + RANDOM_NUMBER_GENERATORS.accuratePrng = accurateRandomStub; + }); + afterEach(() => { + sandbox.restore(); + RANDOM_NUMBER_GENERATORS.accuratePrng = cachedAccuratePrng; + }); + + it('handles empty experiments list', () => { + // Opt out of experiment. + // TODO(tdrl): remove the direct access to AMP_CONFIG + sandbox.win.AMP_CONFIG['testExperimentId'] = 0.0; + randomlySelectUnsetPageExperiments(sandbox.win, {}); + expect(isExperimentOn(sandbox.win, 'testExperimentId'), + 'experiment is on').to.be.false; + expect(sandbox.win.pageExperimentBranches).to.be.empty; + }); + it('handles experiment not diverted path', () => { + // Opt out of experiment. + sandbox.win.AMP_CONFIG['testExperimentId'] = 0.0; + randomlySelectUnsetPageExperiments(sandbox.win, testExperimentSet); + expect(isExperimentOn(sandbox.win, 'testExperimentId'), + 'experiment is on').to.be.false; + expect(getPageExperimentBranch(sandbox.win, + 'testExperimentId')).to.not.be.ok; + }); + it('handles experiment diverted path: control', () => { + // Force experiment on by setting its triggering probability to 1, then + // force the control branch to be chosen by making the accurate PRNG + // return a value < 0.5. + sandbox.win.AMP_CONFIG['testExperimentId'] = 1.0; + RANDOM_NUMBER_GENERATORS.accuratePrng.onFirstCall().returns(0.3); + randomlySelectUnsetPageExperiments(sandbox.win, testExperimentSet); + expect(isExperimentOn(sandbox.win, 'testExperimentId'), + 'experiment is on').to.be.true; + expect(getPageExperimentBranch(sandbox.win, 'testExperimentId')).to.equal( + testExperimentSet['testExperimentId'].control); + }); + it('handles experiment diverted path: experiment', () => { + // Force experiment on by setting its triggering probability to 1, then + // force the experiment branch to be chosen by making the accurate PRNG + // return a value > 0.5. + sandbox.win.AMP_CONFIG['testExperimentId'] = 1.0; + RANDOM_NUMBER_GENERATORS.accuratePrng.onFirstCall().returns(0.6); + randomlySelectUnsetPageExperiments(sandbox.win, testExperimentSet); + expect(isExperimentOn(sandbox.win, 'testExperimentId'), + 'experiment is on').to.be.true; + expect(getPageExperimentBranch(sandbox.win, 'testExperimentId')).to.equal( + testExperimentSet['testExperimentId'].experiment); + }); + it('handles multiple experiments', () => { + sandbox.win.AMP_CONFIG = {}; + const config = sandbox.win.AMP_CONFIG; + config['expt_0'] = 1.0; + config['expt_1'] = 0.0; + config['expt_2'] = 1.0; + config['expt_3'] = 1.0; + const experimentInfo = { + 'expt_0': { + control: '0_c', + experiment: '0_e', + }, + 'expt_1': { + control: '1_c', + experiment: '1_e', + }, + 'expt_2': { + control: '2_c', + experiment: '2_e', + }, + // expt_3 omitted. + }; + RANDOM_NUMBER_GENERATORS.accuratePrng.returns(0.6); + randomlySelectUnsetPageExperiments(sandbox.win, experimentInfo); + expect(isExperimentOn(sandbox.win, 'expt_0'), + 'expt_0 is on').to.be.true; + expect(isExperimentOn(sandbox.win, 'expt_1'), + 'expt_1 is on').to.be.false; + expect(isExperimentOn(sandbox.win, 'expt_2'), + 'expt_2 is on').to.be.true; + // Note: calling isExperimentOn('expt_3') would actually evaluate the + // frequency for expt_3, possibly enabling it. Since we wanted it to be + // omitted altogether, we'll evaluate it only via its branch. + expect(getPageExperimentBranch(sandbox.win, 'expt_0')).to.equal( + '0_e'); + expect(getPageExperimentBranch(sandbox.win, 'expt_1')).to.not.be.ok; + expect(getPageExperimentBranch(sandbox.win, 'expt_2')).to.equal( + '2_e'); + expect(getPageExperimentBranch(sandbox.win, 'expt_3')).to.not.be.ok; + }); + it('handles multi-way branches', () => { + dev().info(TAG_, 'Testing multi-way branches'); + sandbox.win.AMP_CONFIG = {}; + const config = sandbox.win.AMP_CONFIG; + config['expt_0'] = 1.0; + const experimentInfo = { + 'expt_0': { + b0: '0_0', + b1: '0_1', + b2: '0_2', + b3: '0_3', + b4: '0_4', + }, + }; + RANDOM_NUMBER_GENERATORS.accuratePrng.returns(0.7); + randomlySelectUnsetPageExperiments(sandbox.win, experimentInfo); + expect(isExperimentOn(sandbox.win, 'expt_0'), + 'expt_0 is on').to.be.true; + expect(getPageExperimentBranch(sandbox.win, 'expt_0')).to.equal( + '0_3'); + }); + it('handles multiple experiments with multi-way branches', () => { + sandbox.win.AMP_CONFIG = {}; + const config = sandbox.win.AMP_CONFIG; + config['expt_0'] = 1.0; + config['expt_1'] = 0.0; + config['expt_2'] = 1.0; + config['expt_3'] = 1.0; + const experimentInfo = { + 'expt_0': { + b0: '0_0', + b1: '0_1', + b2: '0_2', + b3: '0_3', + b4: '0_4', + }, + 'expt_1': { + b0: '1_0', + b1: '1_1', + b2: '1_2', + b3: '1_3', + b4: '1_4', + }, + 'expt_2': { + b0: '2_0', + b1: '2_1', + b2: '2_2', + b3: '2_3', + b4: '2_4', + }, + }; + RANDOM_NUMBER_GENERATORS.accuratePrng.onFirstCall().returns(0.7); + RANDOM_NUMBER_GENERATORS.accuratePrng.onSecondCall().returns(0.3); + randomlySelectUnsetPageExperiments(sandbox.win, experimentInfo); + expect(isExperimentOn(sandbox.win, 'expt_0'), + 'expt_0 is on').to.be.true; + expect(isExperimentOn(sandbox.win, 'expt_1'), + 'expt_1 is on').to.be.false; + expect(isExperimentOn(sandbox.win, 'expt_2'), + 'expt_2 is on').to.be.true; + // Note: calling isExperimentOn('expt_3') would actually evaluate the + // frequency for expt_3, possibly enabling it. Since we wanted it to be + // omitted altogether, we'll evaluate it only via its branch. + expect(getPageExperimentBranch(sandbox.win, 'expt_0')).to.equal( + '0_3'); + expect(getPageExperimentBranch(sandbox.win, 'expt_1')).to.not.be.ok; + expect(getPageExperimentBranch(sandbox.win, 'expt_2')).to.equal( + '2_1'); + expect(getPageExperimentBranch(sandbox.win, 'expt_3')).to.not.be.ok; + }); + + it('should not process the same experiment twice', () => { + const exptAInfo = { + 'fooExpt': { + control: '012345', + experiment: '987654', + }, + }; + const exptBInfo = { + 'fooExpt': { + control: '246810', + experiment: '108642', + }, + }; + sandbox.win.AMP_CONFIG = {}; + const config = sandbox.win.AMP_CONFIG; + config['fooExpt'] = 0.0; + randomlySelectUnsetPageExperiments(sandbox.win, exptAInfo); + config['fooExpt'] = 1.0; + randomlySelectUnsetPageExperiments(sandbox.win, exptBInfo); + // Even though we tried to set up a second time, using a config + // parameter that should ensure that the experiment was activated, the + // experiment framework should evaluate each experiment only once per + // page and should not enable it. + expect(isExperimentOn(sandbox.win, 'fooExpt')).to.be.false; + expect(getPageExperimentBranch(sandbox.win, 'fooExpt')).to.not.be.ok; + }); + }); + + describe('#validateExperimentIds', () => { + it('should return true for empty list', () => { + expect(validateExperimentIds([])).to.be.true; + }); + + it('should return true for a singleton numeric list', () => { + expect(validateExperimentIds(['3'])).to.be.true; + }); + + it('should return false for a singleton non-numeric list', () => { + expect(validateExperimentIds(['blargh'])).to.be.false; + expect(validateExperimentIds([''])).to.be.false; + }); + + it('should return true for a multi-item valid list', () => { + expect(validateExperimentIds(['0', '1', '2', '3'])).to.be.true; + }); + + it('should return false for a multi-item invalid list', () => { + expect(validateExperimentIds(['0', '1', 'k2', '3'])).to.be.false; + }); + }); + + describe('#addExperimentIdToElement', () => { + it('should add attribute when there is none present to begin with', () => { + const element = document.createElement('div'); + expect(element.getAttribute(EXPERIMENT_ATTRIBUTE)).to.not.be.ok; + addExperimentIdToElement('3', element); + expect(element.getAttribute(EXPERIMENT_ATTRIBUTE)).to.equal('3'); + }); + + it('should append experiment to already valid single experiment', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, '99'); + addExperimentIdToElement('3', element); + expect(element.getAttribute(EXPERIMENT_ATTRIBUTE)).to.equal('99,3'); + }); + + it('should append experiment to already valid multiple experiments', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, '99,77,11,0122345'); + addExperimentIdToElement('3', element); + expect(element.getAttribute(EXPERIMENT_ATTRIBUTE)).to.equal( + '99,77,11,0122345,3'); + }); + + it('should should replace existing invalid experiments', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, '99,14,873,k,44'); + addExperimentIdToElement('3', element); + expect(element.getAttribute(EXPERIMENT_ATTRIBUTE)).to.equal('3'); + }); + }); + + describe('#mergeExperimentIds', () => { + it('should merge a single id to itself', () => { + expect(mergeExperimentIds('12345')).to.equal('12345'); + }); + it('should merge a single ID to a list', () => { + expect(mergeExperimentIds('12345', '3,4,5,6')).to.equal('3,4,5,6,12345'); + }); + it('should discard invalid ID', () => { + expect(mergeExperimentIds('frob', '3,4,5,6')).to.equal('3,4,5,6'); + }); + it('should return empty string for invalid input', () => { + expect(mergeExperimentIds('frob')).to.equal(''); + }); + }); + + describe('#isInExperiment', () => { + it('should return false for empty element and any query', () => { + const element = document.createElement('div'); + expect(isInExperiment(element, '')).to.be.false; + expect(isInExperiment(element, null)).to.be.false; + expect(isInExperiment(element, 'frob')).to.be.false; + }); + it('should return false for empty attribute and any query', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, ''); + expect(isInExperiment(element, '')).to.be.false; + expect(isInExperiment(element, null)).to.be.false; + expect(isInExperiment(element, 'frob')).to.be.false; + }); + it('should return false for real data string but mismatching query', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, 'frob,gunk,zort'); + expect(isInExperiment(element, 'blub')).to.be.false; + expect(isInExperiment(element, 'ort')).to.be.false; + expect(isInExperiment(element, 'fro')).to.be.false; + expect(isInExperiment(element, 'gunk,zort')).to.be.false; + }); + it('should return true for singleton data and matching query', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, 'frob'); + expect(isInExperiment(element, 'frob')).to.be.true; + }); + it('should return true for matching query', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, 'frob,gunk,zort'); + expect(isInExperiment(element, 'frob')).to.be.true; + expect(isInExperiment(element, 'gunk')).to.be.true; + expect(isInExperiment(element, 'zort')).to.be.true; + }); + }); +}); diff --git a/ads/google/a4a/test/test-utils.js b/ads/google/a4a/test/test-utils.js new file mode 100644 index 000000000000..84f61aa38185 --- /dev/null +++ b/ads/google/a4a/test/test-utils.js @@ -0,0 +1,99 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + extractGoogleAdCreativeAndSignature, + additionalDimensions, +} from '../utils'; +import {base64UrlDecodeToBytes} from '../../../../src/utils/base64'; + +describe('Google A4A utils', () => { + describe('#extractGoogleAdCreativeAndSignature', () => { + it('should return body and signature', () => { + const creative = 'some test data'; + const headerData = { + 'X-AmpAdSignature': 'AQAB', + }; + const headers = { + has: h => { return h in headerData; }, + get: h => { return headerData[h]; }, + }; + return expect(extractGoogleAdCreativeAndSignature(creative, headers)) + .to.eventually.deep.equal({ + creative, + signature: base64UrlDecodeToBytes('AQAB'), + size: null, + }); + }); + + it('should return body and signature and size', () => { + const creative = 'some test data'; + const headerData = { + 'X-AmpAdSignature': 'AQAB', + 'X-CreativeSize': '320x50', + }; + const headers = { + has: h => { return h in headerData; }, + get: h => { return headerData[h]; }, + }; + return expect(extractGoogleAdCreativeAndSignature(creative, headers)) + .to.eventually.deep.equal({ + creative, + signature: base64UrlDecodeToBytes('AQAB'), + size: [320, 50], + }); + }); + + it('should return null when no signature header is present', () => { + const creative = 'some test data'; + const headers = { + has: unused => { return false; }, + get: h => { throw new Error('Tried to get ' + h); }, + }; + return expect(extractGoogleAdCreativeAndSignature(creative, headers)) + .to.eventually.deep.equal({ + creative, + signature: null, + size: null, + }); + }); + }); + + //TODO: Add tests for other utils functions. + + describe('#additionalDimensions', () => { + it('should return the right value when fed mocked inputs', () => { + const fakeWin = { + screenX: 1, + screenY: 2, + screenLeft: 3, + screenTop: 4, + outerWidth: 5, + outerHeight: 6, + screen: { + availWidth: 11, + availTop: 12, + }, + }; + const fakeSize = { + width: '100px', + height: '101px', + }; + return expect(additionalDimensions(fakeWin, fakeSize)).to.equal( + '3,4,1,2,11,12,5,6,100px,101px'); + }); + }); +}); diff --git a/ads/google/a4a/traffic-experiments.js b/ads/google/a4a/traffic-experiments.js new file mode 100644 index 000000000000..4a9024bde9c4 --- /dev/null +++ b/ads/google/a4a/traffic-experiments.js @@ -0,0 +1,355 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Machinery for doing "traffic-level" experiments. That is, rather than + * a single user choosing to opt-in to an experimental version of a module, + * this framework allows you to do randomized, controlled experiments on all + * AMP page loads to, for example, test relative performance or look for + * impacts on click-throughs. + */ + +import {isGoogleAdsA4AValidEnvironment, EXPERIMENT_ATTRIBUTE} from './utils'; +import {isExperimentOn, toggleExperiment} from '../../../src/experiments'; +import {dev} from '../../../src/log'; +import {viewerForDoc} from '../../../src/viewer'; +import {parseQueryString} from '../../../src/url'; + +/** @typedef {{control: string, experiment: string}} */ +export let ExperimentInfo; + +/** @type {!string} @private */ +const MANUAL_EXPERIMENT_ID = '117152632'; + +/** + * Check whether Google Ads supports the A4A rendering pathway for a given ad + * Element on a given Window. The tests we use are: + * + * - The page must have originated in the `cdn.ampproject.org` CDN _or_ we must + * be running in local dev mode. + * - We must be selected in to an A4A traffic experiment and be selected into + * the "experiment" branch. + * + * If we're selected into the overall traffic experiment, this function will + * also attach an experiment or control branch ID to the `Element` as + * a side-effect. + * + * @param {!Window} win Host window for the ad. + * @param {!Element} element Ad tag Element. + * @param {string} experimentName Overall name for the experiment. + * @param {!ExperimentInfo} externalBranches experiment and control branch IDs to use + * when experiment is triggered externally (e.g., via Google Search + * results page). + * @param {!ExperimentInfo} internalBranches experiment and control branch IDs to + * use when experiment is triggered internally (i.e., via client-side + * selection). + * @return {boolean} Whether Google Ads should attempt to render via the A4A + * pathway. + */ +export function googleAdsIsA4AEnabled(win, element, experimentName, + externalBranches, internalBranches) { + if (isGoogleAdsA4AValidEnvironment(win)) { + maybeSetExperimentFromUrl(win, element, + experimentName, externalBranches.control, + externalBranches.experiment, MANUAL_EXPERIMENT_ID); + const experimentInfo = {}; + experimentInfo[experimentName] = internalBranches; + // Note: Because the same experimentName is being used everywhere here, + // randomlySelectUnsetPageExperiments won't add new IDs if + // maybeSetExperimentFromUrl has already set something for this + // experimentName. + randomlySelectUnsetPageExperiments(win, experimentInfo); + if (isExperimentOn(win, experimentName)) { + // Page is selected into the overall traffic experiment. + const selectedBranch = getPageExperimentBranch(win, experimentName); + addExperimentIdToElement(selectedBranch, element); + // Detect whether page is on the "experiment" (i.e., use A4A rendering + // pathway) branch of the overall traffic experiment or it's on the + // "control" (i.e., use traditional, 3p iframe rendering pathway). + return selectedBranch == internalBranches.experiment || + selectedBranch == externalBranches.experiment || + selectedBranch == MANUAL_EXPERIMENT_ID; + } + } + // Serving location doesn't qualify for A4A treatment or page is not in the + // traffic experiment. + return false; +} + +/** + * Set experiment state from URL parameter, if present. This looks for the + * presence of a URL parameter of the form + * `exp=expt0:val0,expt1:val1,...,a4a:X,...,exptN:valN` + * and interprets the X as one of the following: + * - `-1`: Manually-triggered experiment. For testing only. Sets + * `adtest=on` on the ad request, so that it will not bill or record + * user clicks as ad CTR. Ad request will be accounted in a special + * 'testing only' experiment statistic pool so that we can track usage + * of this feature. + * - `0`: Ad is explicitly opted out of the overall A4A-vs-3p iframe + * experiment. Ad will serve into a 3p iframe, as traditional, but ad + * request and clicks will not be accounted in experiment statistics. + * - `1`: Ad is on the control branch of the overall A4A-vs-3p iframe + * experiment. Ad will serve into a 3p iframe, and ad requests and + * clicks _will_ be accounted in experiment statistics. + * - `2`: Ad is on the experimental branch of the overall A4A-vs-3p iframe + * experiment. Ad will render via the A4A path, including early ad + * request and (possibly) early rendering in shadow DOM or iframe. + * + * @param {!Window} win Window. + * @param {!Element} element Ad tag Element. + * @param {!string} experimentName Name of the overall experiment. + * @param {!string} controlBranchId Experiment ID string for control branch of + * the overall experiment. + * @param {!string} treatmentBranchId Experiment ID string for the 'treatment' + * (i.e., a4a) branch of the overall experiment. + * @param {!string} manualId ID of the manual experiment. + */ +function maybeSetExperimentFromUrl(win, element, experimentName, + controlBranchId, treatmentBranchId, manualId) { + const expParam = viewerForDoc(element).getParam('exp') || + parseQueryString(win.location.search)['exp']; + if (!expParam) { + return; + } + const match = /(^|,)(a4a:[^,]*)/.exec(expParam); + const a4aParam = match && match[2]; + if (!a4aParam) { + return; + } + // In the future, we may want to specify multiple experiments in the a4a + // arg. For the moment, however, assume that it's just a single flag. + const arg = a4aParam.split(':', 2)[1]; + const argMapping = { + '-1': manualId, + '0': null, + '1': controlBranchId, + '2': treatmentBranchId, + }; + if (argMapping.hasOwnProperty(arg)) { + forceExperimentBranch(win, experimentName, argMapping[arg]); + } else { + dev().warn('A4A-CONFIG', 'Unknown a4a URL parameter: ', a4aParam, + ' expected one of -1 (manual), 0 (not in experiment), 1 (control ' + + 'branch), or 2 (a4a experiment branch)'); + } +} + +// TODO(tdrl): New test case: Invoke randomlySelectUnsetPageExperiments twice for different +// experiment lists. + +/** + * In some browser implementations of Math.random(), sequential calls of + * Math.random() are correlated and can cause a bias. In particular, + * if the previous random() call was < 0.001 (as it will be if we select + * into an experiment), the next value could be less than 0.5 more than + * 50.7% of the time. This provides an implementation that roots down into + * the crypto API, when available, to produce less biased samples. + * + * @return {number} Pseudo-random floating-point value on the range [0, 1). + */ +function slowButAccuratePrng() { + // TODO(tdrl): Implement. + return Math.random(); +} + +/** + * Container for alternate random number generator implementations. This + * allows us to set an "accurate" PRNG for branch selection, but to mock it + * out easily in tests. + * + * @visibleForTesting + * @const {!{accuratePrng: function():number}} + */ +export const RANDOM_NUMBER_GENERATORS = { + accuratePrng: slowButAccuratePrng, +}; + +/** + * Selects, uniformly at random, a single property name from all + * properties set on a given object. + * + * @param {!Object} obj Object to select from. + * @return {string} Single property name from obj. + */ +function selectRandomProperty(obj) { + const allProperties = Object.keys(obj); + const rn = RANDOM_NUMBER_GENERATORS.accuratePrng(); + return allProperties[Math.floor(rn * allProperties.length)]; +} + +/** + * Selects which page-level experiments, if any, a given amp-ad will + * participate in. If a given experiment name is already set (including to + * the null / no branches selected state), this won't alter its state. + * + * Check whether a given experiment is set using isExperimentOn(win, + * experimentName) and, if it is on, look for which branch is selected in + * win.pageExperimentBranches[experimentName]. + * + * @param {!Window} win Window context on which to save experiment + * selection state. + * @param {!Object} experiments Set of experiments to + * configure for this page load. + * @visibleForTesting + */ +export function randomlySelectUnsetPageExperiments(win, experiments) { + win.pageExperimentBranches = win.pageExperimentBranches || {}; + for (const experimentName in experiments) { + // Skip experimentName if it is not a key of experiments object or if it + // has already been populated by some other property. + if (!experiments.hasOwnProperty(experimentName) || + win.pageExperimentBranches.hasOwnProperty(experimentName)) { + continue; + } + // If we're in the experiment, but we haven't already forced a specific + // experiment branch (e.g., via a test setup), then randomize the branch + // choice. + if (!win.pageExperimentBranches[experimentName] && + isExperimentOn(win, experimentName)) { + const branches = experiments[experimentName]; + const branch = selectRandomProperty(branches); + win.pageExperimentBranches[experimentName] = branches[branch]; + } + } +} + +/** + * Returns the experiment branch enabled for the given experiment ID. + * For example, 'control' or 'experiment'. + * + * @param {!Window} win Window context to check for experiment state. + * @param {!string} experimentName Name of the experiment to check. + * @return {string} Active experiment branch ID for experimentName (possibly + * null/false if experimentName has been tested but no branch was enabled). + */ +export function getPageExperimentBranch(win, experimentName) { + return win.pageExperimentBranches[experimentName]; +} + +/** + * Force enable (or disable) a specific branch of a given experiment name. + * Disables the experiment name altogether if branchId is falseish. + * + * @param {!Window} win Window context to check for experiment state. + * @param {!string} experimentName Name of the experiment to check. + * @param {?string} branchId ID of branch to force or null/false to disable + * altogether. + * @visibleForTesting + */ +export function forceExperimentBranch(win, experimentName, branchId) { + win.pageExperimentBranches = win.pageExperimentBranches || {}; + toggleExperiment(win, experimentName, !!branchId, true); + win.pageExperimentBranches[experimentName] = branchId; +} + +/** + * Sets of experiment IDs can be attached to Elements via attributes. In + * that case, we encode them as a string containing a comma-separated list + * of experiment IDs. This parses a comma-separated list from a string into + * a list of ID strings. If the input string is empty or null, this returns + * the empty list. This does no validity checking on the ID formats -- for + * that, use validateExperimentIds. + * + * @param {?string} idString String to parse. + * @returns {!Array} List of experiment IDs (possibly empty). + * @see validateExperimentIds + */ +export function parseExperimentIds(idString) { + if (idString) { + return idString.split(','); + } + return []; +} + +/** + * Checks whether the given element is a member of the given experiment branch. + * I.e., whether the element's data-experiment-id attribute contains the id + * value (possibly because the host page URL contains a 'exp=a4a:X' parameter + * and #maybeSetExperimentFromUrl has added the appropriate EID). + * + * @param element {!Element} Element to check for membership in a specific + * experiment. + * @param id {?string} Experiment ID to check for on `element`. + * @return {boolean} + */ +export function isInExperiment(element, id) { + return parseExperimentIds(element.getAttribute(EXPERIMENT_ATTRIBUTE)).some( + x => { return x === id; }); +} + +/** + * Checks whether the given element is a member of the 'manually triggered + * "experiment" branch'. I.e., whether the element's data-experiment-id + * attribute contains the MANUAL_EXPERIMENT_ID value (hopefully because the + * user has manually specified 'exp=a4a:-1' in the host page URL and + * #maybeSetExperimentFromUrl has added it). + * + * @param {!Element} element Element to check for manual experiment membership. + * @returns {boolean} + */ +export function isInManualExperiment(element) { + return isInExperiment(element, MANUAL_EXPERIMENT_ID); +} + +/** + * Checks that all string experiment IDs in a list are syntactically valid + * (integer base 10). + * + * @param {!Array} idList List of experiment IDs. Can be empty. + * @returns {boolean} Whether all list elements are valid experiment IDs. + */ +export function validateExperimentIds(idList) { + return idList.every(id => { return !isNaN(parseInt(id, 10)); }); +} + +/** + * Add a new experiment ID to a (possibly empty) existing set of experiment IDs. + * The {@code currentIdString} may be {@code null} or {@code ''}, but if it is + * populated, it must contain a comma-separated list of integer experiment IDs + * (per {@code parseExperimentIds()}). Returns the new set of IDs, encoded + * as a comma-separated list. Does not de-duplicate ID entries. + * + * @param {!string} newId ID to merge in. Must be a stringified integer + * (base 10). + * @param {?string} currentIdString If present, a string containing a + * comma-separated list of integer experiment IDs. + * @returns {string} New experiment list string, including newId iff it is + * a valid (integer) experiment ID. + * @see parseExperimentIds, validateExperimentIds + */ +export function mergeExperimentIds(newId, currentIdString) { + if (newId && !isNaN(parseInt(newId, 10))) { + return currentIdString ? (currentIdString + ',' + newId) : newId; + } + return currentIdString || ''; +} + +/** + * Adds a single experimentID to an element iff it's a valid experiment ID. + * + * @param {!string} experimentId ID to add to the element. + * @param element Element to add the experiment ID to. + */ +export function addExperimentIdToElement(experimentId, element) { + const currentEids = element.getAttribute(EXPERIMENT_ATTRIBUTE); + if (currentEids && validateExperimentIds(parseExperimentIds(currentEids))) { + element.setAttribute(EXPERIMENT_ATTRIBUTE, + mergeExperimentIds(experimentId, currentEids)); + } else { + element.setAttribute(EXPERIMENT_ATTRIBUTE, experimentId); + } +} diff --git a/ads/google/a4a/url-builder.js b/ads/google/a4a/url-builder.js new file mode 100644 index 000000000000..90d6567b71d0 --- /dev/null +++ b/ads/google/a4a/url-builder.js @@ -0,0 +1,72 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @typedef {{name: string, value: (string|number|null)}} */ +export let QueryParameterDef; + +/** + * Builds a URL from query parameters, truncating to a maximum length if + * necessary. + * @param {string} baseUrl scheme, domain, and path for the URL. + * @param {!Array} queryParams query parameters for the URL. + * @param {number} maxLength length to truncate the URL to if necessary. + * @param {?QueryParameterDef=} opt_truncationQueryParam query parameter to + * append to the URL iff any query parameters were truncated. + * @return {string} the fully constructed URL. + */ +export function buildUrl( + baseUrl, queryParams, maxLength, opt_truncationQueryParam) { + const encodedParams = []; + const encodedTruncationParam = + opt_truncationQueryParam && + !(opt_truncationQueryParam.value == null || + opt_truncationQueryParam.value === '') ? + encodeURIComponent(opt_truncationQueryParam.name) + '=' + + encodeURIComponent(String(opt_truncationQueryParam.value)) : + null; + let capacity = maxLength - baseUrl.length; + if (encodedTruncationParam) { + capacity -= encodedTruncationParam.length + 1; + } + for (let i = 0; i < queryParams.length; i++) { + const param = queryParams[i]; + if (param.value == null || param.value === '') { + continue; + } + const encodedNameAndSep = encodeURIComponent(param.name) + '='; + const encodedValue = encodeURIComponent(String(param.value)); + const fullLength = encodedNameAndSep.length + encodedValue.length + 1; + if (fullLength > capacity) { + const truncatedValue = encodedValue + .substr(0, capacity - encodedNameAndSep.length - 1) + // Don't end with a partially truncated escape sequence + .replace(/%\w?$/, ''); + if (truncatedValue) { + encodedParams.push(encodedNameAndSep + truncatedValue); + } + if (encodedTruncationParam) { + encodedParams.push(encodedTruncationParam); + } + break; + } + encodedParams.push(encodedNameAndSep + encodedValue); + capacity -= fullLength; + } + if (!encodedParams.length) { + return baseUrl; + } + return baseUrl + '?' + encodedParams.join('&'); +} diff --git a/ads/google/a4a/utils.js b/ads/google/a4a/utils.js new file mode 100644 index 000000000000..8b70507a924e --- /dev/null +++ b/ads/google/a4a/utils.js @@ -0,0 +1,333 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {buildUrl} from './url-builder'; +import {makeCorrelator} from '../correlator'; +import {getAdCid} from '../../../src/ad-cid'; +import {documentInfoForDoc} from '../../../src/document-info'; +import {dev} from '../../../src/log'; +import {getMode} from '../../../src/mode'; +import {isProxyOrigin} from '../../../src/url'; +import {viewerForDoc} from '../../../src/viewer'; +import {base64UrlDecodeToBytes} from '../../../src/utils/base64'; +import {domFingerprint} from '../../../src/utils/dom-fingerprint'; + +/** @const {string} */ +const AMP_SIGNATURE_HEADER = 'X-AmpAdSignature'; + +/** @const {string} */ +const CREATIVE_SIZE_HEADER = 'X-CreativeSize'; + +/** @const {number} */ +const MAX_URL_LENGTH = 4096; + +/** @enum {string} */ +const AmpAdImplementation = { + AMP_AD_XHR_TO_IFRAME: '2', + AMP_AD_XHR_TO_IFRAME_OR_AMP: '3', +}; + +/** @const {!Object} */ +export const ValidAdContainerTypes = [ + 'AMP-STICKY-AD', + 'AMP-FX-FLYING-CARPET', +]; + +/** @const {string} */ +export const QQID_HEADER = 'X-QQID'; + +/** + * Element attribute that stores experiment IDs. + * + * Note: This attribute should be used only for tracking experimental + * implementations of AMP tags, e.g., by AMPHTML implementors. It should not be + * added by a publisher page. + * + * @const {!string} + * @visibleForTesting + */ +export const EXPERIMENT_ATTRIBUTE = 'data-experiment-id'; + +/** + * Check whether Google Ads supports the A4A rendering pathway is valid for the + * environment by ensuring native crypto support and page originated in the + * the {@code cdn.ampproject.org} CDN or we must be running in local + * dev mode. + * + * @param {!Window} win Host window for the ad. + * @returns {boolean} Whether Google Ads should attempt to render via the A4A + * pathway. + */ +export function isGoogleAdsA4AValidEnvironment(win) { + const supportsNativeCrypto = win.crypto && + (win.crypto.subtle || win.crypto.webkitSubtle); + // Note: Theoretically, isProxyOrigin is the right way to do this, b/c it + // will be kept up to date with known proxies. However, it doesn't seem to + // be compatible with loading the example files from localhost. To hack + // around that, just say that we're A4A eligible if we're in local dev + // mode, regardless of origin path. + return supportsNativeCrypto && + (isProxyOrigin(win.location) || getMode().localDev || getMode().test); +} + +/** + * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a + * @param {string} baseUrl + * @param {number} startTime + * @param {!Array} queryParams + * @param {!Array} unboundedQueryParams + * Parameters that will be put at the end of the URL, where they may be + * elided for length reasons. Intended for parameters with potentially + * long values, like URLs. + * @return {!Promise} + */ +export function googleAdUrl( + a4a, baseUrl, startTime, queryParams, unboundedQueryParams) { + // TODO: Maybe add checks in case these promises fail. + /** @const {!Promise} */ + const referrerPromise = viewerForDoc(a4a.getAmpDoc()).getReferrerUrl(); + return getAdCid(a4a).then(clientId => referrerPromise.then(referrer => { + const adElement = a4a.element; + const slotNumber = adElement.getAttribute('data-amp-slot-index'); + const win = a4a.win; + const documentInfo = documentInfoForDoc(adElement); + // Read by GPT for GA/GPT integration. + win.gaGlobal = win.gaGlobal || + {cid: clientId, hid: documentInfo.pageViewId}; + const slotRect = a4a.getIntersectionElementLayoutBox(); + const screen = win.screen; + const viewport = a4a.getViewport(); + const viewportRect = viewport.getRect(); + const iframeDepth = iframeNestingDepth(win); + const viewportSize = viewport.getSize(); + if (ValidAdContainerTypes.indexOf(adElement.parentElement.tagName) >= 0) { + queryParams.push({name: 'amp_ct', + value: adElement.parentElement.tagName}); + } + const allQueryParams = queryParams.concat( + [ + { + name: 'is_amp', + value: AmpAdImplementation.AMP_AD_XHR_TO_IFRAME_OR_AMP, + }, + {name: 'amp_v', value: '$internalRuntimeVersion$'}, + {name: 'd_imp', value: '1'}, + {name: 'dt', value: startTime}, + {name: 'ifi', value: slotNumber}, + {name: 'adf', value: domFingerprint(adElement)}, + {name: 'c', value: getCorrelator(win, clientId)}, + {name: 'output', value: 'html'}, + {name: 'nhd', value: iframeDepth}, + {name: 'iu', value: adElement.getAttribute('data-ad-slot')}, + {name: 'eid', value: adElement.getAttribute('data-experiment-id')}, + {name: 'biw', value: viewportRect.width}, + {name: 'bih', value: viewportRect.height}, + {name: 'adx', value: slotRect.left}, + {name: 'ady', value: slotRect.top}, + {name: 'u_aw', value: screen ? screen.availWidth : null}, + {name: 'u_ah', value: screen ? screen.availHeight : null}, + {name: 'u_cd', value: screen ? screen.colorDepth : null}, + {name: 'u_w', value: screen ? screen.width : null}, + {name: 'u_h', value: screen ? screen.height : null}, + {name: 'u_tz', value: -new Date().getTimezoneOffset()}, + {name: 'u_his', value: getHistoryLength(win)}, + {name: 'oid', value: '2'}, + {name: 'brdim', value: additionalDimensions(win, viewportSize)}, + {name: 'isw', value: viewportSize.width}, + {name: 'ish', value: viewportSize.height}, + ], + unboundedQueryParams, + [ + {name: 'url', value: documentInfo.canonicalUrl}, + {name: 'top', value: iframeDepth ? topWindowUrlOrDomain(win) : null}, + { + name: 'loc', + value: win.location.href == documentInfo.canonicalUrl ? + null : win.location.href, + }, + {name: 'ref', value: referrer}, + ] + ); + const url = buildUrl(baseUrl, allQueryParams, MAX_URL_LENGTH - 10, + {name: 'trunc', value: '1'}); + return url + '&dtd=' + elapsedTimeWithCeiling(Date.now(), startTime); + })); +} + + +/** + * @param {!ArrayBuffer} creative + * @param {!../../../src/service/xhr-impl.FetchResponseHeaders} responseHeaders + * @return {!Promise} + */ +export function extractGoogleAdCreativeAndSignature( + creative, responseHeaders) { + let signature = null; + let size = null; + try { + if (responseHeaders.has(AMP_SIGNATURE_HEADER)) { + signature = + base64UrlDecodeToBytes(dev().assertString( + responseHeaders.get(AMP_SIGNATURE_HEADER))); + } + if (responseHeaders.has(CREATIVE_SIZE_HEADER)) { + const sizeStr = responseHeaders.get(CREATIVE_SIZE_HEADER); + // We should trust that the server returns the size information in the + // form of a WxH string. + size = sizeStr.split('x').map(dim => Number(dim)); + } + } finally { + return Promise.resolve(/** @type { + !../../../extensions/amp-a4a/0.1/amp-a4a.AdResponseDef} */ ( + {creative, signature, size})); + } +} + +/** + * @param {!Window} win + * @return {number} + */ +function iframeNestingDepth(win) { + let w = win; + let depth = 0; + while (w != w.parent && depth < 100) { + w = w.parent; + depth++; + } + dev().assert(w == win.top); + return depth; +} + +/** + * @param {!Window} win + * @return {number} + */ +function getHistoryLength(win) { + // We have seen cases where accessing history length causes errors. + try { + return win.history.length; + } catch (e) { + return 0; + } +} + +/** + * @param {!Window} win + * @return {?string} + */ +function topWindowUrlOrDomain(win) { + const ancestorOrigins = win.location.ancestorOrigins; + if (ancestorOrigins) { + const origin = win.location.origin; + const topOrigin = ancestorOrigins[ancestorOrigins.length - 1]; + if (origin == topOrigin) { + return win.top.location.href; + } + const secondFromTop = secondWindowFromTop(win); + if (secondFromTop == win || + origin == ancestorOrigins[ancestorOrigins.length - 2]) { + return secondFromTop./*REVIEW*/document.referrer; + } + return topOrigin; + } else { + try { + return win.top.location.href; + } catch (e) {} + const secondFromTop = secondWindowFromTop(win); + try { + return secondFromTop./*REVIEW*/document.referrer; + } catch (e) {} + return null; + } +} + +/** + * @param {!Window} win + * @return {!Window} + */ +function secondWindowFromTop(win) { + let secondFromTop = win; + let depth = 0; + while (secondFromTop.parent != secondFromTop.parent.parent && + depth < 100) { + secondFromTop = secondFromTop.parent; + depth++; + } + dev().assert(secondFromTop.parent == win.top); + return secondFromTop; +} + +/** + * @param {number} time + * @param {number} start + * @return {(number|string)} + */ +function elapsedTimeWithCeiling(time, start) { + const duration = time - start; + if (duration >= 1e6) { + return 'M'; + } else if (duration >= 0) { + return duration; + } + return '-M'; +} + +/** + * @param {!Window} win + * @param {string=} opt_cid + * @return {number} The correlator. + */ +export function getCorrelator(win, opt_cid) { + if (!win.ampAdPageCorrelator) { + win.ampAdPageCorrelator = makeCorrelator( + opt_cid, documentInfoForDoc(win.document).pageViewId); + } + return win.ampAdPageCorrelator; +} + +/** + * Collect additional dimensions for the brdim parameter. + * @param {!Window} win The window for which we read the browser dimensions. + * @param {{width: number, height: number}|null} viewportSize + * @return {string} + * @visibleForTesting + */ +export function additionalDimensions(win, viewportSize) { + // Some browsers throw errors on some of these. + let screenX, screenY, outerWidth, outerHeight, innerWidth, innerHeight; + try { + screenX = win.screenX; + screenY = win.screenY; + } catch (e) {} + try { + outerWidth = win.outerWidth; + outerHeight = win.outerHeight; + } catch (e) {} + try { + innerWidth = viewportSize.width; + innerHeight = viewportSize.height; + } catch (e) {} + return [win.screenLeft, + win.screenTop, + screenX, + screenY, + win.screen ? win.screen.availWidth : undefined, + win.screen ? win.screen.availTop : undefined, + outerWidth, + outerHeight, + innerWidth, + innerHeight].join(); +}; diff --git a/ads/google/adsense.js b/ads/google/adsense.js index fde50488bbbf..7dce60275e4c 100644 --- a/ads/google/adsense.js +++ b/ads/google/adsense.js @@ -14,18 +14,24 @@ * limitations under the License. */ -import {checkData} from '../../3p/3p'; +import {validateData} from '../../3p/3p'; +import {setStyles} from '../../src/style'; /** + * Make an adsense iframe. * @param {!Window} global * @param {!Object} data */ export function adsense(global, data) { - checkData(data, ['adClient', 'adSlot', 'adHost', 'adtest', 'tagOrigin']); + // TODO: check mandatory fields + validateData(data, [], + ['adClient', 'adSlot', 'adHost', 'adtest', 'tagOrigin', 'experimentId', + 'ampSlotIndex']); + if (global.context.clientId) { // Read by GPT for GA/GPT integration. global.gaGlobal = { - vid: global.context.clientId, + cid: global.context.clientId, hid: global.context.pageViewId, }; } @@ -41,7 +47,7 @@ export function adsense(global, data) { if (data['adHost']) { i.setAttribute('data-ad-host', data['adHost']); } - if (data['adtest']) { + if (data['adtest'] != null) { i.setAttribute('data-adtest', data['adtest']); } if (data['tagOrigin']) { @@ -49,7 +55,22 @@ export function adsense(global, data) { } i.setAttribute('data-page-url', global.context.canonicalUrl); i.setAttribute('class', 'adsbygoogle'); - i.style.cssText = 'display:inline-block;width:100%;height:100%;'; + setStyles(i, { + display: 'inline-block', + width: '100%', + height: '100%', + }); + const initializer = {}; + if (data['experimentId']) { + const experimentIdList = data['experimentId'].split(','); + if (experimentIdList) { + initializer['params'] = { + 'google_ad_modifications': { + 'eids': experimentIdList, + }, + }; + } + } global.document.getElementById('c').appendChild(i); - (global.adsbygoogle = global.adsbygoogle || []).push({}); + (global.adsbygoogle = global.adsbygoogle || []).push(initializer); } diff --git a/ads/google/adsense.md b/ads/google/adsense.md index c786f5b39fe2..df1d27920ba8 100644 --- a/ads/google/adsense.md +++ b/ads/google/adsense.md @@ -19,16 +19,16 @@ limitations under the License. ## Example ```html - - + + ``` ## Configuration -For semantics of configuration, please see ad network documentation. +For semantics of configuration, please see [ad network documentation](https://support.google.com/adsense/answer/7183212?hl=en). For AdSense for Search and AdSense for Shopping, please see the [CSA AMP ad type](https://github.com/ampproject/amphtml/blob/master/ads/google/csa.md). Supported parameters: diff --git a/ads/google/correlator.js b/ads/google/correlator.js new file mode 100644 index 000000000000..d0123cbe4960 --- /dev/null +++ b/ads/google/correlator.js @@ -0,0 +1,33 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @param {(string|undefined)} clientId + * @param {string} pageViewId + * @return {number} + */ +export function makeCorrelator(clientId, pageViewId) { + const pageViewIdNumeric = Number(pageViewId || 0); + if (clientId) { + return pageViewIdNumeric + (clientId.replace(/\D/g, '') % 1e6) * 1e6; + } else { + // In this case, pageViewIdNumeric is only 4 digits => too low entropy + // to be useful as a page correlator. So synthesize one from scratch. + // 4503599627370496 == 2^52. The guaranteed range of JS Number is at least + // 2^53 - 1. + return Math.floor(4503599627370496 * Math.random()); + } +} diff --git a/ads/google/csa.js b/ads/google/csa.js new file mode 100644 index 000000000000..3d926acc80b7 --- /dev/null +++ b/ads/google/csa.js @@ -0,0 +1,370 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {validateData, loadScript} from '../../3p/3p'; +import {tryParseJson} from '../../src/json.js'; +import {getStyle, setStyle, setStyles} from '../../src/style'; + +// Keep track of current height of AMP iframe +let currentAmpHeight = null; + +// Height of overflow element +const overflowHeight = 40; + +/** + * Enum for different AdSense Products + * @enum {number} + * @visibleForTesting + */ +export const AD_TYPE = { + /** Value if we can't determine which product to request */ + UNSUPPORTED: 0, + /** AdSense for Search */ + AFS: 1, + /** AdSense for Shopping */ + AFSH: 2, + /** AdSense for Shopping, backfilled with AdSense for Search */ + AFSH_BACKFILL: 3, +}; + + +/** + * Request Custom Search Ads (Adsense for Search or AdSense for Shopping). + * @param {!Window} global The window object of the iframe + * @param {!Object} data + */ +export function csa(global, data) { + // Get parent width in case we want to override + const width = global.document.body./*OK*/clientWidth; + + validateData(data, [], [ + 'afshPageOptions', + 'afshAdblockOptions', + 'afsPageOptions', + 'afsAdblockOptions', + 'ampSlotIndex', + ]); + + // Add the ad container to the document + const containerDiv = global.document.createElement('div'); + const containerId = 'csacontainer'; + containerDiv.id = containerId; + global.document.getElementById('c').appendChild(containerDiv); + + const pageOptions = {source: 'amp', referer: global.context.referrer}; + const adblockOptions = {container: containerId}; + + // Parse all the options + const afshPage = Object.assign( + Object(tryParseJson(data['afshPageOptions'])), pageOptions); + const afsPage = Object.assign( + Object(tryParseJson(data['afsPageOptions'])), pageOptions); + const afshAd = Object.assign( + Object(tryParseJson(data['afshAdblockOptions'])), adblockOptions); + const afsAd = Object.assign( + Object(tryParseJson(data['afsAdblockOptions'])), adblockOptions); + + // Special case for AFSh when "auto" is the requested width + if (afshAd['width'] == 'auto') { + afshAd['width'] = width; + } + + // Event listener needed for iOS9 bug + global.addEventListener('orientationchange', + orientationChangeHandler.bind(null, global, containerDiv)); + + // Register resize callbacks + global.context.onResizeSuccess( + resizeSuccessHandler.bind(null, global, containerDiv)); + global.context.onResizeDenied( + resizeDeniedHandler.bind(null, global, containerDiv)); + + // Only call for ads once the script has loaded + loadScript(global, 'https://www.google.com/adsense/search/ads.js', + requestCsaAds.bind(null, global, data, afsPage, afsAd, afshPage, afshAd)); +} + +/** + * Resize the AMP iframe if the CSA container changes in size upon rotation. + * This is needed for an iOS bug found in versions 10.0.1 and below that + * doesn't properly reflow the iframe upon orientation change. + * @param {!Window} global The window object of the iframe + * @param {!Element} containerDiv The CSA container + */ +function orientationChangeHandler(global, containerDiv) { + // Save the height of the container before the event listener triggers + const oldHeight = getStyle(containerDiv, 'height'); + global.setTimeout(() => { + // Force DOM reflow and repaint + /*eslint-disable no-unused-vars*/ + const ignore = global.document.body./*OK*/offsetHeight; + /*eslint-enable no-unused-vars*/ + // Capture new height + let newHeight = getStyle(containerDiv, 'height'); + // In older versions of iOS, this height will be different because the + // container height is resized. + // In Chrome and iOS 10.0.2 the height is the same because + // the container isn't resized. + if (oldHeight != newHeight && newHeight != currentAmpHeight) { + // style.height returns "60px" (for example), so turn this into an int + newHeight = parseInt(newHeight, 10); + // Also update the onclick function to resize to the right height. + const overflow = global.document.getElementById('overflow'); + if (overflow) { + overflow.onclick = + global.context.requestResize.bind(null, undefined, newHeight); + } + // Resize the container to the correct height + global.context.requestResize(undefined, newHeight); + } + }, 250); /* 250 is time in ms to wait before executing orientation */ +} + +/** + * Hanlder for when a resize request succeeds + * Hide the overflow and resize the container + * @param {!Window} global The window object of the iframe + * @param {!Element} container The CSA container + * @param {!number} requestedHeight The height of the resize request + * @visibleForTesting + */ +export function resizeSuccessHandler(global, container, requestedHeight) { + currentAmpHeight = requestedHeight; + const overflow = global.document.getElementById('overflow'); + if (overflow) { + setStyle(overflow, 'display', 'none'); + resizeCsa(container, requestedHeight); + } +} + +/** + * Hanlder for When a resize request is denied + * If the container is larger than the AMP container and an overflow already + * exists, show the overflow and resize the container to fit inside the AMP + * container. If an overflow doesn't exist, create one. + * @param {!Window} global The window object of the iframe + * @param {!Element} container The CSA container + * @param {!number} requestedHeight The height of the resize request + * @visibleForTesting + */ +export function resizeDeniedHandler(global, container, requestedHeight) { + const overflow = global.document.getElementById('overflow'); + const containerHeight = parseInt(getStyle(container, 'height'), 10); + if (containerHeight > currentAmpHeight) { + if (overflow) { + setStyle(overflow, 'display', ''); + resizeCsa(container, currentAmpHeight - overflowHeight); + } else { + createOverflow(global, container, requestedHeight); + } + } +} + +/** + * Make a request for either AFS or AFSh + * @param {!Window} global The window object of the iframe + * @param {!Object} data The data passed in by the partner + * @param {!Object} afsP The parsed AFS page options object + * @param {!Object} afsA The parsed AFS adblock options object + * @param {!Object} afshP The parsed AFSh page options object + * @param {!Object} afshA The parsed AFSh adblock options object + */ +function requestCsaAds(global, data, afsP, afsA, afshP, afshA) { + const type = getAdType(data); + const callback = callbackWithNoBackfill.bind(null, global); + const callbackBackfill = callbackWithBackfill.bind(null, global, afsP, afsA); + + switch (type) { + case AD_TYPE.AFS: + /** Do not backfill, request AFS */ + afsA['adLoadedCallback'] = callback; + global._googCsa('ads', afsP, afsA); + break; + case AD_TYPE.AFSH: + /** Do not backfill, request AFSh */ + afshA['adLoadedCallback'] = callback; + global._googCsa('plas', afshP, afshA); + break; + case AD_TYPE.AFSH_BACKFILL: + /** Backfill with AFS, request AFSh */ + afshA['adLoadedCallback'] = callbackBackfill; + global._googCsa('plas', afshP, afshA); + break; + } +} + +/** + * Helper function to determine which product to request + * @param {!Object} data The data passed in by the partner + * @return {!number} Enum of ad type + */ +function getAdType(data) { + if (data['afsPageOptions'] != null && data['afshPageOptions'] == null) { + return AD_TYPE.AFS; + } + if (data['afsPageOptions'] == null && data['afshPageOptions'] != null) { + return AD_TYPE.AFSH; + } + if (data['afsPageOptions'] != null && data['afshPageOptions'] != null) { + return AD_TYPE.AFSH_BACKFILL; + } + else { + return AD_TYPE.UNSUPPORTED; + } +} + +/** + * The adsLoadedCallback for requests without a backfill. If ads were returned, + * resize the iframe. If ads weren't returned, tell AMP we don't have ads. + * @param {!Window} global The window object of the iframe + * @param {!string} containerName The name of the CSA container + * @param {!boolean} hasAd Whether or not CSA returned an ad + * @visibleForTesting + */ +export function callbackWithNoBackfill(global, containerName, hasAd) { + if (hasAd) { + resizeIframe(global, containerName); + } else { + global.context.noContentAvailable(); + } +} + +/** + * The adsLoadedCallback for requests with a backfill. If ads were returned, + * resize the iframe. If ads weren't returned, backfill the ads. + * @param {!Window} global The window object of the iframe + * @param {!Object} page The parsed AFS page options to backfill the unit with + * @param {!Object} ad The parsed AFS page options to backfill the unit with + * @param {!string} containerName The name of the CSA container + * @param {!boolean} hasAd Whether or not CSA returned an ad + * @visibleForTesting +*/ +export function callbackWithBackfill(global, page, ad, containerName, hasAd) { + if (hasAd) { + resizeIframe(global, containerName); + } else { + ad['adLoadedCallback'] = callbackWithNoBackfill.bind(null, global); + global['_googCsa']('ads', page, ad); + } +} + +/** + * CSA callback function to resize the iframe when ads were returned + * @param {!string} containerName Name of the container ('csacontainer') + * @visibleForTesting + */ +export function resizeIframe(global, containerName) { + // Get actual height of container + const container = global.document.getElementById(containerName); + const height = container./*OK*/offsetHeight; + // Set initial AMP height + currentAmpHeight = + global.context.initialIntersection.boundingClientRect.height; + + // If the height of the container is larger than the height of the + // initially requested AMP tag, add the overflow element + if (height > currentAmpHeight) { + createOverflow(global, container, height); + } + // Attempt to resize to actual CSA container height + global.context.requestResize(undefined, height); +} + +/** + * Helper function to create an overflow element + * @param {!Window} global The window object of the iframe + * @param {!Element} container HTML element of the CSA container + * @param {!number} height The full height the CSA container should be when the + * overflow element is clicked. + */ +function createOverflow(global, container, height) { + const overflow = getOverflowElement(global); + // When overflow is clicked, resize to full height + overflow.onclick = global.context.requestResize.bind(null, undefined, height); + global.document.getElementById('c').appendChild(overflow); + // Resize the CSA container to not conflict with overflow + resizeCsa(container, currentAmpHeight - overflowHeight); +} + +/** + * Helper function to create the base overflow element + * @param {!Window} global The window object of the iframe + * @return {!Element} + */ +function getOverflowElement(global) { + const overflow = global.document.createElement('div'); + overflow.id = 'overflow'; + setStyles(overflow, { + position: 'absolute', + height: overflowHeight + 'px', + width: '100%', + }); + overflow.appendChild(getOverflowLine(global)); + overflow.appendChild(getOverflowChevron(global)); + return overflow; +} + +/** + * Helper function to create a line element for the overflow element + * @param {!Window} global The window object of the iframe + * @return {!Element} + */ +function getOverflowLine(global) { + const line = global.document.createElement('div'); + setStyles(line, { + background: 'rgba(0,0,0,.16)', + height: '1px', + }); + return line; +} + +/** + * Helper function to create a chevron element for the overflow element + * @param {!Window} global The window object of the iframe + * @return {!Element} + */ +function getOverflowChevron(global) { + const svg = '' + + ' '; + + const chevron = global.document.createElement('div'); + setStyles(chevron, { + width: '36px', + height: '36px', + marginLeft: 'auto', + marginRight: 'auto', + display: 'block', + }); + chevron./*OK*/innerHTML = svg; + return chevron; +} + +/** + * Helper function to resize the height of a CSA container and its child iframe + * @param {!Element} container HTML element of the CSA container + * @param {!number} height Height to resize, in pixels + */ +function resizeCsa(container, height) { + const iframe = container.firstElementChild; + if (iframe) { + setStyles(iframe, { + height: height + 'px', + width: '100%', + }); + } + setStyle(container, 'height', height + 'px'); +} diff --git a/ads/google/csa.md b/ads/google/csa.md new file mode 100644 index 000000000000..e87274a009c4 --- /dev/null +++ b/ads/google/csa.md @@ -0,0 +1,95 @@ + +# Custom Search Ads + +_The Custom Search Ads integration is offered as a Beta and is subject to the +conditions of the Google Program Guidelines._ + +## AdSense For Search (AFS) + +To request AdSense for Search ads on the Custom Search Ads protocol, use the +**data-afs-page-options** and **data-afs-adblock-options** attributes in the +amp-ad tag. The values you pass should be set to a stringified version of the +Javascript object you would pass in the ad request of a standard CSA request. + +```html + + +``` + +Please see documentation for [AdSense for Search](https://developers.google.com/custom-search-ads/docs/implementation-guide) +for more information. + +## AdSense For Shopping (AFSh) + +To request AdSense for Shopping ads on the Custom Search Ads protocol, use the +**data-afsh-page-options** and **data-afsh-adblock-options** attributes in the +amp-ad tag. The values you pass should be set to a stringified version of the +Javascript object you would pass in the ad request of a standard CSA request. + +```html + + +``` + +To request an AFSh ad with a width equal to the screen width, use "auto" for +the CSA width parameter. Please note that "auto" width is not supported in +non-AMP implementations. + +Note that only the [multiple-format](https://developers.google.com/adsense-for-shopping/docs/multiplereference) AdSense for Shopping ads are supported under this integration. + +Please see documentation for [AdSense for Shopping](https://developers.google.com/adsense-for-shopping/docs/implementation-guide) +for more information. + +### AFSh with AFS Backfill + +To request AFS ads when AFSh does not return any ads, include both the +**data-afs-*** and **data-afsh-*** attributes in the amp-ad tag. If AFSh does +not return ads, AMP will request AFS ads with the values from the **data-afs-*** +attributes. + +```html + + +``` + +## Requirements + +- Each amp-ad tag contains one adblock. Only one **data-afs-adblock-options** +and/or one **data-afsh-adblock-options** attribute can be specified in the tag. +- Above the fold ads are required to have a minimum height of 300 pixels. +- When requesting ads above the fold: + - You must use the maxTop parameter instead of the number parameter to specify the number of ads. + - You can only request one ad ("maxTop": 1) in an ad unit that is above the fold. + - You must use a fallback div to show alternate content when no ads are returned. If no ads are returned the ad will not be collapsed because it is above the fold. + +## Demos + +Please visit [google-ads-amp-demos.com](http://google-ads-amp-demos.com/) for demos and additional requirements when implementing these ads. diff --git a/ads/google/doubleclick.js b/ads/google/doubleclick.js index 9577cbf75612..1ff7e01eae24 100644 --- a/ads/google/doubleclick.js +++ b/ads/google/doubleclick.js @@ -14,8 +14,11 @@ * limitations under the License. */ -import {loadScript, checkData} from '../../3p/3p'; -import {getCorrelator} from './utils'; +import {makeCorrelator} from './correlator'; +import {validateData, loadScript} from '../../3p/3p'; +import {dev} from '../../src/log'; +import {setStyles} from '../../src/style'; +import {getMultiSizeDimensions} from './utils'; /** * @enum {number} @@ -24,7 +27,8 @@ import {getCorrelator} from './utils'; const GladeExperiment = { NO_EXPERIMENT: 0, GLADE_CONTROL: 1, - GLADE_OPT_OUT: 2, + GLADE_EXPERIMENT: 2, + GLADE_OPT_OUT: 3, }; /** @@ -32,35 +36,48 @@ const GladeExperiment = { * @param {!Object} data */ export function doubleclick(global, data) { - const experimentFraction = 0.5; + const experimentFraction = 0.1; - checkData(data, [ + // TODO: check mandatory fields + validateData(data, [], [ 'slot', 'targeting', 'categoryExclusions', 'tagForChildDirectedTreatment', 'cookieOptions', 'overrideWidth', 'overrideHeight', 'loadingStrategy', 'consentNotificationId', 'useSameDomainRenderingUntilDeprecated', + 'experimentId', 'multiSize', 'multiSizeValidation', 'ampSlotIndex', ]); if (global.context.clientId) { // Read by GPT/Glade for GA/Doubleclick integration. global.gaGlobal = { - vid: global.context.clientId, + cid: global.context.clientId, hid: global.context.pageViewId, }; } - if (data.useSameDomainRenderingUntilDeprecated != undefined) { + // Center the ad in the container. + const container = global.document.querySelector('#c'); + setStyles(dev().assertElement(container), { + top: '50%', + left: '50%', + bottom: '', + right: '', + transform: 'translate(-50%, -50%)', + }); + + if (data.useSameDomainRenderingUntilDeprecated != undefined || + data.multiSize) { doubleClickWithGpt(global, data, GladeExperiment.GLADE_OPT_OUT); } else { const dice = Math.random(); const href = global.context.location.href; - if ((href.indexOf('google_glade=1') > 0 || dice < experimentFraction) - && href.indexOf('google_glade=0') < 0) { - doubleClickWithGlade(global, data); + if ((href.indexOf('google_glade=0') > 0 || dice < experimentFraction) + && href.indexOf('google_glade=1') < 0) { + doubleClickWithGpt(global, data, GladeExperiment.GLADE_CONTROL); } else { const exp = (dice < 2 * experimentFraction) ? - GladeExperiment.GLADE_CONTROL : GladeExperiment.NO_EXPERIMENT; - doubleClickWithGpt(global, data, exp); + GladeExperiment.GLADE_EXPERIMENT : GladeExperiment.NO_EXPERIMENT; + doubleClickWithGlade(global, data, exp); } } } @@ -76,6 +93,21 @@ function doubleClickWithGpt(global, data, gladeExperiment) { parseInt(data.overrideHeight || data.height, 10), ]]; + // Handle multi-size data parsing, validation, and inclusion into dimensions. + const multiSizeDataStr = data.multiSize || null; + if (multiSizeDataStr) { + const primarySize = dimensions[0]; + const primaryWidth = primarySize[0]; + const primaryHeight = primarySize[1]; + + getMultiSizeDimensions( + multiSizeDataStr, + primaryWidth, + primaryHeight, + (data.multiSizeValidation || 'true') == 'true', + dimensions); + } + loadScript(global, 'https://www.googletagservices.com/tag/js/gpt.js', () => { global.googletag.cmd.push(() => { const googletag = global.googletag; @@ -89,6 +121,13 @@ function doubleClickWithGpt(global, data, gladeExperiment) { pubads.markAsGladeOptOut(); } + if (data['experimentId']) { + const experimentIdList = data['experimentId'].split(','); + pubads.forceExperiment = pubads.forceExperiment || function() {}; + experimentIdList && + experimentIdList.forEach(eid => pubads.forceExperiment(eid)); + } + pubads.markAsAmp(); pubads.set('page_url', global.context.canonicalUrl); pubads.setCorrelator(Number(getCorrelator(global))); @@ -96,8 +135,8 @@ function doubleClickWithGpt(global, data, gladeExperiment) { if (data.categoryExclusions) { if (Array.isArray(data.categoryExclusions)) { - for (const categoryExclusion of data.categoryExclusions) { - slot.setCategoryExclusion(categoryExclusion); + for (let i = 0; i < data.categoryExclusions.length; i++) { + slot.setCategoryExclusion(data.categoryExclusions[i]); } } else { slot.setCategoryExclusion(data.categoryExclusions); @@ -120,10 +159,37 @@ function doubleClickWithGpt(global, data, gladeExperiment) { } pubads.addEventListener('slotRenderEnded', event => { + const primaryInvSize = dimensions[0]; + const pWidth = primaryInvSize[0]; + const pHeight = primaryInvSize[1]; + const returnedSize = event.size; + const rWidth = returnedSize ? returnedSize[0] : null; + const rHeight = returnedSize ? returnedSize[1] : null; + let creativeId = event.creativeId || '_backfill_'; - if (event.isEmpty) { + + // If the creative is empty, or either dimension of the returned size + // is larger than its counterpart in the primary size, then we don't + // want to render the creative. + if (event.isEmpty || + returnedSize && (rWidth > pWidth || rHeight > pHeight)) { global.context.noContentAvailable(); creativeId = '_empty_'; + } else { + // We only want to call renderStart with a specific size if the + // returned creative size matches one of the multi-size sizes. + let newSize; + for (let i = 1; i < dimensions.length; i++) { + // dimensions[0] is the primary or overridden size. + if (dimensions[i][0] == rWidth && dimensions[i][1] == rHeight) { + newSize = { + width: rWidth, + height: rHeight, + }; + break; + } + } + global.context.renderStart(newSize); } global.context.reportRenderedEntityIdentifier('dfp-' + creativeId); }); @@ -138,8 +204,9 @@ function doubleClickWithGpt(global, data, gladeExperiment) { /** * @param {!Window} global * @param {!Object} data + * @param {!GladeExperiment} gladeExperiment */ -function doubleClickWithGlade(global, data) { +function doubleClickWithGlade(global, data, gladeExperiment) { const requestHeight = parseInt(data.overrideHeight || data.height, 10); const requestWidth = parseInt(data.overrideWidth || data.width, 10); @@ -157,6 +224,15 @@ function doubleClickWithGlade(global, data) { if (data.targeting) { jsonParameters.targeting = data.targeting; } + if (gladeExperiment === GladeExperiment.GLADE_EXPERIMENT) { + jsonParameters.gladeEids = '108809102'; + } + const expIds = data['experimentId']; + if (expIds) { + jsonParameters.gladeEids = jsonParameters.gladeEids ? + jsonParameters.gladeEids + ',' + expIds : expIds; + } + const slot = global.document.querySelector('#c'); slot.setAttribute('data-glade', ''); @@ -167,22 +243,25 @@ function doubleClickWithGlade(global, data) { } slot.setAttribute('data-page-url', global.context.canonicalUrl); - // Size setup. - // The ad container should simply fill the amp-ad iframe, but we still - // need to request a specific size from the ad server. - // The ad container size will be relative to the amp-iframe, so if the - // latter changes the ad container will match it. - slot.setAttribute('width', 'fill'); - slot.setAttribute('height', 'fill'); - slot.setAttribute('data-request-height', requestHeight); - slot.setAttribute('data-request-width', requestWidth); + // Center the ad in the container. + slot.setAttribute('height', requestHeight); + slot.setAttribute('width', requestWidth); slot.addEventListener('gladeAdFetched', event => { if (event.detail.empty) { global.context.noContentAvailable(); } + global.context.renderStart(); }); window.glade = {correlator: getCorrelator(global)}; loadScript(global, 'https://securepubads.g.doubleclick.net/static/glade.js'); } + +/** + * @param {!Window} global + * @return {number} + */ +function getCorrelator(global) { + return makeCorrelator(global.context.clientId, global.context.pageViewId); +} diff --git a/ads/google/doubleclick.md b/ads/google/doubleclick.md index 25c0fc9d1c09..6c1a3d2da905 100644 --- a/ads/google/doubleclick.md +++ b/ads/google/doubleclick.md @@ -57,9 +57,44 @@ Example: ``` +### Multi-size Ad + +To request an ad with multiple sizes, pass a string of comma-separated sizes to +the `data-multi-size` attribute. Each size in the list must be a width followed +by a lowercase 'x' followed by a height. Secondary sizes must not be larger than +their corresponding dimensions specified by the `width` and `height` attributes, +or the `data-override-width` and `data-override-height` attributes, if they are +set. Further, the secondary sizes must not be smaller than 2/3rds of their +primary size counterpart, unless `data-multi-size-validation` is explicitly set +to false. + +Examples: + +#### With multi-size request +```html + + +``` + +#### With multi-size request ignoring size validation +```html + + +``` + + ### Supported parameters - `data-slot` +- `data-multi-size` +- `data-multi-size-validation` Supported via `json` attribute: @@ -70,7 +105,7 @@ Supported via `json` attribute: - `useSameDomainRenderingUntilDeprecated` ### Temporary use of useSameDomainRenderingUntilDeprecated -An experiment to use the higher performance GLADE tag in place of the DoubleClick GPT tag causes the ad to render in a second cross domain iframe within the outer AMP iframe. This prevents ads from accessing the iframe sandbox information and methods which are provided by the AMP runtime. Until this API is available to work in the second level iframe, publishers can opt out of this experiment by including "useSameDomainRenderingUntilDeprecated": 1 as a json attribute. This attribute will be deprecated by the end of June 2016, when the GLADE tag will become the default, and all ads will always be rendered inside a second cross domain iframe +An experiment to use the higher performance GPT Light tag in place of the DoubleClick GPT tag causes the ad to render in a second cross domain iframe within the outer AMP iframe. This prevents ads from accessing the iframe sandbox information and methods which are provided by the AMP runtime. Until this API is available to work in the second level iframe, publishers can opt out of this experiment by including "useSameDomainRenderingUntilDeprecated": 1 as a json attribute. This attribute will be deprecated once the [new window.context implementation](https://github.com/ampproject/amphtml/issues/6829) is complete. After that point, the GPT Light tag will become the default and all eligible ads will always be rendered inside a second cross domain iframe. Example: ```html @@ -85,7 +120,6 @@ Example: ### Unsupported DFP Features & Formats #### Unsupported Features: -- Multisize requests. Since the size of the adslot needs to be defined prior to the ad request, multisize requests are not supported. - Guaranteed Roadblocks. Non-guaranteed roadblocks (As many as possible, One or More) delivery is supported #### Unsupported Formats/Creatives: @@ -100,3 +134,6 @@ Example: + + + diff --git a/ads/google/utils.js b/ads/google/utils.js index 03bc9b4db598..04a36e9cf36c 100644 --- a/ads/google/utils.js +++ b/ads/google/utils.js @@ -14,16 +14,131 @@ * limitations under the License. */ +import {user} from '../../src/log'; + /** - * @param {!Window} global - * @return {number} + * Given the amp-ad data attribute containing the multi-size dimensions, and a + * set of primary dimensions, this function will return all valid multi-size + * [width, height] pairs in an array. + * + * @param {string} multiSizeDataStr The amp-ad data attribute containing the + * multi-size dimensions. + * @param {number} primaryWidth The primary width of the ad slot. + * @param {number} primaryHeight The primary height of the ad slot. + * @param {!Array>=} opt_dimensions An array into which to put + * the multi-size dimensions. + * @param {boolean} multiSizeValidation A flag that if set to true will enforce + * the rule that ensures multi-size dimensions are no less than 2/3rds of + * their primary dimension's counterpart. + * @return {!Array>} An array of dimensions. */ -export function getCorrelator(global) { - const clientId = global.context.clientId; - const pageViewId = global.context.pageViewId; - if (global.context.clientId) { - return pageViewId + (clientId.replace(/\D/g, '') % 1e6) * 1e6; - } else { - return pageViewId; +export function getMultiSizeDimensions( + multiSizeDataStr, + primaryWidth, + primaryHeight, + multiSizeValidation, + opt_dimensions) { + + const dimensions = opt_dimensions || []; + const arrayOfSizeStrs = multiSizeDataStr.split(','); + + arrayOfSizeStrs.forEach(sizeStr => { + + const size = sizeStr.split('x'); + + // Make sure that each size is specified in the form val1xval2. + if (size.length != 2) { + user().error('AMP-AD', `Invalid multi-size data format '${sizeStr}'.`); + return; + } + + const width = Number(size[0]); + const height = Number(size[1]); + + // Make sure that both dimensions given are numbers. + if (!validateDimensions(width, height, + w => isNaN(w), + h => isNaN(h), + ({badDim, badVal}) => + `Invalid ${badDim} of ${badVal} given for secondary size.`)) { + return; + } + + // Check that secondary size is not larger than primary size. + if (!validateDimensions(width, height, + w => w > primaryWidth, + h => h > primaryHeight, + ({badDim, badVal}) => `Secondary ${badDim} ${badVal} ` + + `can't be larger than the primary ${badDim}.`)) { + return; + } + + // Check that if multi-size-validation is on, that the secondary sizes + // are at least minRatio of the primary size. + if (multiSizeValidation) { + // The minimum ratio of each secondary dimension to its corresponding + // primary dimension. + const minRatio = 2 / 3; + const minWidth = minRatio * primaryWidth; + const minHeight = minRatio * primaryHeight; + if (!validateDimensions(width, height, + w => w < minWidth, + h => h < minHeight, + ({badDim, badVal}) => `Secondary ${badDim} ${badVal} is ` + + `smaller than 2/3rds of the primary ${badDim}.`)) { + return; + } + } + + // Passed all checks! Push additional size to dimensions. + dimensions.push([width, height]); + }); + + return dimensions; +} + +/** + * A helper function for determining whether a given width or height violates + * some condition. + * + * Checks the width and height against their corresponding conditions. If + * either of the conditions fail, the errorBuilder function will be called with + * the appropriate arguments, its result will be logged to user().error, and + * validateDimensions will return false. Otherwise, validateDimensions will + * only return true. + * + * @param {(number|string)} width + * @param {(number|string)} height + * @param {!function((number|string)): boolean} widthCond + * @param {!function((number|string)): boolean} heightCond + * @param {!function(!{badDim: string, badVal: (string|number)}): string} + * errorBuilder A function that will produce an informative error message. + * @return {boolean} + */ +function validateDimensions(width, height, widthCond, heightCond, + errorBuilder) { + let badParams = null; + if (widthCond(width) && heightCond(height)) { + badParams = { + badDim: 'width and height', + badVal: width + 'x' + height, + }; + } + else if (widthCond(width)) { + badParams = { + badDim: 'width', + badVal: width, + }; + } + else if (heightCond(height)) { + badParams = { + badDim: 'height', + badVal: height, + }; + } + if (badParams) { + user().error('AMP-AD', errorBuilder(badParams)); + return false; } + return true; } diff --git a/ads/holder.js b/ads/holder.js new file mode 100644 index 000000000000..f2d605c993da --- /dev/null +++ b/ads/holder.js @@ -0,0 +1,33 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function holder(global, data) { + validateData(data, ['block'], []); + const wcl = global.context.location; + const n = navigator.userAgent; + let l = '&r' + Math.round((Math.random() * 10000000)) + '&h' + wcl.href; + if (!(n.indexOf('Safari') != -1 && n.indexOf('Chrome') == -1)) { + l += '&c1'; + } + data.queue = l; + writeScript(global,'https://i.holder.com.ua/js2/holder/ajax/ampv1.js'); +} diff --git a/ads/holder.md b/ads/holder.md new file mode 100644 index 000000000000..aba0104e7a8b --- /dev/null +++ b/ads/holder.md @@ -0,0 +1,37 @@ + + +# Holder + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact techinfo@holder.com.ua + +Supported parameters: + +- data-block diff --git a/ads/ibillboard.js b/ads/ibillboard.js new file mode 100644 index 000000000000..b42cb4c7eba3 --- /dev/null +++ b/ads/ibillboard.js @@ -0,0 +1,38 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData, validateSrcPrefix} from '../3p/3p'; + +const validHosts = [ + 'https://go.eu.bbelements.com', + 'https://go.idnes.bbelements.com', + 'https://go.goldbachpoland.bbelements.com', + 'https://go.pol.bbelements.com', + 'https://go.idmnet.bbelements.com', +]; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function ibillboard(global, data) { + + validateData(data, ['src']); + const src = data.src; + validateSrcPrefix(validHosts, src); + + writeScript(global, src); +} diff --git a/ads/ibillboard.md b/ads/ibillboard.md new file mode 100644 index 000000000000..59c8826544e0 --- /dev/null +++ b/ads/ibillboard.md @@ -0,0 +1,37 @@ + + +# iBILLBOARD AdServer + +Please visit [our website](https://www.ibillboard.com/) for more information. + +## Example + +### Ad tag with `src` + +```html + + +``` + +### Supported parameters + +- `src` + +The `src` parameter must use **https** protocol and must be from one of the +allowed iBillboard hosts. diff --git a/ads/improvedigital.js b/ads/improvedigital.js index 74a65cf21666..062fe4c9f6ea 100755 --- a/ads/improvedigital.js +++ b/ads/improvedigital.js @@ -13,19 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {writeScript, checkData, validateDataExists} from '../3p/3p'; -const adImproveField = ['width', 'height', 'placement', 'optin', 'keyvalue']; -const adImproveMandatoryField = ['placement']; +import {writeScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ -export function improvedigital(global, data) -{ - checkData(data, adImproveField); - validateDataExists(data, adImproveMandatoryField); +export function improvedigital(global, data) { + validateData(data, ['placement'], ['width', 'height', 'optin', 'keyvalue']); let url = 'https://ad.360yield.com' + '/adj?' + diff --git a/ads/inabox/inabox-host.js b/ads/inabox/inabox-host.js new file mode 100644 index 000000000000..aaa5379bbe89 --- /dev/null +++ b/ads/inabox/inabox-host.js @@ -0,0 +1,49 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Inabox host script is installed on a non-AMP host page to provide APIs for + * its embed AMP content (such as an ad created in AMP). + */ + +import '../../third_party/babel/custom-babel-helpers'; +import {dev, initLogConstructor} from '../../src/log'; +import {InaboxMessagingHost} from './inabox-messaging-host'; + +const TAG = 'inabox-host'; +run(self); + +/** + * @param win {!Window} + */ +function run(win) { + // Prevent double initialization + if (win['ampInaboxInitialized']) { + dev().info(TAG, 'Skip a 2nd attempt of initializing AMP inabox host.'); + return; + } + + win['ampInaboxInitialized'] = true; + initLogConstructor(); + + const host = new InaboxMessagingHost(win, win['ampInaboxIframes']); + + win['ampInaboxPendingMessages'].forEach(message => { + host.processMessage(message); + }); + + win.addEventListener('message', host.processMessage.bind(host)); +} diff --git a/ads/inabox/inabox-host.md b/ads/inabox/inabox-host.md new file mode 100644 index 000000000000..ea6c010af1be --- /dev/null +++ b/ads/inabox/inabox-host.md @@ -0,0 +1,17 @@ + + +This is still in development. Check [here](https://github.com/ampproject/amphtml/issues/5700) for details. \ No newline at end of file diff --git a/ads/inabox/inabox-messaging-host.js b/ads/inabox/inabox-messaging-host.js new file mode 100644 index 000000000000..93d654dc0825 --- /dev/null +++ b/ads/inabox/inabox-messaging-host.js @@ -0,0 +1,113 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {PositionObserver} from './position-observer'; +import { + serializeMessage, + deserializeMessage, + MessageType, +} from '../../src/3p-frame'; +import {dev} from '../../src/log'; + +/** @const */ +const TAG = 'InaboxMessagingHost'; + +export class InaboxMessagingHost { + + constructor(win, iframes) { + this.win_ = win; + this.iframes_ = iframes; + this.iframeMap_ = Object.create(null); + this.registeredIframeSentinels_ = Object.create(null); + this.positionObserver_ = new PositionObserver(win); + } + + /** + * Process a single post message. + * + * A valid message has to be formatted as a string starting with "amp-". The + * rest part should be a stringified JSON object of + * {type: string, sentinel: string}. The allowed types are listed in the + * REQUEST_TYPE enum. + * + * @param message {!{data: *, source: !Window, origin: string}} + * @return {boolean} true if message get successfully processed + */ + processMessage(message) { + const request = deserializeMessage(message.data); + if (!request || !request.sentinel) { + dev().fine(TAG, 'Ignored non-AMP message:', message); + return false; + } + + const iframe = + this.getFrameElement_(message.source, request.sentinel); + if (!iframe) { + dev().info(TAG, 'Ignored message from untrusted iframe:', message); + return false; + } + + if (request.type == MessageType.SEND_POSITIONS) { + // To prevent double tracking for the same requester. + if (this.registeredIframeSentinels_[request.sentinel]) { + return false; + } + this.registeredIframeSentinels_[request.sentinel] = true; + this.positionObserver_.observe(iframe, data => { + dev().fine(TAG, `Sent position data to [${request.sentinel}]`, data); + message.source./*OK*/postMessage( + serializeMessage(MessageType.POSITION, request.sentinel, data), + message.origin); + }); + return true; + } else { + dev().warn(TAG, 'Unprocessed AMP message:', message); + return false; + } + } + + /** + * Returns source window's ancestor iframe who is the direct child of host + * doc. The sentinel should be unique to the source window, and the result + * is cached using the sentinel as the key. + * + * @param source {!Window} + * @param sentinel {string} + * @returns {?HTMLIFrameElement} + * @private + */ + getFrameElement_(source, sentinel) { + if (this.iframeMap_[sentinel]) { + return this.iframeMap_[sentinel]; + } + + // Walk up on the window tree until find host's direct child window + while (source.parent !== this.win_ && source !== this.win_.top) { + source = source.parent; + } + + // Find the iframe element that hosts the message source window + for (let i = 0; i < this.iframes_.length; i++) { + const iframe = this.iframes_[i]; + if (iframe.contentWindow === source) { + // Cache the found iframe with its sentinel. + this.iframeMap_[sentinel] = iframe; + return iframe; + } + } + return null; + } +} diff --git a/ads/inabox/position-observer.js b/ads/inabox/position-observer.js new file mode 100644 index 000000000000..3f2ed122825e --- /dev/null +++ b/ads/inabox/position-observer.js @@ -0,0 +1,135 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {layoutRectLtwh, LayoutRectDef} from '../../src/layout-rect'; +import {Observable} from '../../src/observable'; +import {rateLimit} from '../../src/utils/rate-limit'; + +/** + * @typedef {{ + * viewport: !LayoutRectDef, + * target: !LayoutRectDef + * }} + */ +let PositionEntryDef; + +/** @const */ +const MIN_EVENT_INTERVAL_IN_MS = 100; + +export class PositionObserver { + + /** + * @param win {!Window} + */ + constructor(win) { + /** @private {!Window} */ + this.win_ = win; + /** @private {?Observable} */ + this.positionObservable_ = null; + /** @private {!Element} */ + this.scrollingElement_ = getScrollingElement(this.win_); + /** @private {?number} */ + this.scrollLeft_ = null; + /** @private {?number} */ + this.scrollTop_ = null; + /** @private {?LayoutRectDef} */ + this.viewportRect_ = null; + } + + /** + * Start to observe the target element's position change and trigger callback. + * TODO: maybe take DOM mutation into consideration + * @param element {!Element} + * @param callback {function(!PositionEntryDef)} + */ + observe(element, callback) { + if (!this.positionObservable_) { + this.positionObservable_ = new Observable(); + const listener = rateLimit(this.win_, () => { + this.update_(); + this.positionObservable_.fire(); + }, MIN_EVENT_INTERVAL_IN_MS); + this.update_(); + this.win_.addEventListener('scroll', listener, true); + this.win_.addEventListener('resize', listener, true); + } + // Send the 1st ping immediately + callback(this.getPositionEntry_(element)); + this.positionObservable_.add(() => { + callback(this.getPositionEntry_(element)); + }); + } + + update_() { + this.scrollLeft_ = this.scrollingElement_./*OK*/scrollLeft + || this.win_./*OK*/pageXOffset; + this.scrollTop_ = this.scrollingElement_./*OK*/scrollTop + || this.win_./*OK*/pageYOffset; + this.viewportRect_ = layoutRectLtwh( + Math.round(this.scrollLeft_), + Math.round(this.scrollTop_), + this.win_./*OK*/innerWidth, + this.win_./*OK*/innerHeight); + } + + /** + * @param element {!Element} + * @returns {!PositionEntryDef} + * @private + */ + getPositionEntry_(element) { + const b = element./*OK*/getBoundingClientRect(); + return { + viewport: /** @type {!LayoutRectDef} */(this.viewportRect_), + // relative position to host doc + target: layoutRectLtwh( + Math.round(b.left + this.scrollLeft_), + Math.round(b.top + this.scrollTop_), + Math.round(b.width), + Math.round(b.height)), + }; + } +} + +/** + * @param win {!Window} + * @returns {!Element} + */ +function getScrollingElement(win) { + const doc = win.document; + if (doc./*OK*/scrollingElement) { + return doc./*OK*/scrollingElement; + } + if (doc.body + // Due to https://bugs.webkit.org/show_bug.cgi?id=106133, WebKit + // browsers have to use `body` and NOT `documentElement` for + // scrolling purposes. This has mostly being resolved via + // `scrollingElement` property, but this branch is still necessary + // for backward compatibility purposes. + && isWebKit(win.navigator.userAgent)) { + return doc.body; + } + return doc.documentElement; +} + +/** + * Whether the current browser is based on the WebKit engine. + * @param ua {string} + * @return {boolean} + */ +function isWebKit(ua) { + return /WebKit/i.test(ua) && !/Edge/i.test(ua); +} diff --git a/ads/industrybrains.md b/ads/industrybrains.md index e1045f049491..72055f18c08a 100644 --- a/ads/industrybrains.md +++ b/ads/industrybrains.md @@ -21,8 +21,8 @@ limitations under the License. ```html ``` @@ -34,5 +34,5 @@ For semantics of configuration, please see [ad network documentation](https://ww Supported parameters: - data-cid -- width -- height +- data-width +- data-height diff --git a/ads/inmobi.js b/ads/inmobi.js new file mode 100644 index 000000000000..4b7d4b4173e5 --- /dev/null +++ b/ads/inmobi.js @@ -0,0 +1,45 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function inmobi(global, data) { + validateData(data, ['siteid','slotid'], []); + + const inmobiConf = { + siteid: data.siteid, + slot: data.slotid, + manual: true, + onError: code => { + if (code == 'nfr') { + global.context.noContentAvailable(); + document.getElementById('my-ad-slot').style./*OK*/display = 'none'; + } + }, + onSuccess: () => { + global.context.renderStart(); + }, + }; + + writeScript(global, 'https://cf.cdn.inmobi.com/ad/inmobi.secure.js', () => { + global.document.write('
'); + global._inmobi.getNewAd(document.getElementById('my-ad-slot'), inmobiConf); + }); +} diff --git a/ads/inmobi.md b/ads/inmobi.md new file mode 100644 index 000000000000..c30c0967dc09 --- /dev/null +++ b/ads/inmobi.md @@ -0,0 +1,58 @@ + + +# InMobi + +## Examples + +### For 320x50 Ad + +```html + + +``` + +### For 300x250 Ad + +```html + + +``` + +### Banner Ad SlotIds + +| AdSize | SlotId | +|---------|--------| +| 320x50 | 15 | +| 300x250 | 10 | +| 468x60 | 12 | +| 728x90 | 11 | +| 120x600 | 13 | + +### Supported parameters: + +**Required** +- data-siteid: Site Id is the InMobi property id. You can get this from InMobi dashboard. +- data-slotid: Slot Id is the ad size. + +**For Test Ads** +- To get test ads, you need to enable Diagnostic Mode from Site settings. diff --git a/ads/ix.js b/ads/ix.js new file mode 100644 index 000000000000..2a6b933a7292 --- /dev/null +++ b/ads/ix.js @@ -0,0 +1,96 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript} from '../3p/3p'; +import {doubleclick} from '../ads/google/doubleclick'; + +/** + * @param {!Window} global + * @param {!Object} data + */ + +export function ix(global, data) { + if (!('slot' in data)) { + global.CasaleArgs = data; + writeScript(global, 'https://js-sec.indexww.com/indexJTag.js'); + } else { //DFP ad request call + if (typeof data.ixId === 'undefined' || isNaN(data.ixId)) { + callDoubleclick(global, data); + return; + } else { + data.ixId = parseInt(data.ixId, 10); + } + + if (typeof data.ixSlot === 'undefined' || isNaN(data.ixSlot)) { + data.ixSlot = 1; + } else { + data.ixSlot = parseInt(data.ixSlot, 10); + } + + if (typeof global._IndexRequestData === 'undefined') { + global._IndexRequestData = {}; + global._IndexRequestData.impIDToSlotID = {}; + global._IndexRequestData.reqOptions = {}; + global._IndexRequestData.targetIDToBid = {}; + } + + global.IndexArgs = { + siteID: data.ixId, + slots: [{ + width: data.width, + height: data.height, + id: data.ixSlot, + }], + callback: (responseID, bids) => { + data.targeting = data.targeting || {}; + if (typeof bids !== 'undefined' && bids.length > 0) { + const target = bids[0].target.substring(0,2) === 'O_' ? 'IOM' : 'IPM'; + data.targeting[target] = bids[0].target.substring(2); + } + data.targeting['IX_AMP'] = '1'; + callDoubleclick(global, data); + }, + }; + + global.addEventListener('message', event => { + if (typeof event.data !== 'string' || + event.data.substring(0,11) !== 'ix-message-') { + return; + } + indexAmpRender(document, event.data.substring(11), global); + }); + + writeScript(global, 'https://js-sec.indexww.com/apl/apl6.js'); + } +} + + +function callDoubleclick(global, data) { + delete data['ixId']; + delete data['ixSlot']; + doubleclick(global, data); +} + +function indexAmpRender(doc, targetID, global) { + try { + const ad = global._IndexRequestData.targetIDToBid[targetID].pop(); + if (ad != null) { + const admDiv = document.createElement('div'); + admDiv./*OK*/innerHTML = ad; + doc.body.appendChild(admDiv); + } + } catch (e) {}; +} diff --git a/ads/ix.md b/ads/ix.md new file mode 100644 index 000000000000..b57ca33c978f --- /dev/null +++ b/ads/ix.md @@ -0,0 +1,82 @@ + + +# Index Exchange + +Index supports both direct ad tags and Header Tag style bidding using Doubleclick as the ad server. + +## Examples + +### Ad tag ### + +```html + +``` + +### Header Tag ### + +```html + +``` + +## Configuration + +For semantics of configuration, please contact your account manager at Index Exchange. + +### Ad tag ### + +__Required:__ + +- `data-ad-units` +- `data-casale-i-d` or `data-app-i-d` +- `data-version` + + +__Optional:__ + +- `data-default-ad-unit` +- `data-floor` +- `data-floor-currency` +- `data-interstitial` +- `data-position-i-d` +- `data-pub-default` +- `data-pub-passback` +- `data-referrer` +- `data-ifa` + +### Header Tag ### + +__Required:__ + +- `data-ix-id` +- `data-slot` + + +__Optional:__ + +- `data-ix-slot` + +Additional parameters including `json` will be passed through in the resulting call to DFP. For details please see the [Doubleclick documentation](https://github.com/ampproject/amphtml/blob/master/ads/google/doubleclick.md). + diff --git a/ads/kargo.js b/ads/kargo.js new file mode 100644 index 000000000000..77ff0a5a8a51 --- /dev/null +++ b/ads/kargo.js @@ -0,0 +1,70 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + loadScript, + validateData, + computeInMasterFrame, +} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function kargo(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + + validateData(data, ['site', 'slot'], ['options']); + + // Kargo AdTag url + const kargoScriptUrl = 'https://storage.cloud.kargo.com/ad/network/tag/v3/' + data.site + '.js'; + + // parse extra ad call options (optional) + let options = {}; + if (data.options != null) { + try { + options = JSON.parse(data.options); + } catch (e) {} + } + + // Add window source reference to ad options + options.source_window = global; + + computeInMasterFrame(global, 'kargo-load', function(done) { + // load AdTag in Master window + loadScript(this, kargoScriptUrl, () => { + let success = false; + if (this.Kargo != null && this.Kargo.loaded) { + success = true; + } + + done(success); + }); + }, success => { + if (success) { + const w = options.source_window; + + // Add reference to Kargo api to this window if it's not the Master window + if (!w.context.isMaster) { + w.Kargo = w.context.master.Kargo; + } + + w.Kargo.getAd(data.slot, options); + } else { + throw new Error('Kargo AdTag failed to load'); + } + }); +} diff --git a/ads/kargo.md b/ads/kargo.md new file mode 100644 index 000000000000..997b77fd85ca --- /dev/null +++ b/ads/kargo.md @@ -0,0 +1,40 @@ + + +# Kargo + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please [contact Kargo](http://www.kargo.com/contact/). + +Supported parameters: + +- data-site +- data-slot +- data-options diff --git a/ads/kixer.js b/ads/kixer.js new file mode 100644 index 000000000000..60b272c76dff --- /dev/null +++ b/ads/kixer.js @@ -0,0 +1,53 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/* global +__kxamp: false, +__kx_ad_slots: false, +__kx_ad_start: false, +*/ + +/** + * @param {!Window} global + * @param {!Object} data + */ + +export function kixer(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + validateData(data, ['adslot'], []); + + const d = global.document.createElement('div'); + d.id = '__kx_ad_' + data.adslot; + global.document.getElementById('c').appendChild(d); + + const kxload = function() { + d.removeEventListener('load', kxload, false); + if (d.childNodes.length > 0) { + global.context.renderStart(); + } else { + global.context.noContentAvailable(); + } + }; + d.addEventListener('load', kxload, false); + + loadScript(global, 'https://cdn.kixer.com/ad/load.js', () => { + __kxamp[data.adslot] = 1; + __kx_ad_slots.push(data.adslot); + __kx_ad_start(); + }); +} diff --git a/ads/kixer.md b/ads/kixer.md new file mode 100644 index 000000000000..f1f3017fd19e --- /dev/null +++ b/ads/kixer.md @@ -0,0 +1,34 @@ + + +# Kixer + +## Example + +```html + + +``` + +## Configuration + +For ad slot setup, please [contact Kixer](http://kixer.com). + +Supported parameters: + +- `data-adslot` diff --git a/ads/ligatus.js b/ads/ligatus.js new file mode 100644 index 000000000000..0ceb697902fe --- /dev/null +++ b/ads/ligatus.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateSrcPrefix} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function ligatus(global, data) { + const src = data.src; + validateSrcPrefix('https://a-ssl.ligatus.com/', src); + writeScript(global, src); +} diff --git a/ads/ligatus.md b/ads/ligatus.md new file mode 100644 index 000000000000..6051af613b62 --- /dev/null +++ b/ads/ligatus.md @@ -0,0 +1,34 @@ + + +# Ligatus + +## Example + +```html + + +``` + +## Configuration + +For further configuration details, please contact mobile@ligatus.com + +Supported parameters: + +- `src` \ No newline at end of file diff --git a/ads/loka.js b/ads/loka.js new file mode 100644 index 000000000000..f7ab8aee3186 --- /dev/null +++ b/ads/loka.js @@ -0,0 +1,38 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function loka(global, data) { + validateData(data, ['unitParams'], []); + + global.lokaParams = data; + + const container = global.document.querySelector('#c'); + container.addEventListener('lokaUnitLoaded', e => { + if (e.detail.isReady) { + global.context.renderStart(); + } else { + global.context.noContentAvailable(); + } + }); + + loadScript(global, 'https://loka-cdn.akamaized.net/scene/amp.js'); +} diff --git a/ads/loka.md b/ads/loka.md new file mode 100644 index 000000000000..cb9bc0dc28cc --- /dev/null +++ b/ads/loka.md @@ -0,0 +1,56 @@ + + +## Example + +### Mobile Video Banner + +```html + + +``` + +### Backdrop + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://lokaplatform.com/contact/ + +Supported parameters: + +* data-unit-params - JSON value + +Required JSON fields: + +* unit - Ad type +* id - Delivery ID for Ad + +Optional JSON fields: + +* tag - User specified value + +## For more design information + +Please visit [SCENE](https://lokaplatform.com/scene) and sign up as publisher to create your own placement. diff --git a/ads/mads.js b/ads/mads.js new file mode 100644 index 000000000000..7c53787bd0e8 --- /dev/null +++ b/ads/mads.js @@ -0,0 +1,29 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function mads(global, data) { + validateData(data, ['adrequest'], []); + + writeScript(global, 'https://eu2.madsone.com/js/tags.js', function() { + window.MADSAdrequest.adrequest(JSON.parse(data.adrequest)); + }); +} diff --git a/ads/mads.md b/ads/mads.md new file mode 100644 index 000000000000..d06d3611192c --- /dev/null +++ b/ads/mads.md @@ -0,0 +1,36 @@ + + +# MADS + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For semantics of configuration, please see [MADS documentation](http://wiki.mads.com/sites/javascript-ad-tags/). + +Supported parameters: + +- data-adrequest: MADS Adrequest parameters diff --git a/ads/mantis.js b/ads/mantis.js index 8d1161a48442..ba18a9571d8e 100644 --- a/ads/mantis.js +++ b/ads/mantis.js @@ -14,15 +14,14 @@ * limitations under the License. */ -import {loadScript, checkData, validateDataExists} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function mantisDisplay(global, data) { - checkData(data, ['property', 'zone']); - validateDataExists(data, ['property', 'zone']); + validateData(data, ['property', 'zone'], []); global.mantis = global.mantis || []; global.mantis.push(['display', 'load', { @@ -37,8 +36,7 @@ export function mantisDisplay(global, data) { } export function mantisRecommend(global, data) { - checkData(data, ['property', 'css']); - validateDataExists(data, ['property']); + validateData(data, ['property'], ['css']); global.mantis = global.mantis || []; global.mantis.push(['recommend', 'load', { diff --git a/ads/mediaimpact.js b/ads/mediaimpact.js index c4780b16e6fd..c280d487fe40 100644 --- a/ads/mediaimpact.js +++ b/ads/mediaimpact.js @@ -25,6 +25,21 @@ import {loadScript} from '../3p/3p'; export function mediaimpact(global, data) { global.fif = false; + /* eslint google-camelcase/google-camelcase: 0 */ + global.sas_loadHandler = function(f) { + if (f.hasAd) { + f.crea1 || (f.crea1 = { + width: 300, + height: 250, + }); + global.context.renderStart({ + width: f.crea1.width, + height: f.crea1.height, + }); + } else { + global.context.noContentAvailable(); + } + }; window.addEventListener('load', function() { asmi.sas.call(data.site + '/(' + data.page + ')', data.format, @@ -39,7 +54,7 @@ export function mediaimpact(global, data) { view: 'm', async: true, }; - loadScript(global, 'https://ec-ns.sascdn.com/diff/251/divscripte/amp.js?dom=' + window.context.location.host, () => { + loadScript(global, 'https://ec-ns.sascdn.com/diff/251/pages/amp_default.js', () => { if (!document.getElementById('sas_' + data.slot.replace('sas_',''))) { const adContainer = global.document.createElement('div'); adContainer.id = 'sas_' + data.slot.replace('sas_',''); diff --git a/ads/mediaimpact.md b/ads/mediaimpact.md index e0c8962e1ca0..1b0773687df7 100644 --- a/ads/mediaimpact.md +++ b/ads/mediaimpact.md @@ -41,3 +41,8 @@ Supported parameters: for further information regarding this implementation please contact adtechnology@axelspringer.de or visit http://www.mediaimpact.de/ + +## Optional features + +- loading placeholder for ads, see [official AMP readme]('../extensions/amp-ad/amp-ad.md#placeholder') +- noad fallback for ads, see [official AMP readme]('../extensions/amp-ad/amp-ad.md#no-ad-available') \ No newline at end of file diff --git a/ads/medianet.js b/ads/medianet.js new file mode 100644 index 000000000000..7f69af0db0eb --- /dev/null +++ b/ads/medianet.js @@ -0,0 +1,195 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData, computeInMasterFrame} from '../3p/3p'; +import {getSourceUrl} from '../src/url'; +import {doubleclick} from '../ads/google/doubleclick'; + +const mandatoryParams = ['tagtype', 'cid'], + optionalParams = [ + 'timeout', 'crid', 'misc', + 'slot', 'targeting', 'categoryExclusions', + 'tagForChildDirectedTreatment', 'cookieOptions', + 'overrideWidth', 'overrideHeight', 'loadingStrategy', + 'consentNotificationId', 'useSameDomainRenderingUntilDeprecated', + 'experimentId', 'multiSize', 'multiSizeValidation', + ], + dfpParams = [ + 'slot', 'targeting', 'categoryExclusions', + 'tagForChildDirectedTreatment', 'cookieOptions', + 'overrideWidth', 'overrideHeight', 'loadingStrategy', + 'consentNotificationId', 'useSameDomainRenderingUntilDeprecated', + 'experimentId', 'multiSize', 'multiSizeValidation', + ], + dfpDefaultTimeout = 1000; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function medianet(global, data) { + validateData(data, mandatoryParams, optionalParams); + + const publisherUrl = global.context.canonicalUrl || + getSourceUrl(global.context.location.href), + referrerUrl = global.context.referrer; + + if (data.tagtype === 'headerbidder') { //parameter tagtype is used to identify the product the publisher is using. Going ahead we plan to support more product types. + loadHBTag(global, data, publisherUrl, referrerUrl); + } else if (data.tagtype === 'cm' && data.crid) { + loadCMTag(global, data, publisherUrl, referrerUrl); + } else { + global.context.noContentAvailable(); + } +} + +/** + * @param {!Window} global + * @param {!Object} data + * @param {!string} publisherUrl + * @param {?string} referrerUrl + */ +function loadCMTag(global, data, publisherUrl, referrerUrl) { + /*eslint "google-camelcase/google-camelcase": 0*/ + function setMacro(type) { + if (!type) { + return; + } + const name = 'medianet_' + type; + if (data.hasOwnProperty(type)) { + global[name] = data[type]; + } + } + + function setAdditionalData() { + data.requrl = publisherUrl || ''; + data.refurl = referrerUrl || ''; + data.versionId = '211213'; + + setMacro('width'); + setMacro('height'); + setMacro('crid'); + setMacro('requrl'); + setMacro('refurl'); + setMacro('versionId'); + setMacro('misc'); + } + + function setCallbacks() { + global._mNAmp = { + renderStartCb: opt_data => { + global.context.renderStart(opt_data); + }, + reportRenderedEntityIdentifierCb: ampId => { + global.context.reportRenderedEntityIdentifier(ampId); + }, + noContentAvailableCb: () => { + global.context.noContentAvailable(); + }, + }; + } + + function loadScript() { + let url = 'https://contextual.media.net/ampnmedianet.js?'; + url += 'cid=' + encodeURIComponent(data.cid); + url += '&https=1'; + url += '&requrl=' + encodeURIComponent(data.requrl); + url += '&refurl=' + encodeURIComponent(data.refurl); + writeScript(global, url); + } + + function init() { + setAdditionalData(); + setCallbacks(); + loadScript(); + } + + init(); +} + +/** + * @param {!Window} global + * @param {!Object} data + * @param {!string} publisherUrl + * @param {?string} referrerUrl + */ +function loadHBTag(global, data, publisherUrl, referrerUrl) { + function deleteUnexpectedDoubleclickParams() { + const allParams = mandatoryParams.concat(optionalParams); + let currentParam = ''; + for (let i = 0; i < allParams.length; i++) { + currentParam = allParams[i]; + if (dfpParams.indexOf(currentParam) === -1 && data[currentParam]) { + delete data[currentParam]; + } + } + } + + let isDoubleClickCalled = false; + + function loadDFP() { + if (isDoubleClickCalled) { + return; + } + isDoubleClickCalled = true; + + global.advBidxc = global.context.master.advBidxc; + if (global.advBidxc && typeof global.advBidxc.renderAmpAd === 'function') { + global.addEventListener('message', event => { + global.advBidxc.renderAmpAd(event, global); + }); + } + + data.targeting = data.targeting || {}; + + if (global.advBidxc && + typeof global.advBidxc.setAmpTargeting === 'function') { + global.advBidxc.setAmpTargeting(global, data); + } + deleteUnexpectedDoubleclickParams(); + doubleclick(global, data); + } + + function mnetHBHandle() { + global.advBidxc = global.context.master.advBidxc; + if (global.advBidxc && + typeof global.advBidxc.registerAmpSlot === 'function') { + global.advBidxc.registerAmpSlot({ + cb: loadDFP, + data, + winObj: global, + }); + } + } + + global.setTimeout(() => { + loadDFP(); + }, data.timeout || dfpDefaultTimeout); + + computeInMasterFrame(global, 'medianet-hb-load', done => { + /*eslint "google-camelcase/google-camelcase": 0*/ + global.advBidxc_requrl = publisherUrl; + global.advBidxc_refurl = referrerUrl; + global.advBidxc = { + registerAmpSlot: () => {}, + setAmpTargeting: () => {}, + renderAmpAd: () => {}, + }; + writeScript(global, 'https://contextual.media.net/bidexchange.js?https=1&=1&cid=' + encodeURIComponent(data.cid), () => { + done(null); + }); + }, mnetHBHandle); +} diff --git a/ads/medianet.md b/ads/medianet.md new file mode 100644 index 000000000000..a77249d75dc0 --- /dev/null +++ b/ads/medianet.md @@ -0,0 +1,98 @@ + + +# Media.net + +Media.net adapter supports the integration of both, its Contextual Monetization solution and the Header Bidding solution. +The example listed below states the configuration and the implementation related details. + + +## Example + + +### Media.net Contextual Monetization + +``` html + + +``` + +### Media.net Header Bidder + +``` html + + +``` + +## Configuration + +### Dimensions + +The ad size depends on the `width` and `height` attributes specified in the `amp-ad` tag. The `amp-ad` component requires the following mandatory HTML attributes to be added before parsing the Ad. + + * `width` + * `height` + * `type = "medianet"` + +If you have questions, please feel free to reach out to your Media.net contact. + + +## Supported Parameters + +###Media.net Contextual Monetization + +**Mandatory Parameters** + +* `data-tagtype` - This parameter represents the product the publisher is using; It should be **`cm`** for our **Contextual Monetization solution** +* `data-cid` - Represents the unique customer identifier +* `data-crid` - Media.net Ad unit + +**Optional Parameters** + +* `data-misc` - Accepts a json value & used to send additional data + + + +###Media.net Header Bidder + +**Mandatory Parameters** + +* `data-tagtype` - This parameter represents the product the publisher is using; It should be **`headerbidder`** for our **Header Bidding solution** +* `data-cid` - Represents the unique customer identifier +* `data-slot` - Ad unit as specified in DFP + +**Some of the parameters supported via Json attribute (DFP Parameters)** + +* `targeting` +* `categoryExclusions` + +For an exhaustive list of updated parameters supported by DoubleClick refer to the guide - [here](google/doubleclick.md). + + +## Support +For further queries, please feel free to reach out to your contact at Media.net. + +Otherwise you can write to our support team: +Email: **pubsupport@media.net** diff --git a/ads/mediavine.js b/ads/mediavine.js new file mode 100644 index 000000000000..5ea3218d9121 --- /dev/null +++ b/ads/mediavine.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function mediavine(global, data) { + validateData(data, ['site'], ['sizes']); + loadScript(global, 'https://scripts.mediavine.com/amp/' + + encodeURIComponent(data.site) + '.js'); +} diff --git a/ads/mediavine.md b/ads/mediavine.md new file mode 100644 index 000000000000..82b071b3c13d --- /dev/null +++ b/ads/mediavine.md @@ -0,0 +1,45 @@ + + +# Mediavine + +## Examples + +```html + + + + + + +``` + +## Configuration + +__Required:__ + +`data-site` - The site's unique name this ad will be served on. This is the same name from your Mediavine script wrapper. + +__Optional:__ + +`data-sizes` - Comma separated list of ad sizes this placement should support. Will cause iframe resize if different from hard-coded width and height. + +Each site must be approved and signed up with [Mediavine](http://www.mediavine.com) prior to launch. The site name will be the same as name in the Mediavine script wrapper. The site name `amp-project` can be used for testing and will serve placeholder ads. diff --git a/ads/meg.js b/ads/meg.js new file mode 100644 index 000000000000..14071236e2bc --- /dev/null +++ b/ads/meg.js @@ -0,0 +1,43 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function meg(global, data) { + validateData(data, ['code']); + const code = data.code; + const lang = global.encodeURIComponent(global.navigator.language); + const ref = global.encodeURIComponent(global.context.referrer); + const params = ['lang=' + lang, 'ref=' + ref].join('&'); + const url = 'https://apps.meg.com/embedjs/' + code + '?' + params; + global._megAdsLoaderCallbacks = { + onSuccess: () => { + global.context.renderStart(); + }, + onError: () => { + global.context.noContentAvailable(); + }, + }; + loadScript(global, url, () => { + // Meg has been loaded + }, () => { + // Cannot load meg embed.js + global.context.noContentAvailable(); + }); +} diff --git a/ads/meg.md b/ads/meg.md new file mode 100644 index 000000000000..bce77ece8fb7 --- /dev/null +++ b/ads/meg.md @@ -0,0 +1,38 @@ + + +# Meg + +Please visit our [website](https://www.meg.com) for more information about us. + +## Example + +```html + + +``` + +## Configuration + +Supported parameters: + +- data-code: unique ad code given by Meg.com + +for further information regarding this implementation please contact support@meg.com \ No newline at end of file diff --git a/ads/microad.js b/ads/microad.js index 782246a3d60c..616e86bc6536 100644 --- a/ads/microad.js +++ b/ads/microad.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {loadScript, validateDataExists, checkData} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /* global MicroAd: false */ @@ -23,8 +23,9 @@ import {loadScript, validateDataExists, checkData} from '../3p/3p'; * @param {!Object} data */ export function microad(global, data) { - checkData(data, ['spot']); - validateDataExists(data, ['spot']); + // TODO: check mandatory fields + validateData(data, [], ['spot', 'url', 'referrer', 'ifa', 'appid', 'geo']); + global.document.getElementById('c').setAttribute('id', data.spot); loadScript(global, 'https://j.microad.net/js/camp.js', () => { MicroAd.Compass.showAd(data); diff --git a/ads/microad.md b/ads/microad.md index e9cd41fb52b5..c7fa1d8b012f 100644 --- a/ads/microad.md +++ b/ads/microad.md @@ -23,7 +23,10 @@ limitations under the License. type="microad" data-spot="b4bf837c5ddd69758d5ac924d04cf502" data-url="${COMPASS_EXT_URL}" - data-referrer="${COMPASS_EXT_REF}"> + data-referrer="${COMPASS_EXT_REF}" + data-ifa="${COMPASS_EXT_IFA}" + data-appid="${COMPASS_EXT_APPID}" + data-geo="${COMPASS_EXT_GEO}"> ``` @@ -38,3 +41,6 @@ Supported parameters: - data-spot - data-url - data-referrer +- data-ifa +- data-appid +- data-geo diff --git a/ads/mixpo.js b/ads/mixpo.js new file mode 100644 index 000000000000..999cab05d48a --- /dev/null +++ b/ads/mixpo.js @@ -0,0 +1,52 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript,validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function mixpo(global, data) { + validateData(data, [ + 'guid', + 'subdomain', + ]); + + const g = global, + cdnSubdomain = (data.subdomain == 'www') ? 'cdn' : data.subdomain + '-cdn', + url = data.loader || `https://${cdnSubdomain}.mixpo.com/js/loader.js`; + + g.mixpoAd = { + amp: true, + noflash: true, + width: data.width, + height: data.height, + guid: data.guid, + subdomain: data.subdomain, + embedv: data.embedv, + clicktag: data.clicktag, + customTarget: data.customtarget, + dynClickthrough: data.dynclickthrough, + viewTracking: data.viewtracking, + customCSS: data.customcss, + local: data.local, + enableMRAID: data.enablemraid, + jsPlayer: data.jsplayer, + }; + + writeScript(g, url); +} diff --git a/ads/mixpo.md b/ads/mixpo.md new file mode 100644 index 000000000000..f5d662b2e54a --- /dev/null +++ b/ads/mixpo.md @@ -0,0 +1,49 @@ + + +# Mixpo + +## Example + +``` + + +``` + +## Configuration + +For configuration and implementation details, please contact [Mixpo support](support@mixpo.com). + +Required Attributes: + +- data-guid +- data-subdomain + +Additional Supported Attributes: + +- data-embedv +- data-clicktag +- data-customtarget +- data-dynclickthrough +- data-viewtracking +- data-customcss +- data-local +- data-enablemraid +- data-jsplayer +- data-loader diff --git a/ads/nativo.js b/ads/nativo.js new file mode 100644 index 000000000000..f8e9a4cefec7 --- /dev/null +++ b/ads/nativo.js @@ -0,0 +1,123 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS-IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {loadScript} from '../3p/3p'; +export function nativo(global, data) { + let ntvAd; + (function(ntvAd, global, data) { + global + .history + .replaceState(null, + null, + location.pathname + location.hash.replace(/({).*(})/, '')); + // Private + let delayedAdLoad = false; + let percentageOfadViewed; + const loc = global.context.location; + function isValidDelayTime(delay) { + return ((typeof delay != 'undefined' + && !isNaN(delay) + && parseInt(delay,10) >= 0)); + } + function isDelayedTimeStart(data) { + return (isValidDelayTime(data.delayByTime) + && ('delay' in data) + && !('delayByView' in data)); + } + function isDelayedViewStart(data) { + return (isValidDelayTime(data.delayByTime) + && ('delayByView' in data)); + } + function loadAdWhenViewed() { + const g = global; + global.context.observeIntersection(function(positions) { + const coordinates = getLastPositionCoordinates(positions); + if (typeof coordinates.rootBounds != 'undefined' + && (coordinates + .intersectionRect + .top == (coordinates + .rootBounds.top + coordinates + .boundingClientRect + .y))) { + if (isDelayedViewStart(data) && !delayedAdLoad) { + g.PostRelease.Start(); + delayedAdLoad = true; + } + } + }); + } + function loadAdWhenTimedout() { + const g = global; + setTimeout(function() { + g.PostRelease.Start(); + delayedAdLoad = true; + }, parseInt(data.delayByTime, 10)); + } + function getLastPositionCoordinates(positions) { + return positions[positions.length - 1]; + } + function setPercentageOfadViewed(percentage) { + percentageOfadViewed = percentage; + } + // Used to track ad during scrolling event and trigger checkIsAdVisible method on PostRelease instance + function viewabilityConfiguration(positions) { + const coordinates = getLastPositionCoordinates(positions); + setPercentageOfadViewed( + (((coordinates.intersectionRect + .height * 100) / coordinates + .boundingClientRect + .height) / 100)); + global.PostRelease.checkIsAdVisible(); + } + // Public + ntvAd.getPercentageOfadViewed = function() { + return percentageOfadViewed; + }; + ntvAd.getScriptURL = function() { + return 'https://s.ntv.io/serve/load.js'; + }; + // Configuration setup is based on the parameters/attributes associated with the amp-ad node + ntvAd.setupAd = function() { + global._prx = [['cfg.Amp']]; + global._prx.push(['cfg.RequestUrl', data['requestUrl'] || loc.href]); + for (const key in data) { + switch (key) { + case 'premium': global._prx.push(['cfg.SetUserPremium']); break; + case 'debug': global._prx.push(['cfg.Debug']); break; + case 'delay': if (isValidDelayTime(data.delayByTime)) { + global._prx.push(['cfg.SetNoAutoStart']); + } break; + } + } + }; + // Used to Delay Start and Initalize Tracking. This is a callback AMP will use once script is loaded + ntvAd.Start = function() { + if (isDelayedTimeStart(data)) { + loadAdWhenTimedout(); + } else if (isDelayedViewStart(data)) { + loadAdWhenViewed(); + } + global.PostRelease.checkAmpViewability = function() { + return ntvAd.getPercentageOfadViewed(); + }; + // ADD TRACKING HANDLER TO OBSERVER + global.context.observeIntersection(viewabilityConfiguration); + }; + })(ntvAd || (ntvAd = {}), global, data); + // Setup Configurations + ntvAd.setupAd(); + // Load Nativo Script + loadScript(global, ntvAd.getScriptURL(), ntvAd.Start); +} diff --git a/ads/nativo.md b/ads/nativo.md new file mode 100644 index 000000000000..de219694421c --- /dev/null +++ b/ads/nativo.md @@ -0,0 +1,33 @@ + + +# Nativo + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please [contact Nativo](http://www.nativo.net/#contact-us). + +- data-premium: (optional) Will switch to premium diff --git a/ads/nend.js b/ads/nend.js index 1ccad450549a..536299cbcca9 100644 --- a/ads/nend.js +++ b/ads/nend.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {writeScript, checkData, validateDataExists} from '../3p/3p'; +import {writeScript, validateData} from '../3p/3p'; const nendFields = ['nend_params']; @@ -23,8 +23,7 @@ const nendFields = ['nend_params']; * @param {!Object} data */ export function nend(global, data) { - checkData(data, nendFields); - validateDataExists(data, nendFields); + validateData(data, nendFields, []); global.nendParam = data; writeScript(global, 'https://js1.nend.net/js/amp.js'); diff --git a/ads/nokta.js b/ads/nokta.js new file mode 100644 index 000000000000..8b75c6f715d0 --- /dev/null +++ b/ads/nokta.js @@ -0,0 +1,31 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function nokta(global, data) { + validateData(data, ['category', 'site', 'zone']); + global.category = data.category; + global.site = data.site; + global.zone = data.zone; + global.iwidth = data.width; + global.iheight = data.height; + writeScript(global, 'https://static.virgul.com/theme/mockups/noktaamp/ampjs.js'); +} diff --git a/ads/nokta.md b/ads/nokta.md new file mode 100644 index 000000000000..3fc6b6cf4ea4 --- /dev/null +++ b/ads/nokta.md @@ -0,0 +1,43 @@ + + +# Nokta + +## Example + +```html + +
+
+
+``` + + +## Configuration + +For semantics of configuration, please see ad network documentation. + +### Pubmine Required parameters + +| Parameter | Description | +|:------------- |:-------------| +| **`data-category`** | Site category for ad unit | +| **`data-site`** | Site descriptor for ad | +| **`data-zone`** | Zone id to show related ad | diff --git a/ads/openadstream.js b/ads/openadstream.js index 196c974ca42f..53209047ffe5 100644 --- a/ads/openadstream.js +++ b/ads/openadstream.js @@ -14,22 +14,21 @@ * limitations under the License. */ -import {checkData, validateDataExists, writeScript} from '../3p/3p'; - +import {validateData, writeScript} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function openadstream(global, data) { - //all available fields - checkData(data, ['adhost', 'sitepage', 'pos', 'query']); - //required fields - validateDataExists(data, ['adhost', 'sitepage', 'pos']); - let url = 'https://' + encodeURIComponent(data.adhost) + '/3/' + data.sitepage + '/1' + String(Math.random()).substring(2, 11) + '@' + data.pos; + validateData(data, ['adhost', 'sitepage', 'pos'], ['query']); + + let url = 'https://' + encodeURIComponent(data.adhost) + + '/3/' + data.sitepage + + '/1' + String(Math.random()).substring(2, 11) + '@' + data.pos; + if (data.query) { url = url + '?' + data.query; } writeScript(global, url); - } diff --git a/ads/openx.js b/ads/openx.js index 91c439ef777a..d8fc9c613df1 100644 --- a/ads/openx.js +++ b/ads/openx.js @@ -14,9 +14,27 @@ * limitations under the License. */ -import {loadScript, writeScript, checkData} from '../3p/3p'; +import {loadScript, writeScript, validateData} from '../3p/3p'; import {doubleclick} from '../ads/google/doubleclick'; +const hasOwnProperty = Object.prototype.hasOwnProperty; + +/** + * Sort of like Object.assign. + * @param {!Object} target + * @param {!Object} source + * @return {!Object} + */ +function assign(target, source) { + for (const prop in source) { + if (hasOwnProperty.call(source, prop)) { + target[prop] = source[prop]; + } + } + + return target; +} + /* global OX: false */ /** @@ -25,9 +43,10 @@ import {doubleclick} from '../ads/google/doubleclick'; */ export function openx(global, data) { const openxData = ['host', 'nc', 'auid', 'dfpSlot', 'dfp']; - const dfpData = Object.assign({}, data); // Make a copy for dfp. - checkData(data, openxData); + const dfpData = assign({}, data); // Make a copy for dfp. + // TODO: check mandatory fields + validateData(data, [], openxData); // Consolidate Doubleclick inputs for forwarding - // conversion rules are explained in openx.md. if (data.dfpSlot) { @@ -46,7 +65,7 @@ export function openx(global, data) { // Promote the whole 'dfp' object. if ('dfp' in data) { - Object.assign(dfpData, dfpData.dfp); + assign(dfpData, dfpData.dfp); delete dfpData['dfp']; } } @@ -54,13 +73,14 @@ export function openx(global, data) { // Decide how to render. if (data.host) { let jssdk = `https://${data.host}/mw/1.0/jstag`; - if (data.nc && data.dfpSlot) { // Use DFP Bidder + + if (data.nc && data.dfpSlot) { jssdk += '?nc=' + encodeURIComponent(data.nc); - writeScript(global, jssdk, () => { - /*eslint "google-camelcase/google-camelcase": 0*/ - OX._requestArgs['amp'] = 1; - doubleclick(global, dfpData); - }); + if (data.auid) { + advanceImplementation(global, jssdk, dfpData, data); + } else { + standardImplementation(global, jssdk, dfpData); + } } else if (data.auid) { // Just show an ad. global.OX_cmds = [ () => { @@ -68,10 +88,10 @@ export function openx(global, data) { const oxAnchor = global.document.createElement('div'); global.document.body.appendChild(oxAnchor); /*eslint "google-camelcase/google-camelcase": 0*/ - OX._requestArgs['amp'] = 1; oxRequest.addAdUnit(data.auid); oxRequest.setAdSizes([data.width + 'x' + data.height]); oxRequest.getOrCreateAdUnit(data.auid).set('anchor', oxAnchor); + global.context.renderStart(); oxRequest.load(); }, ]; @@ -81,3 +101,28 @@ export function openx(global, data) { doubleclick(global, dfpData); } } + +function standardImplementation(global, jssdk, dfpData) { + writeScript(global, jssdk, () => { + /*eslint "google-camelcase/google-camelcase": 0*/ + doubleclick(global, dfpData); + }); +} + +function advanceImplementation(global, jssdk, dfpData, data) { + const size = [data.width + 'x' + data.height]; + global.OX_bidder_options = { + bidderType: 'hb_amp', + callback: () => { + const priceMap = global.oxhbjs && global.oxhbjs.getPriceMap(); + const slot = priceMap && priceMap['c']; + const targeting = slot ? + `${slot.size}_${slot.price},hb-bid-${slot.bid_id}` : 'none_t'; + dfpData.targeting = dfpData.targeting || {}; + assign(dfpData.targeting, {oxb: targeting}); + doubleclick(global, dfpData); + }, + }; + global.OX_bidder_ads = [[data.dfpSlot, size, 'c']]; + loadScript(global, jssdk); +} diff --git a/ads/plista.js b/ads/plista.js index 8ac52c931859..d7ffaf0521ca 100644 --- a/ads/plista.js +++ b/ads/plista.js @@ -14,14 +14,15 @@ * limitations under the License. */ -import {loadScript, checkData} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function plista(global, data) { - checkData(data, [ + // TODO: check mandatory fields + validateData(data, [], [ 'publickey', 'widgetname', 'urlprefix', 'item', 'geo', 'categories', 'countrycode', ]); diff --git a/ads/popin.js b/ads/popin.js new file mode 100644 index 000000000000..d5ff9199ce30 --- /dev/null +++ b/ads/popin.js @@ -0,0 +1,33 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData, loadScript} from '../3p/3p'; + +/** +* @param {!Window} global +* @param {!Object} data +*/ +export function popin(global, data) { + validateData(data, ['mediaid']); + + const d = global.document.createElement('div'); + d.id = '_popIn_amp_recommend'; + global.document.getElementById('c').appendChild(d); + + const url = 'https://api.popin.cc/searchbox/' + encodeURIComponent(data['mediaid']) + '.js'; + + loadScript(global, url); +} diff --git a/ads/popin.md b/ads/popin.md new file mode 100644 index 000000000000..6a3e79ad3825 --- /dev/null +++ b/ads/popin.md @@ -0,0 +1,41 @@ + + + +# popIn + +## Display + +```html + +``` + +### Configuration + +For configuration details, please contact http://www.popin.cc/discovery/#contact + +Supported parameters: + +- height +- width +- data-mediaid diff --git a/ads/pubmine.js b/ads/pubmine.js new file mode 100644 index 000000000000..77cadcac46dd --- /dev/null +++ b/ads/pubmine.js @@ -0,0 +1,55 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; +import {getSourceOrigin, getSourceUrl} from '../src/url'; + +const pubmineOptional = ['adsafe', 'section', 'wordads'], + pubmineRequired = ['siteid'], + pubmineURL = 'https://s.pubmine.com/head.js'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function pubmine(global, data) { + validateData(data, pubmineRequired, pubmineOptional); + + global._ipw_custom = { // eslint-disable-line google-camelcase/google-camelcase + adSafe: 'adsafe' in data ? data.adsafe : '0', + amznPay: [], + domain: getSourceOrigin(global.context.location.href), + pageURL: getSourceUrl(global.context.location.href), + wordAds: 'wordads' in data ? data.wordads : '0', + renderStartCallback: () => global.context.renderStart(), + }; + writeScript(global, pubmineURL); + + const o = { + sectionId: data['siteid'] + ('section' in data ? data.section : '1'), + height: data.height, + width: data.width, + }, + wr = global.document.write; + + wr.call(global.document, + `` + ); +} diff --git a/ads/pubmine.md b/ads/pubmine.md new file mode 100644 index 000000000000..033ebf5d6609 --- /dev/null +++ b/ads/pubmine.md @@ -0,0 +1,58 @@ + + +# Pubmine + +## Example + +### Basic + +```html + + +``` + +### With all attributes + +```html + + +``` + +## Configuration + +For further Pubmine configuration information, please [contact us](https://wordpress.com/help/contact). + +### Pubmine Required parameters + +| Parameter | Description | +|:------------- |:-------------| +| **`data-siteid`** | Pubmine publisher site number | + +### Pubmine Optional parameters + +| Parameter | Description | +|:------------- |:-------------| +| **`data-adsafe`** | Flag for adsafe content | +| **`data-section`** | Pubmine slot identifier | +| **`data-wordads`** | Flag for WordAds or other | diff --git a/ads/pulsepoint.js b/ads/pulsepoint.js new file mode 100644 index 000000000000..174f03efc6ca --- /dev/null +++ b/ads/pulsepoint.js @@ -0,0 +1,74 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, loadScript, validateData} from '../3p/3p'; +import {doubleclick} from '../ads/google/doubleclick'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function pulsepoint(global, data) { + // TODO: check mandatory fields + validateData(data, [], [ + 'pid', 'tagid', 'tagtype', 'slot', 'timeout', + ]); + if (data.tagtype === 'hb') { + headerBidding(global, data); + } else { + tag(global, data); + } +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function tag(global, data) { + writeScript(global, 'https://tag.contextweb.com/getjs.aspx?action=VIEWAD' + + '&cwpid=' + encodeURIComponent(data.pid) + + '&cwtagid=' + encodeURIComponent(data.tagid) + + '&cwadformat=' + encodeURIComponent(data.width + 'X' + data.height)); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function headerBidding(global, data) { + loadScript(global, 'https://ads.contextweb.com/ht.js', () => { + const hbConfig = { + timeout: data.timeout || 1000, + slots: [{ + cp: data.pid, + ct: data.tagid, + cf: data.width + 'x' + data.height, + placement: data.slot, + elementId: 'c', + }], + done: function(targeting) { + doubleclick(global, { + width: data.width, + height: data.height, + slot: data.slot, + targeting: targeting[data.slot], + }); + }, + }; + new window.PulsePointHeaderTag(hbConfig).init(); + }); +} + diff --git a/ads/pulsepoint.md b/ads/pulsepoint.md new file mode 100644 index 000000000000..0cc87dd3a411 --- /dev/null +++ b/ads/pulsepoint.md @@ -0,0 +1,54 @@ + + +# PulsePoint + +## Tag Example + +```html + + +``` + +## Header Bidding Tag Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see [ad network documentation](https://www.pulsepoint.com). + +Supported parameters: + +- pid - Publisher Id +- tagid - Tag Id +- tagtype - Tag Type. "hb" represents Header bidding, otherwise treated as regular tag. +- size - Ad Size represented 'widthxheight' +- slot - DFP slot id, required for header bidding tag +- timeout - optional timeout for header bidding, default is 1000ms. \ No newline at end of file diff --git a/ads/purch.js b/ads/purch.js new file mode 100644 index 000000000000..dbff78401616 --- /dev/null +++ b/ads/purch.js @@ -0,0 +1,34 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + writeScript, + validateData, + validateSrcPrefix} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function purch(global, data) { + validateData(data, [], + ['pid', 'divid']); + global.data = data; + + const adsrc = 'https://ramp.purch.com/serve/creative_amp.js'; + validateSrcPrefix('https:', adsrc); + writeScript(global, adsrc); +} diff --git a/ads/purch.md b/ads/purch.md new file mode 100644 index 000000000000..b2240138f5c1 --- /dev/null +++ b/ads/purch.md @@ -0,0 +1,34 @@ + + +# Purch + +## Example + +```html + + +``` + +## Configuration + +Supported parameters: + +- `data-pid` placement id +- `data-divid` div id of unit \ No newline at end of file diff --git a/ads/relap.js b/ads/relap.js new file mode 100644 index 000000000000..252e832c35ed --- /dev/null +++ b/ads/relap.js @@ -0,0 +1,40 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function relap(global, data) { + validateData(data, [], ['token', 'url', 'anchorid']); + + window.relapV6WidgetReady = function() { + window.context.renderStart(); + }; + + window.relapV6WidgetNoSimilarPages = function() { + window.context.noContentAvailable(); + }; + + const url = `https://relap.io/api/v6/head.js?token=${encodeURIComponent(data['token'])}&url=${encodeURIComponent(data['url'])}`; + loadScript(global, url); + + const anchorEl = global.document.createElement('div'); + anchorEl.id = data['anchorid']; + global.document.getElementById('c').appendChild(anchorEl); +} diff --git a/ads/relap.md b/ads/relap.md new file mode 100644 index 000000000000..397662f385b6 --- /dev/null +++ b/ads/relap.md @@ -0,0 +1,40 @@ + + +# Relap + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see ad network documentation. Currently supported all anchor based widgets. + +Supported parameters: + +- data-token +- data-url +- data-anchorid diff --git a/ads/revcontent.js b/ads/revcontent.js index 594e3896dfd0..4d4d7033f36f 100644 --- a/ads/revcontent.js +++ b/ads/revcontent.js @@ -14,15 +14,42 @@ * limitations under the License. */ -import {writeScript, checkData} from '../3p/3p'; +import {writeScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function revcontent(global, data) { - checkData(data, ['id', 'width', 'height', 'endpoint', 'ssl', 'wrapper']); - const revampServiceUrl = 'https://labs-cdn.revcontent.com/build/amphtml/revcontent.amp.min.js'; + const endpoint = 'https://labs-cdn.revcontent.com/build/amphtml/revcontent.amp.min.js'; + const required = [ + 'id', + 'width', + 'height', + 'wrapper', + ]; + const optional = [ + 'api', + 'key', + 'ssl', + 'adxw', + 'adxh', + 'rows', + 'cols', + 'domain', + 'source', + 'testing', + 'endpoint', + 'publisher', + 'branding', + 'font', + 'css', + 'sizer', + 'debug', + 'ampcreative', + ]; + + validateData(data, required, optional); global.data = data; - writeScript(window, revampServiceUrl); + writeScript(window, endpoint); } diff --git a/ads/revcontent.md b/ads/revcontent.md index 501b6c42f9b0..2e1db8c01973 100644 --- a/ads/revcontent.md +++ b/ads/revcontent.md @@ -19,11 +19,24 @@ limitations under the License. ## Example ```html - + +
Loading ...
``` @@ -37,4 +50,8 @@ Supported parameters: - data-wrapper - data-endpoint - data-ssl -- data-testing \ No newline at end of file +- data-testing + +## Auto-sizing of Ads + +Revcontent's AMP service will be updated to support resizing of ads for improved rendering, no additional tag parameters are required at this time. \ No newline at end of file diff --git a/ads/rubicon.js b/ads/rubicon.js index c2bc716f902b..21cc7f0f7606 100644 --- a/ads/rubicon.js +++ b/ads/rubicon.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {writeScript, loadScript, checkData} from '../3p/3p'; +import {writeScript, loadScript, validateData} from '../3p/3p'; import {getSourceUrl} from '../src/url'; import {doubleclick} from '../ads/google/doubleclick'; @@ -25,7 +25,8 @@ import {doubleclick} from '../ads/google/doubleclick'; * @param {!Object} data */ export function rubicon(global, data) { - checkData(data, [ + // TODO: check mandatory fields + validateData(data, [], [ 'slot', 'targeting', 'categoryExclusions', 'tagForChildDirectedTreatment', 'cookieOptions', 'overrideWidth', 'overrideHeight', 'loadingStrategy', @@ -65,7 +66,7 @@ function fastLane(global, data) { } } } - }; + } let gptran = false; function gptrun() { @@ -81,32 +82,31 @@ function fastLane(global, data) { ASTargeting = ASTargeting[i].values; } } - if (!data.targeting) { data.targeting = {}; }; + if (!data.targeting) { data.targeting = {}; } data.targeting['rpfl_' + data.account] = ASTargeting; data.targeting['rpfl_elemid'] = 'c'; - if (data['method']) { delete data['method']; }; - if (data['account']) { delete data['account']; }; - if (data['pos']) { delete data['pos']; }; - if (data['kw']) { delete data['kw']; }; - if (data['visitor']) { delete data['visitor']; }; - if (data['inventory']) { delete data['inventory']; }; + if (data['method']) { delete data['method']; } + if (data['account']) { delete data['account']; } + if (data['pos']) { delete data['pos']; } + if (data['kw']) { delete data['kw']; } + if (data['visitor']) { delete data['visitor']; } + if (data['inventory']) { delete data['inventory']; } doubleclick(global, data); - }; + } loadScript(global, 'https://ads.rubiconproject.com/header/' + encodeURIComponent(data.account) + '.js', () => { global.rubicontag.cmd.push(() => { const rubicontag = global.rubicontag; const slot = rubicontag.defineSlot(data.slot, dimensions, 'c'); - if (data.pos) { slot.setPosition(data.pos); }; - if (data.kw) { rubicontag.addKW(data.kw); }; - if (data.visitor) { setFPD('V', data.visitor); }; - if (data.inventory) { setFPD('I', data.inventory); }; + if (data.pos) { slot.setPosition(data.pos); } + if (data.kw) { rubicontag.addKW(data.kw); } + if (data.visitor) { setFPD('V', data.visitor); } + if (data.inventory) { setFPD('I', data.inventory); } rubicontag.setUrl(getSourceUrl(context.location.href)); rubicontag.setIntegration('amp'); rubicontag.run(gptrun, 1000); - }); }); } @@ -129,6 +129,6 @@ function smartTag(global, data) { global.rp_amp = 'st'; global.rp_callback = data.callback; /* eslint-enable */ - writeScript(global, 'https://ads.rubiconproject.com/ad/' + encodeURIComponent(data.account) + '.js'); + writeScript(global, 'https://ads.rubiconproject.com/ad/' + + encodeURIComponent(data.account) + '.js'); } - diff --git a/ads/sharethrough.js b/ads/sharethrough.js index 8c15d0c5d0ab..70e4b6158e48 100644 --- a/ads/sharethrough.js +++ b/ads/sharethrough.js @@ -14,15 +14,14 @@ * limitations under the License. */ -import {writeScript, checkData, validateDataExists} from '../3p/3p'; +import {writeScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function sharethrough(global, data) { - checkData(data, ['pkey']); - validateDataExists(data, ['pkey']); + validateData(data, ['pkey'], []); global.pkey = data.pkey; writeScript(global, 'https://native.sharethrough.com/iframe/amp.js'); } diff --git a/ads/sklik.js b/ads/sklik.js new file mode 100644 index 000000000000..c7809e5b7951 --- /dev/null +++ b/ads/sklik.js @@ -0,0 +1,38 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript} from '../3p/3p'; + +/* global sklikProvider: false */ + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function sklik(global, data) { + loadScript(global, 'https://c.imedia.cz/js/amp.js', () => { + const parentId = 'sklik_parent'; + + const parentElement = document.createElement('div'); + parentElement.id = parentId; + window.document.body.appendChild(parentElement); + + data.elm = parentId; + data.url = global.context.location.href; + + sklikProvider.show(data); + }); +} diff --git a/ads/sklik.md b/ads/sklik.md new file mode 100644 index 000000000000..1f50695795d8 --- /dev/null +++ b/ads/sklik.md @@ -0,0 +1,37 @@ + + +# Sklik.cz + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see [ad network documentation](https://napoveda.sklik.cz/partner/reklamni-kod/). + +Supported parameters: + +- width +- height +- json + diff --git a/ads/smartadserver.js b/ads/smartadserver.js index 6e60076cc6be..f7f85bdae3d0 100644 --- a/ads/smartadserver.js +++ b/ads/smartadserver.js @@ -21,24 +21,8 @@ import {loadScript} from '../3p/3p'; * @param {!Object} data */ export function smartadserver(global, data) { - loadScript(global, 'https://ec-ns.sascdn.com/diff/js/smart.js', () => { - if (!global.document.getElementById(data.tag)) { - const divElt = global.document.createElement('div'); - divElt.Id = data.tag; - const cElt = global.document.getElementById('c'); - cElt.appendChild(divElt); - } - global.sas.cmd.push(function() { - global.sas.setup({'domain': data.domain, 'async': true}); - }); - global.sas.cmd.push(function() { - global.sas.call(data.call, { - 'siteId': data.site, - 'pageId': data.page, - 'formatId': data.format, - 'target': data.target, - 'tagId': data.tag, - }); - }); + // For more flexibility, we construct the call to SmartAdServer's URL in the external loader, based on the data received from the AMP tag. + loadScript(global, 'https://ec-ns.sascdn.com/diff/js/amp.v0.js', () => { + global.sas.callAmpAd(data); }); } diff --git a/ads/smartadserver.md b/ads/smartadserver.md index e162c9c51add..ccd388d29e40 100644 --- a/ads/smartadserver.md +++ b/ads/smartadserver.md @@ -16,6 +16,21 @@ limitations under the License. # SmartAdServer +## Supported Smart AdServer parameters in the amp-ad tag + +All of the parameters listed here should be prefixed with "data-" when used. + +| Parameter name | Description | Required | +|----------------|-------------------------------------|----------| +| site | Your Smart AdServer Site ID | Yes | +| page | Your Smart AdServer Page ID | Yes | +| format | Your Smart AdServer Format ID | Yes | +| domain | Your Smart AdServer call domain | Yes | +| target | Your targeting string | No | +| tag | An ID for the tag containing the ad | No | + +Note: If any of the required parameters is missing, the ad slot won't be filled. + ## Example ### Basic call @@ -23,7 +38,6 @@ limitations under the License. ```html ``, use the domain assigned to your network (e. g. www3.smartadserver.com); It can be found in Smart AdServer's config.js library (e. g. http://www3.smartadserver.com/config.js?nwid=1234). - -For semantics of configuration, please see [Smart AdServer help center](http://help.smartadserver.com/). \ No newline at end of file +For semantics of configuration, please see [Smart AdServer help center](http://help.smartadserver.com/). diff --git a/ads/smartclip.js b/ads/smartclip.js new file mode 100644 index 000000000000..48319134576e --- /dev/null +++ b/ads/smartclip.js @@ -0,0 +1,39 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function smartclip(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + global._smartclip_amp = { + allowed_data: ['extra'], + mandatory_data: ['plc', 'sz'], + data, + }; + + validateData(data, + global._smartclip_amp.mandatory_data, global._smartclip_amp.allowed_data); + + const rand = Math.round(Math.random() * 100000000); + + loadScript(global, 'https://des.smartclip.net/ads?type=dyn&plc=' + + encodeURIComponent(data.plc) + '&sz=' + encodeURIComponent(data.sz) + + (data.extra ? '&' + encodeURI(data.extra) : '') + '&rnd=' + rand); +} diff --git a/ads/smartclip.md b/ads/smartclip.md new file mode 100644 index 000000000000..a157a4f7fd5c --- /dev/null +++ b/ads/smartclip.md @@ -0,0 +1,40 @@ + + +# smartclip + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please contact [smartclip](mailto:adtech@smartclip.de). + +Supported parameters: + +All parameters are mandatory, only `data-extra` is optional. + +- ```data-plc``` (String, non-empty) +- ```data-sz``` (String, non-empty) +- ```data-extra``` (String) diff --git a/ads/sortable.js b/ads/sortable.js index a45612094a81..1bd85dd973ff 100644 --- a/ads/sortable.js +++ b/ads/sortable.js @@ -14,14 +14,14 @@ * limitations under the License. */ -import {loadScript, validateDataExists} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function sortable(global, data) { - validateDataExists(data, ['site', 'name']); + validateData(data, ['site', 'name']); const slot = global.document.getElementById('c'); const ad = global.document.createElement('div'); @@ -29,5 +29,6 @@ export function sortable(global, data) { ad.setAttribute('data-ad-name', data.name); ad.setAttribute('data-ad-size', data.width + 'x' + data.height); slot.appendChild(ad); - loadScript(global, 'https://tags-cdn.deployads.com/a/' + encodeURIComponent(data.site) + '.js'); + loadScript(global, 'https://tags-cdn.deployads.com/a/' + + encodeURIComponent(data.site) + '.js'); } diff --git a/ads/swoop.js b/ads/swoop.js new file mode 100644 index 000000000000..9fa6e7510637 --- /dev/null +++ b/ads/swoop.js @@ -0,0 +1,46 @@ +/** + * Copyright 2017 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + loadScript, + validateData, + computeInMasterFrame, +} from '../3p/3p'; + +export function swoop(global, data) { + // Required properties + validateData(data, [ + 'layout', + 'placement', + 'publisher', + 'slot', + ]); + + computeInMasterFrame(global, 'swoop-load', done => { + global.swoopIabConfig = data; + + loadScript(global, 'https://www.swoop-amp.com/amp.js', () => done(global.Swoop != null)); + }, success => { + if (success) { + if (!global.context.isMaster) { + global.context.master.Swoop.announcePlace(global, data); + } + } else { + global.context.noContentAvailable(); + throw new Error('Swoop failed to load'); + } + }); +} diff --git a/ads/swoop.md b/ads/swoop.md new file mode 100644 index 000000000000..0d6074abc4a2 --- /dev/null +++ b/ads/swoop.md @@ -0,0 +1,43 @@ + + +# Swoop + +## Example + +```html + +
+
+
+``` + +## Configuration + +For semantics of configuration, please see ad network documentation. + +Required parameters: + +- `layout`: AMP layout style, should match the `layout` attribute of the `amp-ad` tag +- `publisher`: Publisher ID +- `placement`: Placement type +- `slot`: Slot ID diff --git a/ads/taboola.js b/ads/taboola.js index 56c9bd41d50e..381a0a0a16e9 100644 --- a/ads/taboola.js +++ b/ads/taboola.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {loadScript, validateDataExists, validateExactlyOne} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global @@ -27,9 +27,8 @@ export function taboola(global, data) { // ensure we have vlid publisher, placement and mode // and exactly one page-type - validateDataExists(data, ['publisher', 'placement', 'mode']); - validateExactlyOne(data, ['article', 'video', 'photo', 'search', 'category', - 'homepage', 'others']); + validateData(data, ['publisher', 'placement', 'mode', + ['article', 'video', 'photo', 'search', 'category', 'homepage', 'others']]); // setup default values for referrer and url const params = { diff --git a/ads/teads.js b/ads/teads.js index bee8b4ee07ae..a02b807c62ae 100644 --- a/ads/teads.js +++ b/ads/teads.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {loadScript, checkData, validateDataExists} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global @@ -29,11 +29,11 @@ export function teads(global, data) { data, }; - validateDataExists(data, global._teads_amp.mandatory_data); - checkData(data, global._teads_amp.allowed_data); + validateData(data, + global._teads_amp.mandatory_data, global._teads_amp.allowed_data); if (data.tag) { - validateDataExists(data.tag, global._teads_amp.mandatory_tag_data); + validateData(data.tag, global._teads_amp.mandatory_tag_data); global._tta = data.tag.tta; global._ttp = data.tag.ttp; @@ -41,5 +41,4 @@ export function teads(global, data) { } else { loadScript(global, 'https://a.teads.tv/page/' + encodeURIComponent(data.pid) + '/tag'); } - } diff --git a/ads/valuecommerce.js b/ads/valuecommerce.js new file mode 100644 index 000000000000..383900d5984f --- /dev/null +++ b/ads/valuecommerce.js @@ -0,0 +1,28 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function valuecommerce(global, data) { + validateData(data, ['pid'], ['sid', 'vcptn','om']); + global.vcParam = data; + writeScript(global, 'https://amp.valuecommerce.com/amp_bridge.js'); +} + diff --git a/ads/valuecommerce.md b/ads/valuecommerce.md new file mode 100644 index 000000000000..97aa2aafa25c --- /dev/null +++ b/ads/valuecommerce.md @@ -0,0 +1,48 @@ + + +# Valuecommerce + +## Example + +### Normal ad + +```html + + +``` + +### Omakase ad + +```html + + +``` + +### + +## Configuration + +For configuration details and to generate your tags, please contact https://www.valuecommerce.ne.jp/info/ + diff --git a/ads/webediads.js b/ads/webediads.js index b943667d65ef..c16845ca3f7c 100644 --- a/ads/webediads.js +++ b/ads/webediads.js @@ -14,14 +14,17 @@ * limitations under the License. */ -import {loadScript, checkData, validateDataExists} from '../3p/3p'; +import {loadScript, validateData} from '../3p/3p'; -/** Created by Webedia on 07/03/16 */ +/** + * Created by Webedia on 07/03/16 - last updated on 11/10/16 + * @param {!Window} global + * @param {!Object} data + */ export function webediads(global, data) { - checkData(data, ['site', 'page', 'position', 'query']); - validateDataExists(data, ['site', 'page', 'position']); + validateData(data, ['site', 'page', 'position'], ['query']); loadScript(global, 'https://eu1.wbdds.com/amp.min.js', () => { - global.wads.init({ + global.wads.amp.init({ 'site': data.site, 'page': data.page, 'position': data.position, diff --git a/ads/webediads.md b/ads/webediads.md index 16f40caa2ab1..b2875789e1e1 100644 --- a/ads/webediads.md +++ b/ads/webediads.md @@ -26,12 +26,12 @@ This method allow you to call one ad position with a specific configuration. ### a. Basic Example -``` +```html @@ -39,14 +39,28 @@ This method allow you to call one ad position with a specific configuration. ### b. Query Example +```html + + ``` + +### c. Placeholder and Fallback example +```html + data-query="amptest=1"> +
Loading...
+
No ad
``` @@ -60,11 +74,11 @@ All parameters are mandatory, only "query" can be empty. - ```data-page``` (String, non-empty) - ```data-position``` (String, non-empty) - ```data-query``` (String) - - ```key``` are separated with ```&``` - - ```value``` are separted with ```|``` + - ```key``` are separated with ```&``` + - ```value``` are separted with ```|``` -__Example__ - - ```key1=value1|value2|value3&key2=value4&key3=value5|value6``` + __Example__ + - ```key1=value1|value2|value3&key2=value4&key3=value5|value6``` ## 5. Support diff --git a/ads/weborama.js b/ads/weborama.js index e2245ff32928..1a576cb68c92 100644 --- a/ads/weborama.js +++ b/ads/weborama.js @@ -14,14 +14,14 @@ * limitations under the License. */ -import {writeScript, checkData, validateDataExists} from '../3p/3p'; +import {writeScript, validateData} from '../3p/3p'; /** * @param {!Window} global * @param {!Object} data */ export function weboramaDisplay(global, data) { - const mandatoryWeboramaFields = [ + const mandatoryFields = [ 'width', 'height', 'wbo_account_id', @@ -29,7 +29,7 @@ export function weboramaDisplay(global, data) { 'wbo_fullhost', ]; - const allWeboramaFields = [ + const optionalFields = [ 'wbo_bid_price', 'wbo_price_paid', 'wbo_random', @@ -46,13 +46,9 @@ export function weboramaDisplay(global, data) { 'wbo_is_mobile', 'wbo_vars', 'wbo_weak_encoding', - ].concat(mandatoryWeboramaFields); - - // Warn on extra fields - checkData(data, allWeboramaFields); + ]; - // Validate if mandatory fields exist - validateDataExists(data, mandatoryWeboramaFields); + validateData(data, mandatoryFields, optionalFields); /*eslint "google-camelcase/google-camelcase": 0*/ global.weborama_display_tag = { diff --git a/ads/widespace.js b/ads/widespace.js new file mode 100644 index 000000000000..d7fcf4486e8b --- /dev/null +++ b/ads/widespace.js @@ -0,0 +1,30 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function widespace(global, data) { + validateData(data, ['sid'], []); + + const url = 'https://engine.widespace.com/map/engine/dynamic?isamp=1&sid=' + + encodeURIComponent(data.sid); + + writeScript(global, url); +} diff --git a/ads/widespace.md b/ads/widespace.md new file mode 100644 index 000000000000..d89d89b721ef --- /dev/null +++ b/ads/widespace.md @@ -0,0 +1,37 @@ + + +# Widespace + + +## Examples + +```html + + +``` + + +### Required parameter + +- `data-sid` + + +## Configuration + +For configuration details please contact integrations@widespace.com diff --git a/ads/xlift.js b/ads/xlift.js new file mode 100644 index 000000000000..9859a66b274f --- /dev/null +++ b/ads/xlift.js @@ -0,0 +1,52 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function xlift(global, data) { + validateData(data, ['mediaid']); + + global.xliftParams = data; + const d = global.document.createElement('div'); + d.id = '_XL_recommend'; + global.document.getElementById('c').appendChild(d); + + d.addEventListener('SuccessLoadedXliftAd', function(e) { + e.detail = e.detail || {adSizeInfo: {}}; + global.context.renderStart(e.detail.adSizeInfo); + }); + d.addEventListener('FailureLoadedXliftAd', function() { + global.context.noContentAvailable(); + }); + + //assign XliftAmpHelper property to global(window) + global.XliftAmpHelper = null; + + loadScript(global, 'https://cdn.x-lift.jp/resources/common/xlift_amp.js', () => { + if (!global.XliftAmpHelper) { + global.context.noContentAvailable(); + } + else { + global.XliftAmpHelper.show(); + } + }, () => { + global.context.noContentAvailable(); + }); +} diff --git a/ads/xlift.md b/ads/xlift.md new file mode 100644 index 000000000000..72c3caa57d06 --- /dev/null +++ b/ads/xlift.md @@ -0,0 +1,35 @@ + + +# Xlift + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://www.x-lift.jp/#contact + +Supported parameters: + +- data-mediaid: For loading javascripts each media. This parameter is required. + diff --git a/ads/yahoo.js b/ads/yahoo.js new file mode 100644 index 000000000000..d74d815095db --- /dev/null +++ b/ads/yahoo.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData, writeScript} from '../3p/3p'; + +/** +* @param {!Window} global +* @param {!Object} data +*/ +export function yahoo(global, data) { + validateData(data, ['sid', 'site', 'sa']); + global.yadData = data; + writeScript(global, 'https://s.yimg.com/os/ampad/display.js'); +} diff --git a/ads/yahoo.md b/ads/yahoo.md new file mode 100644 index 000000000000..9cd6b7f40625 --- /dev/null +++ b/ads/yahoo.md @@ -0,0 +1,42 @@ + + +# yahoo + +## Display + +```html + + +``` + +### Configuration + +For configuration details, please contact https://advertising.yahoo.com/contact + +Supported parameters: + +- height +- width +- data-sid +- data-site +- data-sa \ No newline at end of file diff --git a/ads/yahoojp.js b/ads/yahoojp.js index 256f7f4fe69e..aa28e001a46e 100644 --- a/ads/yahoojp.js +++ b/ads/yahoojp.js @@ -14,14 +14,15 @@ * limitations under the License. */ -import {writeScript, validateDataExists, checkData} from '../3p/3p'; +import {writeScript, validateData} from '../3p/3p'; + /** * @param {!Window} global * @param {!Object} data */ export function yahoojp(global, data) { - checkData(data, ['yadsid']); - validateDataExists(data, ['yadsid']); + validateData(data, ['yadsid'], []); global.yahoojpParam = data; - writeScript(global, 'https://s.yimg.jp/images/listing/tool/yads/ydn/amp/amp.js'); + writeScript(global, + 'https://s.yimg.jp/images/listing/tool/yads/ydn/amp/amp.js'); } diff --git a/ads/yieldbot.js b/ads/yieldbot.js index a99e4688004e..9546e27ceb23 100644 --- a/ads/yieldbot.js +++ b/ads/yieldbot.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {validateDataExists, checkData, loadScript} from '../3p/3p'; +import {validateData, loadScript} from '../3p/3p'; import {doubleclick} from '../ads/google/doubleclick'; import {rethrowAsync} from '../src/log'; @@ -23,15 +23,14 @@ import {rethrowAsync} from '../src/log'; * @param {!Object} data */ export function yieldbot(global, data) { - checkData(data, ['psn', 'ybSlot', 'slot', - 'targeting', 'categoryExclusions', - 'tagForChildDirectedTreatment', 'cookieOptions', - 'overrideWidth', 'overrideHeight']); - validateDataExists(data, ['psn', 'ybSlot', 'slot']); + validateData(data, ['psn', 'ybSlot', 'slot'], [ + 'targeting', 'categoryExclusions', 'tagForChildDirectedTreatment', + 'cookieOptions','overrideWidth', 'overrideHeight', + ]); global.ybotq = global.ybotq || []; - loadScript(global, 'https://cdn.yldbt.com/js/yieldbot.intent.js', () => { + loadScript(global, 'https://cdn.yldbt.com/js/yieldbot.intent.amp.js', () => { global.ybotq.push(() => { try { const dimensions = [[ diff --git a/ads/yieldmo.md b/ads/yieldmo.md index 44c6e94cf34b..4cceaba760e1 100644 --- a/ads/yieldmo.md +++ b/ads/yieldmo.md @@ -31,4 +31,24 @@ For semantics of configuration, please [contact Yieldmo](https://yieldmo.com/#co Supported parameters: -- data-ymid +- `data-ymid` + +## Multi-size Ad + +Yieldmo implicitly handles rendering different sized ads that are bid to the same placement. No additional configuration is required for the tag. + +--- + +Above the fold ads do not resize, so as not to not disrupt the user experience: + +![](http://test.yieldmo.com.s3.amazonaws.com/amp-demo/big-notResized.gif) +![](http://test.yieldmo.com.s3.amazonaws.com/amp-demo/small-notResized.gif) + +--- + +Below the fold, ads resize: + +![](http://test.yieldmo.com.s3.amazonaws.com/amp-demo/big-resized.gif) +![](http://test.yieldmo.com.s3.amazonaws.com/amp-demo/small-resized.gif) + +--- diff --git a/ads/yieldone.js b/ads/yieldone.js new file mode 100644 index 000000000000..522e1ea046fc --- /dev/null +++ b/ads/yieldone.js @@ -0,0 +1,29 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ + export function yieldone(global, data) { + validateData(data, ['pubid', 'pid','width', 'height'], []); + + global.yieldoneParam = data; + writeScript(global, + 'https://img.ak.impact-ad.jp/ic/pone/commonjs/yone-amp.js'); + } diff --git a/ads/yieldone.md b/ads/yieldone.md new file mode 100644 index 000000000000..c3499e6c0529 --- /dev/null +++ b/ads/yieldone.md @@ -0,0 +1,24 @@ + # YIELD ONE + --- + + ## Example + --- + + + + + + ## Configuration +--- + For configuration details and to generate your tags, please contact https://yieldone.com/service/contact/media/index.php?act=input or + + Supported parameters: + + - data-pubid + - data-pid diff --git a/ads/zedo.js b/ads/zedo.js new file mode 100644 index 000000000000..36bb34916d08 --- /dev/null +++ b/ads/zedo.js @@ -0,0 +1,63 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData, loadScript} from '../3p/3p'; + + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function zedo(global, data) { + // check mandatory fields + validateData(data, + ['superId', 'network', 'placementId', 'channel', 'publisher', 'dim'], + ['charset', 'callback', 'renderer']); + + loadScript(global, 'https://ss3.zedo.com/gecko/tag/Gecko.amp.min.js', () => { + const ZGTag = global.ZGTag; + const charset = data.charset || ''; + const callback = data.callback || function() {}; + const geckoTag = new ZGTag(data.superId, data.network, '', '', + charset, callback); + geckoTag.setAMP(); + // define placement + const placement = geckoTag.addPlacement(data.placementId, + data.channel, data.publisher, data.dim, data.width, data.height); + if (data.renderer) { + for (const key in data.renderer) { + placement.includeRenderer(data.renderer[key].name, + data.renderer[key].value); + } + } else { + placement.includeRenderer('display', {}); + } + //create a slot div to display ad + const slot = global.document.createElement('div'); + slot.id = 'zdt_' + data.placementId; + + const divContainer = global.document.getElementById('c'); + if (divContainer) { + divContainer.appendChild(slot); + } + + // call load ads + geckoTag.loadAds(); + + // call div ready + geckoTag.placementReady(data.placementId); + }); +} diff --git a/ads/zedo.md b/ads/zedo.md new file mode 100644 index 000000000000..74c4c1f6b483 --- /dev/null +++ b/ads/zedo.md @@ -0,0 +1,53 @@ + + +# Zedo + +## Example + +### Basic + +```html + +``` + + +## Configuration + +For semantics of configuration, please see contact us [support@zedo.com] + + +### Required parameters + +- `data-super-id` +- `data-network` +- `data-placement-id` +- `data-channel` +- `data-publisher` +- `data-dim` + +### Optional parameters +- `data-charset` +- `data-callback` +- `data-renderer` diff --git a/ads/zergnet.js b/ads/zergnet.js new file mode 100644 index 000000000000..239ac2ed198d --- /dev/null +++ b/ads/zergnet.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {writeScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function zergnet(global, data) { + validateData(data, ['zergid'], []); + global.zergnetWidgetId = data.zergid; + writeScript(global, 'https://www.zergnet.com/zerg-amp.js'); +} diff --git a/ads/zergnet.md b/ads/zergnet.md new file mode 100644 index 000000000000..5a9223ca91e9 --- /dev/null +++ b/ads/zergnet.md @@ -0,0 +1,39 @@ + + +# ZergNet + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For semantics of configuration, please see ad network documentation. + +Supported parameters: + +- data-zergid diff --git a/ads/zucks.js b/ads/zucks.js new file mode 100644 index 000000000000..c287ac53042a --- /dev/null +++ b/ads/zucks.js @@ -0,0 +1,26 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function zucks(global, data) { + validateData(data, ['frameId']); + writeScript(global, `https://j.zucks.net.zimg.jp/j?f=${data['frameId']}`); +} diff --git a/ads/zucks.md b/ads/zucks.md new file mode 100644 index 000000000000..0acaa7ef6fff --- /dev/null +++ b/ads/zucks.md @@ -0,0 +1,33 @@ + + +# Zucks + +## Example + +```html + + +``` + +## Configuration + +For more information, please [contact Zucks](https://zucks.co.jp/contact/). + +Supported parameters: +- data-frame-id \ No newline at end of file diff --git a/build-system/OWNERS.yaml b/build-system/OWNERS.yaml new file mode 100644 index 000000000000..3c098dee1a65 --- /dev/null +++ b/build-system/OWNERS.yaml @@ -0,0 +1,2 @@ +- erwinmombay +- cramforce diff --git a/build-system/SERVING.md b/build-system/SERVING.md new file mode 100644 index 000000000000..4a7b26c0bb92 --- /dev/null +++ b/build-system/SERVING.md @@ -0,0 +1,62 @@ + + +# How AMP HTML is deployed + +## Requirements +- git +- node + npm +- gulp (installed globally) +- java 8 +- yarn (see https://yarnpkg.com/) + +## Steps +```bash +git clone https://github.com/ampproject/amphtml.git +cd amphtml +# Checkout a tag +git checkout 123456789 +yarn +gulp clean +# We only need to build the css files, no need to generate `max` files +gulp build --css-only +gulp dist --version 123456789 --type prod --hostname cdn.myowncdn.org --hostname3p 3p.myowncdn.net +mkdir -p /path/to/cdn/production/ +mkdir -p /path/to/cdn/3p/ +# this would be the files hosted on www.ampproject.org/ +cp -R dist/* /path/to/cdn/production/ +# this would be the files hosted on 3p.ampproject.net/ +cp -R dist.3p/* /path/to/cdn/3p + +# Unfortunately we need these replace lines to compensate for the transition +# to the new code. We should be able to remove this in the next couple of weeks +# as we no longer prefix the global AMP_CONFIG during `gulp dist` in the latest +# code in master. We use -i.bak for cross compatibility on GNU and BSD/Mac. +sed -i.bak "s#^.*\/\*AMP_CONFIG\*\/##" /path/to/cdn/production/v0.js +rm /path/to/cdn/production/v0.js.bak +sed -i.bak "s#^.*\/\*AMP_CONFIG\*\/##" /path/to/cdn/production/alp.js +rm /path/to/cdn/production/alp.js.bak + +# make sure and prepend the global production config to main binaries +gulp prepend-global --target /path/to/cdn/production/v0.js --prod +gulp prepend-global --target /path/to/cdn/production/alp.js --prod +gulp prepend-global --target /path/to/3p/cdn/production/f.js --prod + +# The following commands below are optional if you want to host a similar +# experiments page like https://cdn.ampproject.org/experiments.html +cp dist.tools/experiments/experiments.cdn.html /path/to/cdn/production/experiments.html +cp dist.tools/experiments/{experiments.js,experiments.js.map} /path/to/cdn/production/v0/ +``` diff --git a/build-system/amp.extern.js b/build-system/amp.extern.js index b929d6587f04..1153a497c5a1 100644 --- a/build-system/amp.extern.js +++ b/build-system/amp.extern.js @@ -21,7 +21,15 @@ process.end.NODE_ENV; // Exposed to ads. window.context = {}; +window.context.sentinel; window.context.amp3pSentinel; +window.context.clientId; +window.context.initialIntersection; +window.context.sourceUrl; +window.context.experimentToggles; + +// Service Holder +window.services; // Exposed to custom ad iframes. /* @type {!Function} */ @@ -29,14 +37,32 @@ window.draw3p; // AMP's globals window.AMP_TEST; +window.AMP_TEST_IFRAME; window.AMP_TAG; -window.AMP_CONFIG; +window.AMP_CONFIG = {}; window.AMP = {}; -// Externed explicitly because we do not export Class shaped names -// by default. -window.AMP.BaseElement; -window.AMP.BaseTemplate; +window.AMP_CONFIG.thirdPartyUrl; +window.AMP_CONFIG.thirdPartyFrameHost; +window.AMP_CONFIG.thirdPartyFrameRegex; +window.AMP_CONFIG.cdnUrl; +window.AMP_CONFIG.errorReportingUrl; + +// amp-viz-vega related externs. +/** + * @typedef {{spec: function(!JSONType, function())}} + */ +let VegaParser; +/** + * @typedef {{parse: VegaParser}} + */ +let VegaObject; +/* @type {VegaObject} */ +window.vg; + +// Should have been defined in the closure compiler's extern file for +// IntersectionObserverEntry, but appears to have been omitted. +IntersectionObserverEntry.prototype.rootBounds; // Externed explicitly because this private property is read across // binaries. @@ -63,13 +89,12 @@ var AmpElement; var AccessService = function() {}; /** @constructor */ var UserNotificationManager = function() {}; +UserNotificationManager.prototype.get; /** @constructor */ var Cid = function() {}; /** @constructor */ var Activity = function() {}; - - // data var data; data.tweetid; @@ -91,3 +116,123 @@ twttr.widgets.createTweet; var FB; FB.init; +// Validator +var amp; +amp.validator; +amp.validator.validateUrlAndLog = function(string, doc, filter) {} + +// Temporary Access types (delete when amp-access is compiled +// for type checking). +var Activity; +Activity.prototype.getTotalEngagedTime = function() {}; +var AccessService; +AccessService.prototype.getAccessReaderId = function() {}; +AccessService.prototype.getAuthdataField = function(field) {}; +// Same for amp-analytics +/** + * The "get CID" parameters. + * - createCookieIfNotPresent: Whether CID is allowed to create a cookie when. + * Default value is `false`. + * @typedef {{ + * scope: string, + * createCookieIfNotPresent: (boolean|undefined), + * }} + */ +var GetCidDef; +var Cid; +/** + * @param {string|!GetCidDef} externalCidScope Name of the fallback cookie + * for the case where this doc is not served by an AMP proxy. GetCidDef + * structure can also instruct CID to create a cookie if one doesn't yet + * exist in a non-proxy case. + * @param {!Promise} consent Promise for when the user has given consent + * (if deemed necessary by the publisher) for use of the client + * identifier. + * @param {!Promise=} opt_persistenceConsent Dedicated promise for when + * it is OK to persist a new tracking identifier. This could be + * supplied ONLY by the code that supplies the actual consent + * cookie. + * If this is given, the consent param should be a resolved promise + * because this call should be only made in order to get consent. + * The consent promise passed to other calls should then itself + * depend on the opt_persistenceConsent promise (and the actual + * consent, of course). + * @return {!Promise} A client identifier that should be used + * within the current source origin and externalCidScope. Might be + * null if no identifier was found or could be made. + * This promise may take a long time to resolve if consent isn't + * given. + */ +Cid.prototype.get = function( + externalCidScope, consent, opt_persistenceConsent) {} + +var AMP = {}; +window.AMP; +// Externed explicitly because we do not export Class shaped names +// by default. +/** + * This uses the internal name of the type, because there appears to be no + * other way to reference an ES6 type from an extern that is defined in + * the app. + * @constructor + * @extends {BaseElement$$module$src$base_element} + */ +AMP.BaseElement = class { + /** @param {!AmpElement} element */ + constructor(element) {} +}; + +/** + * This uses the internal name of the type, because there appears to be no + * other way to reference an ES6 type from an extern that is defined in + * the app. + * @constructor + * @extends {AmpAdXOriginIframeHandler$$module$extensions$amp_ad$0_1$amp_ad_xorigin_iframe_handler} + */ +AMP.AmpAdXOriginIframeHandler = class { + /** + * @param {!AmpAd3PImpl$$module$extensions$amp_ad$0_1$amp_ad_3p_impl|!AmpA4A$$module$extensions$amp_a4a$0_1$amp_a4a} baseInstance + */ + constructor(baseInstance) {} +}; + +/** + * This uses the internal name of the type, because there appears to be no + * other way to reference an ES6 type from an extern that is defined in + * the app. + * @constructor + * @extends {AmpAdUIHandler$$module$extensions$amp_ad$0_1$amp_ad_ui} + */ +AMP.AmpAdUIHandler = class { + /** + * @param {!AMP.BaseElement} baseInstance + */ + constructor(baseInstance) {} +}; + +/* + \ \ / \ / / / \ | _ \ | \ | | | | | \ | | / _____| + \ \/ \/ / / ^ \ | |_) | | \| | | | | \| | | | __ + \ / / /_\ \ | / | . ` | | | | . ` | | | |_ | + \ /\ / / _____ \ | |\ \----.| |\ | | | | |\ | | |__| | + \__/ \__/ /__/ \__\ | _| `._____||__| \__| |__| |__| \__| \______| + + Any private property for BaseElement should be declared in + build-system/amp.extern.js, this is so closure compiler doesn't rename + the private properties of BaseElement since if it did there is a + possibility that the private property's new symbol in the core compilation + unit would collide with a renamed private property in the inheriting class + in extensions. + */ +var SomeBaseElementLikeClass; +SomeBaseElementLikeClass.prototype.layout_; + +/** @type {number} */ +SomeBaseElementLikeClass.prototype.layoutWidth_; + +/** @type {boolean} */ +SomeBaseElementLikeClass.prototype.inViewport_; + +SomeBaseElementLikeClass.prototype.actionMap_; + +AMP.BaseTemplate; diff --git a/build-system/amp.nti.extern.js b/build-system/amp.nti.extern.js new file mode 100644 index 000000000000..b53ab3e49536 --- /dev/null +++ b/build-system/amp.nti.extern.js @@ -0,0 +1,30 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Extern specifically for New Type Inference (NTI) type checking mode. + +/** + * TransitionDef function that accepts normtime, typically between 0 and 1 and + * performs an arbitrary animation action. Notice that sometimes normtime can + * dip above 1 or below 0. This is an acceptable case for some curves. The + * second argument is a boolean value that equals "true" for the completed + * transition and "false" for ongoing. + * @param {number} unusedNormtime + * @param {boolean} unusedCompleted + * @return {function(number):T} + * @template T + */ +var TransitionDef = function(unusedNormtime, unusedCompleted) {}; diff --git a/build-system/amp.oti.extern.js b/build-system/amp.oti.extern.js new file mode 100644 index 000000000000..45fcd07deb7e --- /dev/null +++ b/build-system/amp.oti.extern.js @@ -0,0 +1,27 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Extern specifically for Old Type Inference (OTI) type checking mode. + +/** + * TransitionDef function that accepts normtime, typically between 0 and 1 and + * performs an arbitrary animation action. Notice that sometimes normtime can + * dip above 1 or below 0. This is an acceptable case for some curves. The + * second argument is a boolean value that equals "true" for the completed + * transition and "false" for ongoing. + * @typedef {function(number, boolean):?|function(number):?} + */ +var TransitionDef; diff --git a/build-system/config.js b/build-system/config.js index 2d21159471b5..c6b06c6d3b4c 100644 --- a/build-system/config.js +++ b/build-system/config.js @@ -14,13 +14,15 @@ * limitations under the License. */ -var path = require('path'); - -var karmaConf = path.resolve('karma.conf.js'); - var commonTestPaths = [ 'test/_init_tests.js', - 'test/fixtures/**/*.html', + 'test/fixtures/*.html', + { + pattern: 'test/fixtures/served/*.html', + included: false, + nocache: false, + watched: true, + }, { pattern: 'dist/**/*.js', included: false, @@ -45,10 +47,11 @@ var commonTestPaths = [ nocache: false, watched: true, }, -] +]; var testPaths = commonTestPaths.concat([ 'test/**/*.js', + 'ads/**/test/test-*.js', 'extensions/**/test/**/*.js', ]); @@ -57,96 +60,47 @@ var integrationTestPaths = commonTestPaths.concat([ 'extensions/**/test/integration/**/*.js', ]); -var karma = { - default: { - configFile: karmaConf, - singleRun: true, - client: { - captureConsole: false, - } - }, - firefox: { - configFile: karmaConf, - singleRun: true, - browsers: ['Firefox'], - client: { - mocha: { - timeout: 10000 - }, - captureConsole: false - } - }, - safari: { - configFile: karmaConf, - singleRun: true, - browsers: ['Safari'], - client: { - mocha: { - timeout: 10000 - }, - captureConsole: false - } - }, - saucelabs: { - configFile: karmaConf, - reporters: ['dots', 'saucelabs'], - browsers: [ - 'SL_Chrome_android', - 'SL_Chrome_latest', - 'SL_Chrome_37', - 'SL_Firefox_latest', - 'SL_Safari_8', - 'SL_Safari_9', - 'SL_Edge_latest', - // TODO(#895) Enable these. - //'SL_iOS_9_1', - //'SL_IE_11', - ], - singleRun: true, - client: { - mocha: { - timeout: 10000 - }, - captureConsole: false, - }, - browserDisconnectTimeout: 10000, - browserDisconnectTolerance: 1, - browserNoActivityTimeout: 4 * 60 * 1000, - captureTimeout: 4 * 60 * 1000, - } -}; - /** @const */ module.exports = { commonTestPaths: commonTestPaths, testPaths: testPaths, integrationTestPaths: integrationTestPaths, - karma: karma, lintGlobs: [ '**/*.js', + '!**/*.extern.js', '!{node_modules,build,dist,dist.3p,dist.tools,' + 'third_party,build-system}/**/*.*', - '!{testing,examples,examples.build}/**/*.*', + '!{testing,examples}/**/*.*', // TODO: temporary, remove when validator is up to date '!validator/**/*.*', + '!eslint-rules/**/*.*', '!gulpfile.js', '!karma.conf.js', '!**/local-amp-chrome-extension/background.js', '!extensions/amp-access/0.1/access-expr-impl.js', + '!extensions/amp-bind/0.1/bind-expr-impl.js', ], presubmitGlobs: [ '**/*.{css,js,go}', // This does match dist.3p/current, so we run presubmit checks on the // built 3p binary. This is done, so we make sure our special 3p checks // run against the entire transitive closure of deps. - '!{node_modules,build,examples.build,dist,dist.tools,' + + '!{node_modules,build,dist,dist.tools,' + 'dist.3p/[0-9]*,dist.3p/current-min}/**/*.*', + '!dist.3p/current/**/ampcontext-lib.js', + '!validator/dist/**/*.*', '!validator/node_modules/**/*.*', + '!validator/nodejs/node_modules/**/*.*', '!build-system/tasks/presubmit-checks.js', '!build/polyfills.js', '!build/polyfills/*.js', '!gulpfile.js', '!third_party/**/*.*', + '!validator/chromeextension/*.*', + // Files in this testdata dir are machine-generated and are not part + // of the AMP runtime, so shouldn't be checked. + '!extensions/amp-a4a/*/test/testdata/*.js', + '!examples/*.js', ], changelogIgnoreFileTypes: /\.md|\.json|\.yaml|LICENSE|CONTRIBUTORS$/ }; diff --git a/build-system/conformance-config.textproto b/build-system/conformance-config.textproto new file mode 100644 index 000000000000..295117e48fd5 --- /dev/null +++ b/build-system/conformance-config.textproto @@ -0,0 +1,162 @@ +# Custom Rules + +requirement: { + rule_id: 'closure:throwOfNonErrorTypes' + type: CUSTOM + java_class: 'com.google.javascript.jscomp.ConformanceRules$BanThrowOfNonErrorTypes' + error_message: 'Should not throw a non-Error object.' +} + +# Strings + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.padStart is not allowed' + value: 'String.prototype.padStart' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.padEnd is not allowed' + value: 'String.prototype.padEnd' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.startsWith is not allowed' + value: 'String.prototype.startsWith' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.endsWith is not allowed' + value: 'String.prototype.endsWith' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.includes is not allowed' + value: 'String.prototype.includes' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.repeat is not allowed' + value: 'String.prototype.repeat' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.normalize is not allowed' + value: 'String.prototype.normalize' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'String.prototype.codePointAt is not allowed' + value: 'String.prototype.codePointAt' +} + +requirement: { + type: BANNED_NAME + error_message: 'String.fromCodePoint is not allowed' + value: 'Sring.fromCodePoint' +} + +# Array + +requirement: { + type: BANNED_NAME + error_message: 'Array.from is not allowed' + value: 'Array.from' +} + +requirement: { + type: BANNED_NAME + error_message: 'Array.of is not allowed' + value: 'Array.of' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'Array.prototype.find is not allowed' + value: 'Array.prototype.find' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'Array.prototype.findIndex is not allowed' + value: 'Array.prototype.findIndex' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'Array.prototype.includes is not allowed' + value: 'Array.prototype.includes' +} + +# Math + +requirement: { + type: BANNED_NAME + error_message: 'Math.trunc is not allowed' + value: 'Math.trunc' +} + +# Number + +requirement: { + type: BANNED_NAME + error_message: 'Number.isSafeInteger is not allowed' + value: 'Number.isSafeInteger' +} + +requirement: { + type: BANNED_NAME + error_message: 'Number.isNaN is not allowed' + value: 'Number.isNaN' +} + +requirement: { + type: BANNED_NAME + error_message: 'Number.isFinite is not allowed' + value: 'Number.isFinite' +} + +requirement: { + type: BANNED_NAME + error_message: 'Number.EPSILON is not allowed' + value: 'Number.EPSILON' +} + +# Structures + +requirement: { + type: BANNED_NAME + error_message: 'Map is not allowed' + value: 'Map' +} + +requirement: { + type: BANNED_NAME + error_message: 'WeakMap is not allowed' + value: 'WeakMap' +} + +requirement: { + type: BANNED_NAME + error_message: 'Set is not allowed' + value: 'Set' +} + +requirement: { + type: BANNED_NAME + error_message: 'WeakSet is not allowed' + value: 'WeakSet' +} + +requirement: { + type: BANNED_NAME + error_message: 'Symbol is not allowed' + value: 'Symbol' +} diff --git a/build-system/dep-check-config.js b/build-system/dep-check-config.js index ad35b743e86a..b55ffd15f1a7 100644 --- a/build-system/dep-check-config.js +++ b/build-system/dep-check-config.js @@ -37,19 +37,25 @@ exports.rules = [ { filesMatching: '**/*.js', mustNotDependOn: 'src/sanitizer.js', - whitelist: 'extensions/amp-mustache/0.1/amp-mustache.js->' + - 'src/sanitizer.js', + whitelist: [ + 'extensions/amp-mustache/0.1/amp-mustache.js->src/sanitizer.js', + 'extensions/amp-bind/0.1/bind-evaluator.js->src/sanitizer.js', + ], }, { filesMatching: '**/*.js', mustNotDependOn: 'third_party/**/*.js', whitelist: [ - 'extensions/amp-analytics/**/*.js->' + + 'extensions/amp-crypto-polyfill/**/*.js->' + 'third_party/closure-library/sha384-generated.js', 'extensions/amp-mustache/0.1/amp-mustache.js->' + 'third_party/mustache/mustache.js', '3p/polyfills.js->third_party/babel/custom-babel-helpers.js', 'src/sanitizer.js->third_party/caja/html-sanitizer.js', + 'extensions/amp-viz-vega/**->third_party/vega/vega.js', + 'extensions/amp-viz-vega/**->third_party/d3/d3.js', + 'src/dom.js->third_party/css-escape/css-escape.js', + 'src/shadow-embed.js->third_party/webcomponentsjs/ShadowCSS.js', ] }, // Rules for 3p @@ -57,10 +63,16 @@ exports.rules = [ filesMatching: '3p/**/*.js', mustNotDependOn: 'src/**/*.js', whitelist: [ + '3p/**->src/utils/object.js', '3p/**->src/log.js', '3p/**->src/types.js', '3p/**->src/string.js', + '3p/**->src/style.js', '3p/**->src/url.js', + '3p/**->src/config.js', + '3p/**->src/mode.js', + '3p/polyfills.js->src/polyfills/math-sign.js', + '3p/polyfills.js->src/polyfills/object-assign.js', ], }, { @@ -72,28 +84,109 @@ exports.rules = [ filesMatching: 'ads/**/*.js', mustNotDependOn: 'src/**/*.js', whitelist: [ + 'ads/**->src/utils/base64.js', + 'ads/**->src/utils/dom-fingerprint.js', + 'ads/**->src/utils/object.js', 'ads/**->src/log.js', + 'ads/**->src/mode.js', + 'ads/**->src/url.js', 'ads/**->src/types.js', 'ads/**->src/string.js', - 'ads/**->src/url.js', + 'ads/**->src/style.js', + // ads/google/a4a doesn't contain 3P ad code and should probably move + // somewhere else at some point + 'ads/google/a4a/**->src/ad-cid.js', + 'ads/google/a4a/**->src/document-info.js', + 'ads/google/a4a/**->src/experiments.js', + 'ads/google/a4a/**->src/timer.js', + 'ads/google/a4a/**->src/viewer.js', + 'ads/google/a4a/**->src/viewport.js', + 'ads/google/a4a/performance.js->src/url-replacements.js', + 'ads/google/a4a/performance.js->src/service/variable-source.js', + // alp handler needs to depend on src files + 'ads/alp/handler.js->src/dom.js', + 'ads/alp/handler.js->src/config.js', + // Some ads need to depend on json.js + 'ads/**->src/json.js', ], }, { filesMatching: 'ads/**/*.js', mustNotDependOn: 'extensions/**/*.js', + whitelist: [ + // See todo note in ads/_a4a-config.js + 'ads/_a4a-config.js->' + + 'extensions/amp-ad-network-adsense-impl/0.1/adsense-a4a-config.js', + 'ads/_a4a-config.js->' + + 'extensions/amp-ad-network-doubleclick-impl/0.1/' + + 'doubleclick-a4a-config.js', + 'ads/_a4a-config.js->' + + 'extensions/amp-ad-network-fake-impl/0.1/fake-a4a-config.js', + 'ads/_a4a-config.js->' + + 'extensions/amp-ad-network-triplelift-impl/0.1/triplelift-a4a-config.js', + 'ads/_a4a-config.js->' + + 'extensions/amp-ad-network-cloudflare-impl/0.1/cloudflare-a4a-config.js', + 'ads/google/a4a/google-data-reporter.js->' + + 'extensions/amp-ad-network-adsense-impl/0.1/adsense-a4a-config.js', + 'ads/google/a4a/google-data-reporter.js->' + + 'extensions/amp-ad-network-doubleclick-impl/0.1/' + + 'doubleclick-a4a-config.js', + 'ads/google/a4a/performance.js->extensions/amp-a4a/0.1/amp-a4a.js', + ], }, + // Rules for extensions and main src. + { + filesMatching: '{src,extensions}/**/*.js', + mustNotDependOn: '3p/**/*.js', + }, + // Rules for extensions. { filesMatching: 'extensions/**/*.js', mustNotDependOn: 'src/service/**/*.js', + whitelist: [ + 'extensions/amp-a4a/0.1/a4a-variable-source.js->' + + 'src/service/variable-source.js', + 'extensions/amp-a4a/0.1/amp-a4a.js->' + + 'src/service/url-replacements-impl.js', + 'extensions/amp-video/0.1/amp-video.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-ooyala-player/0.1/amp-ooyala-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-youtube/0.1/amp-youtube.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-a4a/0.1/amp-a4a.js->src/service/variable-source.js', + ], }, { filesMatching: 'extensions/**/*.js', mustNotDependOn: 'src/base-element.js', }, { - filesMatching: 'extensions/**/*.js', + filesMatching: '**/*.js', mustNotDependOn: 'src/polyfills/**/*.js', + whitelist: [ + // DO NOT add extensions/ files + '3p/polyfills.js->src/polyfills/math-sign.js', + '3p/polyfills.js->src/polyfills/object-assign.js', + 'src/polyfills.js->src/polyfills/domtokenlist-toggle.js', + 'src/polyfills.js->src/polyfills/document-contains.js', + 'src/polyfills.js->src/polyfills/math-sign.js', + 'src/polyfills.js->src/polyfills/object-assign.js', + 'src/polyfills.js->src/polyfills/promise.js', + 'src/service/extensions-impl.js->src/polyfills/document-contains.js', + 'src/service/extensions-impl.js->src/polyfills/domtokenlist-toggle.js', + ], + }, + { + filesMatching: '**/*.js', + mustNotDependOn: 'src/polyfills.js', + whitelist: [ + 'src/amp.js->src/polyfills.js', + 'src/service.js->src/polyfills.js', + 'src/service/timer-impl.js->src/polyfills.js', + 'src/service/extensions-impl.js->src/polyfills.js', + ], }, // Rules for main src. @@ -106,8 +199,15 @@ exports.rules = [ mustNotDependOn: 'ads/**/*.js', whitelist: 'src/ad-cid.js->ads/_config.js', }, + + // A4A { - filesMatching: 'src/**/*.js', - mustNotDependOn: '3p/**/*.js', + filesMatching: 'extensions/**/*-ad-network-*.js', + mustNotDependOn: [ + 'extensions/amp-ad/0.1/amp-ad-xorigin-iframe-handler.js', + 'extensions/amp-ad/0.1/concurrent-load.js', + 'src/3p-frame.js', + 'src/iframe-helper.js', + ], }, ]; diff --git a/build-system/eslint-rules/enforce-private-props.js b/build-system/eslint-rules/enforce-private-props.js new file mode 100644 index 000000000000..49cdcc37f041 --- /dev/null +++ b/build-system/eslint-rules/enforce-private-props.js @@ -0,0 +1,75 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = function(context) { + /** + * @param {!Array|undefined} commentLines + * @return {boolean} + */ + function hasPrivateAnnotation(commentLines) { + if (!commentLines) { + return false; + } + return commentLines.some(function(comment) { + return comment.type == 'Block' && /@private/.test(comment.value); + }); + } + + /** + * @param {string} + * @return {boolean} + */ + function hasTrailingUnderscore(fnName) { + return /_$/.test(fnName); + } + + /** + * @param {string} + * @return {boolean} + */ + function hasExplicitNoInline(fnName) { + return /NoInline$/.test(fnName); + } + + /** + * @param {!Node} + * @return {boolean} + */ + function isThisMemberExpression(node) { + return node.type == 'MemberExpression' && + node.object.type == 'ThisExpression'; + } + return { + MethodDefinition: function(node) { + if (hasPrivateAnnotation(node.leadingComments) && + !hasExplicitNoInline(node.key.name) && + !hasTrailingUnderscore(node.key.name)) { + context.report(node, + 'Method marked as private but has no trailing underscore.'); + } + }, + AssignmentExpression: function(node) { + if (node.parent.type == 'ExpressionStatement' && + hasPrivateAnnotation(node.parent.leadingComments) && + isThisMemberExpression(node.left) && + !hasExplicitNoInline(node.left.property.name) && + !hasTrailingUnderscore(node.left.property.name)) { + context.report(node, + 'Property marked as private but has no trailing underscore.'); + } + } + } +}; diff --git a/src/framerate.js b/build-system/eslint-rules/no-array-destructuring.js similarity index 72% rename from src/framerate.js rename to build-system/eslint-rules/no-array-destructuring.js index 0de51fdca1a2..df7eae08748a 100644 --- a/src/framerate.js +++ b/build-system/eslint-rules/no-array-destructuring.js @@ -14,16 +14,10 @@ * limitations under the License. */ -/** - * @fileoverview Factory for ./service/cid-impl.js - */ - -import {getService} from './service'; - -/** - * @param {!Window} window - * @return {!Framerate} - */ -export function framerateFor(window) { - return getService(window, 'framerate'); +module.exports = function(context) { + return { + ArrayPattern: function(node) { + context.report(node, 'No Array destructuring allowed.'); + } + }; }; diff --git a/build-system/eslint-rules/no-es2015-number-props.js b/build-system/eslint-rules/no-es2015-number-props.js new file mode 100644 index 000000000000..23c72b4146e7 --- /dev/null +++ b/build-system/eslint-rules/no-es2015-number-props.js @@ -0,0 +1,44 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var INVALID_PROPS = [ + 'EPSILON', + 'MAX_SAFE_INTEGER', + 'MIN_SAFE_INTEGER', + 'isFinite', + 'isInteger', + 'isNaN', + 'isSafeInteger', + 'parseFloat', + 'parseInt', +]; + +function isInvalidProperty(property) { + return INVALID_PROPS.indexOf(property) != -1; +} + +module.exports = function(context) { + return { + MemberExpression: function(node) { + if (node.object.name == 'Number' && + isInvalidProperty(node.property.name)) { + context.report(node, + 'no ES2015 "Number" methods and properties allowed to be used.'); + } + } + }; +}; diff --git a/build-system/eslint-rules/no-export-side-effect.js b/build-system/eslint-rules/no-export-side-effect.js new file mode 100644 index 000000000000..023b326ade91 --- /dev/null +++ b/build-system/eslint-rules/no-export-side-effect.js @@ -0,0 +1,39 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = function(context) { + return { + ExportNamedDeclaration: function(node) { + if (node.declaration) { + var declaration = node.declaration; + if (declaration.type === 'VariableDeclaration') { + declaration.declarations + .map(function(declarator) { + return declarator.init + }).filter(function(init) { + return init && /(?:Call|New)Expression/.test(init.type) + // Special case creating a map object from a literal. + && init.callee.name != 'map'; + }).forEach(function(init) { + context.report(init, 'Cannot export side-effect'); + }); + } + } else if (node.specifiers) { + context.report(node, 'Side-effect linting not implemented'); + } + }, + }; +}; diff --git a/build-system/eslint-rules/no-for-of-statement.js b/build-system/eslint-rules/no-for-of-statement.js new file mode 100644 index 000000000000..89ba907e76f6 --- /dev/null +++ b/build-system/eslint-rules/no-for-of-statement.js @@ -0,0 +1,23 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = function(context) { + return { + ForOfStatement: function(node) { + context.report(node, 'No for-of statement allowed.'); + } + }; +}; diff --git a/build-system/eslint-rules/no-global.js b/build-system/eslint-rules/no-global.js new file mode 100644 index 000000000000..d43a5a0ffea6 --- /dev/null +++ b/build-system/eslint-rules/no-global.js @@ -0,0 +1,51 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var astUtils = require('eslint/lib/ast-utils'); + +var GLOBALS = Object.create(null); +GLOBALS.window = 'Use `self` instead.'; +GLOBALS.document = 'Reference it as `self.document` or similar instead.'; + +module.exports = function(context) { + return { + Identifier: function(node) { + var name = node.name; + if (!(name in GLOBALS)) { + return; + } + if (!(/Expression/.test(node.parent.type))) { + return; + } + + if (node.parent.type === 'MemberExpression' && + node.parent.property === node) { + return; + } + + var variable = astUtils.getVariableByName(context.getScope(), node.name); + if (variable.defs.length > 0) { + return; + } + + var message = 'Forbidden global `' + node.name + '`.'; + if (GLOBALS[name]) { + message += ' ' + GLOBALS[name]; + } + context.report(node, message); + } + }; +}; diff --git a/build-system/eslint-rules/no-spread.js b/build-system/eslint-rules/no-spread.js new file mode 100644 index 000000000000..1aa98a2404b2 --- /dev/null +++ b/build-system/eslint-rules/no-spread.js @@ -0,0 +1,23 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = function(context) { + return { + SpreadElement: function(node) { + context.report(node, 'No spread element allowed.'); + } + }; +}; diff --git a/build-system/global-configs/README.md b/build-system/global-configs/README.md new file mode 100644 index 000000000000..1a0eaba852ec --- /dev/null +++ b/build-system/global-configs/README.md @@ -0,0 +1,6 @@ +# Flags + +Please be aware that canary-config.json is actually 1% of production (which is +99%). There are some instances where you might not want this and you should +instead configure the prod-config.json file with a correct frequency value +besides 1 or 0. diff --git a/build-system/global-configs/canary-config.json b/build-system/global-configs/canary-config.json new file mode 100644 index 000000000000..cbb9c092532c --- /dev/null +++ b/build-system/global-configs/canary-config.json @@ -0,0 +1,23 @@ +{ + "allow-doc-opt-in": ["visibility-v2"], + + "canary": 1, + "amp-ios-overflow-x": 1, + "amp-experiment": 1, + "expAdsenseA4A": 1, + "expDoubleclickA4A": 1, + "pan-y": 1, + "a4aProfilingRate": 1, + "amp-form-custom-validations": 1, + "ad-type-custom": 1, + "amp-scrollable-carousel": 1, + "amp-inabox": 1, + "amp-ad-loading-ux": 1, + "ios-embed-wrapper": 1, + "amp-apester-media": 1, + "amp-accordion-session-state-optout": 1, + "amp-playbuzz": 1, + "sentinel-name-change": 1, + "chunked-amp": 1, + "make-body-relative": 1 +} diff --git a/build-system/global-configs/prod-config.json b/build-system/global-configs/prod-config.json new file mode 100644 index 000000000000..568cf5f9895f --- /dev/null +++ b/build-system/global-configs/prod-config.json @@ -0,0 +1,20 @@ +{ + "allow-doc-opt-in": ["visibility-v2"], + + "canary": 0, + "amp-ios-overflow-x": 1, + "amp-experiment": 1, + "pan-y": 1, + "expAdsenseA4A": 0.1, + "expDoubleclickA4A": 0.1, + "a4aProfilingRate": 1, + "amp-form-custom-validations": 1, + "ad-type-custom": 1, + "amp-scrollable-carousel": 1, + "amp-inabox": 1, + "ios-embed-wrapper": 1, + "amp-apester-media": 1, + "amp-accordion-session-state-optout": 1, + "amp-playbuzz": 1, + "make-body-relative": 1 +} diff --git a/build-system/global-configs/sw-config.json b/build-system/global-configs/sw-config.json new file mode 100644 index 000000000000..0406a03e0c42 --- /dev/null +++ b/build-system/global-configs/sw-config.json @@ -0,0 +1,3 @@ +{ + "cache-service-worker-blacklist": [1473441896765, 1473791909894, 1473894434081, 1474060428356] +} diff --git a/build-system/internal-version.js b/build-system/internal-version.js index 592ebe1b6332..0e79c299858f 100644 --- a/build-system/internal-version.js +++ b/build-system/internal-version.js @@ -14,11 +14,13 @@ * limitations under the License. */ +var argv = require('minimist')(process.argv.slice(2)); var crypto = require('crypto'); // Used to e.g. references the ads binary from the runtime to get // version lock. -exports.VERSION = String(new Date().getTime()); +exports.VERSION = argv.version ? + String(argv.version) : String(Date.now()); // A token that changes its value each time we release AMP. This is intended // to verify that two iframes of AMP have the same version of AMP. It is diff --git a/build-system/pr-check.js b/build-system/pr-check.js new file mode 100644 index 000000000000..6692782e9e6d --- /dev/null +++ b/build-system/pr-check.js @@ -0,0 +1,256 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview This file is executed by Travis (configured via + * .travis.yml in the root directory) and is the main driver script + * for running tests. Execution herein is entirely synchronous, that + * is, commands are executed on after the other (see the exec + * function). Should a command fail, this script will then also fail. + * This script attempts to introduce some granularity for our + * presubmit checking, via the determineBuildTargets method. + */ +const child_process = require('child_process'); +const path = require('path'); +const minimist = require('minimist'); + +const gulp = 'node_modules/gulp/bin/gulp.js'; + +/** + * Executes the provided command, returning its stdout as an array of lines. + * This will throw an exception if something goes wrong. + * @param {string} cmd + * @return {!Array} + */ +function exec(cmd) { + return child_process.execSync(cmd, {'encoding': 'utf-8'}).trim().split('\n'); +} + +/** + * Executes the provided command; terminates this program in case of failure. + * @param {string} cmd + */ +function execOrDie(cmd) { + console.log(`\npr-check.js: ${cmd}\n`); + const p = + child_process.spawnSync('/bin/sh', ['-c', cmd], {'stdio': 'inherit'}); + if (p.status != 0) { + console.error(`\npr-check.js - exiting due to failing command: ${cmd}\n`); + process.exit(p.status) + } +} + +/** + * For a provided commit range identifiying a pull request (PR), + * yields the list of files. + * @param {string} travisCommitRange + * @return {!Array} + */ +function filesInPr(travisCommitRange) { + return exec(`git diff --name-only ${travisCommitRange}`); +} + +/** + * Determines whether the given file belongs to the Validator webui, + * that is, the 'VALIDATOR_WEBUI' target. + * @param {string} filePath + * @return {boolean} + */ +function isValidatorWebuiFile(filePath) { + return filePath.startsWith('validator/webui'); +} + +/** + * Determines whether the given file belongs to the Validator webui, + * that is, the 'BUILD_SYSTEM' target. + * @param {string} filePath + * @return {boolean} + */ +function isBuildSystemFile(filePath) { + return filePath.startsWith('build-system') && + // Exclude textproto from build-system since we want it to trigger + // tests and type check. + path.extname(filePath) != '.textproto'; +} + +/** + * Determines whether the given file belongs to the validator, + * that is, the 'VALIDATOR' target. This assumes (but does not + * check) that the file is not part of 'VALIDATOR_WEBUI'. + * @param {string} filePath + * @return {boolean} + */ +function isValidatorFile(filePath) { + if (filePath.startsWith('validator/')) return true; + if (!path.dirname(filePath).endsWith('0.1') && + !path.dirname(filePath).endsWith('test')) + return false; + const name = path.basename(filePath); + return name.startsWith('validator-') && + (name.endsWith('.out') || name.endsWith('.html') || + name.endsWith('.protoascii')); +} + +/** + * @param {string} filePath + * @return {boolean} + */ +function isDocFile(filePath) { + return path.extname(filePath) == '.md'; +} + +/** + * Determines the targets that will be executed by the main method of + * this script. The order within this function matters. + * @param {!Array} filePaths + * @returns {!Set} + */ +function determineBuildTargets(filePaths) { + if (filePaths.length == 0) { + return new Set(['BUILD_SYSTEM', 'VALIDATOR_WEBUI', 'VALIDATOR', 'RUNTIME', + 'DOCS']); + } + const targetSet = new Set(); + for (p of filePaths) { + if (isBuildSystemFile(p)) { + targetSet.add('BUILD_SYSTEM'); + } else if (isValidatorWebuiFile(p)) { + targetSet.add('VALIDATOR_WEBUI'); + } else if (isValidatorFile(p)) { + targetSet.add('VALIDATOR'); + } else if (isDocFile(p)) { + targetSet.add('DOCS'); + } else { + targetSet.add('RUNTIME'); + } + } + return targetSet; +} + + +const command = { + testBuildSystem: function() { + execOrDie('npm run ava'); + }, + buildRuntime: function() { + execOrDie(`${gulp} clean`); + execOrDie(`${gulp} lint`); + execOrDie(`${gulp} build`); + execOrDie(`${gulp} check-types`); + execOrDie(`${gulp} dist --fortesting`); + }, + testRuntime: function() { + // dep-check needs to occur after build since we rely on build to generate + // the css files into js files. + execOrDie(`${gulp} dep-check`); + // Unit tests with Travis' default chromium + execOrDie(`${gulp} test --nobuild --compiled`); + // Integration tests with all saucelabs browsers + execOrDie(`${gulp} test --nobuild --saucelabs --integration --compiled`); + // All unit tests with an old chrome (best we can do right now to pass tests + // and not start relying on new features). + // Disabled because it regressed. Better to run the other saucelabs tests. + execOrDie(`${gulp} test --nobuild --saucelabs --oldchrome --compiled`); + }, + presubmit: function() { + execOrDie(`${gulp} presubmit`); + }, + buildValidatorWebUI: function() { + execOrDie('cd validator/webui && python build.py'); + }, + buildValidator: function() { + execOrDie('cd validator && python build.py'); + }, +}; + +function runAllCommands() { + command.testBuildSystem(); + command.buildRuntime(); + command.presubmit(); + command.testRuntime(); + command.buildValidatorWebUI(); + command.buildValidator(); +} + +/** + * The main method for the script execution which much like a C main function + * receives the command line arguments and returns an exit status. + * @param {!Array} argv + * @returns {number} + */ +function main(argv) { + // If $TRAVIS_PULL_REQUEST_SHA is empty then it is a push build and not a PR. + if (!process.env.TRAVIS_PULL_REQUEST_SHA) { + console.log('Running all commands on push build.'); + runAllCommands(); + return 0; + } + const travisCommitRange = `master...${process.env.TRAVIS_PULL_REQUEST_SHA}`; + const files = filesInPr(travisCommitRange); + const buildTargets = determineBuildTargets(files); + + if (buildTargets.length == 1 && buildTargets.has('DOCS')) { + console.log('Only docs were updated, stopping build process.'); + return 0; + } + + if (files.includes('package.json') ? + !files.includes('yarn.lock') : files.includes('yarn.lock')) { + console.error('pr-check.js - any update to package.json or yarn.lock ' + + 'must include the other file. Please update through yarn.'); + process.exit(1); + } + + const sortedBuildTargets = []; + for (const t of buildTargets) { + sortedBuildTargets.push(t); + } + sortedBuildTargets.sort(); + + console.log( + '\npr-check.js: detected build targets: ' + + sortedBuildTargets.join(', ') + '\n'); + + if (buildTargets.has('BUILD_SYSTEM')) { + command.testBuildSystem(); + } + + if (buildTargets.has('RUNTIME')) { + command.buildRuntime(); + } + + // Presubmit needs to run after `gulp dist` as some checks runs through the + // dist/ folder. + // Also presubmit always needs to run even for just docs to check for + // copyright at the top. + command.presubmit(); + + if (buildTargets.has('RUNTIME')) { + command.testRuntime(); + } + + if (buildTargets.has('VALIDATOR_WEBUI')) { + command.buildValidatorWebUI(); + } + + if (buildTargets.has('VALIDATOR')) { + command.buildValidator(); + } + + return 0; +} + +process.exit(main()); diff --git a/build-system/runner/dist/runner.jar b/build-system/runner/dist/runner.jar index 486a23fd4a6b..746e60f689da 100644 Binary files a/build-system/runner/dist/runner.jar and b/build-system/runner/dist/runner.jar differ diff --git a/build-system/runner/src/org/ampproject/AmpCodingConvention.java b/build-system/runner/src/org/ampproject/AmpCodingConvention.java index 92e6ac4de302..6fdb01c949ca 100644 --- a/build-system/runner/src/org/ampproject/AmpCodingConvention.java +++ b/build-system/runner/src/org/ampproject/AmpCodingConvention.java @@ -17,6 +17,7 @@ package org.ampproject; import com.google.common.collect.ImmutableList; +import com.google.javascript.jscomp.ClosureCodingConvention.AssertFunctionByTypeName; import com.google.javascript.jscomp.CodingConvention; import com.google.javascript.jscomp.CodingConvention.AssertionFunctionSpec; import com.google.javascript.jscomp.CodingConventions; @@ -45,8 +46,10 @@ public AmpCodingConvention(CodingConvention convention) { return ImmutableList.of( new AssertionFunctionSpec("user.assert", JSType.TRUTHY), new AssertionFunctionSpec("dev.assert", JSType.TRUTHY), - new AssertionFunctionSpec("module$src$log.user.assert", JSType.TRUTHY), - new AssertionFunctionSpec("module$src$log.dev.assert", JSType.TRUTHY) + new AssertionFunctionSpec("Log$$module$src$log.prototype.assert", JSType.TRUTHY), + new AssertFunctionByTypeName("Log$$module$src$log.prototype.assertElement", "Element"), + new AssertFunctionByTypeName("Log$$module$src$log.prototype.assertString", "string"), + new AssertFunctionByTypeName("Log$$module$src$log.prototype.assertNumber", "number") ); } @@ -58,9 +61,22 @@ public AmpCodingConvention(CodingConvention convention) { * delivery), this could go away there. */ @Override public boolean isExported(String name, boolean local) { + // This stops compiler from inlining functions (local or not) that end with + // NoInline in their name. Mostly used for externing try-catch to avoid v8 + // de-optimization (https://goo.gl/gvzlDp) + if (name.endsWith("NoInline")) { + return true; + } + if (local) { return false; } + // This is a special case, of compiler generated super globals. + // Because we otherwise use ES6 modules throughout, we don't + // have any other similar variables. + if (name.startsWith("JSCompiler_")) { + return false; + } // ES6 generated module names are not exported. if (name.contains("$")) { return false; diff --git a/build-system/runner/src/org/ampproject/AmpCommandLineRunner.java b/build-system/runner/src/org/ampproject/AmpCommandLineRunner.java index 778f1dc73b1e..04f9768e2a7b 100644 --- a/build-system/runner/src/org/ampproject/AmpCommandLineRunner.java +++ b/build-system/runner/src/org/ampproject/AmpCommandLineRunner.java @@ -16,14 +16,18 @@ package org.ampproject; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.CommandLineRunner; import com.google.javascript.jscomp.CompilerOptions; import com.google.javascript.jscomp.CustomPassExecutionTime; import com.google.javascript.jscomp.PropertyRenamingPolicy; import com.google.javascript.jscomp.VariableRenamingPolicy; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.Node; import java.io.IOException; +import java.util.Set; /** @@ -36,11 +40,27 @@ public class AmpCommandLineRunner extends CommandLineRunner { */ private boolean typecheck_only = false; + private boolean pseudo_names = false; + + private boolean is_production_env = true; + /** * List of string suffixes to eliminate from the AST. */ - ImmutableSet suffixTypes = ImmutableSet.of( - "dev.fine"); + ImmutableMap> suffixTypes = ImmutableMap.of( + "module$src$log.dev", ImmutableSet.of( + "assert", "fine", "assertElement", "assertString", + "assertNumber", "assertBoolean"), + "module$src$log.user", ImmutableSet.of("fine")); + + + ImmutableMap assignmentReplacements = ImmutableMap.of( + "IS_MINIFIED", + IR.trueNode()); + + ImmutableMap prodAssignmentReplacements = ImmutableMap.of( + "IS_DEV", + IR.falseNode()); protected AmpCommandLineRunner(String[] args) { super(args); @@ -52,7 +72,8 @@ protected AmpCommandLineRunner(String[] args) { } CompilerOptions options = super.createOptions(); options.setCollapseProperties(true); - AmpPass ampPass = new AmpPass(getCompiler(), suffixTypes); + AmpPass ampPass = new AmpPass(getCompiler(), is_production_env, suffixTypes, + assignmentReplacements, prodAssignmentReplacements); options.addCustomPass(CustomPassExecutionTime.BEFORE_OPTIMIZATIONS, ampPass); options.setDevirtualizePrototypeMethods(true); options.setExtractPrototypeMemberDeclarations(true); @@ -70,6 +91,7 @@ protected AmpCommandLineRunner(String[] args) { options.setRenamingPolicy(VariableRenamingPolicy.ALL, PropertyRenamingPolicy.ALL_UNQUOTED); options.setDisambiguatePrivateProperties(true); + options.setGeneratePseudoNames(pseudo_names); return options; } @@ -85,21 +107,21 @@ protected AmpCommandLineRunner(String[] args) { protected CompilerOptions createTypeCheckingOptions() { CompilerOptions options = super.createOptions(); options.setCheckTypes(true); + options.setInferTypes(true); return options; } - protected void setTypeCheckOnly(boolean value) { - typecheck_only = value; - } - public static void main(String[] args) { AmpCommandLineRunner runner = new AmpCommandLineRunner(args); // Scan for TYPECHECK_ONLY string which we pass in as a --define for (String arg : args) { - if (arg.contains("TYPECHECK_ONLY=true")) { - runner.setTypeCheckOnly(true); - break; + if (arg.contains("--define=TYPECHECK_ONLY=true")) { + runner.typecheck_only = true; + } else if (arg.contains("--define=FORTESTING=true")) { + runner.is_production_env = false; + } else if (arg.contains("--define=PSEUDO_NAMES=true")) { + runner.pseudo_names = true; } } diff --git a/build-system/runner/src/org/ampproject/AmpPass.java b/build-system/runner/src/org/ampproject/AmpPass.java index fbbcf3fabeb5..d79c8389b487 100644 --- a/build-system/runner/src/org/ampproject/AmpPass.java +++ b/build-system/runner/src/org/ampproject/AmpPass.java @@ -15,12 +15,15 @@ */ package org.ampproject; +import java.util.Map; import java.util.Set; +import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.AbstractCompiler; import com.google.javascript.jscomp.HotSwapCompilerPass; import com.google.javascript.jscomp.NodeTraversal; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; +import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; /** @@ -40,11 +43,19 @@ class AmpPass extends AbstractPostOrderCallback implements HotSwapCompilerPass { final AbstractCompiler compiler; - private final Set stripTypeSuffixes; + private final Map> stripTypeSuffixes; + private final Map assignmentReplacements; + private final Map prodAssignmentReplacements; + final boolean isProd; - public AmpPass(AbstractCompiler compiler, Set stripTypeSuffixes) { + public AmpPass(AbstractCompiler compiler, boolean isProd, + Map> stripTypeSuffixes, + Map assignmentReplacements, Map prodAssignmentReplacements) { this.compiler = compiler; this.stripTypeSuffixes = stripTypeSuffixes; + this.isProd = isProd; + this.assignmentReplacements = assignmentReplacements; + this.prodAssignmentReplacements = prodAssignmentReplacements; } @Override public void process(Node externs, Node root) { @@ -56,73 +67,245 @@ public AmpPass(AbstractCompiler compiler, Set stripTypeSuffixes) { } @Override public void visit(NodeTraversal t, Node n, Node parent) { - if (isDevAssertCall(n)) { + if (isCallRemovable(n)) { maybeEliminateCallExceptFirstParam(n, parent); - } else if (n.isExprResult()) { - maybeEliminateExpressionBySuffixName(n, parent); + } else if (isAmpExtensionCall(n)) { + inlineAmpExtensionCall(n, parent); + // Remove any `getMode().localDev` and `getMode().test` calls and replace it with `false`. + } else if (isProd && isFunctionInvokeAndPropAccess(n, "$mode.getMode", + ImmutableSet.of("localDev", "test"))) { + replaceWithBooleanExpression(false, n, parent); + // Remove any `getMode().minified` calls and replace it with `true`. + } else if (isProd && isFunctionInvokeAndPropAccess(n, "$mode.getMode", + ImmutableSet.of("minified"))) { + replaceWithBooleanExpression(true, n, parent); + } else { + if (isProd) { + maybeReplaceRValueInVar(n, prodAssignmentReplacements); + } + maybeReplaceRValueInVar(n, assignmentReplacements); } } - private boolean isDevAssertCall(Node n) { - if (n.isCall()) { - Node expression = n.getFirstChild(); - if (expression == null) { - return false; - } + /** + * We don't care about the deep GETPROP. What we care about is finding a + * call which has an `extension` name which then has `AMP` as its + * previous getprop or name, and has a function as the 2nd argument. + * + * CALL 3 [length: 96] [source_file: input0] + * GETPROP 3 [length: 37] [source_file: input0] + * GETPROP 3 [length: 24] [source_file: input0] + * GETPROP 3 [length: 20] [source_file: input0] + * NAME self 3 [length: 4] [source_file: input0] + * STRING someproperty 3 [length: 15] [source_file: input0] + * STRING AMP 3 [length: 3] [source_file: input0] + * STRING extension 3 [length: 12] [source_file: input0] + * STRING some-string 3 [length: 9] [source_file: input0] + * FUNCTION 3 [length: 46] [source_file: input0] + */ + private boolean isAmpExtensionCall(Node n) { + if (n != null && n.isCall()) { + Node getprop = n.getFirstChild(); - String name = expression.getQualifiedName(); - if (name == null) { - return false; + // The AST has the last getprop higher in the hierarchy. + if (isGetPropName(getprop, "extension")) { + Node firstChild = getprop.getFirstChild(); + // We have to handle both explicit/implicit top level `AMP` + if ((firstChild != null && firstChild.isName() && + firstChild.getString() == "AMP") || + isGetPropName(firstChild, "AMP")) { + // Child at index 1 should be the "string" value (first argument) + Node func = getAmpExtensionCallback(n); + return func != null && func.isFunction(); + } } + } + return false; + } + + private boolean isGetPropName(Node n, String name) { + if (n != null && n.isGetProp()) { + Node nodeName = n.getSecondChild(); + return nodeName != null && nodeName.isString() && + nodeName.getString() == name; + } + return false; + } + + /** + * This operation should be guarded stringently by `isAmpExtensionCall` + * predicate. + * + * AMP.extension('some-name', '0.1', function(AMP) { + * // BODY... + * }); + * + * is turned into: + * (function(AMP) { + * // BODY... + * })(self.AMP); + */ + private void inlineAmpExtensionCall(Node n, Node expr) { + if (expr == null || !expr.isExprResult()) { + return; + } + Node func = getAmpExtensionCallback(n); + func.detachFromParent(); + Node arg1 = IR.getprop(IR.name("self"), IR.string("AMP")); + arg1.setLength("self.AMP".length()); + arg1.useSourceInfoIfMissingFromForTree(func); + Node newcall = IR.call(func); + newcall.putBooleanProp(Node.FREE_CALL, true); + newcall.addChildToBack(arg1); + expr.replaceChild(n, newcall); + compiler.reportCodeChange(); + } + + private Node getAmpExtensionCallback(Node n) { + return n.getLastChild(); + } + + /** + * For a function that looks like: + * function fun(val) { + * return dev().assert(val); + * } + * + * The AST would look like: + * RETURN 24 [length: 25] [source_file: ./src/main.js] + * CALL 24 [length: 17] [source_file: ./src/main.js] + * GETPROP 24 [length: 12] [source_file: ./src/main.js] + * CALL 24 [length: 5] [source_file: ./src/main.js] + * NAME $dev$$module$src$log$$ 38 [length: 3] [originalname: dev] [source_file: ./src/log.js] + * STRING assert 24 [length: 6] [source_file: ./src/main.js] + * NAME $val$$ 24 [length: 3] [source_file: ./src/main.js] + * + * We are looking for the `CALL` that has a child NAME "$dev$$module$src$log$$" (or any signature from keys) + * and a child STRING "assert" (or any other signature from Set value) + */ + private boolean isCallRemovable(Node n) { + if (n == null || !n.isCall()) { + return false; + } + + Node callGetprop = n.getFirstChild(); + if (callGetprop == null || !callGetprop.isGetProp()) { + return false; + } - if (name.endsWith("dev.assert")) { + Node parentCall = callGetprop.getFirstChild(); + if (parentCall == null || !parentCall.isCall()) { + return false; + } + + Node parentCallGetprop = parentCall.getFirstChild(); + Node methodName = parentCall.getNext(); + if (parentCallGetprop == null || !parentCallGetprop.isGetProp() || + methodName == null || !methodName.isString()) { + return false; + } + + String parentMethodName = parentCallGetprop.getQualifiedName(); + Set methodCallNames = stripTypeSuffixes.get(parentMethodName); + if (methodCallNames == null) { + return false; + } + + for (String methodCallName : methodCallNames) { + if (methodCallName == methodName.getString()) { return true; } } return false; } + private void maybeReplaceRValueInVar(Node n, Map map) { + if (n != null && (n.isVar() || n.isLet() || n.isConst())) { + Node varNode = n.getFirstChild(); + if (varNode != null) { + for (Map.Entry mapping : map.entrySet()) { + if (varNode.getString() == mapping.getKey()) { + varNode.replaceChild(varNode.getFirstChild(), mapping.getValue()); + compiler.reportCodeChange(); + return; + } + } + } + } + } + /** - * Checks if expression is a GETPROP() (method invocation) and the property - * name ends with one of the items in stripTypeSuffixes. + * Predicate for any fnQualifiedName.props call. + * example: + * isFunctionInvokeAndPropAccess(n, "getMode", "test"); // matches `getMode().test` */ - private void maybeEliminateExpressionBySuffixName(Node n, Node parent) { - // n = EXPRESSION_RESULT > CALL > GETPROP + private boolean isFunctionInvokeAndPropAccess(Node n, String fnQualifiedName, Set props) { + // mode.getMode().localDev + // mode [property] -> + // getMode [call] + // ${property} [string] + if (!n.isGetProp()) { + return false; + } Node call = n.getFirstChild(); - if (call == null) { - return; + if (!call.isCall()) { + return false; } - Node expression = call.getFirstChild(); - if (expression == null) { - return; + Node fullQualifiedFnName = call.getFirstChild(); + if (fullQualifiedFnName == null) { + return false; } - if (qualifiedNameEndsWithStripType(expression)) { - if (parent.isExprResult()) { - Node grandparent = parent.getParent(); - grandparent.removeChild(parent); - } else { - parent.removeChild(n); + String qualifiedName = fullQualifiedFnName.getQualifiedName(); + if (qualifiedName != null && qualifiedName.endsWith(fnQualifiedName)) { + Node maybeProp = n.getSecondChild(); + if (maybeProp != null && maybeProp.isString()) { + String name = maybeProp.getString(); + for (String prop : props) { + if (prop == name) { + return true; + } + } } - compiler.reportCodeChange(); } + + return false; } - private void maybeEliminateCallExceptFirstParam(Node n, Node p) { - Node call = n.getFirstChild(); - if (call == null) { - return; + private void replaceWithBooleanExpression(boolean bool, Node n, Node parent) { + Node booleanNode = bool ? IR.trueNode() : IR.falseNode(); + booleanNode.useSourceInfoIfMissingFrom(n); + parent.replaceChild(n, booleanNode); + compiler.reportCodeChange(); + } + + private void removeExpression(Node n, Node parent) { + if (parent.isExprResult()) { + Node grandparent = parent.getParent(); + grandparent.removeChild(parent); + } else { + parent.removeChild(n); } + compiler.reportCodeChange(); + } - Node firstArg = call.getNext(); + private void maybeEliminateCallExceptFirstParam(Node call, Node parent) { + // Extra precaution if the item we're traversing has already been detached. + if (call == null || parent == null) { + return; + } + Node getprop = call.getFirstChild(); + if (getprop == null) { + return; + } + Node firstArg = getprop.getNext(); if (firstArg == null) { - p.removeChild(n); - compiler.reportCodeChange(); + removeExpression(call, parent); return; } firstArg.detachFromParent(); - p.replaceChild(n, firstArg); + parent.replaceChild(call, firstArg); compiler.reportCodeChange(); } @@ -130,17 +313,17 @@ private void maybeEliminateCallExceptFirstParam(Node n, Node p) { * Checks the nodes qualified name if it ends with one of the items in * stripTypeSuffixes */ - boolean qualifiedNameEndsWithStripType(Node n) { + boolean qualifiedNameEndsWithStripType(Node n, Set suffixes) { String name = n.getQualifiedName(); - return qualifiedNameEndsWithStripType(name); + return qualifiedNameEndsWithStripType(name, suffixes); } /** * Checks if the string ends with one of the items in stripTypeSuffixes */ - boolean qualifiedNameEndsWithStripType(String name) { + boolean qualifiedNameEndsWithStripType(String name, Set suffixes) { if (name != null) { - for (String suffix : stripTypeSuffixes) { + for (String suffix : suffixes) { if (name.endsWith(suffix)) { return true; } diff --git a/build-system/runner/test/org/ampproject/AmpPassTest.java b/build-system/runner/test/org/ampproject/AmpPassTest.java index 069b7899185f..9a1613727ae6 100644 --- a/build-system/runner/test/org/ampproject/AmpPassTest.java +++ b/build-system/runner/test/org/ampproject/AmpPassTest.java @@ -2,10 +2,15 @@ package org.ampproject; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.Compiler; import com.google.javascript.jscomp.CompilerPass; import com.google.javascript.jscomp.Es6CompilerTestCase; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.Node; /** @@ -13,11 +18,22 @@ */ public class AmpPassTest extends Es6CompilerTestCase { - ImmutableSet suffixTypes = ImmutableSet.of( - "dev.fine"); + ImmutableMap> suffixTypes = ImmutableMap.of( + "module$src$log.dev", + ImmutableSet.of("assert", "fine", "assertElement", "assertString", "assertNumber"), + "module$src$log.user", ImmutableSet.of("fine")); + + ImmutableMap assignmentReplacements = ImmutableMap.of( + "IS_MINIFIED", + IR.trueNode()); + + ImmutableMap prodAssignmentReplacements = ImmutableMap.of( + "IS_DEV", + IR.falseNode()); @Override protected CompilerPass getProcessor(Compiler compiler) { - return new AmpPass(compiler, suffixTypes); + return new AmpPass(compiler, /* isProd */ true, suffixTypes, assignmentReplacements, + prodAssignmentReplacements); } @Override protected int getNumRepetitions() { @@ -29,25 +45,26 @@ public void testDevFineRemoval() throws Exception { test( LINE_JOINER.join( "(function() {", - " var log = { dev: { fine: function() {} } };", - " log.dev.fine('hello world');", + " var module$src$log = { dev: function() { return { fine: function() {}} } };", + " module$src$log.dev().fine('hello world');", " console.log('this is preserved');", "})()"), LINE_JOINER.join( "(function() {", - " var log = { dev: { fine: function() {} } };", + " var module$src$log = { dev: function() { return { fine: function() {}} } };", + " 'hello world';", " console.log('this is preserved');", "})()")); test( LINE_JOINER.join( "(function() {", - " var log = { dev: { fine: function() {} } };", - " log.dev.fine();", + " var module$src$log = { dev: function() { return { fine: function() {}} } };", + " module$src$log.dev().fine();", " console.log('this is preserved');", "})()"), LINE_JOINER.join( "(function() {", - " var log = { dev: { fine: function() {} } };", + " var module$src$log = { dev: function() { return { fine: function() {}} } };", " console.log('this is preserved');", "})()")); } @@ -72,26 +89,26 @@ public void testDevAssertExpressionRemoval() throws Exception { test( LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", - " log.dev.assert('hello world');", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " module$src$log.dev().assert('hello world');", " console.log('this is preserved');", "})()"), LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", - "\"hello world\";", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " \"hello world\";", " console.log('this is preserved');", "})()")); test( LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", - " var someValue = log.dev.assert();", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " var someValue = module$src$log.dev().assert();", " console.log('this is preserved', someValue);", "})()"), LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", " var someValue;", " console.log('this is preserved', someValue);", "})()")); @@ -101,16 +118,32 @@ public void testDevAssertPreserveFirstArg() throws Exception { test( LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", - " var someValue = log.dev.assert(true, 'This is an error');", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " var someValue = module$src$log.dev().assert(true, 'This is an error');", " console.log('this is preserved', someValue);", "})()"), LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", " var someValue = true;", " console.log('this is preserved', someValue);", "})()")); + + test( + LINE_JOINER.join( + "(function() {", + " function add(a, b) { return a + b; }", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " var someValue = add(module$src$log.dev().assert(3), module$src$log.dev().assert(3));", + " console.log('this is preserved', someValue);", + "})()"), + LINE_JOINER.join( + "(function() {", + " function add(a, b) { return a + b; }", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " var someValue = add(3, 3);", + " console.log('this is preserved', someValue);", + "})()")); } public void testShouldPreserveNoneCalls() throws Exception { @@ -118,15 +151,208 @@ public void testShouldPreserveNoneCalls() throws Exception { // Does reliasing LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", - " var someValue = log.dev.assert;", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " var someValue = module$src$log.dev().assert;", " console.log('this is preserved', someValue);", "})()"), LINE_JOINER.join( "(function() {", - " var log = { dev: { assert: function() {} } };", - " var someValue = log.dev.assert;", + " var module$src$log = { dev: function() { return { assert: function() {}} } };", + " var someValue = module$src$log.dev().assert;", " console.log('this is preserved', someValue);", "})()")); } + + public void testGetModeLocalDevPropertyReplacement() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { localDev: true } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().localDev) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { localDev: true }; }", + "var $mode = { getMode: getMode };", + " if (false) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testGetModeTestPropertyReplacement() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { test: true } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().test) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { test: true }; }", + "var $mode = { getMode: getMode };", + " if (false) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testGetModeMinifiedPropertyReplacement() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().minified) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false }; }", + "var $mode = { getMode: getMode };", + " if (true) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testGetModeWinTestPropertyReplacement() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { test: true } }", + "var win = {};", + "var $mode = { getMode: getMode };", + " if ($mode.getMode(win).test) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { test: true }; }", + "var win = {};", + "var $mode = { getMode: getMode };", + " if (false) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testGetModeWinMinifiedPropertyReplacement() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false } }", + "var win = {};", + "var $mode = { getMode: getMode };", + " if ($mode.getMode(win).minified) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false }; }", + "var win = {};", + "var $mode = { getMode: getMode };", + " if (true) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testGetModePreserve() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode()) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false }; }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode()) {", + " console.log('hello world');", + " }", + "})()")); + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { otherProp: true } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().otherProp) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { otherProp: true }; }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().otherProp) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testOptimizeGetModeFunction() throws Exception { + testEs6( + LINE_JOINER.join( + "(function() {", + "const IS_DEV = true;", + "const IS_MINIFIED = false;", + "const IS_SOMETHING = true;", + "})()"), + LINE_JOINER.join( + "(function() {", + "const IS_DEV = false;", + "const IS_MINIFIED = true;", + "const IS_SOMETHING = true;", + "})()")); + } + + public void testRemoveAmpAddExtensionCallWithExplicitContext() throws Exception { + testEs6( + LINE_JOINER.join( + "var a = 'hello';", + "self.AMP.extension('hello', '0.1', function(AMP) {", + " var a = 'world';", + " console.log(a);", + "});", + "console.log(a);"), + LINE_JOINER.join( + "var a = 'hello';", + "(function(AMP) {", + " var a = 'world';", + " console.log(a);", + "})(self.AMP);", + "console.log(a);")); + } + + public void testRemoveAmpAddExtensionCallWithNoContext() throws Exception { + testEs6( + LINE_JOINER.join( + "var a = 'hello';", + "AMP.extension('hello', '0.1', function(AMP) {", + " var a = 'world';", + " console.log(a);", + "});", + "console.log(a);"), + LINE_JOINER.join( + "var a = 'hello';", + "(function(AMP) {", + " var a = 'world';", + " console.log(a);", + "})(self.AMP);", + "console.log(a);")); + } } diff --git a/build-system/runner/test/org/ampproject/AmpPassTestEnvTest.java b/build-system/runner/test/org/ampproject/AmpPassTestEnvTest.java new file mode 100644 index 000000000000..8a5a9a4ae556 --- /dev/null +++ b/build-system/runner/test/org/ampproject/AmpPassTestEnvTest.java @@ -0,0 +1,114 @@ +package org.ampproject; + + +import java.util.Set; + +import com.google.common.collect.ImmutableMap; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.Node; +import com.google.common.collect.ImmutableSet; +import com.google.javascript.jscomp.Compiler; +import com.google.javascript.jscomp.CompilerPass; +import com.google.javascript.jscomp.Es6CompilerTestCase; + + +/** + * Tests {@link AmpPass}. + */ +public class AmpPassTestEnvTest extends Es6CompilerTestCase { + + ImmutableMap> suffixTypes = ImmutableMap.of(); + ImmutableMap assignmentReplacements = ImmutableMap.of( + "IS_MINIFIED", + IR.trueNode()); + + ImmutableMap prodAssignmentReplacements = ImmutableMap.of( + "IS_DEV", + IR.falseNode()); + + @Override protected CompilerPass getProcessor(Compiler compiler) { + return new AmpPass(compiler, /* isProd */ false, suffixTypes, assignmentReplacements, + prodAssignmentReplacements); + } + + @Override protected int getNumRepetitions() { + // This pass only runs once. + return 1; + } + + public void testGetModeLocalDevPropertyReplacementInTestingEnv() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { localDev: true } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().localDev) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { localDev: true }; }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().localDev) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testGetModeTestPropertyReplacementInTestingEnv() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { test: true } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().test) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { test: true }; }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().test) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testGetModeMinifiedPropertyReplacementInTestingEnv() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false } }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().minified) {", + " console.log('hello world');", + " }", + "})()"), + LINE_JOINER.join( + "(function() {", + "function getMode() { return { minified: false }; }", + "var $mode = { getMode: getMode };", + " if ($mode.getMode().minified) {", + " console.log('hello world');", + " }", + "})()")); + } + + public void testOptimizeGetModeFunction() throws Exception { + testEs6( + LINE_JOINER.join( + "(function() {", + "const IS_DEV = true;", + "const IS_MINIFIED = false;", + "const IS_SOMETHING = true;", + "})()"), + LINE_JOINER.join( + "(function() {", + "const IS_DEV = true;", + "const IS_MINIFIED = true;", + "const IS_SOMETHING = true;", + "})()")); + } +} diff --git a/build-system/server-a4a-template.html b/build-system/server-a4a-template.html new file mode 100644 index 000000000000..f2c1f0e24510 --- /dev/null +++ b/build-system/server-a4a-template.html @@ -0,0 +1,27 @@ + + + + + A4A Envelope + + + + + + +

A4A Envelope

+
3p: FORCE3P
+
url: AD_URL
+
size: AD_WIDTHxAD_HEIGHT
+ + + + + + diff --git a/build-system/server.js b/build-system/server.js index 3e0f31a72a9f..6c3c2b456f1a 100644 --- a/build-system/server.js +++ b/build-system/server.js @@ -20,8 +20,8 @@ */ var BBPromise = require('bluebird'); var app = require('express')(); +var bacon = require('baconipsum'); var bodyParser = require('body-parser'); -var morgan = require('morgan'); var fs = BBPromise.promisifyAll(require('fs')); var formidable = require('formidable'); var jsdom = require('jsdom'); @@ -30,10 +30,34 @@ var request = require('request'); var url = require('url'); app.use(bodyParser.json()); -app.use(morgan('dev')); -app.use('/examples', function(req, res) { - res.redirect('/examples.build'); +app.use('/pwa', function(req, res, next) { + var file; + var contentType; + if (!req.url || req.url == '/') { + // pwa.html + contentType = 'text/html'; + file = '/examples/pwa/pwa.html'; + } else if (req.url == '/pwa.js') { + // pwa.js + contentType = 'application/javascript'; + file = '/examples/pwa/pwa.js'; + } else if (req.url == '/pwa-sw.js') { + // pwa.js + contentType = 'application/javascript'; + file = '/examples/pwa/pwa-sw.js'; + } else { + // Redirect to the underlying resource. + // TODO(dvoytenko): would be nicer to do forward instead of redirect. + res.writeHead(302, {'Location': req.url}); + res.end(); + return; + } + res.statusCode = 200; + res.setHeader('Content-Type', contentType); + fs.readFileAsync(process.cwd() + file).then((file) => { + res.end(file); + }); }); app.use('/api/show', function(req, res) { @@ -53,7 +77,27 @@ app.use('/api/echo/post', function(req, res) { res.end(JSON.stringify(req.body, null, 2)); }); +/** + * In practice this would be *.ampproject.org and the publishers + * origin. Please see AMP CORS docs for more details: + * https://goo.gl/F6uCAY + * @type {RegExp} + */ +const ORIGIN_REGEX = new RegExp('^http://localhost:8000|' + + '^https?://.+\.herokuapp\.com'); + +/** + * In practice this would be the publishers origin. + * Please see AMP CORS docs for more details: + * https://goo.gl/F6uCAY + * @type {RegExp} + */ +const SOURCE_ORIGIN_REGEX = new RegExp('^http://localhost:8000|' + + '^https?://.+\.herokuapp\.com'); + app.use('/form/html/post', function(req, res) { + assertCors(req, res, ['POST']); + var form = new formidable.IncomingForm(); form.parse(req, function(err, fields) { res.setHeader('Content-Type', 'text/html'); @@ -72,19 +116,122 @@ app.use('/form/html/post', function(req, res) { }); }); + +app.use('/form/redirect-to/post', function(req, res) { + assertCors(req, res, ['POST'], ['AMP-Redirect-To']); + res.setHeader('AMP-Redirect-To', 'https://google.com'); + res.end('{}'); +}); + + +function assertCors(req, res, opt_validMethods, opt_exposeHeaders) { + const validMethods = opt_validMethods || ['GET', 'POST', 'OPTIONS']; + const invalidMethod = req.method + ' method is not allowed. Use POST.'; + const invalidOrigin = 'Origin header is invalid.'; + const invalidSourceOrigin = '__amp_source_origin parameter is invalid.'; + const unauthorized = 'Unauthorized Request'; + var origin; + + if (validMethods.indexOf(req.method) == -1) { + res.statusCode = 405; + res.end(JSON.stringify({message: invalidMethod})); + throw invalidMethod; + } + + if (req.headers.origin) { + origin = req.headers.origin; + if (!ORIGIN_REGEX.test(req.headers.origin)) { + res.statusCode = 500; + res.end(JSON.stringify({message: invalidOrigin})); + throw invalidOrigin; + } + + if (!SOURCE_ORIGIN_REGEX.test(req.query.__amp_source_origin)) { + res.statusCode = 500; + res.end(JSON.stringify({message: invalidSourceOrigin})); + throw invalidSourceOrigin; + } + } else if (req.headers['amp-same-origin'] == 'true') { + origin = getUrlPrefix(req); + } else { + res.statusCode = 401; + res.end(JSON.stringify({message: unauthorized})); + throw unauthorized; + } + + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Expose-Headers', + ['AMP-Access-Control-Allow-Source-Origin'] + .concat(opt_exposeHeaders || []).join(', ')); + res.setHeader('AMP-Access-Control-Allow-Source-Origin', + req.query.__amp_source_origin); +} + app.use('/form/echo-json/post', function(req, res) { + assertCors(req, res, ['POST']); var form = new formidable.IncomingForm(); form.parse(req, function(err, fields) { - res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Type', 'application/json; charset=utf-8'); if (fields['email'] == 'already@subscribed.com') { res.statusCode = 500; } - res.setHeader('AMP-Access-Control-Allow-Source-Origin', - req.protocol + '://' + req.headers.host); res.end(JSON.stringify(fields)); }); }); +app.use('/form/json/poll1', function(req, res) { + assertCors(req, res, ['POST']); + var form = new formidable.IncomingForm(); + form.parse(req, function(err, fields) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ + result: [{ + answer: 'Penguins', + percentage: new Array(77), + }, { + answer: 'Ostriches', + percentage: new Array(8), + }, { + answer: 'Kiwis', + percentage: new Array(14), + }, { + answer: 'Wekas', + percentage: new Array(1), + },] + })); + }); +}); + +app.use('/form/search-html/get', function(req, res) { + res.setHeader('Content-Type', 'text/html'); + res.end(` +

Here's results for your search

+
    +
  • Result 1
  • +
  • Result 2
  • +
  • Result 3
  • +
+ `); +}); + + +app.use('/form/search-json/get', function(req, res) { + assertCors(req, res, ['GET']); + res.json({ + results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}] + }); +}); + + +app.use('/share-tracking/get-outgoing-fragment', function(req, res) { + res.setHeader('AMP-Access-Control-Allow-Source-Origin', + req.protocol + '://' + req.headers.host); + res.json({ + fragment: '54321' + }); +}); + // Fetches an AMP document from the AMP proxy and replaces JS // URLs, so that they point to localhost. function proxyToAmpProxy(req, res, minify) { @@ -110,21 +257,23 @@ function proxyToAmpProxy(req, res, minify) { }); } -app.use('/examples.build/live-list.amp.max.html', function(req, res) { - res.setHeader('Content-Type', 'text/html'); - res.statusCode = 200; - fs.readFileAsync(process.cwd() + - '/examples.build/live-list.amp.max.html').then((file) => { - res.end(file); - }); -}); - -var liveListUpdateFile = '/examples.build/live-list-update.amp.max.html'; +var liveListUpdateFile = '/examples/live-list-update.amp.html'; var liveListCtr = 0; var itemCtr = 2; var liveListDoc = null; var doctype = '\n'; -app.use(liveListUpdateFile, function(req, res) { +// Only handle min/max +app.use('/examples/live-list-update.amp.(min|max).html', function(req, res) { + var filePath = req.baseUrl; + var mode = getPathMode(filePath); + // When we already have state in memory and user refreshes page, we flush + // the dom we maintain on the server. + if (!('amp_latest_update_time' in req.query) && liveListDoc) { + var outerHTML = liveListDoc.documentElement./*OK*/outerHTML; + outerHTML = replaceUrls(mode, outerHTML); + res.send(`${doctype}${outerHTML}`); + return; + } if (!liveListDoc) { var liveListUpdateFullPath = `${process.cwd()}${liveListUpdateFile}`; var liveListFile = fs.readFileSync(liveListUpdateFullPath); @@ -132,9 +281,10 @@ app.use(liveListUpdateFile, function(req, res) { } var action = Math.floor(Math.random() * 3); var liveList = liveListDoc.querySelector('#my-live-list'); + var perPage = Number(liveList.getAttribute('data-max-items-per-page')); + var items = liveList.querySelector('[items]'); + var pagination = liveListDoc.querySelector('#my-live-list [pagination]'); var item1 = liveList.querySelector('#list-item-1'); - res.setHeader('Content-Type', 'text/html'); - res.statusCode = 200; if (liveListCtr != 0) { if (Math.random() < .8) { // Always run a replace on the first item @@ -147,15 +297,27 @@ app.use(liveListUpdateFile, function(req, res) { if (Math.random() < .8) { liveListInsert(liveList, item1); } + pagination.textContent = ''; + var liveChildren = [].slice.call(items.children) + .filter(x => !x.hasAttribute('data-tombstone')); + + var pageCount = Math.ceil(liveChildren.length / perPage); + var pageListItems = Array.apply(null, Array(pageCount)) + .map((_, i) => `
  • ${i + 1}
  • `).join(''); + var newPagination = ''; + pagination./*OK*/innerHTML = newPagination; } else { // Sometimes we want an empty response to simulate no changes. - res.end(`${doctype}`); + res.send(`${doctype}`); return; } } var outerHTML = liveListDoc.documentElement./*OK*/outerHTML; - res.end(`${doctype}${outerHTML}`); + outerHTML = replaceUrls(mode, outerHTML); liveListCtr++; + res.send(`${doctype}${outerHTML}`); }); function liveListReplace(item) { @@ -182,13 +344,110 @@ function liveListTombstone(liveList) { // We can tombstone any list item except item-1 since we always do a // replace example on item-1. if (tombstoneId != 1) { - var item = liveList.querySelector(`#list-item-${tombstoneId}`); + var item = liveList./*OK*/querySelector(`#list-item-${tombstoneId}`); if (item) { item.setAttribute('data-tombstone', ''); } } } +function getLiveBlogItem() { + var now = Date.now(); + // Value is inclusive of both min and max values. + function range(min, max) { + var values = Array.apply(null, Array(max - min + 1)).map((_, i) => min + i); + return values[Math.round(Math.random() * (max - min))] + } + function flip() { + return !!Math.floor(Math.random() * 2); + } + // Generate a 3 to 7 worded headline + var headline = bacon(range(3, 7)); + var numOfParagraphs = range(1, 2); + var body = Array.apply(null, Array(numOfParagraphs)).map(x => { + return `

    ${bacon(range(50, 90))}

    `; + }).join('\n'); + + var img = ` + `; + return ` + + +
    +
    +

    + ${headline} +

    +
    + +
    +
    ${body}
    + ${img} + +
    +
    +
    `; +} + +app.use('/examples/live-blog(-non-floating-button)?.amp.(min.|max.)?html', + function(req, res, next) { + if ('amp_latest_update_time' in req.query) { + res.setHeader('Content-Type', 'text/html'); + res.end(getLiveBlogItem()); + return; + } + next(); +}); + +app.use('/examples/amp-fresh.amp.(min.|max.)?html', function(req, res, next) { + if ('amp-fresh' in req.query && req.query['amp-fresh']) { + res.setHeader('Content-Type', 'text/html'); + res.end(` + + + hello world! + foo bar + + `); + return; + } + next(); +}); + + +app.use('/impression-proxy/', function(req, res) { + assertCors(req, res, ['GET']); + // Fake response with the following optional fields: + // location: The Url the that server would have sent redirect to w/o ALP + // tracking_url: URL that should be requested to track click + // gclid: The conversion tracking value + const body = { + 'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123', + 'tracking_url': 'tracking_url', + 'gclid': '1234', + }; + res.send(body); +}); + // Proxy with unminified JS. // Example: // http://localhost:8000/max/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/ @@ -203,11 +462,221 @@ app.use('/min/', function(req, res) { proxyToAmpProxy(req, res, /* minify */ true); }); -app.use('/examples.build/analytics.config.json', function (req, res, next) { +// A4A envelope. +// Examples: +// http://localhost:8000/a4a[-3p]/examples/animations.amp.max.html +// http://localhost:8000/a4a[-3p]/max/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/ +// http://localhost:8000/a4a[-3p]/min/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/ +app.use('/a4a(|-3p)/', function(req, res) { + var force3p = req.baseUrl.indexOf('/a4a-3p') == 0; + var adUrl = req.url; + var templatePath = '/build-system/server-a4a-template.html'; + var urlPrefix = getUrlPrefix(req); + if (force3p && !adUrl.startsWith('/m') && + urlPrefix.indexOf('//localhost') != -1) { + // This is a special case for testing. `localhost` URLs are transformed to + // `ads.localhost` to ensure that the iframe is fully x-origin. + adUrl = urlPrefix.replace('localhost', 'ads.localhost') + adUrl; + } + fs.readFileAsync(process.cwd() + templatePath, 'utf8').then(template => { + var result = template + .replace(/FORCE3P/g, force3p) + .replace(/AD_URL/g, adUrl) + .replace(/AD_WIDTH/g, req.query.width || '300') + .replace(/AD_HEIGHT/g, req.query.height || '250'); + res.end(result); + }); +}); + +app.use('/examples/analytics.config.json', function(req, res, next) { res.setHeader('AMP-Access-Control-Allow-Source-Origin', getUrlPrefix(req)); next(); }); +app.use(['/examples/*', '/extensions/*'], function (req, res, next) { + var sourceOrigin = req.query['__amp_source_origin']; + if (sourceOrigin) { + res.setHeader('AMP-Access-Control-Allow-Source-Origin', sourceOrigin); + } + next(); +}); + +/** + * Append ?sleep=5 to any included JS file in examples to emulate delay in loading that + * file. This allows you to test issues with your extension being late to load + * and testing user interaction with your element before your code loads. + * + * Example delay loading amp-form script by 5 seconds: + * + */ +app.use(['/dist/v0/amp-*.js'], function(req, res, next) { + var sleep = parseInt(req.query.sleep || 0) * 1000; + setTimeout(next, sleep); +}); + +app.get(['/examples/*', '/test/manual/*'], function(req, res, next) { + var filePath = req.path; + var mode = getPathMode(filePath); + if (!mode) { + return next(); + } + filePath = filePath.substr(0, filePath.length - 9) + '.html'; + fs.readFileAsync(process.cwd() + filePath, 'utf8').then(file => { + file = replaceUrls(mode, file); + + // Extract amp-ad for the given 'type' specified in URL query. + if (req.path.indexOf('/examples/ads.amp') == 0 && req.query.type) { + var ads = file.match(new RegExp('<(amp-ad|amp-embed) [^>]*[\'"]' + + req.query.type + '[\'"][^>]*>([\\s\\S]+?)<\/(amp-ad|amp-embed)>', 'gm')); + file = file.replace( + /[\s\S]+<\/body>/m, '' + ads.join('') + ''); + } + + res.send(file); + }).catch(() => { + next(); + }); +}); + +app.use('/bind/form/get', function(req, res, next) { + assertCors(req, res, ['GET']); + res.json({ + bindXhrResult: 'I was fetched from the server!' + }); +}); + +// Simulated Cloudflare signed Ad server + +const cloudflareDataDir = '/extensions/amp-ad-network-cloudflare-impl/0.1/data'; +const fakeAdNetworkDataDir = '/extensions/amp-ad-network-fake-impl/0.1/data' + +/** + * Handle CORS headers + */ +app.use([cloudflareDataDir], function fakeCors(req, res, next) { + assertCors(req, res, ['GET', 'OPTIONS'], ['X-AmpAdSignature']); + + if (req.method=='OPTIONS') { + res.status(204).end(); + } else { + next(); + } +}); + +/** + * Handle fake a4a data + */ +app.get([ fakeAdNetworkDataDir + '/*', cloudflareDataDir + '/*'], function(req, res) { + var filePath = req.path; + var unwrap = false; + if (req.path.endsWith('.html')) { + filePath = req.path.slice(0,-5) + unwrap = true + } + filePath = process.cwd() + filePath + fs.readFileAsync(filePath).then(file => { + if (!unwrap) { + res.end(file) + return + } + const metadata = JSON.parse(file); + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-AmpAdSignature', metadata.signature); + res.end(metadata.creative); + }).error( () => { + res.status(404); + res.end("Not found: " + filePath); + }); +}); + +/* + * Start Cache SW LOCALDEV section + */ +app.get(['/dist/sw.js', '/dist/sw.max.js'], function(req, res, next) { + var filePath = req.path; + fs.readFileAsync(process.cwd() + filePath, 'utf8').then(file => { + var n = new Date(); + // Round down to the nearest 5 minutes. + n -= ((n.getMinutes() % 5) * 1000 * 60) + (n.getSeconds() * 1000) + n.getMilliseconds(); + res.setHeader('Content-Type', 'application/javascript'); + file = 'self.AMP_CONFIG = {v: "99' + n + '",' + + 'cdnUrl: "http://localhost:8000/dist"};' + + file; + res.end(file); + }).catch(next); +}); + +app.get('/dist/rtv/99*/*.js', function(req, res, next) { + var filePath = req.path.replace(/\/rtv\/\d{15}/, ''); + fs.readFileAsync(process.cwd() + filePath, 'utf8').then(file => { + // Cause a delay, to show the "stale-while-revalidate" + setTimeout(() => { + res.setHeader('Content-Type', 'application/javascript'); + res.end(file); + }, 2000); + }).catch(next); +}); + +app.get(['/dist/cache-sw.min.html', '/dist/cache-sw.max.html'], function(req, res, next) { + var filePath = '/test/manual/cache-sw.html'; + fs.readFileAsync(process.cwd() + filePath, 'utf8').then(file => { + res.setHeader('Content-Type', 'text/html'); + res.end(file); + }).catch(next); +}); +/* + * End Cache SW LOCALDEV section + */ + + + +/** + * @param {string} mode + * @param {string} file + */ +function replaceUrls(mode, file) { + if (mode) { + file = file.replace('https://cdn.ampproject.org/viewer/google/v5.js', 'https://cdn1.ampproject.org/viewer/google/v5.js'); + file = file.replace(/(https:\/\/cdn.ampproject.org\/.+?).js/g, '$1.max.js'); + file = file.replace('https://cdn.ampproject.org/v0.max.js', '/dist/amp.js'); + file = file.replace('https://cdn.ampproject.org/amp4ads-v0.max.js', '/dist/amp-inabox.js'); + file = file.replace(/https:\/\/cdn.ampproject.org\/v0\//g, '/dist/v0/'); + file = file.replace('https://cdn1.ampproject.org/viewer/google/v5.js', 'https://cdn.ampproject.org/viewer/google/v5.js'); + } + if (mode == 'min') { + file = file.replace(/\.max\.js/g, '.js'); + file = file.replace('/dist/amp.js', '/dist/v0.js'); + file = file.replace('/dist/amp-inabox.js', '/dist/amp4ads-v0.js'); + file = file.replace(/\/dist.3p\/current\/(.*)\.max.html/, + '/dist.3p/current-min/$1.html'); + } + return file; +} + +/** + * @param {string} path + * @return {string} + */ +function extractFilePathSuffix(path) { + return path.substr(-9); +} + +/** + * @param {string} path + * @return {?string} + */ +function getPathMode(path) { + var suffix = extractFilePathSuffix(path); + if (suffix == '.max.html') { + return 'max'; + } else if (suffix == '.min.html') { + return 'min'; + } else { + return null; + } +} + exports.app = app; function getUrlPrefix(req) { diff --git a/build-system/tasks/changelog.js b/build-system/tasks/changelog.js index e35cafb830d1..98c6ccf990a0 100644 --- a/build-system/tasks/changelog.js +++ b/build-system/tasks/changelog.js @@ -43,8 +43,16 @@ const pullOptions = { url: 'https://api.github.com/repos/ampproject/amphtml/pulls', headers: { 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json' }, }; +const latestReleaseOptions = { + url: 'https://api.github.com/repos/ampproject/amphtml/releases/latest', + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json' + }, +} if (GITHUB_ACCESS_TOKEN) { pullOptions.qs = { @@ -52,6 +60,12 @@ if (GITHUB_ACCESS_TOKEN) { } } +if (GITHUB_ACCESS_TOKEN) { + latestReleaseOptions.qs = { + access_token: GITHUB_ACCESS_TOKEN + } +} + /** * @typedef {{ * logs: !Array, @@ -222,6 +236,7 @@ function buildChangelog(gitMetadata) { if (!pr) { return true; } + // Ignore pr's that are just all docs changes. return !pr.filenames.every(function(filename) { return config.changelogIgnoreFileTypes.test(filename); }); @@ -237,12 +252,12 @@ function buildChangelog(gitMetadata) { var sections = buildSections(gitMetadata); Object.keys(sections).sort().forEach(function(section) { - changelog += `### ${section}\n\n`; + changelog += `
    \n${section}\n`; var uniqueItems = sections[section].filter(function(title, idx) { return sections[section].indexOf(title) == idx; }); changelog += uniqueItems.join(''); - changelog += '\n'; + changelog += '
    \n'; }); gitMetadata.changelog = changelog; @@ -263,8 +278,7 @@ function buildSections(gitMetadata) { var hasNonDocChange = !pr.filenames.every(function(filename) { return config.changelogIgnoreFileTypes.test(filename); }); - var listItem = ` - ${pr.title.trim()}\n`; - + var listItem = `${pr.title.trim()} (#${pr.id})\n`; if (hasNonDocChange) { changelog += listItem; } @@ -299,7 +313,7 @@ function buildSections(gitMetadata) { // if its the validator section, read the body of the PR // and format it correctly under the bullet list. if (section == 'validator') { - body = `\n ${pr.body.split('\n').join('\n ')}`; + body = `${pr.body}\n`; } sections[section].push(listItem + body); } @@ -314,15 +328,9 @@ function buildSections(gitMetadata) { * @return {!Promise} */ function getLastGitTag(gitMetadata) { - var options = { - args: `describe --abbrev=0 --first-parent --tags ${branch}` - }; - return gitExec(options).then(function(tag) { - if (!tag) { - throw new Error('Could not find latest ' + branch + ' tag.'); - } - gitMetadata.tag = tag.trim(); - util.log(util.colors.green('Current latest tag: ' + gitMetadata.tag)); + return request(latestReleaseOptions).then(res => { + var body = JSON.parse(res.body); + gitMetadata.tag = body.tag_name; return gitMetadata; }); } diff --git a/build-system/tasks/clean.js b/build-system/tasks/clean.js index f563a6203cda..67d335a885f7 100644 --- a/build-system/tasks/clean.js +++ b/build-system/tasks/clean.js @@ -29,7 +29,6 @@ function clean() { 'dist.3p', 'dist.tools', 'build', - 'examples.build', '.amp-build', ]); } diff --git a/build-system/tasks/compile-bind-expr.js b/build-system/tasks/compile-bind-expr.js new file mode 100644 index 000000000000..f24674282713 --- /dev/null +++ b/build-system/tasks/compile-bind-expr.js @@ -0,0 +1,43 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var jison = require('jison'); +var gulp = require('gulp'); +var fs = require('fs-extra'); + +gulp.task('compile-bind-expr', function() { + var path = 'extensions/amp-bind/0.1/'; + + var bnf = fs.readFileSync(path + 'bind-expr-impl.jison', 'utf8'); + var settings = {type: 'lalr', debug: false, moduleType: 'js'}; + var generator = new jison.Generator(bnf, settings); + var jsModule = generator.generate(settings); + + var license = fs.readFileSync( + 'build-system/tasks/js-license.txt', 'utf8'); + var suppressCheckTypes = '/** @fileoverview ' + + '@suppress {checkTypes, suspiciousCode, uselessCode} */'; + var imports = 'import {AstNode, AstNodeType} from \'./bind-expr-defines\';'; + var jsExports = 'exports.parser = parser;'; + + var out = [ + license, + suppressCheckTypes, + imports, + jsModule, + jsExports].join('\n\n') + '\n'; + fs.writeFileSync(path + 'bind-expr-impl.js', out); +}); diff --git a/build-system/tasks/compile.js b/build-system/tasks/compile.js index 69e676a5f967..0accd44abfd6 100644 --- a/build-system/tasks/compile.js +++ b/build-system/tasks/compile.js @@ -16,11 +16,11 @@ var fs = require('fs-extra'); var argv = require('minimist')(process.argv.slice(2)); -var windowConfig = require('../window-config'); var closureCompiler = require('gulp-closure-compiler'); var gulp = require('gulp'); var rename = require('gulp-rename'); var replace = require('gulp-replace'); +var util = require('gulp-util'); var internalRuntimeVersion = require('../internal-version').VERSION; var internalRuntimeToken = require('../internal-version').TOKEN; var rimraf = require('rimraf'); @@ -42,16 +42,26 @@ exports.closureCompile = function(entryModuleFilename, outputDir, inProgress++; compile(entryModuleFilename, outputDir, outputFilename, options) .then(function() { + if (process.env.TRAVIS) { + // When printing simplified log in travis, use dot for each task. + process.stdout.write('.'); + } inProgress--; next(); resolve(); }, function(e) { - console./*OK*/error('Compilation error', e.message); + console./*OK*/error(util.colors.red('Compilation error', + e.message)); process.exit(1); }); } function next() { if (!queue.length) { + // When printing simplified log in travis, print EOF after + // all closure compiling task are done. + if (process.env.TRAVIS) { + process.stdout.write('\n'); + } return; } if (inProgress < MAX_PARALLEL_CLOSURE_INVOCATIONS) { @@ -73,14 +83,22 @@ function cleanupBuildDir() { } exports.cleanupBuildDir = cleanupBuildDir; -function compile(entryModuleFilename, outputDir, +function compile(entryModuleFilenames, outputDir, outputFilename, options) { return new Promise(function(resolve, reject) { + var entryModuleFilename; + if (entryModuleFilenames instanceof Array) { + entryModuleFilename = entryModuleFilenames[0]; + } else { + entryModuleFilename = entryModuleFilenames; + entryModuleFilenames = [entryModuleFilename]; + } const checkTypes = options.checkTypes || argv.typecheck_only; var intermediateFilename = 'build/cc/' + entryModuleFilename.replace(/\//g, '_').replace(/^\./, ''); - console./*OK*/log('Starting closure compiler for', entryModuleFilename); - + if (!process.env.TRAVIS) { + util.log('Starting closure compiler for', entryModuleFilenames); + } // If undefined/null or false then we're ok executing the deletions // and mkdir. if (!options.preventRemoveAndMakeDir) { @@ -89,17 +107,11 @@ function compile(entryModuleFilename, outputDir, var unneededFiles = [ 'build/fake-module/third_party/babel/custom-babel-helpers.js', ]; - var wrapper = (options.includeWindowConfig ? - windowConfig.getTemplate() : '') + - '(function(){var process={env:{NODE_ENV:"production"}};' + - '%output%})();'; + var wrapper = '(function(){%output%})();'; if (options.wrapper) { - wrapper = options.wrapper.replace('<%= contents %>', - // TODO(@cramforce): Switch to define. - 'var process={env:{NODE_ENV:"production"}};%output%'); + wrapper = options.wrapper.replace('<%= contents %>', '%output%'); } - wrapper += '\n//# sourceMappingURL=' + - outputFilename + '.map\n'; + wrapper += '\n//# sourceMappingURL=' + outputFilename + '.map\n'; patchRegisterElement(); if (fs.existsSync(intermediateFilename)) { fs.unlinkSync(intermediateFilename); @@ -114,32 +126,71 @@ function compile(entryModuleFilename, outputDir, internalRuntimeVersion + '/'; } const srcs = [ - '3p/**/*.js', - 'ads/**/*.js', - 'extensions/**/*.js', - 'build/**/*.js', - '!build/cc/**', - '!build/polyfills.js', - '!build/polyfills/**/*.js', - 'src/**/*.js', + '3p/3p.js', + // Ads config files. + 'ads/_*.js', + 'ads/alp/**/*.js', + 'ads/google/**/*.js', + 'ads/inabox/**/*.js', + // Files under build/. Should be sparse. + 'build/css.js', + 'build/*.css.js', + 'build/fake-module/**/*.js', + 'build/patched-module/**/*.js', + 'build/experiments/**/*.js', + // Strange access/login related files. + 'build/all/v0/*.js', + // A4A has these cross extension deps. + 'extensions/**/*-config.js', + 'extensions/amp-ad/**/*.js', + 'extensions/amp-a4a/**/*.js', + // Currently needed for crypto.js and visibility.js. + // Should consider refactoring. + 'extensions/amp-analytics/**/*.js', + 'src/*.js', + 'src/!(inabox)*/**/*.js', '!third_party/babel/custom-babel-helpers.js', // Exclude since it's not part of the runtime/extension binaries. '!extensions/amp-access/0.1/amp-login-done.js', 'builtins/**.js', 'third_party/caja/html-sanitizer.js', 'third_party/closure-library/sha384-generated.js', + 'third_party/css-escape/css-escape.js', 'third_party/mustache/**/*.js', + 'third_party/vega/**/*.js', + 'third_party/d3/**/*.js', + 'third_party/webcomponentsjs/ShadowCSS.js', 'node_modules/promise-pjs/promise.js', + 'node_modules/web-animations-js/web-animations.install.js', 'build/patched-module/document-register-element/build/' + - 'document-register-element.max.js', - 'node_modules/core-js/modules/**.js', + 'document-register-element.node.js', + //'node_modules/core-js/modules/**.js', // Not sure what these files are, but they seem to duplicate code // one level below and confuse the compiler. '!node_modules/core-js/modules/library/**.js', // Don't include tests. '!**_test.js', '!**/test-*.js', + '!**/*.extern.js', ]; + // Add needed path for extensions. + // Instead of globbing all extensions, this will only add the actual + // extension path for much quicker build times. + entryModuleFilenames.forEach(function(filename) { + if (filename.indexOf('extensions/') == -1) { + return; + } + var path = filename.replace(/\/[^/]+\.js$/, '/**/*.js'); + srcs.push(path); + }); + if (options.extraGlobs) { + srcs.push.apply(srcs, options.extraGlobs); + } + if (options.include3pDirectories) { + srcs.push( + '3p/**/*.js', + 'ads/**/*.js') + } // Many files include the polyfills, but we only want to deliver them // once. Since all files automatically wait for the main binary to load // this works fine. @@ -149,14 +200,8 @@ function compile(entryModuleFilename, outputDir, '!build/fake-module/src/polyfills/**/*.js' ); } else { - srcs.push( - '!src/polyfills.js', - '!src/polyfills/**/*.js' - ); - unneededFiles.push( - 'build/fake-module/src/polyfills.js', - 'build/fake-module/src/polyfills/promise.js', - 'build/fake-module/src/polyfills/math-sign.js'); + srcs.push('!src/polyfills.js'); + unneededFiles.push('build/fake-module/src/polyfills.js'); } unneededFiles.forEach(function(fake) { if (!fs.existsSync(fake)) { @@ -166,13 +211,23 @@ function compile(entryModuleFilename, outputDir, } }); + var externs = [ + 'build-system/amp.extern.js', + 'third_party/closure-compiler/externs/intersection_observer.js', + 'third_party/closure-compiler/externs/shadow_dom.js', + 'third_party/closure-compiler/externs/web_animations.js', + ]; + if (options.externs) { + externs = externs.concat(options.externs); + } + /*eslint "google-camelcase/google-camelcase": 0*/ var compilerOptions = { // Temporary shipping with our own compiler that has a single patch // applied compilerPath: 'build-system/runner/dist/runner.jar', fileName: intermediateFilename, - continueWithWarnings: true, + continueWithWarnings: false, tieredCompilation: true, // Magic speed up. compilerFlags: { compilation_level: 'SIMPLE_OPTIMIZATIONS', @@ -181,16 +236,17 @@ function compile(entryModuleFilename, outputDir, // Transpile from ES6 to ES5. language_in: 'ECMASCRIPT6', language_out: 'ECMASCRIPT5', - externs: [ - 'build-system/amp.extern.js', - 'third_party/closure-compiler/externs/intersection_observer.js', - ], + // We do not use the polyfills provided by closure compiler. + // If you need a polyfill. Manually include them in the + // respective top level polyfills.js files. + rewrite_polyfills: false, + externs: externs, js_module_root: [ 'node_modules/', 'build/patched-module/', 'build/fake-module/', ], - entry_point: entryModuleFilename, + entry_point: entryModuleFilenames, process_common_js_modules: true, // This strips all files from the input set that aren't explicitly // required. @@ -200,11 +256,22 @@ function compile(entryModuleFilename, outputDir, source_map_location_mapping: '|' + sourceMapBase, warning_level: 'DEFAULT', + // Turn off warning for "Unknown @define" since we use define to pass + // args such as FORTESTING to our runner. + jscomp_off: ['unknownDefines'], + define: [], hide_warnings_for: [ - 'ads/', // TODO(@cramforce): Remove when we are better at typing. + 'third_party/caja/', + 'third_party/closure-library/sha384-generated.js', + 'third_party/d3/', + 'third_party/vega/', + 'third_party/webcomponentsjs/', 'node_modules/', 'build/patched-module/', + // Can't seem to suppress `(0, win.eval)` suspicious code warning + '3p/environment.js', ], + jscomp_error: [], } }; @@ -212,14 +279,47 @@ function compile(entryModuleFilename, outputDir, if (argv.typecheck_only || checkTypes) { // Don't modify compilation_level to a lower level since // it won't do strict type checking if its whitespace only. - compilerOptions.compilerFlags.define = 'TYPECHECK_ONLY=true'; - compilerOptions.compilerFlags.jscomp_error = 'checkTypes'; + compilerOptions.compilerFlags.define.push('TYPECHECK_ONLY=true'); + compilerOptions.compilerFlags.jscomp_error.push( + 'checkTypes', + 'accessControls', + 'const', + 'constantProperty', + 'globalThis'); + compilerOptions.compilerFlags.conformance_configs = + 'build-system/conformance-config.textproto'; + + // TODO(aghassemi): Remove when NTI is the default. + if (argv.nti) { + compilerOptions.compilerFlags.new_type_inf = true; + compilerOptions.compilerFlags.jscomp_off.push( + 'newCheckTypesExtraChecks'); + compilerOptions.compilerFlags.externs.push( + 'build-system/amp.nti.extern.js' + ); + } else { + compilerOptions.compilerFlags.externs.push( + 'build-system/amp.oti.extern.js' + ); + } + } + if (argv.pseudo_names) { + compilerOptions.compilerFlags.define.push('PSEUDO_NAMES=true'); + } + if (argv.fortesting) { + compilerOptions.compilerFlags.define.push('FORTESTING=true'); + } + + if (compilerOptions.compilerFlags.define.length == 0) { + delete compilerOptions.compilerFlags.define; } var stream = gulp.src(srcs) .pipe(closureCompiler(compilerOptions)) .on('error', function(err) { - console./*OK*/error(err.message); + console./*OK*/error(util.colors.red('Error compiling', + entryModuleFilenames)); + console./*OK*/error(util.colors.red(err.message)); process.exit(1); }); @@ -231,8 +331,10 @@ function compile(entryModuleFilename, outputDir, .pipe(replace(/\$internalRuntimeToken\$/g, internalRuntimeToken)) .pipe(gulp.dest(outputDir)) .on('end', function() { - console./*OK*/log('Compiled', entryModuleFilename, 'to', - outputDir + '/' + outputFilename, 'via', intermediateFilename); + if (!process.env.TRAVIS) { + util.log('Compiled', entryModuleFilename, 'to', + outputDir + '/' + outputFilename, 'via', intermediateFilename); + } gulp.src(intermediateFilename + '.map') .pipe(rename(outputFilename + '.map')) .pipe(gulp.dest(outputDir)) @@ -244,18 +346,34 @@ function compile(entryModuleFilename, outputDir, }; function patchRegisterElement() { + var file; // Copies document-register-element into a new file that has an export. // This works around a bug in closure compiler, where without the // export this module does not generate a goog.provide which fails // compilation. // Details https://github.com/google/closure-compiler/issues/1831 const patchedName = 'build/patched-module/document-register-element' + - '/build/document-register-element.max.js'; + '/build/document-register-element.node.js'; if (!fs.existsSync(patchedName)) { - fs.writeFileSync(patchedName, - fs.readFileSync( - 'node_modules/document-register-element/build/' + - 'document-register-element.max.js') + - '\n\nexport function deadCode() {}\n'); + file = fs.readFileSync( + 'node_modules/document-register-element/build/' + + 'document-register-element.node.js').toString(); + if (argv.fortesting) { + // Need to switch global to self since closure doesn't wrap the module + // like CommonJS + file = file.replace('installCustomElements(global);', + 'installCustomElements(self);'); + } else { + // Get rid of the side effect the module has so we can tree shake it + // better and control installation, unless --fortesting flag + // is passed since we also treat `--fortesting` mode as "dev". + file = file.replace('installCustomElements(global);', ''); } + // Closure Compiler does not generate a `default` property even though + // to interop CommonJS and ES6 modules. This is the same issue typescript + // ran into here https://github.com/Microsoft/TypeScript/issues/2719 + file = file.replace('module.exports = installCustomElements;', + 'exports.default = installCustomElements;'); + fs.writeFileSync(patchedName, file); + } } diff --git a/build-system/tasks/csvify-size/index.js b/build-system/tasks/csvify-size/index.js new file mode 100644 index 000000000000..81f28f61b508 --- /dev/null +++ b/build-system/tasks/csvify-size/index.js @@ -0,0 +1,262 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var BBPromise = require('bluebird'); +var child_process = require('child_process'); +var exec = BBPromise.promisify(child_process.exec); +var fs = BBPromise.promisifyAll(require('fs')); +var gulp = require('gulp-help')(require('gulp')); +var util = require('gulp-util'); + + +var prettyBytesUnits = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + +/** + * @typedef {!Array} + */ +var Tables; + +/** + * @typedef {{ + * name: string, + * dateTime: string, + * size: string + * }} + */ +var Fields; + +var filePath = 'test/size.txt'; + +var fileSizes = Object.create(null); + +var tableHeaders = [ + ['"datetime"'] +]; + +var dateTimes = []; + +/** + * @param {string} format + * @return {!Array} + */ +function getLog(format) { + return exec(`git log --format="${format}" ${filePath}`) + .then(logs => logs.trim().split('\n')); +} + +/** + * @param {string} file + * @return {!Tables} + */ +function parseSizeFile(file) { + var lines = file.trim().split('\n'); + var minSizePos = 0; + var headers = lines[0].trim().split('|').map(x => x.trim()); + var minPos = -1; + // Find the "min" column which is the closure compiled or the "size" column + // which was previously babelify compiled file. + for (var i = 0; i < headers.length; i++) { + if (headers[i] == 'min' || headers[i] == 'size') { + minPos = i; + break; + } + } + + // Remove headers + lines.shift(); + // Remove separator + lines.shift(); + + return lines.map(line => { + var columns = line.split('|').map(x => x.trim()); + var name = columns[columns.length - 1]; + + // Older size.txt files contained duplicate entries of the same "entity", + // for example a file had an entry for its .min and its .max file. + var shouldSkip = (name.endsWith('max.js') && + !name.endsWith('alp.max.js') && !/\s\/\s/.test(name)) + || name == 'current/integration.js' || name == 'amp.js' || + name == 'cc.js' || name.endsWith('-latest.js'); + + + if (shouldSkip) { + return null; + } + + // Normalize names. We made mistakes at some point with duplicate entries + // or renamed entries so we make sure to identify these entities + // and put then into the same column. + if (name == 'v0.js / amp.js' || name == 'current-min/v0.js') { + name = 'v0.js'; + } else if (name == 'current-min/f.js / current/integration.js' || + name == 'current-min/f.js') { + name = 'f.js'; + } else if (name == 'alp.max.js' || name == 'alp.js / install-alp.js' || + name == 'alp.js / alp.max.js') { + name = 'alp.js'; + } else if (name == 'sw.js / sw.max.js') { + name = 'sw.js'; + } else if (name == 'sw-kill.js / sw-kill.max.js') { + name = 'sw-kill.js'; + } else if (name == 'a4a-host-v0.js / amp-inabox-host.js') { + name = 'amp4ads-host-v0.js / amp-inabox-host.js'; + } else if (name == 'a4a-v0.js / amp-inabox.js') { + name = 'amp4ads-v0.js / amp-inabox.js'; + } + + return { + name: `"${name}"`, + size: `"${reversePrettyBytes(columns[minPos])}"`, + }; + }).filter(x => !!x); +} + +/** + * @param {!Array} dateTimes + * @param {!Tables} tables + * @return {!Array>} + */ +function mergeTables(dateTimes, tables) { + // Where key is filename + /** @typedef {!Object>} */ + var obj = Object.create(null); + var rows = []; + + // Aggregate all fields with same file name into an array + tables.forEach(table => { + table.forEach(field => { + var name = field.name; + if (!obj[name]) { + obj[name] = []; + } + obj[name].push({ + size: field.size, + dateTime: field.dateTime, + }); + }); + }); + + // Populate the headers array with unique file names for row 1 + Object.keys(obj).sort().forEach(fileName => { + // TODO(erwinm): figure out where this is occurring. + if (fileName.trim() == '""') { + return; + } + tableHeaders[0].push(fileName); + }); + + // Populate column A with all the dates we've seen and then + // populate all other columns with their respective file size if any. + dateTimes.forEach(dateTime => { + // Seed array with empty string values + var row = Array.apply(null, Array(tableHeaders[0].length)).map(x => '""'); + rows.push(row); + row[0] = dateTime; + // Exclude the datetime column + tableHeaders[0].slice(1).forEach((fileName, colIdx) => { + var colIdx = colIdx + 1; + var curField = null; + for (var i = 0; i < obj[fileName].length; i++) { + curField = obj[fileName][i]; + if (curField.dateTime == dateTime) { + row[colIdx] = curField.size; + break; + } + } + }); + }); + return rows; +} + +/** + * @param {string} prettyBytes + * @return {number} + */ +function reversePrettyBytes(prettyBytes) { + var triple = prettyBytes.match( + /(\d+(?:\.\d+)?)\s+(B|kB|MB|GB|TB|PB|EB|ZB|YB)/); + if (!triple) { + throw new Error('No matching bytes data found'); + } + var value = triple[1]; + var unit = triple[2]; + + if (!(value && unit)) { + return 0; + } + var exponent = prettyBytesUnits.indexOf(unit); + return (Number(value) * Math.pow(1000, exponent)).toFixed(3); +} + +/** + * Iterates through the commits and tries to checkout the file + * @param {!Array} logs + * @return {!Promise} + */ +function serializeCheckout(logs) { + var tables = []; + var promise = logs.reduce((acc, cur, i) => { + var parts = logs[i].split(' '); + var sha = parts.shift(); + var dateTime = parts.join(' '); + + return acc.then(tables => { + // We checkout all the known commits for the file and accumulate + // all the tables. + return exec(`git checkout ${sha} ${filePath}`).then(() => { + return fs.readFileAsync(`${filePath}`); + }).then(file => { + var quotedDateTime = `"${dateTime}"`; + dateTimes.push(quotedDateTime); + // We convert the read file string into an Table objects + var fields = parseSizeFile(file.toString()).map(field => { + field.dateTime = quotedDateTime; + return field; + }); + tables.push(fields); + return tables; + }).catch(e => { + // Ignore if pathspec error. This can happen if the file was + // deleted in git. + if (/error: pathspec/.test(e.message)) { + tables.push([]); + return tables; + } + util.log(util.colors.red(e.message)); + }); + }); + }, Promise.resolve(tables)); + return promise.then(mergeTables.bind(null, dateTimes)); +} + +function csvify() { + var shaAndDate = "%H %ai"; + return getLog(shaAndDate) + .then(logs => { + // Reverse it from oldest to newest + return serializeCheckout(logs.reverse()).then(rows => { + rows.unshift.apply(rows, tableHeaders); + var tbl = rows.map(row => row.join(',')).join('\n'); + return fs.writeFileAsync('test/size.csv', `${tbl}\n`); + }); + }); +} + +gulp.task('csvify-size', 'create a CSV file out of the size.txt file', csvify); + +exports.parseSizeFile = parseSizeFile; +exports.mergeTables = mergeTables; diff --git a/build-system/tasks/csvify-size/test.js b/build-system/tasks/csvify-size/test.js new file mode 100644 index 000000000000..723a827a3f79 --- /dev/null +++ b/build-system/tasks/csvify-size/test.js @@ -0,0 +1,70 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var test = require('ava'); +var m = require('./'); + + +test('sync - parse size.txt', t => { + t.plan(2); + var sizeFiles = [ + ` max | min | gzip | brotli | file + -- | --- | --- | --- | --- + 12.04 kB | 5.5 kB | 3.2 kB | 1.12 kB | v0.js / amp.js + 100.46 kB | 60.11 kB | 23.22 kB | 10.11 kB | current-min/f.js / current/integration.js + `, + ` max | size | file + -- | --- | --- + 13 B | 12 B | v0.js / amp.js + 120.46 kB | 70.11 kB | current-min/f.js + ` + ]; + var table1 = m.parseSizeFile(sizeFiles[0]); + t.deepEqual(table1, [ + {name:'"v0.js"', size:'"5500.000"'}, + {name:'"f.js"', size:'"60110.000"'}, + ]); + var table2 = m.parseSizeFile(sizeFiles[1]); + t.deepEqual(table2, [ + {name:'"v0.js"', size:'"12.000"'}, + {name:'"f.js"', size:'"70110.000"'}, + ]); +}); + +test('sync - parse table typedef', t => { + t.plan(1); + var dateTimes = ['"0"', '"1"', '"2"']; + var tables = [ + [ + {name:'"v0.js"', size:'"5.5"', dateTime: '"0"'}, + ], + [ + {name:'"v0.js"', size:'"8.5"', dateTime: '"1"'}, + {name:'"f.js"', size:'"70.11"', dateTime: '"1"'}, + ], + [ + {name:'"v0.js"', size:'"8.53"', dateTime: '"2"'}, + {name:'"f.js"', size:'"71.11"', dateTime: '"2"'}, + ], + ]; + var csv = m.mergeTables(dateTimes, tables); + t.deepEqual(csv, [ + ['"0"', '""', '"5.5"'], + ['"1"', '"70.11"', '"8.5"'], + ['"2"', '"71.11"', '"8.53"'], + ]); +}); diff --git a/build-system/tasks/get-zindex/index.js b/build-system/tasks/get-zindex/index.js new file mode 100644 index 000000000000..522d85bdb4c6 --- /dev/null +++ b/build-system/tasks/get-zindex/index.js @@ -0,0 +1,138 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var fs = require('fs'); +var gulp = require('gulp-help')(require('gulp')); +var postcss = require('postcss'); +var table = require('text-table'); +var through = require('through2'); +var util = require('gulp-util'); + +var tableHeaders = [ + ['selector', 'z-index', 'file'], + ['---', '---', '---'], +]; + +var tableOptions = { + align: ['l', 'l', 'l'], + hsep: ' | ', +}; + + +/** + * @param {!Object} acc accumulator object for selectors + * @param {!Rules} css post css rules object + */ +function zIndexCollector(acc, css) { + css.walkRules(rule => { + rule.walkDecls(decl => { + // Split out multi selector rules + var selectorNames = rule.selector.replace('\n', ''); + selectorNames = selectorNames.split(','); + if (decl.prop == 'z-index') { + selectorNames.forEach(selector => { + // If multiple redeclaration of a selector and z index + // are done in a single file, this will get overridden. + acc[selector] = decl.value; + }); + } + }); + }); +} + +/** + * @param {!Vinyl} file vinyl fs object + * @param {string} enc encoding value + * @param {function(err: ?Object, data: !Vinyl|string)} cb chunk data through + */ +function onFileThrough(file, enc, cb) { + if (file.isNull()) { + cb(null, file); + return; + } + + if (file.isStream()) { + cb(new util.PluginError('size', 'Stream not supported')); + return; + } + + var selectors = Object.create(null); + + postcss([zIndexCollector.bind(null, selectors)]) + .process(file.contents.toString(), { + from: file.relative + }).then(res => { + cb(null, { name: file.relative, selectors: selectors }); + }); +} + +/** + * @param {!Object} filesData + * accumulation of files and the rules and z index values. + * @param {function()} cb callback to end the stream + * @return {!Array>} + */ +function createTable(filesData, cb) { + var rows = []; + Object.keys(filesData).sort().forEach((fileName, fileIdx) => { + var selectors = filesData[fileName]; + Object.keys(selectors).sort().forEach((selectorName, selectorIdx) => { + var zIndex = selectors[selectorName]; + var row = [selectorName, zIndex, fileName]; + rows.push(row); + }); + }); + rows.sort((a, b) => { + var aZIndex = parseInt(a[1], 10); + var bZIndex = parseInt(b[1], 10); + return aZIndex - bZIndex; + }); + return rows; +} + + +/** + * @return {!Stream} + */ +function getZindex(glob) { + return gulp.src(glob).pipe(through.obj(onFileThrough)); +} + +/** + * @param {function()} cb + */ +function getZindexForAmp(cb) { + var filesData = Object.create(null); + // Don't return the stream here since we do a `writeFileSync` + getZindex('{css,src,extensions}/**/*.css') + .on('data', (chunk) => { + filesData[chunk.name] = chunk.selectors; + }) + .on('end', () => { + var rows = createTable(filesData); + rows.unshift.apply(rows, tableHeaders); + var tbl = table(rows, tableOptions); + fs.writeFileSync('css/Z_INDEX.md', tbl); + cb(); + }); +} + +gulp.task('get-zindex', 'Runs through all css files of project to gather ' + + 'z-index values', getZindexForAmp); + +exports.getZindex = getZindex; +exports.createTable = createTable; diff --git a/build-system/tasks/get-zindex/test-2.css b/build-system/tasks/get-zindex/test-2.css new file mode 100644 index 000000000000..8db19a008a8d --- /dev/null +++ b/build-system/tasks/get-zindex/test-2.css @@ -0,0 +1,30 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Horizontal scrolling interferes with embedded scenarios and predominantly + * the result of the non-responsive design. + * + * Notice that it's critical that `overflow-x: hidden` is only set on `html` + * and not `body`. Otherwise, adding `overflow-x: hidden` forces `overflow-y` + * to be computed to `auto` on both the `body` and `html` elements so they both + * potentially get a scrolling box. See #3108 for more details. + */ + + +.selector-4 { + z-index: 80; +} diff --git a/build-system/tasks/get-zindex/test.css b/build-system/tasks/get-zindex/test.css new file mode 100644 index 000000000000..99e240bbae17 --- /dev/null +++ b/build-system/tasks/get-zindex/test.css @@ -0,0 +1,40 @@ + +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Horizontal scrolling interferes with embedded scenarios and predominantly + * the result of the non-responsive design. + * + * Notice that it's critical that `overflow-x: hidden` is only set on `html` + * and not `body`. Otherwise, adding `overflow-x: hidden` forces `overflow-y` + * to be computed to `auto` on both the `body` and `html` elements so they both + * potentially get a scrolling box. See #3108 for more details. + */ + + +.selector-1 { + z-index: 1; +} + +.selector-2, +.selector-3 { + z-index: 0; +} + +.selector-3 { + z-index: 99; +} diff --git a/build-system/tasks/get-zindex/test.js b/build-system/tasks/get-zindex/test.js new file mode 100644 index 000000000000..72c90266e84b --- /dev/null +++ b/build-system/tasks/get-zindex/test.js @@ -0,0 +1,54 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var test = require('ava'); +var m = require('./'); + +var result = { + 'test.css': { + '.selector-1': '1', + '.selector-2': '0', + '.selector-3': '99', + }, + 'test-2.css': { + '.selector-4': '80', + }, +}; + +test.cb('collects selectors', t => { + var data = Object.create(null); + m.getZindex('./*.css') + .on('data', chunk => { + data[chunk.name] = chunk.selectors; + }) + .on('end', () => { + t.deepEqual(data, result); + t.end(); + }); +}); + +test('sync - create array of arrays with z index order', t => { + t.plan(1); + var table = m.createTable(result); + var expected = [ + ['.selector-2', '0', 'test.css'], + ['.selector-1', '1', 'test.css'], + ['.selector-4', '80', 'test-2.css'], + ['.selector-3', '99', 'test.css'], + ]; + t.deepEqual(table, expected); +}); diff --git a/build-system/tasks/index.js b/build-system/tasks/index.js index b2d96987d089..9da07bde980a 100644 --- a/build-system/tasks/index.js +++ b/build-system/tasks/index.js @@ -19,11 +19,14 @@ require('./changelog'); require('./clean'); require('./compile'); require('./compile-access-expr'); +require('./compile-bind-expr'); +require('./csvify-size'); require('./dep-check'); +require('./get-zindex'); require('./lint'); -require('./make-golden'); +require('./prepend-global'); require('./presubmit-checks'); require('./serve'); require('./size'); -require('./test'); +require('./runtime-test'); require('./validator'); diff --git a/build-system/tasks/jsify-css.js b/build-system/tasks/jsify-css.js index 02d9b7dd0be9..f86a7aca6f68 100644 --- a/build-system/tasks/jsify-css.js +++ b/build-system/tasks/jsify-css.js @@ -35,9 +35,16 @@ var cssprefixer = autoprefixer({ ] }); +// See http://cssnano.co/optimisations/ for full list. +// We try and turn off any optimization that is marked unsafe. cssnano = cssnano({ + autoprefixer: false, convertValues: false, - zindex: false + discardUnused: false, + // `mergeIdents` this is only unsafe if you rely on those animation names in JavaScript. + mergeIdents: true, + reduceIdents: false, + zindex: false, }); @@ -55,12 +62,11 @@ exports.jsifyCssAsync = function(filename) { var transformers = [cssprefixer, cssnano]; return postcss(transformers).use(postcssImport).process(css.toString(), { 'from': filename - }) - .then(function(result) { + }).then(function(result) { result.warnings().forEach(function(warn) { $$.util.log($$.util.colors.red(warn.toString())); }); var css = result.css; - return JSON.stringify(css + '\n/*# sourceURL=/' + filename + '*/'); + return css + '\n/*# sourceURL=/' + filename + '*/'; }); }; diff --git a/build-system/tasks/karma.conf.js b/build-system/tasks/karma.conf.js new file mode 100644 index 000000000000..2086652397d2 --- /dev/null +++ b/build-system/tasks/karma.conf.js @@ -0,0 +1,185 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var app = require('../server').app; + +/** + * @param {!Object} config + */ +module.exports = { + frameworks: [ + 'fixture', + 'browserify', + 'mocha', + 'chai-as-promised', + 'sinon-chai', + 'chai', + ], + + preprocessors: { + 'test/fixtures/*.html': ['html2js'], + 'src/**/*.js': ['browserify'], + 'test/**/*.js': ['browserify'], + 'ads/**/test/test-*.js': ['browserify'], + 'extensions/**/test/**/*.js': ['browserify'], + 'testing/**/*.js': ['browserify'], + }, + + browserify: { + watch: true, + debug: true, + transform: ['babelify'], + bundleDelay: 900, + }, + + reporters: [process.env.TRAVIS ? 'dots' : 'progress'], + + port: 9876, + + colors: true, + + proxies: { + '/ads/': '/base/ads/', + '/dist/': '/base/dist/', + '/dist.3p/': '/base/dist.3p/', + '/examples/': '/base/examples/', + '/extensions/': '/base/extensions/', + '/src/': '/base/src/', + '/test/': '/base/test/', + }, + + // Can't import Karma constants config.LOG_ERROR & config.LOG_WARN, + // so we hard code the strings here. Hopefully they'll never change. + logLevel: process.env.TRAVIS ? 'ERROR' : 'WARN', + + autoWatch: true, + + browsers: [ + process.env.TRAVIS ? 'Chrome_travis_ci' : 'Chrome_no_extensions', + ], + + customLaunchers: { + /*eslint "google-camelcase/google-camelcase": 0*/ + Chrome_travis_ci: { + base: 'Chrome', + flags: ['--no-sandbox', '--disable-extensions'], + }, + Chrome_no_extensions: { + base: 'Chrome', + // Dramatically speeds up iframe creation time. + flags: ['--disable-extensions'], + }, + // SauceLabs configurations. + // New configurations can be created here: + // https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/ + SL_Chrome_android: { + base: 'SauceLabs', + browserName: 'android', + }, + SL_Chrome_latest: { + base: 'SauceLabs', + browserName: 'chrome', + }, + SL_Chrome_45: { + base: 'SauceLabs', + browserName: 'chrome', + version: '45', + }, + SL_iOS_8_4: { + base: 'SauceLabs', + browserName: 'iphone', + version: '8.4', + }, + SL_iOS_9_1: { + base: 'SauceLabs', + browserName: 'iphone', + version: '9.1', + }, + SL_iOS_10_0: { + base: 'SauceLabs', + browserName: 'iphone', + version: '10.0', + }, + SL_Firefox_latest: { + base: 'SauceLabs', + browserName: 'firefox', + }, + SL_IE_11: { + base: 'SauceLabs', + browserName: 'internet explorer', + version: 11, + }, + SL_Edge_latest: { + base: 'SauceLabs', + browserName: 'microsoftedge', + }, + SL_Safari_9: { + base: 'SauceLabs', + browserName: 'safari', + version: 9, + }, + SL_Safari_8: { + base: 'SauceLabs', + browserName: 'safari', + version: 8, + }, + }, + + sauceLabs: { + testName: 'AMP HTML on Sauce', + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, + startConnect: false, + connectOptions: { + port: 5757, + logfile: 'sauce_connect.log', + }, + }, + + client: { + mocha: { + reporter: 'html', + // Longer timeout on Travis; fail quickly at local. + timeout: process.env.TRAVIS ? 10000 : 2000, + }, + captureConsole: false, + }, + + singleRun: true, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 2, + browserNoActivityTimeout: 4 * 60 * 1000, + captureTimeout: 4 * 60 * 1000, + + // Import our gulp webserver as a Karma server middleware + // So we instantly have all the custom server endpoints available + middleware: ['custom'], + plugins: [ + 'karma-browserify', + 'karma-chai', + 'karma-chai-as-promised', + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-fixture', + 'karma-html2js-preprocessor', + 'karma-mocha', + 'karma-safari-launcher', + 'karma-sauce-launcher', + 'karma-sinon-chai', + { + 'middleware:custom': ['factory', function() {return app;}], + }, + ], +}; diff --git a/build-system/tasks/lint.js b/build-system/tasks/lint.js index 8ff996c92497..175bafcab34b 100644 --- a/build-system/tasks/lint.js +++ b/build-system/tasks/lint.js @@ -29,14 +29,8 @@ var isWatching = (argv.watch || argv.w) || false; var options = { fix: false, + rulePaths: ['build-system/eslint-rules/'], plugins: ['eslint-plugin-google-camelcase'], - "ecmaFeatures": { - "modules": true, - "arrowFunctions": true, - "blockBindings": true, - "forOf": false, - "destructuring": false - }, }; var watcher = lazypipe().pipe(watch, config.lintGlobs); diff --git a/build-system/tasks/make-golden.js b/build-system/tasks/make-golden.js index d6ec7749ca35..458edde7d1bb 100644 --- a/build-system/tasks/make-golden.js +++ b/build-system/tasks/make-golden.js @@ -19,6 +19,8 @@ var dirname = require('path').dirname; var exec = require('child_process').exec; var fs = require('fs-extra'); var gulp = require('gulp'); +// imageDiff is currently a bad dependency as it has a fixed node 0.8 engine +// requirement. var imageDiff = require('gulp-image-diff'); var util = require('gulp-util'); @@ -64,7 +66,7 @@ function doScreenshot(host, path, output, device, verbose, cb) { /** * Make a golden image of the url. * Ex: - * `gulp make-golden --path=examples.build/everything.amp.max.html \ + * `gulp make-golden --path=examples/everything.amp.max.html \ * --host=http://localhost:8000` * @param {function} cb callback function */ diff --git a/build-system/tasks/prepend-global/index.js b/build-system/tasks/prepend-global/index.js new file mode 100644 index 000000000000..4095305648e9 --- /dev/null +++ b/build-system/tasks/prepend-global/index.js @@ -0,0 +1,168 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var BBPromise = require('bluebird'); +var argv = require('minimist')(process.argv.slice(2)); +var child_process = require('child_process'); +var exec = BBPromise.promisify(child_process.exec); +var fs = BBPromise.promisifyAll(require('fs')); +var gulp = require('gulp-help')(require('gulp')); +var util = require('gulp-util'); + + +/** + * Checks that only 1 AMP_CONFIG should exist after append. + * + * @param {string} str + * @return {boolean} + */ +function sanityCheck(str) { + var re = /\/\*AMP_CONFIG\*\//g; + // There must be one and exactly 1 match. + var matches = str.match(re) + return matches != null && matches.length == 1; +} + +/** + * @param {string} filename + * @param {string=} opt_branch + * @return {!Promise} + */ +function checkoutBranchConfigs(filename, opt_branch) { + if (argv.local) { + return Promise.resolve(); + } + var branch = opt_branch || 'origin/master'; + // One bad path here will fail the whole operation. + return exec(`git checkout ${branch} ${filename}`) + .catch(function(e) { + // This means the files don't exist in master. Assume that it exists + // in the current branch. + if (/did not match any file/.test(e.message)) { + return; + } + throw e; + }); +} + +/** + * @param {string} configString + * @param {string} fileString + * @return {string} + */ +function prependConfig(configString, fileString) { + return `self.AMP_CONFIG||(self.AMP_CONFIG=${configString});` + + `/*AMP_CONFIG*/${fileString}`; +} + +/** + * @param {string} filename + * @param {string} fileString + * @param {boolean=} opt_dryrun + * @return {!Promise} + */ +function writeTarget(filename, fileString, opt_dryrun) { + if (opt_dryrun) { + util.log(util.colors.blue(`overwriting: ${filename}`)); + util.log(fileString); + return Promise.resolve(); + } + return fs.writeFileAsync(filename, fileString); +} + +/** + * @param {string|boolean} + * @param {string} + * @return {string} + */ +function valueOrDefault(value, defaultValue) { + if (typeof value == 'string') { + return value; + } + return defaultValue; +} + +function main() { + var TESTING_HOST = process.env.AMP_TESTING_HOST; + var target = argv.target || TESTING_HOST; + + if (!target) { + util.log(util.colors.red('Missing --target.')); + return; + } + + if (!(argv.prod || argv.canary)) { + util.log(util.colors.red('One of --prod or --canary should be provided.')); + return; + } + + var globs = [].concat(argv.files).filter(x => typeof x == 'string'); + var branch = argv.branch; + var filename = ''; + + // Prod by default. + if (argv.canary) { + filename = valueOrDefault(argv.canary, + 'build-system/global-configs/canary-config.json'); + } else { + filename = valueOrDefault(argv.prod, + 'build-system/global-configs/prod-config.json'); + } + return checkoutBranchConfigs(filename, branch) + .then(() => { + return Promise.all([ + fs.readFileAsync(filename), + fs.readFileAsync(target), + ]); + }) + .then(files => { + var configFile; + try { + configFile = JSON.stringify(JSON.parse(files[0].toString())); + } catch (e) { + util.log(util.colors.red(`Error parsing config file: ${filename}`)); + throw e; + } + var targetFile = files[1].toString(); + return prependConfig(configFile, targetFile); + }) + .then(fileString => { + if (!sanityCheck(fileString)) { + throw new Error('Found 0 or > 1 AMP_CONFIG(s) before write. ' + + 'aborting'); + } + return writeTarget(target, fileString, argv.dryrun); + }); +} + +gulp.task('prepend-global', 'Prepends a json config to a target file', main, { + options: { + 'target': ' The file to prepend the json config to.', + 'canary': ' Prepend the default canary config. ' + + 'Takes in an optional value for a custom canary config source.', + 'prod': ' Prepend the default prod config. ' + + 'Takes in an optional value for a custom prod config source.', + 'branch': ' Switch to a git branch to get config source from. ' + + 'Uses master by default.', + 'local': ' Don\'t switch branches and use local config', + } +}); + +exports.checkoutBranchConfigs = checkoutBranchConfigs; +exports.prependConfig = prependConfig; +exports.writeTarget = writeTarget; +exports.valueOrDefault = valueOrDefault; +exports.sanityCheck = sanityCheck; diff --git a/build-system/tasks/prepend-global/test.js b/build-system/tasks/prepend-global/test.js new file mode 100644 index 000000000000..8e2e5936e1a9 --- /dev/null +++ b/build-system/tasks/prepend-global/test.js @@ -0,0 +1,54 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var BBPromise = require('bluebird'); +var fs = BBPromise.promisifyAll(require('fs')); +var m = require('./'); +var test = require('ava'); + +var targetFile = 'target-file.js'; + +test('sync - prepends global config', t => { + t.plan(1); + var res = m.prependConfig('{"hello":"world"}', 'var x = 1 + 1;'); + t.is(res, 'self.AMP_CONFIG||(self.AMP_CONFIG={"hello":"world"});' + + '/*AMP_CONFIG*/var x = 1 + 1;'); +}); + +test('sync - valueOrDefault', t => { + t.plan(2); + var res = m.valueOrDefault(true, 'hello'); + t.is(res, 'hello'); + res = m.valueOrDefault('world', 'hello'); + t.is(res, 'world'); +}); + +test('sync - sanityCheck', t => { + t.plan(3); + var badStr = 'self.AMP_CONFIG||(self.AMP_CONFIG={"hello":"world"})' + + '/*AMP_CONFIG*/' + + 'self.AMP_CONFIG||(self.AMP_CONFIG={"hello":"world"})' + + '/*AMP_CONFIG*/' + + 'var x = 1 + 1;'; + var badStr2 = 'var x = 1 + 1;'; + var goodStr = 'self.AMP_CONFIG||(self.AMP_CONFIG={"hello":"world"})' + + '/*AMP_CONFIG*/' + + 'var x = 1 + 1;'; + t.false(m.sanityCheck(badStr)); + t.true(m.sanityCheck(goodStr)); + t.false(m.sanityCheck(badStr2)); +}); diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js index 15aa97533b18..94b0312c85b2 100644 --- a/build-system/tasks/presubmit-checks.js +++ b/build-system/tasks/presubmit-checks.js @@ -18,6 +18,7 @@ var gulp = require('gulp-help')(require('gulp')); var path = require('path'); var srcGlobs = require('../config').presubmitGlobs; var util = require('gulp-util'); +var through2 = require('through2'); var dedicatedCopyrightNoteSources = /(\.js|\.css|\.go)$/; @@ -36,32 +37,91 @@ var privateServiceFactory = 'This service should only be installed in ' + var shouldNeverBeUsed = 'Usage of this API is not allowed - only for internal purposes.'; +var backwardCompat = 'This method must not be called. It is only retained ' + + 'for backward compatibility during rollout.'; + +var realiasGetMode = 'Do not re-alias getMode or its return so it can be ' + + 'DCE\'d. Use explicitly like "getMode().localDev" instead.'; + // Terms that must not appear in our source files. var forbiddenTerms = { 'DO NOT SUBMIT': '', + // TODO(dvoytenko, #6463): Enable this check once the current uses have + // been cleaned up. + // '(^-amp-|\\W-amp-)': 'Switch to new internal class form', + // '(^i-amp-|\\Wi-amp-)': 'Switch to new internal ID form', 'describe\\.only': '', + 'describes.*\\.only': '', 'it\\.only': '', + 'Math\.random[^;()]*=': 'Use Sinon to stub!!!', 'sinon\\.(spy|stub|mock)\\(': { - message: 'Use a sandbox instead to avoid repeated `#restore` calls' + message: 'Use a sandbox instead to avoid repeated `#restore` calls', }, '(\\w*([sS]py|[sS]tub|[mM]ock|clock).restore)': { - message: 'Use a sandbox instead to avoid repeated `#restore` calls' + message: 'Use a sandbox instead to avoid repeated `#restore` calls', }, 'sinon\\.useFake\\w+': { - message: 'Use a sandbox instead to avoid repeated `#restore` calls' + message: 'Use a sandbox instead to avoid repeated `#restore` calls', + }, + 'sandbox\\.(spy|stub|mock)\\([^,\\s]*[iI]?frame[^,\\s]*,': { + message: 'Do NOT stub on a cross domain iframe! #5359\n' + + ' If this is same domain, mark /*OK*/.\n' + + ' If this is cross domain, overwrite the method directly.', }, 'console\\.\\w+\\(': { message: 'If you run against this, use console/*OK*/.log to ' + 'whitelist a legit case.', - // TODO: temporary, remove when validator is up to date whitelist: [ + 'build-system/pr-check.js', 'build-system/server.js', - 'validator/index.js', // NodeJs only. - 'validator/parse-css.js', - 'validator/validator-full.js', - 'validator/validator-in-browser.js', - 'validator/validator.js', - ] + 'validator/nodejs/index.js', // NodeJs only. + 'validator/engine/parse-css.js', + 'validator/engine/validator-in-browser.js', + 'validator/engine/validator.js', + ], + checkInTestFolder: true, + }, + // Match `getMode` that is not followed by a "()." and is assigned + // as a variable. + '\\bgetMode\\([^)]*\\)(?!\\.)': { + message: realiasGetMode, + whitelist: [ + 'src/mode.js', + 'dist.3p/current/integration.js', + ], + }, + 'import[^}]*\\bgetMode as': { + message: realiasGetMode, + }, + '\\bgetModeObject\\(': { + message: realiasGetMode, + whitelist: [ + 'src/mode-object.js', + 'src/3p-frame.js', + 'src/log.js', + 'dist.3p/current/integration.js', + ], + }, + '(?:var|let|const) +IS_DEV +=': { + message: 'IS_DEV local var only allowed in mode.js and ' + + 'dist.3p/current/integration.js', + whitelist: [ + 'src/mode.js', + 'dist.3p/current/integration.js', + ], + }, + '\\.prefetch\\(': { + message: 'Do not use preconnect.prefetch, use preconnect.preload instead.', + }, + 'documentStateFor': { + message: privateServiceFactory, + whitelist: [ + 'src/custom-element.js', + 'src/style-installer.js', + 'src/service/document-state.js', + 'src/service/viewer-impl.js', + 'src/service/vsync-impl.js', + ], }, 'iframePing': { message: 'This is only available in vendor config for ' + @@ -71,12 +131,12 @@ var forbiddenTerms = { ], }, // Service factories that should only be installed once. - 'installActionService': { + 'installActionServiceForDoc': { message: privateServiceFactory, whitelist: [ 'src/service/action-impl.js', 'src/service/standard-actions-impl.js', - 'src/amp-core-service.js', + 'src/runtime.js', ], }, 'installActionHandler': { @@ -84,76 +144,104 @@ var forbiddenTerms = { whitelist: [ 'src/service/action-impl.js', 'extensions/amp-access/0.1/amp-access.js', + 'extensions/amp-form/0.1/amp-form.js', ], }, 'installActivityService': { message: privateServiceFactory, whitelist: [ 'extensions/amp-analytics/0.1/activity-impl.js', - 'extensions/amp-analytics/0.1/amp-analytics.js' - ] + 'extensions/amp-analytics/0.1/amp-analytics.js', + ], }, - 'installCidService': { + 'installCidServiceForDocForTesting': { message: privateServiceFactory, whitelist: [ 'extensions/amp-analytics/0.1/cid-impl.js', - 'extensions/amp-access/0.1/amp-access.js', - 'extensions/amp-analytics/0.1/amp-analytics.js', + ], + }, + 'installCryptoService': { + message: privateServiceFactory, + whitelist: [ + 'src/service/crypto-impl.js', + 'src/runtime.js', + ], + }, + 'installDocService': { + message: privateServiceFactory, + whitelist: [ + 'src/amp.js', + 'src/amp-shadow.js', + 'src/inabox/amp-inabox.js', + 'src/service/ampdoc-impl.js', + 'testing/describes.js', + 'testing/iframe.js', ], }, 'installPerformanceService': { message: privateServiceFactory, whitelist: [ 'src/amp.js', + 'src/inabox/amp-inabox.js', 'src/service/performance-impl.js', ], }, - 'installStorageService': { + 'installStorageServiceForDoc': { message: privateServiceFactory, whitelist: [ - 'extensions/amp-analytics/0.1/amp-analytics.js', - 'extensions/amp-analytics/0.1/storage-impl.js', + 'src/runtime.js', + 'src/service/storage-impl.js', + ], + }, + 'installTemplatesService': { + message: privateServiceFactory, + whitelist: [ + 'src/runtime.js', + 'src/service/template-impl.js', ], }, - 'installUrlReplacementsService': { + 'installUrlReplacementsServiceForDoc': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', + 'src/runtime.js', 'src/service/url-replacements-impl.js', ], }, - 'installViewerService': { + 'installViewerServiceForDoc': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', - 'src/service/history-impl.js', - 'src/service/resources-impl.js', + 'src/runtime.js', + 'src/inabox/amp-inabox.js', 'src/service/viewer-impl.js', - 'src/service/viewport-impl.js', - 'src/service/vsync-impl.js', ], }, - 'installViewportService': { + 'setViewerVisibilityState': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', - 'src/service/resources-impl.js', + 'src/runtime.js', + 'src/service/viewer-impl.js', + ], + }, + 'installViewportServiceForDoc': { + message: privateServiceFactory, + whitelist: [ + 'src/runtime.js', 'src/service/viewport-impl.js', ], }, 'installVsyncService': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', + 'src/runtime.js', 'src/service/resources-impl.js', 'src/service/viewport-impl.js', 'src/service/vsync-impl.js', ], }, - 'installResourcesService': { + 'installResourcesServiceForDoc': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', + 'src/runtime.js', 'src/service/resources-impl.js', 'src/service/standard-actions-impl.js', ], @@ -161,21 +249,51 @@ var forbiddenTerms = { 'installXhrService': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', + 'src/runtime.js', 'src/service/xhr-impl.js', ], }, - 'sendMessage': { - message: privateServiceFactory, + 'initLogConstructor': { + message: 'Should only be called from JS binary entry files.', + whitelist: [ + '3p/integration.js', + '3p/ampcontext-lib.js', + 'ads/alp/install-alp.js', + 'ads/inabox/inabox-host.js', + 'dist.3p/current/integration.js', + 'extensions/amp-access/0.1/amp-login-done.js', + 'src/runtime.js', + 'src/log.js', + 'tools/experiments/experiments.js', + ], + }, + '\\.sendMessage\\(': { + message: 'Usages must be reviewed.', + whitelist: [ + // viewer-impl.sendMessage + 'src/service/viewer-impl.js', + 'src/service/viewport-impl.js', + 'src/service/performance-impl.js', + 'src/service/resources-impl.js', + + // iframe-messaging-client.sendMessage + '3p/iframe-messaging-client.js', + '3p/ampcontext.js', + ], + }, + '\\.sendMessageAwaitResponse\\(': { + message: 'Usages must be reviewed.', whitelist: [ 'src/service/viewer-impl.js', - 'extensions/amp-analytics/0.1/storage-impl.js', - 'examples/viewer-integr-messaging.js', + 'src/service/storage-impl.js', + 'src/service/history-impl.js', + 'extensions/amp-analytics/0.1/cid-impl.js', 'extensions/amp-access/0.1/login-dialog.js', + 'extensions/amp-access/0.1/signin.js', ], }, // Privacy sensitive - 'cidFor': { + 'cidForDoc|cidForDocOrNull': { message: requiresReviewPrivacy, whitelist: [ 'src/ad-cid.js', @@ -183,6 +301,7 @@ var forbiddenTerms = { 'src/service/cid-impl.js', 'src/service/url-replacements-impl.js', 'extensions/amp-access/0.1/amp-access.js', + 'extensions/amp-experiment/0.1/variant.js', 'extensions/amp-user-notification/0.1/amp-user-notification.js', ], }, @@ -196,6 +315,7 @@ var forbiddenTerms = { 'cookie\\W': { message: requiresReviewPrivacy, whitelist: [ + 'build-system/test-server.js', 'src/cookies.js', 'extensions/amp-analytics/0.1/cid-impl.js', ], @@ -207,7 +327,7 @@ var forbiddenTerms = { 'src/cookies.js', 'src/experiments.js', 'tools/experiments/experiments.js', - ] + ], }, 'setCookie\\W': { message: requiresReviewPrivacy, @@ -216,45 +336,45 @@ var forbiddenTerms = { 'src/cookies.js', 'src/experiments.js', 'tools/experiments/experiments.js', - ] - }, - 'isDevChannel\\W': { - message: requiresReviewPrivacy, - whitelist: [ - 'extensions/amp-access/0.1/amp-access.js', - 'extensions/amp-user-notification/0.1/amp-user-notification.js', - 'src/3p-frame.js', - 'src/experiments.js', - 'src/service/storage-impl.js', - 'src/service/viewport-impl.js', - 'tools/experiments/experiments.js', - ] + ], }, - 'isDevChannelVersionDoNotUse_\\W': { - message: shouldNeverBeUsed, + 'setReportError\\W': { + message: 'Should only be used in error.js and tests.', whitelist: [ - 'src/experiments.js', - ] + 'dist.3p/current/integration.js', + 'src/error.js', + 'src/event-helper.js', + 'src/log.js', + ], }, - 'isTrusted': { + 'isTrustedViewer': { message: requiresReviewPrivacy, whitelist: [ 'src/service/viewer-impl.js', - ] + 'src/inabox/inabox-viewer.js', + 'extensions/amp-analytics/0.1/cid-impl.js', + ], + }, + 'eval\\(': { + message: shouldNeverBeUsed, + whitelist: [ + 'extension/amp-bind/0.1/test/test-bind-expr.js', + ], }, - 'eval\\(': '', - 'storageFor': { + 'storageForDoc': { message: requiresReviewPrivacy, whitelist: [ 'src/storage.js', 'extensions/amp-user-notification/0.1/amp-user-notification.js', + 'extensions/amp-app-banner/0.1/amp-app-banner.js', ], }, 'localStorage': { message: requiresReviewPrivacy, whitelist: [ 'extensions/amp-analytics/0.1/cid-impl.js', - 'extensions/amp-analytics/0.1/storage-impl.js', + 'src/service/storage-impl.js', + 'testing/fake-dom.js', ], }, 'sessionStorage': { @@ -263,97 +383,53 @@ var forbiddenTerms = { 'extensions/amp-accordion/0.1/amp-accordion.js', ], }, - 'indexedDB': requiresReviewPrivacy, + 'indexedDB': { + message: requiresReviewPrivacy, + whitelist: [ + // https://docs.google.com/document/d/1tH_sj93Lo8XRpLP0cDSFNrBi1K_jmx_-q1sk_ZW3Nbg/edit#heading=h.ko4gxsan9svq + 'src/service-worker/core.js', + 'src/service-worker/kill.js', + ], + }, 'openDatabase': requiresReviewPrivacy, 'requestFileSystem': requiresReviewPrivacy, 'webkitRequestFileSystem': requiresReviewPrivacy, 'getAccessReaderId': { message: requiresReviewPrivacy, whitelist: [ + 'build-system/amp.extern.js', 'extensions/amp-access/0.1/amp-access.js', 'src/service/url-replacements-impl.js', - ] + ], }, 'getAuthdataField': { message: requiresReviewPrivacy, whitelist: [ + 'build-system/amp.extern.js', 'extensions/amp-access/0.1/amp-access.js', 'src/service/url-replacements-impl.js', - ] - }, - 'debugger': '', - - // ES6. These are only the most commonly used. - 'Array\\.of': es6polyfill, - // These currently depend on core-js/modules/web.dom.iterable which - // we don't want. That decision could be reconsidered. - '\\.startsWith': { - message: es6polyfill, - whitelist: [ - 'validator/index.js', // NodeJs only. - 'validator/tokenize-css.js', - 'validator/validator-full.js', - 'validator/validator.js', - // exports.startsWith occurs in babel generated code. - 'dist.3p/current/integration.js', - ] - }, - '\\.endsWith': { - message: es6polyfill, - whitelist: [ - // .endsWith occurs in babel generated code. - 'dist.3p/current/integration.js', ], }, - // TODO: (erwinm) rewrite the destructure and spread warnings as - // eslint rules (takes more time than this quick regex fix). - // No destructuring allowed since we dont ship with Array polyfills. - '^\\s*(?:let|const|var) *(?:\\[[^\\]]+\\]|{[^}]+}) *=': es6polyfill, - // No spread (eg. test(...args) allowed since we dont ship with Array - // polyfills except `arguments` spread as babel does not polyfill - // it since it can assume that it can `slice` w/o the use of helpers. - '\\.\\.\\.(?!arguments\\))[_$A-Za-z0-9]*(?:\\)|])': { - message: es6polyfill, - whitelist: [ - 'extensions/amp-access/0.1/access-expr-impl.js', - ], - }, - + 'debugger': '', // Overridden APIs. '(doc.*)\\.referrer': { message: 'Use Viewer.getReferrerUrl() instead.', whitelist: [ '3p/integration.js', + 'ads/google/a4a/utils.js', 'dist.3p/current/integration.js', 'src/service/viewer-impl.js', 'src/error.js', ], }, - '(doc[^.]*)\\.contains': { - message: 'Use dom.documentContains API.', - whitelist: [ - 'src/dom.js', - ], - }, - '\\sdocument(?![a-zA-Z0-9_])': { - message: 'Use `window.document` or similar to access document, the global' + - '`document` is forbidden', - whitelist: [ - 'build-system/server.js', - 'examples/viewer-integr.js', - 'testing/iframe.js', - 'testing/screenshots/make-screenshot.js', - 'tools/experiments/experiments.js', - 'validator/validator-full.js', - 'validator/validator.js', - ], - }, 'getUnconfirmedReferrerUrl': { message: 'Use Viewer.getReferrerUrl() instead.', whitelist: [ 'extensions/amp-dynamic-css-classes/0.1/amp-dynamic-css-classes.js', 'src/3p-frame.js', + 'src/iframe-attributes.js', 'src/service/viewer-impl.js', + 'src/inabox/inabox-viewer.js', ], }, 'setTimeout.*throw': { @@ -362,12 +438,77 @@ var forbiddenTerms = { 'src/log.js', ], }, + '(dev|user)\\(\\)\\.(fine|info|warn|error)\\((?!\\s*([A-Z0-9-]+|[\'"`][A-Z0-9-]+[\'"`]))[^,)\n]*': { + message: 'Logging message require explicitly `TAG`, or an all uppercase' + + ' string as the first parameter', + }, + '\\.schedulePass\\(': { + message: 'schedulePass is heavy, thinking twice before using it', + whitelist: [ + 'src/service/resources-impl.js', + ], + }, + '\\.updatePriority\\(': { + message: 'updatePriority is a restricted API.', + whitelist: [ + 'extensions/amp-a4a/0.1/amp-a4a.js', + 'src/base-element.js', + 'src/service/resources-impl.js', + ], + }, '(win|Win)(dow)?(\\(\\))?\\.open\\W': { message: 'Use dom.openWindowDialog', whitelist: [ 'src/dom.js', ], }, + '\\.getWin\\(': { + message: backwardCompat, + whitelist: [ + ], + }, + '/\\*\\* @type \\{\\!Element\\} \\*/': { + message: 'Use assertElement instead of casting to !Element.', + whitelist: [ + 'src/log.js', // Has actual implementation of assertElement. + 'dist.3p/current/integration.js', // Includes the previous. + ], + }, + 'startupChunk\\(': { + message: 'startupChunk( should only be used during startup', + whitelist: [ + 'src/amp.js', + 'src/chunk.js', + 'src/inabox/amp-inabox.js', + 'src/runtime.js', + ], + }, + 'style\\.\\w+ = ': { + message: 'Use setStyle instead!', + whitelist: [ + 'testing/iframe.js', + ], + }, + 'AMP_CONFIG': { + message: 'Do not access AMP_CONFIG directly. Use isExperimentOn() ' + + 'and getMode() to access config', + whitelist: [ + 'build-system/server.js', + 'build-system/amp.extern.js', + 'build-system/tasks/prepend-global/test.js', + 'build-system/tasks/prepend-global/index.js', + 'src/service-worker/core.js', + 'src/service-worker/error-reporting.js', + 'src/mode.js', + 'src/experiments.js', + 'src/config.js', + 'dist.3p/current/integration.js', + ], + }, + 'data:image/svg(?!\\+xml;charset=utf-8,)[^,]*,': { + message: 'SVG data images must use charset=utf-8: ' + + '"data:image/svg+xml;charset=utf-8,..."', + } }; var ThreePTermsMessage = 'The 3p bootstrap iframe has no polyfills loaded and' + @@ -379,7 +520,6 @@ var forbidden3pTerms = { // usage in babel's external helpers that is in a code path that we do // not use. '\\.then\\((?!callNext)': ThreePTermsMessage, - 'Math\\.sign' : ThreePTermsMessage, }; var bannedTermsHelpString = 'Please review viewport.js for a helper method ' + @@ -399,7 +539,6 @@ var bannedTermsHelpString = 'Please review viewport.js for a helper method ' + var forbiddenTermsSrcInclusive = { '\\.innerHTML(?!_)': bannedTermsHelpString, '\\.outerHTML(?!_)': bannedTermsHelpString, - '\\.postMessage(?!_)': bannedTermsHelpString, '\\.offsetLeft(?!_)': bannedTermsHelpString, '\\.offsetTop(?!_)': bannedTermsHelpString, '\\.offsetWidth(?!_)': bannedTermsHelpString, @@ -409,40 +548,69 @@ var forbiddenTermsSrcInclusive = { '\\.clientTop(?!_)': bannedTermsHelpString, '\\.clientWidth(?!_)': bannedTermsHelpString, '\\.clientHeight(?!_)': bannedTermsHelpString, - '\\.getClientRects(?!_)': bannedTermsHelpString, - '\\.getBoundingClientRect(?!_)': bannedTermsHelpString, - '\\.scrollBy(?!_)': bannedTermsHelpString, - '\\.scrollTo(?!_|p|p_)': bannedTermsHelpString, - '\\.scrollIntoView(?!_)': bannedTermsHelpString, - '\\.scrollIntoViewIfNeeded(?!_)': bannedTermsHelpString, '\\.scrollWidth(?!_)': 'please use `getScrollWidth()` from viewport', '\\.scrollHeight(?!_)': bannedTermsHelpString, '\\.scrollTop(?!_)': bannedTermsHelpString, '\\.scrollLeft(?!_)': bannedTermsHelpString, - '\\.focus(?!_)': bannedTermsHelpString, '\\.computedRole(?!_)': bannedTermsHelpString, '\\.computedName(?!_)': bannedTermsHelpString, '\\.innerText(?!_)': bannedTermsHelpString, - '\\.getComputedStyle(?!_)': bannedTermsHelpString, '\\.scrollX(?!_)': bannedTermsHelpString, '\\.scrollY(?!_)': bannedTermsHelpString, '\\.pageXOffset(?!_)': bannedTermsHelpString, '\\.pageYOffset(?!_)': bannedTermsHelpString, '\\.innerWidth(?!_)': bannedTermsHelpString, '\\.innerHeight(?!_)': bannedTermsHelpString, - '\\.getMatchedCSSRules(?!_)': bannedTermsHelpString, '\\.scrollingElement(?!_)': bannedTermsHelpString, '\\.computeCTM(?!_)': bannedTermsHelpString, - '\\.getBBox(?!_)': bannedTermsHelpString, - '\\.webkitConvertPointFromNodeToPage(?!_)': bannedTermsHelpString, - '\\.webkitConvertPointFromPageToNode(?!_)': bannedTermsHelpString, - '\\.changeHeight(?!_)': bannedTermsHelpString, - '\\.changeSize(?!_)': bannedTermsHelpString, - 'insertAmpExtensionScript': { + // Functions + '\\.changeHeight\\(': bannedTermsHelpString, + '\\.changeSize\\(': bannedTermsHelpString, + '\\.collapse\\(': bannedTermsHelpString, + '\\.focus\\(': bannedTermsHelpString, + '\\.getBBox\\(': bannedTermsHelpString, + '\\.getBoundingClientRect\\(': bannedTermsHelpString, + '\\.getClientRects\\(': bannedTermsHelpString, + '\\.getComputedStyle\\(': bannedTermsHelpString, + '\\.getMatchedCSSRules\\(': bannedTermsHelpString, + '\\.postMessage\\(': bannedTermsHelpString, + '\\.scrollBy\\(': bannedTermsHelpString, + '\\.scrollIntoView\\(': bannedTermsHelpString, + '\\.scrollIntoViewIfNeeded\\(': bannedTermsHelpString, + '\\.scrollTo\\(': bannedTermsHelpString, + '\\.webkitConvertPointFromNodeToPage\\(': bannedTermsHelpString, + '\\.webkitConvertPointFromPageToNode\\(': bannedTermsHelpString, + '\\.scheduleUnlayout\\(': bannedTermsHelpString, + // Super complicated regex that says "find any querySelector method call that + // is passed as a variable anything that is not a string, or a string that + // contains a space. + '\\b(?:(?!\\w*[dD]oc\\w*)\\w)+\\.querySelector(?:All)?\\((?=\\s*([^\'"\\s]|[^\\s)]+\\s))[^)]*\\)': { + message: 'querySelector is not scoped to the element, but globally and ' + + 'filtered to just the elements inside the element. This leads to ' + + 'obscure bugs if you attempt to match a descendant of a descendant (ie ' + + '"div div"). Instead, use the scopedQuerySelector helper in dom.js', + }, + 'loadExtension': { message: bannedTermsHelpString, whitelist: [ - 'src/insert-extension.js', 'src/element-stub.js', + 'src/friendly-iframe-embed.js', + 'src/runtime.js', + 'src/service/extensions-impl.js', + 'src/service/lightbox-manager-discovery.js', + 'src/service/crypto-impl.js', + 'src/shadow-embed.js', + 'extensions/amp-ad/0.1/amp-ad.js', + 'extensions/amp-a4a/0.1/amp-a4a.js', + ], + }, + 'loadElementClass': { + message: bannedTermsHelpString, + whitelist: [ + 'src/runtime.js', + 'src/service/extensions-impl.js', + 'extensions/amp-ad/0.1/amp-ad.js', + 'extensions/amp-a4a/0.1/amp-a4a.js', ], }, 'reject\\(\\)': { @@ -450,13 +618,58 @@ var forbiddenTermsSrcInclusive = { 'error.cancellation() may be applicable.', whitelist: [ 'extensions/amp-access/0.1/access-expr-impl.js', + 'extensions/amp-bind/0.1/bind-expr-impl.js', ], - } + }, + '[^.]loadPromise': { + message: 'Most users should use BaseElement…loadPromise.', + whitelist: [ + 'src/base-element.js', + 'src/event-helper.js', + 'src/friendly-iframe-embed.js', + 'src/service/performance-impl.js', + 'src/service/url-replacements-impl.js', + 'extensions/amp-ad/0.1/amp-ad-xorigin-iframe-handler.js', + 'extensions/amp-image-lightbox/0.1/amp-image-lightbox.js', + 'extensions/amp-analytics/0.1/transport.js', + ], + }, + '\\.getTime\\(\\)': { + message: 'Unless you do weird date math (whitelist), use Date.now().', + }, + '\\.expandStringSync\\(': { + message: requiresReviewPrivacy, + whitelist: [ + 'extensions/amp-form/0.1/amp-form.js', + 'src/service/url-replacements-impl.js', + ], + }, + '\\.expandStringAsync\\(': { + message: requiresReviewPrivacy, + whitelist: [ + 'extensions/amp-form/0.1/amp-form.js', + 'src/service/url-replacements-impl.js', + ], + }, + '\\.expandInputValueSync\\(': { + message: requiresReviewPrivacy, + whitelist: [ + 'extensions/amp-form/0.1/amp-form.js', + 'src/service/url-replacements-impl.js', + ], + }, + '\\.expandInputValueAsync\\(': { + message: requiresReviewPrivacy, + whitelist: [ + 'extensions/amp-form/0.1/amp-form.js', + 'src/service/url-replacements-impl.js', + ], + }, }; // Terms that must appear in a source file. var requiredTerms = { - 'Copyright 20(15|16) The AMP HTML Authors\\.': + 'Copyright 20(15|16|17) The AMP HTML Authors\\.': dedicatedCopyrightNoteSources, 'Licensed under the Apache License, Version 2\\.0': dedicatedCopyrightNoteSources, @@ -478,11 +691,19 @@ function isInTestFolder(path) { function stripComments(contents) { // Multi-line comments - contents = contents.replace(/\/\*(?!.*\*\/)(.|\n)*?\*\//g, ''); - // Single line comments with only leading whitespace - contents = contents.replace(/\n\s*\/\/.*/g, ''); - // Single line comments following a space, semi-colon, or closing brace - return contents.replace(/( |\}|;)\s*\/\/.*/g, '$1'); + contents = contents.replace(/\/\*(?!.*\*\/)(.|\n)*?\*\//g, function(match) { + // Preserve the newlines + var newlines = []; + for (var i = 0; i < match.length; i++) { + if (match[i] === '\n') { + newlines.push('\n'); + } + } + return newlines.join(''); + }); + // Single line comments either on its own line or following a space, + // semi-colon, or closing brace + return contents.replace(/( |}|;|^) *\/\/.*/g, '$1'); } /** @@ -497,16 +718,16 @@ function stripComments(contents) { * false otherwise */ function matchTerms(file, terms) { - var pathname = file.path; var contents = stripComments(file.contents.toString()); var relative = file.relative; return Object.keys(terms).map(function(term) { var fix; var whitelist = terms[term].whitelist; + var checkInTestFolder = terms[term].checkInTestFolder; // NOTE: we could do a glob test instead of exact check in the future // if needed but that might be too permissive. if (Array.isArray(whitelist) && (whitelist.indexOf(relative) != -1 || - isInTestFolder(relative))) { + isInTestFolder(relative) && !checkInTestFolder)) { return false; } // we can't optimize building the `RegExp` objects early unless we build @@ -514,11 +735,26 @@ function matchTerms(file, terms) { // original term to get the possible fix value. This is ok as the // presubmit doesn't have to be blazing fast and this is most likely // negligible. - var matches = contents.match(new RegExp(term, 'gm')); + var regex = new RegExp(term, 'gm'); + var index = 0; + var line = 1; + var column = 0; + var match; + var hasTerm = false; + + while ((match = regex.exec(contents))) { + hasTerm = true; + for (index; index < match.index; index++) { + if (contents[index] === '\n') { + line++; + column = 1; + } else { + column++; + } + } - if (matches) { - util.log(util.colors.red('Found forbidden: "' + matches[0] + - '" in ' + relative)); + util.log(util.colors.red('Found forbidden: "' + match[0] + + '" in ' + relative + ':' + line + ':' + column)); if (typeof terms[term] == 'string') { fix = terms[term]; } else { @@ -530,9 +766,9 @@ function matchTerms(file, terms) { util.log(util.colors.blue(fix)); } util.log(util.colors.blue('==========')); - return true; } - return false; + + return hasTerm; }).some(function(hasAnyTerm) { return hasAnyTerm; }); @@ -561,10 +797,12 @@ function hasAnyTerms(file) { hasSrcInclusiveTerms = matchTerms(file, forbiddenTermsSrcInclusive); } - var is3pFile = /3p|ads/.test(pathname) || + var is3pFile = /\/(3p|ads)\//.test(pathname) || basename == '3p.js' || basename == 'style.js'; - if (is3pFile && !isTestFile) { + // Yet another reason to move ads/google/a4a somewhere else + var isA4A = /\/a4a\//.test(pathname); + if (is3pFile && !isTestFile && !isA4A) { has3pTerms = matchTerms(file, forbidden3pTerms); } @@ -608,14 +846,10 @@ function checkForbiddenAndRequiredTerms() { var forbiddenFound = false; var missingRequirements = false; return gulp.src(srcGlobs) - .pipe(util.buffer(function(err, files) { - forbiddenFound = files.map(hasAnyTerms).some(function(errorFound) { - return errorFound; - }); - missingRequirements = files.map(isMissingTerms).some( - function(errorFound) { - return errorFound; - }); + .pipe(through2.obj(function(file, enc, cb) { + forbiddenFound = hasAnyTerms(file) || forbiddenFound; + missingRequirements = isMissingTerms(file) || missingRequirements; + cb(); })) .on('end', function() { if (forbiddenFound) { diff --git a/build-system/tasks/test.js b/build-system/tasks/runtime-test.js similarity index 50% rename from build-system/tasks/test.js rename to build-system/tasks/runtime-test.js index b59b2fa2870f..4072d1f5b6d1 100644 --- a/build-system/tasks/test.js +++ b/build-system/tasks/runtime-test.js @@ -18,21 +18,24 @@ var argv = require('minimist')(process.argv.slice(2)); var gulp = require('gulp-help')(require('gulp')); var Karma = require('karma').Server; var config = require('../config'); -var karmaConfig = config.karma; -var extend = require('util')._extend; +var fs = require('fs'); +var path = require('path'); +var util = require('gulp-util'); +var webserver = require('gulp-webserver'); +var app = require('../test-server').app; +var karmaDefault = require('./karma.conf'); /** * Read in and process the configuration settings for karma * @return {!Object} Karma configuration */ function getConfig() { - var obj = Object.create(null); if (argv.safari) { - return extend(obj, karmaConfig.safari); + return Object.assign({}, karmaDefault, {browsers: ['Safari']}); } if (argv.firefox) { - return extend(obj, karmaConfig.firefox); + return Object.assign({}, karmaDefault, {browsers: ['Firefox']}); } if (argv.saucelabs) { @@ -42,26 +45,64 @@ function getConfig() { if (!process.env.SAUCE_ACCESS_KEY) { throw new Error('Missing SAUCE_ACCESS_KEY Env variable'); } - const c = extend(obj, karmaConfig.saucelabs); - if (argv.oldchrome) { - c.browsers = ['SL_Chrome_37'] - } + return Object.assign({}, karmaDefault, { + reporters: ['dots', 'saucelabs'], + browsers: argv.oldchrome + ? ['SL_Chrome_45'] + : [ + 'SL_Chrome_android', + 'SL_Chrome_latest', + 'SL_Chrome_45', + 'SL_Firefox_latest', + //'SL_Safari_8' // Disabled due to flakiness and low market share + 'SL_Safari_9', + 'SL_Edge_latest', + //'SL_iOS_8_4', // Disabled due to flakiness and low market share + 'SL_iOS_9_1', + 'SL_iOS_10_0', + //'SL_IE_11', + ], + }); } - - return extend(obj, karmaConfig.default); + return karmaDefault; } -var prerequisites = ['build']; -if (process.env.TRAVIS) { - // No need to do this because we are guaranteed to have done - // it. - prerequisites = []; +function getAdTypes() { + const namingExceptions = { + // We recommend 3P ad networks use the same string for filename and type. + // Write exceptions here in alphabetic order. + // filename: [type1, type2, ... ] + adblade: ['adblade', 'industrybrains'], + mantis: ['mantis-display', 'mantis-recommend'], + weborama: ['weborama-display'], + }; + + // Start with Google ad types + const adTypes = ['adsense', 'doubleclick']; + + // Add all other ad types + const files = fs.readdirSync('./ads/'); + for (var i = 0; i < files.length; i++) { + if (path.extname(files[i]) == '.js' + && files[i][0] != '_' && files[i] != 'ads.extern.js') { + const adType = path.basename(files[i], '.js'); + const expanded = namingExceptions[adType]; + if (expanded) { + for (var j = 0; j < expanded.length; j++) { + adTypes.push(expanded[j]); + } + } else { + adTypes.push(adType); + } + } + } + return adTypes; } /** * Run tests. */ -gulp.task('test', 'Runs tests', prerequisites, function(done) { +gulp.task('test', 'Runs tests', argv.nobuild ? [] : ['build'], function(done) { if (argv.saucelabs && process.env.MAIN_REPO && // Sauce Labs does not work on Pull Requests directly. // The @ampsauce bot builds these. @@ -76,7 +117,6 @@ gulp.task('test', 'Runs tests', prerequisites, function(done) { } var c = getConfig(); - var browsers = []; if (argv.watch || argv.w) { c.singleRun = false; @@ -94,8 +134,11 @@ gulp.task('test', 'Runs tests', prerequisites, function(done) { c.files = config.testPaths; } + // c.client is available in test browser via window.parent.karma.config c.client.amp = { - useCompiledJs: !!argv.compiled + useCompiledJs: !!argv.compiled, + saucelabs: !!argv.saucelabs, + adTypes: getAdTypes(), }; if (argv.grep) { @@ -104,8 +147,23 @@ gulp.task('test', 'Runs tests', prerequisites, function(done) { }; } - - new Karma(c, done).start(); + // Run fake-server to test XHR responses. + var server = gulp.src(process.cwd()) + .pipe(webserver({ + port: 31862, + host: 'localhost', + directoryListing: true, + middleware: [app], + })); + util.log(util.colors.yellow( + 'Started test responses server on localhost:31862')); + + new Karma(c, function(exitCode) { + util.log(util.colors.yellow( + 'Shutting down test responses server on localhost:31862')); + server.emit('kill'); + done(exitCode); + }).start(); }, { options: { 'verbose': ' With logging enabled', diff --git a/build-system/tasks/serve.js b/build-system/tasks/serve.js index 2572ff274467..c42ca1116ffd 100644 --- a/build-system/tasks/serve.js +++ b/build-system/tasks/serve.js @@ -19,7 +19,8 @@ var gulp = require('gulp-help')(require('gulp')); var util = require('gulp-util'); var webserver = require('gulp-webserver'); var app = require('../server').app; - +var morgan = require('morgan'); +var host = argv.host || 'localhost'; var port = argv.port || process.env.PORT || 8000; var useHttps = argv.https != undefined; @@ -30,14 +31,14 @@ function serve() { var server = gulp.src(process.cwd()) .pipe(webserver({ port, - host: '0.0.0.0', + host, directoryListing: true, https: useHttps, - middleware: [app] + middleware: [morgan('dev'), app], })); util.log(util.colors.yellow('Run `gulp build` then go to ' - + getHost() + '/examples.build/article.amp.max.html' + + getHost() + '/examples/article.amp.max.html' )); return server; } @@ -48,6 +49,7 @@ gulp.task( serve, { options: { + 'host': ' Hostname or IP address to bind to (default: localhost)', 'port': ' Specifies alternative port (default: 8000)', 'https': ' Use HTTPS server (default: false)' } @@ -55,5 +57,5 @@ gulp.task( ); function getHost() { - return (useHttps ? 'https' : 'http') + '://localhost:' + port; + return (useHttps ? 'https' : 'http') + '://' + host + ':' + port; } diff --git a/build-system/tasks/size.js b/build-system/tasks/size.js index ffc01f21c6d6..b160d5344d3b 100644 --- a/build-system/tasks/size.js +++ b/build-system/tasks/size.js @@ -14,7 +14,6 @@ * limitations under the License. */ -var brotliSize = require('brotli-size'); var del = require('del'); var fs = require('fs'); var gulp = require('gulp-help')(require('gulp')); @@ -29,17 +28,16 @@ var tempFolderName = '__size-temp'; var MIN_FILE_SIZE_POS = 0; var GZIP_POS = 1; -var BROTLI_POS = 2; -var FILENAME_POS = 3; +var FILENAME_POS = 2; // normalized table headers var tableHeaders = [ - ['max', 'min', 'gzip', 'brotli', 'file'], - ['---', '---', '---', '---', '---'], + ['max', 'min', 'gzip', 'file'], + ['---', '---', '---', '---'], ]; var tableOptions = { - align: ['r', 'r', 'r', 'r', 'l'], + align: ['r', 'r', 'r', 'l'], hsep: ' | ', }; @@ -99,9 +97,23 @@ function normalizeRows(rows) { // normalize integration.js normalizeRow(rows, 'current-min/f.js', 'current/integration.js', true); + normalizeRow(rows, 'current-min/ampcontext-v0.js', + 'current/ampcontext-lib.js', true); + // normalize alp.js normalizeRow(rows, 'alp.js', 'alp.max.js', true); + // normalize amp-shadow.js + normalizeRow(rows, 'shadow-v0.js', 'amp-shadow.js', true); + + normalizeRow(rows, 'amp4ads-v0.js', 'amp-inabox.js', true); + + normalizeRow(rows, 'amp4ads-host-v0.js', 'amp-inabox-host.js', true); + + // normalize sw.js + normalizeRow(rows, 'sw.js', 'sw.max.js', true); + normalizeRow(rows, 'sw-kill.js', 'sw-kill.max.js', true); + // normalize extensions var curName = null; var i = rows.length; @@ -161,7 +173,6 @@ function onFileThrough(rows, file, enc, cb) { rows.push([ prettyBytes(file.contents.length), prettyBytes(gzipSize.sync(file.contents)), - prettyBytes(brotliSize.sync(file.contents)), file.relative, ]); @@ -207,6 +218,7 @@ function sizeTask() { gulp.src([ 'dist/**/*.js', '!dist/**/*-latest.js', + '!dist/**/*check-types.js', 'dist.3p/{current,current-min}/**/*.js', ]) .pipe(sizer()) diff --git a/build-system/test-server.js b/build-system/test-server.js new file mode 100644 index 000000000000..3d2f5109abed --- /dev/null +++ b/build-system/test-server.js @@ -0,0 +1,93 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Creates an http server to handle responses for different test cases. + */ +var app = require('express')(); +var bodyParser = require('body-parser'); +var morgan = require('morgan'); + +app.use(bodyParser.json()); + +function setCorsHeaders(req, res, next) { + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + if (req.query.__amp_source_origin) { + res.setHeader('Access-Control-Expose-Headers', + 'AMP-Access-Control-Allow-Source-Origin') + res.setHeader('AMP-Access-Control-Allow-Source-Origin', + req.query.__amp_source_origin); + } + next(); +} +app.use(setCorsHeaders); + +app.use('/get', function(req, res) { + res.json({ + args: req.query, + headers: req.headers, + }); +}); + +app.use('/redirect-to', function(req, res) { + res.redirect(302, req.query.url); +}); + + +app.use('/status/404', function(req, res) { + res.status(404).end(); +}); + +app.use('/status/500', function(req, res) { + res.status(500).end(); +}); + +app.use('/cookies/set', function(req, res) { + delete req.query.__amp_source_origin; + for (var name in req.query) { + res./*OK*/cookie(name, req.query[name]); + } + res.json({ + cookies: req.cookies || {}, + }); +}); + +app.use('/response-headers', function(req, res) { + delete req.query.__amp_source_origin; + for (var name in req.query) { + res.setHeader(name, req.query[name]); + } + res.json({}); +}); + +app.use('/post', function(req, res) { + delete req.query.__amp_source_origin; + res.json({ + json: req.body, + }) +}); + +app.use('/form/post', function(req, res) { + delete req.query.__amp_source_origin; + res.json({ + json: req.body, + }) +}); + +exports.app = app; diff --git a/builtins/README.md b/builtins/README.md index 9423b03ae96d..7d20f549e2d2 100644 --- a/builtins/README.md +++ b/builtins/README.md @@ -4,8 +4,5 @@ The following components are always available in AMP documents: | Component | Description | | -------------------------- | --------------------------------------------------- | -| [`amp-ad`](amp-ad.md) | Container to display an ad. | | [`amp-img`](amp-img.md) | Replacement for the HTML `img` tag. | | [`amp-pixel`](amp-pixel.md) | Used as tracking pixel to count page views. | -| [`amp-video`](amp-video.md) | Replacement for the HTML5 `video` tag. | -| [`amp-embed`](amp-embed.md) | An alias to the `amp-ad` tag. | diff --git a/builtins/amp-ad.md b/builtins/amp-ad.md deleted file mode 100644 index f0babf1aaa56..000000000000 --- a/builtins/amp-ad.md +++ /dev/null @@ -1,219 +0,0 @@ -# `amp-ad` - -NOTE: The specification of `amp-ad` is likely to significantly evolve over time. The current approach is designed to bootstrap the format to be able to show ads. - - - - - - - - - - - - - - - - - - - - - - - - -
    DescriptionA container to display an ad. AMP documents only support ads served via HTTPS.
    AvailabilityStable
    Required Script<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
    Supported LayoutsFILL, FIXED, FIXED_HEIGHT, FLEX_ITEM, NODISPLAY, RESPONSIVE
    Examplesamp-ad.html
    ads.amp.html
    - -## Behavior - -Ads are loaded like all other resources in AMP documents, with a special -custom element called ``. No ad network provided JavaScript is allowed to run inside the AMP document. Instead the AMP runtime loads an iframe from a -different origin (via iframe sandbox) as the AMP document and executes the ad -network’s JS inside that iframe sandbox. - -The `` requires width and height values to be specified like all -resources in AMP. It requires a `type` argument that select what ad network is displayed. All `data-*` attributes on the tag are automatically passed as arguments to the code that eventually renders the ad. What `data-` attributes are required for a given type of network depends and must be documented with the ad network. - -```html - - -``` -```html - - -``` - -## Supported ad networks - -- [A9](../ads/a9.md) -- [Adblade](../ads/adblade.md) -- [ADITION](../ads/adition.md) -- [Adform](../ads/adform.md) -- [Adman](../ads/adman.md) -- [AdReactor](../ads/adreactor.md) -- [AdSense](../ads/google/adsense.md) -- [AdStir](../ads/adstir.md) -- [AdTech](../ads/adtech.md) -- [Ad Up Technology](../ads/aduptech.md) -- [AppNexus](../ads/appnexus.md) -- [Chargeads](../ads/chargeads.md) -- [Colombia](../ads/colombia.md) -- [Criteo](../ads/criteo.md) -- [Dot and Media](../ads/dotandads.md) -- [Doubleclick](../ads/google/doubleclick.md) -- [E-Planning](../ads/eplanning.md) -- [Flite](../ads/flite.md) -- [GMOSSP](../ads/gmossp.md) -- [I-Mobile](../ads/imobile.md) -- [Improve Digital](../ads/improvedigital.md) -- [Industrybrains](../ads/industrybrains.md) -- [MANTIS](../ads/mantis.md) -- [MediaImpact](../ads/mediaimpact.md) -- [Nend](../ads/nend.md) -- [Open AdStream (OAS)](../ads/openadstream.md) -- [MicroAd](../ads/microad.md) -- [OpenX](../ads/openx.md) -- [plista](../ads/plista.md) -- [PubMatic](../ads/pubmatic.md) -- [Revcontent](../ads/revcontent.md) -- [Rubicon Project](../ads/rubicon.md) -- [Sharethrough](../ads/sharethrough.md) -- [Smart AdServer](../ads/smartadserver.md) -- [Sortable](../ads/sortable.md) -- [TripleLift](../ads/triplelift.md) -- [Teads](../ads/teads.md) -- [Webediads](../ads/webediads.md) -- [Weborama](../ads/weborama.md) -- [Yieldbot](../ads/yieldbot.md) -- [Yieldmo](../ads/yieldmo.md) -- [YahooJP](../ads/yahoojp.md) - -## Styling - -`` elements may not themselves have or be placed in containers that have CSS `position: fixed` set (with the exception of `amp-lightbox`). -This is due to the UX implications of full page overlay ads. It may be considered to allow similar ad formats in the future inside of AMP controlled containers that maintain certain UX invariants. - -## Attributes - -### type - -Identifier for the ad network. This selects the template that is used for the ad tag. - -### src - -Optional src value for a script tag loaded for this ad network. This can be used with ad networks that require exactly a single script tag to be inserted in the page. The src value must have a prefix that is whitelisted for this ad network. - -### data-foo-bar - -Most ad networks require further configuration. This can be passed to the network using HTML `data-` attributes. The parameter names are subject to standard data attribute dash to camel case conversion. E.g. "data-foo-bar" is send to the ad for configuration as "fooBar". - -### json - -Optional attribute to pass configuration to the ad as an arbitrarily complex JSON object. The object is passed to the ad as-is with no mangling done on the names. - -### data-consent-notification-id - -Optional attribute. If provided will require confirming the [amp-user-notification](../extensions/amp-user-notification/amp-user-notification.md) with the given HTML-id until the "AMP client id" for the user (similar to a cookie) is passed to the ad. The means ad rendering is delayed until the user confirmed the notification. - -### Experimental: data-loading-strategy - -Supported value: `prefer-viewability-over-views`. Instructs AMP to load ads in a way that prefers a high degree of viewability, while sometimes loading too late to generate a view. - -## Placeholder - -Optionally `amp-ad` supports a child element with the `placeholder` attribute. If supported by the ad network, this element is shown until the ad is available for viewing. -```html - -
    Have a great day!
    -
    -``` - -## No Ad available -- `amp-ad` supports a child element with the `fallback` attribute. If supported by the ad network, this element is shown if no ad is available for this slot. -```html - -
    Have a great day!
    -
    -``` - -- If there is no fallback element available, the amp-ad tag will be collapsed (set to display: none) if the ad sends a message that the ad slot cannot be filled and AMP determines that this operation can be performed without affecting the user's scroll position. - -## Running ads from a custom domain - -AMP supports loading the bootstrap iframe that is used to load ads from a custom domain such as your own domain. - -To enable this, copy the file [remote.html](../3p/remote.html) to your web server. Next up add the following meta tag to your AMP file(s): - -```html - -``` - -The `content` attribute of the meta tag is the absolute URL to your copy of the remote.html file on your web server. This URL must use a "https" schema. It is not allowed to reside on the same origin as your AMP files. E.g. if you host AMP files on "www.example.com", this URL must not be on "www.example.com" but e.g. "something-else.example.com" is OK. See the doc ["Iframe origin policy"](../spec/amp-iframe-origin-policy.md) for further details on allowed origins for iframes. - -### Security - -**Validate incoming data** before passing it on to the `draw3p` function, to make sure your iframe only does things it expects to do. This is true, in particular, for ad networks that allow custom JavaScript injection. - -Iframes should also enforce that they are only iframed into origins that they expect to be iframed into. The origins would be: - -- your own origins -- https://cdn.ampproject.org for the AMP cache - -In the case of the AMP cache you also need to check that the "source origin" (origin of the document served by cdn.ampproject.org) is one of your origins. - -Enforcing origins can be done with the 3rd argument to `draw3p` and must additionally be done using the [allow-from](https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options) directive for full browser support. - -### Enhance incoming ad configuration - -This is completely optional: It is sometimes desired to further process the incoming iframe configuration before drawing the ad using AMP's built-in system. - -This is supported by passing a callback to the `draw3p` function call in the [remote.html](../3p/remote.html) file. The callback receives the incoming configuration as first argument and then receives another callback as second argument (Called `done` in the example below). This callback must be called with the updated config in order for ad rendering to proceed. - -Example: - -```JS -draw3p(function(config, done) { - config.targeting = Math.random() > 0.5 ? 'sport' : 'fashion'; - // Don't actually call setTimeout here. This should only serve as an - // example that is OK to call the done callback asynchronously. - setTimeout(function() { - done(config); - }, 100) -}, ['allowed-ad-type'], ['your-domain.com']); -``` - -## Validation - -See [amp-ad rules](https://github.com/ampproject/amphtml/blob/master/validator/validator-main.protoascii) in the AMP validator specification. - -## Notes - -To use ``, the script to the `amp-ad` library is needed. It is recommended to add the script manually but currently it will be automatically fetched when `amp-ad` is used. diff --git a/builtins/amp-embed.md b/builtins/amp-embed.md deleted file mode 100644 index 410b1c8bc8ad..000000000000 --- a/builtins/amp-embed.md +++ /dev/null @@ -1,65 +0,0 @@ - -# `amp-embed` - - - - - - - - - - - - - - - - - - - - - - -
    DescriptionUse amp-embed to embed elements into the AMP page. Can be used instead of amp-ad when semantically more accurate.
    AvailabilityStable
    Required Script<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
    Supported LayoutsFILL, FIXED, FIXED_HEIGHT, FLEX_ITEM, NODISPLAY, RESPONSIVE
    ExamplesNone
    - -## Implementation - -The `` is actually an alias to the [``](amp-ad.md) tag, deriving all of its functionality with a different tag name. - -```html - - -``` - -## Supported embed types - -- [Taboola](../ads/taboola.md) - -## Validation - -See [amp-embed rules](https://github.com/ampproject/amphtml/blob/master/validator/validator-main.protoascii) in the AMP validator specification. - -## Notes - -To use ``, the script to the `amp-ad` library is needed. It is recommended to add the script manually but currently it will be automatically fetched when `amp-embed` is used. diff --git a/builtins/amp-img.js b/builtins/amp-img.js index c29d8ae4e126..150420067822 100644 --- a/builtins/amp-img.js +++ b/builtins/amp-img.js @@ -15,18 +15,60 @@ */ import {BaseElement} from '../src/base-element'; -import {getLengthNumeral, isLayoutSizeDefined} from '../src/layout'; -import {loadPromise} from '../src/event-helper'; +import {isLayoutSizeDefined} from '../src/layout'; import {registerElement} from '../src/custom-element'; import {srcsetFromElement} from '../src/srcset'; +import {user} from '../src/log'; +/** + * Attributes to propagate to internal image when changed externally. + * @type {!Array} + */ +const ATTRIBUTES_TO_PROPAGATE = ['alt', 'title', 'referrerpolicy', 'aria-label', + 'aria-describedby', 'aria-labelledby']; export class AmpImg extends BaseElement { + /** @param {!AmpElement} element */ + constructor(element) { + super(element); + + /** @private {boolean} */ + this.allowImgLoadFallback_ = true; + + /** @private {boolean} */ + this.isPrerenderAllowed_ = true; + + /** @private {?Element} */ + this.img_ = null; + + /** @private {?../src/srcset.Srcset} */ + this.srcset_ = null; + } + /** @override */ - createdCallback() { - /** @private @const {function(!Element, number=): !Promise} */ - this.loadPromise_ = loadPromise; + mutatedAttributesCallback(mutations) { + if (mutations['src'] !== undefined || mutations['srcset'] !== undefined) { + this.srcset_ = srcsetFromElement(this.element); + // This element may not have been laid out yet. + if (this.img_) { + this.updateImageSrc_(); + } + } + + if (this.img_) { + const attrs = ATTRIBUTES_TO_PROPAGATE.filter( + value => mutations[value] !== undefined); + this.propagateAttributes( + attrs, this.img_, /* opt_removeMissingAttrs */ true); + } + } + + /** @override */ + buildCallback() { + this.isPrerenderAllowed_ = !this.element.hasAttribute('noprerender'); + + this.srcset_ = srcsetFromElement(this.element); } /** @override */ @@ -34,38 +76,44 @@ export class AmpImg extends BaseElement { return isLayoutSizeDefined(layout); } - /** @override */ - buildCallback() { - /** @private {boolean} */ + /** + * Create the actual image element and set up instance variables. + * Called lazily in the first `#layoutCallback`. + */ + initialize_() { + if (this.img_) { + return; + } this.allowImgLoadFallback_ = true; - // If this amp-img IS the fallback then don't allow it to have its own // fallback to stop from nested fallback abuse. if (this.element.hasAttribute('fallback')) { this.allowImgLoadFallback_ = false; } - /** @private @const {!Element} */ this.img_ = new Image(); - if (this.element.id) { this.img_.setAttribute('amp-img-id', this.element.id); } - this.propagateAttributes(['alt'], this.img_); - this.applyFillContent(this.img_, true); - this.img_.width = getLengthNumeral(this.element.getAttribute('width')); - this.img_.height = getLengthNumeral(this.element.getAttribute('height')); + // Remove role=img otherwise this breaks screen-readers focus and + // only read "Graphic" when using only 'alt'. + if (this.element.getAttribute('role') == 'img') { + this.element.removeAttribute('role'); + user().error('AMP-IMG', 'Setting role=img on amp-img elements breaks ' + + 'screen readers please just set alt or ARIA attributes, they will ' + + 'be correctly propagated for the underlying element.'); + } - this.element.appendChild(this.img_); + this.propagateAttributes(ATTRIBUTES_TO_PROPAGATE, this.img_); + this.applyFillContent(this.img_, true); - /** @private @const {!../src/srcset.Srcset} */ - this.srcset_ = srcsetFromElement(this.element); + this.element.appendChild(this.img_); } /** @override */ prerenderAllowed() { - return true; + return this.isPrerenderAllowed_; } /** @override */ @@ -73,8 +121,14 @@ export class AmpImg extends BaseElement { return true; } + /** @override */ + reconstructWhenReparented() { + return false; + } + /** @override */ layoutCallback() { + this.initialize_(); let promise = this.updateImageSrc_(); // We only allow to fallback on error on the initial layoutCallback @@ -104,7 +158,7 @@ export class AmpImg extends BaseElement { this.img_.setAttribute('src', src); - return this.loadPromise_(this.img_).then(() => { + return this.loadPromise(this.img_).then(() => { // Clean up the fallback if the src has changed. if (!this.allowImgLoadFallback_ && this.img_.classList.contains('-amp-ghost')) { diff --git a/builtins/amp-img.md b/builtins/amp-img.md index eb1373ac8add..f92aeecd524f 100644 --- a/builtins/amp-img.md +++ b/builtins/amp-img.md @@ -27,11 +27,11 @@ limitations under the License. Supported Layouts - FILL, FIXED, FIXED_HEIGHT, FLEX_ITEM, NODISPLAY, RESPONSIVE + fill, fixed, fixed-height, flex-item, nodisplay, responsive Examples - amp-img.html
    responsive.amp.html + Annotated code example for amp-img @@ -40,7 +40,7 @@ limitations under the License. The runtime may choose to delay or prioritize resource loading based on the viewport position, system resources, connection bandwidth, or other factors. The `amp-img` components allows the runtime to effectively manage image resources this way. `amp-img` components, like all externally fetched AMP resources, must be given an -explicit size (as in width / height) in advance, so that the aspect ratio can be known without fetching the image. Actual layout behavior is determined by the layout attribute. +explicit size (as in width / height) in advance, so that the aspect ratio can be known without fetching the image. Actual layout behavior is determined by the layout attribute. For details on layouts, see [AMP HTML Layout System](https://github.com/ampproject/amphtml/blob/master/spec/amp-html-layout.md) and [Supported Layouts](https://www.ampproject.org/docs/guides/responsive/control_layout.html). If the resource requested by the `amp-img` component fails to load, the space will be blank unless a [`fallback`](https://github.com/ampproject/amphtml/blob/master/spec/amp-html-layout.md#fallback) child is provided. A fallback is only executed on the initial layout and subsequent src changes after the fact (through resize + srcset for example) will not have a fallback for performance implications. @@ -54,9 +54,7 @@ Additional image features like captions can be implemented with standard HTML - **src** -Similar to the `src` attribute on the `img` tag. The value must be a URL that -points to a publicly-cacheable image file. Cache providers may rewrite these -URLs when ingesting AMP files to point to a cached version of the image. +This attribute is similar to the `src` attribute on the `img` tag. The value must be a URL that points to a publicly-cacheable image file. Cache providers may rewrite these URLs when ingesting AMP files to point to a cached version of the image. **srcset** @@ -68,18 +66,34 @@ A string of alternate text, similar to the `alt` attribute on `img`. **attribution** -A string that indicates the attribution of the image. E.g. `attribution="CC courtesy of Cats on Flicker"` +A string that indicates the attribution of the image. For example, `attribution="CC courtesy of Cats on Flicker"` +**height** and **width** + +An explicit size of the image, which is used by the AMP runtime to determine the aspect ratio without fetching the image. + +**common attributes** + +This element includes [common attributes](https://www.ampproject.org/docs/reference/common_attributes) extended to AMP components. ## Styling `amp-img` can be styled directly via CSS properties. Setting a grey background placeholder for example could be achieved via: + ```css amp-img { background-color: grey; } ``` + ## Validation See [amp-img rules](https://github.com/ampproject/amphtml/blob/master/validator/validator-main.protoascii) in the AMP validator specification. + + +## Related documentation + +* Guide: [Include Images & Video](https://www.ampproject.org/docs/guides/amp_replacements) +* Guide: [Layout & Media Queries](https://www.ampproject.org/docs/guides/responsive/control_layout) +* Guide: [Art direction with srcset, sizes & heights](https://www.ampproject.org/docs/guides/responsive/art_direction) diff --git a/builtins/amp-pixel.js b/builtins/amp-pixel.js index d704bd7f74e4..ca3c006e1ccd 100644 --- a/builtins/amp-pixel.js +++ b/builtins/amp-pixel.js @@ -15,66 +15,82 @@ */ import {BaseElement} from '../src/base-element'; -import {Layout} from '../src/layout'; -import {urlReplacementsFor} from '../src/url-replacements'; +import {dev, user} from '../src/log'; import {registerElement} from '../src/custom-element'; -import {toggle} from '../src/style'; -import {user} from '../src/log'; +import {timerFor} from '../src/timer'; +import {urlReplacementsForDoc} from '../src/url-replacements'; +import {viewerForDoc} from '../src/viewer'; + +const TAG = 'amp-pixel'; + /** - * @param {!Window} win Destination window for the new element. - * @this {undefined} // Make linter happy - * @return {undefined} + * A simple analytics instrument. Fires as an impression signal. */ -export function installPixel(win) { - class AmpPixel extends BaseElement { +export class AmpPixel extends BaseElement { + + /** @override */ + constructor(element) { + super(element); + + /** @private {?Promise} */ + this.triggerPromise_ = null; + } - /** @override */ - getPriority() { - // Loads after other content. - return 1; - } + /** @override */ + isLayoutSupported(unusedLayout) { + // No matter what layout is: the pixel is always non-displayed. + return true; + } - /** @override */ - isLayoutSupported(layout) { - return layout == Layout.FIXED; - } + /** @override */ + buildCallback() { + // Element is invisible. + this.element.setAttribute('aria-hidden', 'true'); - /** @override */ - buildCallback() { - // Remove user defined size. Pixels should always be the default size. - this.element.style.width = ''; - this.element.style.height = ''; - // Consider the element invisible. - this.element.setAttribute('aria-hidden', 'true'); - } + // Trigger, but only when visible. + const viewer = viewerForDoc(this.getAmpDoc()); + viewer.whenFirstVisible().then(this.trigger_.bind(this)); + } - /** @override */ - layoutCallback() { - // Now that we are rendered, stop rendering the element to reduce - // resource consumption. - toggle(this.element, false); + /** + * Triggers the signal. + * @private + */ + trigger_() { + // Delay(1) provides a rudimentary "idle" signal. + // TODO(dvoytenko): use an improved idle signal when available. + this.triggerPromise_ = timerFor(this.win).promise(1).then(() => { const src = this.element.getAttribute('src'); - return urlReplacementsFor(this.getWin()).expand(this.assertSource(src)) + return urlReplacementsForDoc(this.element) + .expandAsync(this.assertSource_(src)) .then(src => { const image = new Image(); image.src = src; - image.width = 1; - image.height = 1; - // Make it take zero space - this.element.style.width = 0; - this.element.appendChild(image); + dev().info(TAG, 'pixel triggered: ', src); + return image; }); - } + }); + } + + /** + * @param {?string} src + * @return {string} + * @private + */ + assertSource_(src) { + user().assert( + /^(https\:\/\/|\/\/)/i.test(src), + 'The src attribute must start with ' + + '"https://" or "//". Invalid value: ' + src); + return /** @type {string} */ (src); + } +} - assertSource(src) { - user.assert( - /^(https\:\/\/|\/\/)/i.test(src), - 'The src attribute must start with ' + - '"https://" or "//". Invalid value: ' + src); - return src; - } - }; - registerElement(win, 'amp-pixel', AmpPixel); +/** + * @param {!Window} win Destination window for the new element. + */ +export function installPixel(win) { + registerElement(win, TAG, AmpPixel); } diff --git a/builtins/amp-pixel.md b/builtins/amp-pixel.md index 3244932c416c..6abfd8bc7132 100644 --- a/builtins/amp-pixel.md +++ b/builtins/amp-pixel.md @@ -17,15 +17,19 @@ limitations under the License. # `amp-pixel` - + - + - + + + + + @@ -37,7 +41,7 @@ The `amp-pixel` component behaves like a simple tracking pixel `img`. It takes a ## Attributes -**src** +**src** (required) A simple URL to send a GET request to when the tracking pixel is loaded. diff --git a/builtins/amp-video.js b/builtins/amp-video.js deleted file mode 100644 index e315031402e4..000000000000 --- a/builtins/amp-video.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright 2015 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {BaseElement} from '../src/base-element'; -import {assertHttpsUrl} from '../src/url'; -import {getLengthNumeral, isLayoutSizeDefined} from '../src/layout'; -import {loadPromise} from '../src/event-helper'; -import {registerElement} from '../src/custom-element'; -import {getMode} from '../src/mode'; - -/** - * @param {!Window} win Destination window for the new element. - * @this {undefined} // Make linter happy - * @return {undefined} - */ -export function installVideo(win) { - class AmpVideo extends BaseElement { - - /** @override */ - isLayoutSupported(layout) { - return isLayoutSizeDefined(layout); - } - - /** @override */ - buildCallback() { - /** @private @const {!HTMLVideoElement} */ - this.video_ = this.element.ownerDocument.createElement('video'); - const width = this.element.getAttribute('width'); - const height = this.element.getAttribute('height'); - - this.video_.width = getLengthNumeral(width); - this.video_.height = getLengthNumeral(height); - - const posterAttr = this.element.getAttribute('poster'); - if (!posterAttr && getMode().development) { - console/*OK*/.error( - 'No "poster" attribute has been provided for amp-video.'); - } - - // Disable video preload in prerender mode. - this.video_.setAttribute('preload', 'none'); - this.propagateAttributes(['poster'], this.video_); - this.applyFillContent(this.video_, true); - this.element.appendChild(this.video_); - } - - /** @override */ - layoutCallback() { - if (!this.isVideoSupported_()) { - this.toggleFallback(true); - return Promise.resolve(); - } - - if (this.element.getAttribute('src')) { - assertHttpsUrl(this.element.getAttribute('src'), this.element); - } - this.propagateAttributes( - ['src', 'controls', 'autoplay', 'muted', 'loop'], - this.video_); - - if (this.element.hasAttribute('preload')) { - this.video_.setAttribute( - 'preload', this.element.getAttribute('preload')); - } else { - this.video_.removeAttribute('preload'); - } - - this.getRealChildNodes().forEach(child => { - // Skip the video we already added to the element. - if (this.video_ === child) { - return; - } - if (child.getAttribute && child.getAttribute('src')) { - assertHttpsUrl(child.getAttribute('src'), child); - } - this.video_.appendChild(child); - }); - - return loadPromise(this.video_); - } - - /** @override */ - pauseCallback() { - if (this.video_) { - this.video_.pause(); - } - } - - /** @private */ - isVideoSupported_() { - return !!this.video_.play; - } - } - - registerElement(win, 'amp-video', AmpVideo); -} diff --git a/css/Z_INDEX.md b/css/Z_INDEX.md new file mode 100644 index 000000000000..7a265bb6b2bd --- /dev/null +++ b/css/Z_INDEX.md @@ -0,0 +1,39 @@ +selector | z-index | file +--- | --- | --- +.-amp-image-lightbox-container | 0 | extensions/amp-image-lightbox/0.1/amp-image-lightbox.css +.amp-video-eq | 1 | css/amp.css +.i-amphtml-layout-size-defined > [placeholder] | 1 | css/amp.css +.i-amphtml-loading-container | 1 | css/amp.css +.-amp-image-lightbox-viewer-image | 1 | extensions/amp-image-lightbox/0.1/amp-image-lightbox.css +.-amp-image-lightbox-viewer | 1 | extensions/amp-image-lightbox/0.1/amp-image-lightbox.css +.i-amphtml-layout-size-defined > [fallback] | 1 | css/amp.css +.i-amphtml-loader-moving-line | 2 | css/amp.css +.i-amphtml-element > [overflow] | 2 | css/amp.css +.-amp-image-lightbox-caption | 2 | extensions/amp-image-lightbox/0.1/amp-image-lightbox.css +.amp-carousel-button | 10 | extensions/amp-carousel/0.1/amp-carousel.css +amp-sticky-ad | 11 | extensions/amp-sticky-ad/1.0/amp-sticky-ad.css +amp-sticky-ad | 11 | extensions/amp-sticky-ad/0.1/amp-sticky-ad.css +amp-sticky-ad-top-padding | 12 | extensions/amp-sticky-ad/1.0/amp-sticky-ad.css +amp-app-banner | 13 | extensions/amp-app-banner/0.1/amp-app-banner.css +.amp-app-banner-dismiss-button | 14 | extensions/amp-app-banner/0.1/amp-app-banner.css +i-amp-app-banner-top-padding | 15 | extensions/amp-app-banner/0.1/amp-app-banner.css +amp-image-lightbox | 1000 | extensions/amp-image-lightbox/0.1/amp-image-lightbox.css +amp-user-notification | 1000 | extensions/amp-user-notification/0.1/amp-user-notification.css +amp-live-list > [update] | 1000 | extensions/amp-live-list/0.1/amp-live-list.css +amp-lightbox | 1000 | extensions/amp-lightbox/0.1/amp-lightbox.css +.-amp-lightboxed-ancestor | auto | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.-amp-image-lightbox-trans | 1001 | extensions/amp-image-lightbox/0.1/amp-image-lightbox.css +.-amp-lbv-mask | 2147483642 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.amp-lightboxed | 2147483643 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.amp-lbv-desc-box | 2147483644 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.-amp-lbv-gallery | 2147483645 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.-amp-sidebar-mask | 2147483646 | extensions/amp-sidebar/0.1/amp-sidebar.css +.amp-lbv-button-prev | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +.amp-lbv-button-next | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css + +.amp-lbv-button-gallery | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css + +.amp-lbv-button-back | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css + +.amp-lbv-button-close | 2147483646 | extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.css +amp-sidebar | 2147483647 | extensions/amp-sidebar/0.1/amp-sidebar.css \ No newline at end of file diff --git a/css/amp.css b/css/amp.css index a41212a1f318..aece5c687b04 100644 --- a/css/amp.css +++ b/css/amp.css @@ -47,35 +47,84 @@ body { text-size-adjust: 100%; } -.-amp-element { + +/** + * The enables passive touch handlers, e.g. for document swipe, since they + * no will longer need to try to cancel vertical scrolls during swipes. + * This is only done in the embedded mode because (a) the document swipe + * is only possible in this case, and (b) we'd like to preserve pinch-zoom. + */ +html.i-amphtml-singledoc.i-amphtml-embedded { + touch-action: pan-y; +} + + +/** + * iOS-Embed Wrapper mode. The `body` is wrapped into `#i-amphtml-wrapper` + * element. + */ +html.i-amphtml-ios-embed { + overflow-y: auto !important; + -webkit-overflow-scrolling: touch !important; + position: static; +} + +/** Wrapper for iOS-Embed-Wrapper mode. */ +#i-amphtml-wrapper { + overflow-x: hidden !important; + overflow-y: auto !important; + -webkit-overflow-scrolling: touch !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + margin: 0 !important; + display: block !important; +} + +#i-amphtml-wrapper > body { + position: relative !important; + /* Border is necessary to avoid margins collapsing outside body. + Alternatively we can also set overflow to hidden. */ + border-top: 1px solid transparent !important; +} + + +/** make-body-block experiment */ +.i-amphtml-make-body-block body { + display: block !important; +} + +.i-amphtml-element { display: inline-block; } -.-amp-layout-nodisplay { +.i-amphtml-layout-nodisplay { /* Display is set/reset in JS */ } -.-amp-layout-fixed { +.i-amphtml-layout-fixed { display: inline-block; position: relative; } -.-amp-layout-responsive { +.i-amphtml-layout-responsive { display: block; position: relative; } -.-amp-layout-fixed-height { +.i-amphtml-layout-fixed-height { display: block; position: relative; } -.-amp-layout-container { +.i-amphtml-layout-container { display: block; position: relative; } -.-amp-layout-fill { +.i-amphtml-layout-fill { display: block; overflow: hidden !important; position: absolute; @@ -85,29 +134,36 @@ body { right: 0; } -.-amp-layout-flex-item { +.i-amphtml-layout-flex-item { display: block; position: relative; flex: 1 1 auto; } -.-amp-layout-size-defined { +.i-amphtml-layout-size-defined { overflow: hidden !important; } -i-amp-sizer { +i-amphtml-sizer { display: block !important; } -.-amp-fill-content { +/* TODO(dvoytenko, #6794): Remove old `-amp-fill-content` form after the new + form is in PROD for 1-2 weeks. */ +.-amp-fill-content, +.i-amphtml-fill-content { display: block; - min-width: 100%; /* These lines are a work around to this issue in iOS: */ - max-width: 100%; /* https://bugs.webkit.org/show_bug.cgi?id=155198 */ - height: 100%; + width: 1px; /* These lines are a work around to this issue in iOS: */ + min-width: 100%; /* https://bugs.webkit.org/show_bug.cgi?id=155198 */ + height: 1px; + min-height: 100%; margin: auto; } -.-amp-layout-size-defined .-amp-fill-content { +/* TODO(dvoytenko, #6794): Remove old `-amp-fill-content` form after the new + form is in PROD for 1-2 weeks. */ +.i-amphtml-layout-size-defined .-amp-fill-content, +.i-amphtml-layout-size-defined .i-amphtml-fill-content { position: absolute; top: 0; left: 0; @@ -115,17 +171,52 @@ i-amp-sizer { right: 0; } -.-amp-replaced-content { +/* TODO(dvoytenko, #6794): Remove old `-amp-replaced-content` form after the new + form is in PROD for 1-2 weeks. */ +.-amp-replaced-content, +.i-amphtml-replaced-content { padding: 0 !important; border: none !important; /* TODO(dvoytenko): explore adding here object-fit. */ } +/** + * Makes elements visually invisible but still accessible to screen-readers. + * + * This Css has been carefully tested to ensure screen-readers can read and + * activate (in case of links and buttons) the elements with this class. Please + * use caution when changing anything, even seemingly safe ones. For example + * changing width from 1 to 0 would prevent TalkBack from activating (clicking) + * buttons despite TalkBack reading them just fine. This is because + * element needs to have a defined size and be on viewport otherwise TalkBack + * does not allow activation of buttons. + */ +.-amp-screen-reader { + position: fixed !important; + /* keep it on viewport */ + top: 0px !important; + left: 0px !important; + /* give it non-zero size, VoiceOver on Safari requires at least 2 pixels + before allowing buttons to be activated. */ + width: 2px !important; + height: 2px !important; + /* visually hide it with overflow and opacity */ + opacity: 0 !important; + overflow: hidden !important; + /* remove any margin or padding */ + border: none !important; + margin: 0 !important; + padding: 0 !important; + /* ensure no other style sets display to none */ + display: block !important; + visibility: visible !important; +} + /* For author styling. */ .amp-unresolved { } -.-amp-unresolved { +.i-amphtml-unresolved { position: relative; overflow: hidden !important; } @@ -137,13 +228,13 @@ i-amp-sizer { /* For author styling. */ } -.-amp-notbuilt { +.i-amphtml-notbuilt { position: relative; overflow: hidden !important; color: transparent !important; } -.-amp-notbuilt > * { +.i-amphtml-notbuilt > * { display: none; } @@ -151,29 +242,29 @@ i-amp-sizer { visibility: hidden !important; } -.-amp-layout { +.i-amphtml-layout { /* Just state. */ } -.-amp-error { +.i-amphtml-error { /* Just state. */ } -.-amp-element > [placeholder] { +.i-amphtml-element > [placeholder] { display: block; } -.-amp-element > [placeholder].hidden, -.-amp-element > [placeholder].amp-hidden { +.i-amphtml-element > [placeholder].hidden, +.i-amphtml-element > [placeholder].amp-hidden { visibility: hidden; } -.-amp-element:not(.amp-notsupported) > [fallback] { +.i-amphtml-element:not(.amp-notsupported) > [fallback] { display: none; } -.-amp-layout-size-defined > [placeholder], -.-amp-layout-size-defined > [fallback] { +.i-amphtml-layout-size-defined > [placeholder], +.i-amphtml-layout-size-defined > [fallback] { position: absolute !important; top: 0 !important; left: 0 !important; @@ -182,21 +273,21 @@ i-amp-sizer { z-index: 1 !important; } -.-amp-notbuilt > [placeholder] { +.i-amphtml-notbuilt > [placeholder] { display: block !important; } -.-amp-hidden-by-media-query { +.i-amphtml-hidden-by-media-query { display: none; } -.-amp-element-error { +.i-amphtml-element-error { background: red !important; color: white !important; position: relative !important; } -.-amp-element-error::before { +.i-amphtml-element-error::before { content: attr(error-message); } @@ -222,27 +313,56 @@ i-amp-scroll-container.amp-active { overflow: auto; } -.-amp-loading-container { +.i-amphtml-loading-container { display: block !important; z-index: 1; } -.-amp-notbuilt > .-amp-loading-container { +.i-amphtml-notbuilt > .i-amphtml-loading-container { display: block !important; } /** - * `-amp-loading-container`, `-amp-loader` and `-amp-loader-dot` all support - * a "loading indicator" usecase. `-amp-loading-container` is mostly responsible - * for alighning the loading indicator, while `-amp-loader` and - * `-amp-loader-dot` are an implementation for a default loading indicator. The + * `i-amphtml-loading-container`, `i-amphtml-loader` and `i-amphtml-loader-dot` all support + * a "loading indicator" usecase. `i-amphtml-loading-container` is mostly responsible + * for alighning the loading indicator, while `i-amphtml-loader` and + * `i-amphtml-loader-dot` are an implementation for a default loading indicator. The * default implementation includes the three-dot layout and animation. */ -.-amp-loading-container.amp-hidden { +.i-amphtml-loading-container.amp-hidden { visibility: hidden; } -.-amp-loader { +.i-amphtml-loader-line { + position: absolute; + top:0; + left: 0; + right: 0; + height: 1px; + overflow: hidden !important; + background-color:rgba(151, 151, 151, 0.2); + display: block; +} + +.i-amphtml-loader-moving-line { + display: block; + position: absolute; + width: 100%; + height: 100% !important; + background-color: rgba(0, 0, 0, 0.5); + z-index: 2; +} + +@keyframes i-amphtml-loader-line-moving { + 0% {transform: translateX(-100%);} + 100% {transform: translateX(100%);} +} + +.i-amphtml-loader-line.amp-active .i-amphtml-loader-moving-line { + animation: i-amphtml-loader-line-moving 2s ease infinite; +} + +.i-amphtml-loader { position: absolute; display: block; height: 10px; @@ -253,11 +373,11 @@ i-amp-scroll-container.amp-active { white-space: nowrap; } -.-amp-loader.amp-active .-amp-loader-dot { - animation: -amp-loader-dots 2s infinite; +.i-amphtml-loader.amp-active .i-amphtml-loader-dot { + animation: i-amphtml-loader-dots 2s infinite; } -.-amp-loader-dot { +.i-amphtml-loader-dot { position: relative; display: inline-block; @@ -271,19 +391,19 @@ i-amp-scroll-container.amp-active { will-change: transform; } -.-amp-loader .-amp-loader-dot:nth-child(1) { +.i-amphtml-loader .i-amphtml-loader-dot:nth-child(1) { animation-delay: 0s; } -.-amp-loader .-amp-loader-dot:nth-child(2) { +.i-amphtml-loader .i-amphtml-loader-dot:nth-child(2) { animation-delay: .1s; } -.-amp-loader .-amp-loader-dot:nth-child(3) { +.i-amphtml-loader .i-amphtml-loader-dot:nth-child(3) { animation-delay: .2s; } -@keyframes -amp-loader-dots { +@keyframes i-amphtml-loader-dots { 0%, 100% { transform: scale(.7); background-color: rgba(0, 0, 0, .3); @@ -301,18 +421,21 @@ i-amp-scroll-container.amp-active { * not currently visible. Typically tapping on this element shows the full * content. */ -.-amp-element > [overflow] { +.i-amphtml-element > [overflow] { cursor: pointer; + /* position:relative is critical to ensure that [overflow] is displayed + above an iframe; z-index is not enough. */ + position: relative; z-index: 2; visibility: hidden; } -.-amp-element > [overflow].amp-visible { +.i-amphtml-element > [overflow].amp-visible { visibility: visible; } -/* Polyfill for IE and any other browser that don't understand templates . */ +/* Polyfill for IE and any other browser that don't understand templates. */ template { display: none !important; } @@ -328,24 +451,9 @@ template { box-sizing: border-box; } +/* amp-pixel is always non-displayed. */ amp-pixel { - /* Fixed to make position independent of page other elements. */ - position: fixed !important; - top: 0 !important; - width: 1px !important; - height: 1px !important; /* Only things with height are ever loaded. */ - overflow: hidden !important; - visibility: hidden; -} - -/** - * Force the layout box of the ad iframe to be exactly as big as the actual - * iframe. The `amp-ad` tag itself can be freely styled. - */ -amp-ad iframe { - border: 0 !important; - margin: 0 !important; - padding: 0 !important; + display: none !important; } /** @@ -378,6 +486,13 @@ amp-analytics { visibility: hidden; } +/** + * Iframe allows setting frameborder, so we need to set box-sizing to border-box + * or otherwise the iframe will oevrflow its parent when there is a border. + */ +amp-iframe iframe { + box-sizing: border-box !important; +} /** * Minimal AMP Access CSS. This part has to be here so that the correct UI @@ -386,3 +501,126 @@ amp-analytics { [amp-access][amp-access-hide] { display: none; } + + +/** + * Forms error/success messaging containers should be hidden at first. + */ +form [submit-success], +form [submit-error] { + display: none; +} + +/** + * Hide the update reference point of amp-live-list by default. This is + * reset by the `amp-live-list > .amp-active[update]` selector. + */ +amp-live-list > [update] { + display: none; +} + +/** + * Display none elements + */ +amp-experiment, amp-share-tracking { + display: none; +} + +amp-fresh { + visibility: hidden; +} + +.i-amphtml-jank-meter { + position: fixed; + background-color: rgba(232,72,95,.5); + bottom: 0; + left: 0; + color: #fff; + font-size: 16px; + text-align: center; + z-index: 1000; + padding: 5px; +} + +/** + * Animated equalizer icon for muted autoplaying videos. + * + * TODO(aghassemi, #5306): Move to video extension when video is out of core. + */ +.amp-video-eq { + align-items: flex-end; + bottom: 7px; + display: flex; + height: 12px; + opacity: 0.8; + overflow: hidden; + position: absolute; + right: 7px; + width: 20px; + z-index: 1; +} +.amp-video-eq .amp-video-eq-col { + flex: 1; + height: 100%; + margin-right: 1px; + position: relative; +} +.amp-video-eq .amp-video-eq-col div { + animation-name: amp-video-eq-animation; + animation-timing-function: linear; + animation-iteration-count: infinite; + animation-direction: alternate; + background-color: #FAFAFA; + height: 100%; + position: absolute; + width: 100%; + will-change: transform; +} +.amp-video-eq .amp-video-eq-col div { + animation-play-state: paused; +} +.amp-video-eq[unpausable] .amp-video-eq-col div { + animation-name: none; +} +.amp-video-eq[unpausable].amp-video-eq-play .amp-video-eq-col div { + animation-name: amp-video-eq-animation; +} +.amp-video-eq.amp-video-eq-play .amp-video-eq-col div { + animation-play-state: running; +} +.amp-video-eq-1-1 { + animation-duration: 0.3s; + transform: translateY(60%); +} +.amp-video-eq-1-2 { + animation-duration: 0.45s; + transform: translateY(60%); +} +.amp-video-eq-2-1 { + animation-duration: 0.5s; + transform: translateY(30%); +} +.amp-video-eq-2-2 { + animation-duration: 0.4s; + transform: translateY(30%); +} +.amp-video-eq-3-1 { + animation-duration: 0.3s; + transform: translateY(70%); +} +.amp-video-eq-3-2 { + animation-duration: 0.35s; + transform: translateY(70%); +} +.amp-video-eq-4-1 { + animation-duration: 0.4s; + transform: translateY(50%); +} +.amp-video-eq-4-2 { + animation-duration: 0.25s; + transform: translateY(50%); +} +@keyframes amp-video-eq-animation { + 0% {transform: translateY(100%);} + 100% {transform: translateY(0);} +} diff --git a/examples/OWNERS.yaml b/examples/OWNERS.yaml new file mode 100644 index 000000000000..abf513ce7169 --- /dev/null +++ b/examples/OWNERS.yaml @@ -0,0 +1,2 @@ +- camelburrito +- lannka diff --git a/examples/a4a-multisize.amp.html b/examples/a4a-multisize.amp.html new file mode 100644 index 000000000000..ba87e2c33d0e --- /dev/null +++ b/examples/a4a-multisize.amp.html @@ -0,0 +1,68 @@ + + + + + AMP A4A examples + + + + + + + + + + +

    A4A Examples

    + +

    Doubleclick

    +
    +

    Doubleclick 3p (most likely)

    + +
    +
    +
    + +

    Doubleclick A4A (guaranteed)

    + +
    +
    +
    + + diff --git a/examples/a4a.amp.html b/examples/a4a.amp.html new file mode 100644 index 000000000000..af4f8228fa50 --- /dev/null +++ b/examples/a4a.amp.html @@ -0,0 +1,75 @@ + + + + + AMP A4A examples + + + + + + + + + + +

    A4A Examples

    + +

    Fake ad network

    + +
    +
    +
    + +

    TripleLift ad network

    + +
    +
    +
    + +

    Fake cloudflare as ad network ad

    + +
    +
    +
    + + + diff --git a/examples/ads.with.script.amp.html b/examples/ads-legacy.amp.html similarity index 94% rename from examples/ads.with.script.amp.html rename to examples/ads-legacy.amp.html index 1893d559b895..19f66a1b0d64 100644 --- a/examples/ads.with.script.amp.html +++ b/examples/ads-legacy.amp.html @@ -26,13 +26,14 @@ padding: 0; } - + - +

    A9

    AdSense data-ad-slot="7783467241"> +

    AdSpirit

    + + +

    AdTech

    AppNexus with JSON based configuration multi ad json='{"pageOpts":{"member": 958}, "adUnits": [{"disablePsa": true, "tagId": 6063968,"sizes": [300,250],"targetId": "apn_ad_1"}, {"tagId": 6063968,"sizes": [728,90],"targetId":"apn_ad_2"}]}'> +

    Atomx

    + + +

    Doubleclick

    E-Planning 320x50 data-epl_e="AMP_TEST"> +

    Geniee SSP

    + + + +

    PulsePoint 300x250

    + + + diff --git a/examples/ads.amp.html b/examples/ads.amp.html index 0fa981cc4950..c4e32720ad98 100644 --- a/examples/ads.amp.html +++ b/examples/ads.amp.html @@ -6,14 +6,20 @@ + + + - - -

    A9

    - - - -

    Weborama

    - -
    Loading ad.
    -
    Ad could not be loaded.
    -
    +

    Ad networks in red color have broken examples, please help to fix.

    +
    -

    Fixed positioned ad (will not render)

    - +
    + Fixed positioned ad (will not render) +
    -

    Adblade

    - A9 + +
    +
    +
    + +

    AccessTrade

    + +
    +
    +
    + +

    Adblade

    + +
    +
    -

    ADITION

    - AdButler + +
    +
    +
    + +

    ADITION

    + +
    +
    -

    Adman

    +

    Ad Generation

    + +
    +
    +
    +

    Adman

    - +
    +
    +
    -

    AdReactor

    - AdReactor + +
    +
    -

    AdSense

    - AdSense + + data-ad-client="ca-pub-2005682797531342" + data-ad-slot="7046626912"> +
    +
    +
    + +

    AdsNative

    + +
    +
    -

    AdTech

    - AdSpirit + +
    +
    +
    + +

    AdStir 320x50 banner

    + +
    +
    +
    + +

    AdTech (1x1 fake image ad)

    + +
    +
    -

    Ad Up Technology

    - Ad Up Technology + +
    +
    -

    Teads

    - +

    Adverline

    + +
    +
    +
    -
    Teads fallback - Discover inRead by Teads !
    +

    Adverticum

    + +
    +
    -

    AppNexus single ad with tagid (placement id)

    - +

    AdvertServe

    + +
    +
    -

    AppNexus single ad with member and code

    - +

    Affiliate-B

    + +
    +
    -

    AppNexus with JSON based configuration multi ad

    - AMoAd banner + +
    +
    +
    + +

    AMoAd native

    + +
    +
    +
    + +

    AppNexus with JSON based configuration multi ad

    + +
    +
    - +
    +
    -

    Doubleclick

    - +

    Atomx

    + -

    Doubleclick with JSON based parameters

    - +

    brainy

    + +
    +
    -

    Doubleclick no ad with fallback

    - -
    - Thank you for trying AMP! +

    CA A.J.A. Infeed

    + +
    +
    +
    -

    You got lucky! We have no ad to show to you!

    -
    +

    CA ProFit-X

    + +
    +
    -

    Doubleclick no ad with placeholder and fallback

    - -
    - Placeholder here!!! -
    -
    - Thank you for trying AMP! +

    Chargeads

    + +
    +
    +
    -

    You got lucky! We have no ad to show to you!

    -
    +

    Colombia ad

    + Issue on GitHub
    + +
    +
    -

    Doubleclick with overriden size

    - +

    Content.ad Banner 320x50

    + +
    +
    +
    + +

    Content.ad Banner 300x250

    + +
    +
    +
    + +

    Content.ad Banner 300x250 2x2

    + -

    Challenging ad.

    - - -

    Criteo

    +

    Content.ad Banner 300x600

    + +
    +
    +
    + +

    Content.ad Banner 300x600 5x2

    + +
    +
    +
    + +

    Criteo Passback

    +

    Due to ad targeting, the slot might not load ad.

    + +
    +
    -

    Flite

    - +

    Criteo RTA

    +

    Due to ad targeting, the slot might not load ad.

    + + +
    +
    -

    MANTIS

    - +

    CSA

    + - - +

    Custom leaderboard

    + + +
    +
    +
    -

    Media Impact

    - +

    Custom square

    + + +
    +
    -

    plista responsive widget

    - - +

    Custom leaderboard with no slot specified

    + + +
    +
    +
    -

    Taboola responsive widget

    - - +

    DistroScale

    + +
    +
    +
    -

    DotAndAds masthead

    - DotAndAds masthead + +
    +

    DotAndAds 300x250 box

    - +
    +
    +
    + +

    Doubleclick

    + +
    +
    +
    + +

    Doubleclick with JSON based parameters

    + +
    +
    +
    + +

    Doubleclick no ad

    + +
    +
    +
    + +

    Doubleclick with overriden size

    + +
    +
    +
    + +

    Doubleclick challenging ad

    + +
    +
    -

    Industrybrains

    - E-Planning 320x50 + +
    +
    +
    + +

    Ezoic

    + +
    +
    +
    + +

    FlexOneELEPHANT

    + + + +

    Felmat

    + +
    +
    +
    + +

    Flite

    + +
    +
    +
    + +

    Fusion

    + +
    +
    +
    + +

    Geniee SSP

    + +
    +
    +
    + +

    GMOSSP 320x50 banner

    + +
    +
    +
    + +

    Holder 300x250 banner

    + +
    +
    +
    + +

    iBillboard 300x250 banner

    + + + +

    I-Mobile 320x50 banner

    + +
    +
    +
    + +

    Industrybrains

    + +
    +
    +
    + +

    InMobi

    + +
    +
    +
    + +

    Index Exchange

    + +
    +
    +
    + +

    Index Exchange Header Tag

    + +
    +
    +
    + +

    Kargo

    + +
    +
    +
    + +

    Kixer

    + +
    +
    +
    + +

    Ligatus

    + +
    +
    +
    + +

    LOKA

    + +
    +
    +
    + +

    MADS

    + +
    +
    +
    + +

    MANTIS

    + +
    +
    +
    + + +
    +
    +
    + +

    Media Impact

    + +
    +
    +
    + +

    Media.Net Header Bidder Tag

    + +
    +
    +
    + +

    Media.Net Contextual Monetization Tag

    + +
    +
    +
    + +

    Mediavine

    + +
    +
    +
    + +

    Meg

    + +
    +
    -

    Open AdStream single ad

    - +

    MicroAd 320x50 banner

    + +
    +
    +
    + +

    Mixpo

    + +
    +
    +
    + +

    Nativo

    + +
    +
    -

    OpenX

    +

    Nend

    + +
    +
    +
    + +

    Nokta

    + +
    +
    +
    + +

    Open AdStream single ad

    + +
    +
    +
    + +

    OpenX

    + type="openx" + data-auid="538289845" + data-host="sademo-d.openx.net" + data-nc="90577858-BidderTest"> +
    +
    -

    SmartAdServer ad

    - +

    Plista responsive widget

    + +
    +
    +
    + +

    popIn native ad

    + +
    +
    -

    SOVRN

    - +

    Pubmine 300x250

    + +
    +
    -

    Yieldmo

    - +

    PulsePoint Header Bidding 300x250

    + +
    +
    -

    Revcontent Widget with placeholder and fallback

    - +

    PulsePoint 300x250

    + +
    +
    +
    -
    200 Billion Content recommendations!
    -
    200 Billion Content recommendations!
    +

    Purch 300x250

    + +
    +
    -

    Sortable ad

    - +

    Rambler&Co

    + +
    +
    +
    + +

    Relap

    + +
    +
    -

    TripleLift

    - Revcontent Responsive Tag + + heights="(max-width: 320px) 933px, + (max-width: 360px) 1087px, + (max-width: 375px) 1138px, + (max-width: 412px) 1189px, + (max-width: 414px) 1072px, + (max-width: 568px) 1151px, + (max-width: 640px) 1128px, + (max-width: 667px) 1151px, + (max-width: 732px) 1211px, + (max-width: 736px) 1151px, + (max-width: 768px) 633px, + (max-width: 1024px) 711px, + 86vw" + data-wrapper="rcjsload_2ff711" + data-id="203"> +
    +
    -

    Rubicon Project Smart Tag

    +

    Rubicon Project Smart Tag

    Rubicon Project Smart Tag data-size="43" data-kw="amp-test, test" json='{"visitor":{"age":"18-24","gender":"male"},"inventory":{"section":"amp"}}'> -
    Ad Loading...
    -
    Ad Load Failed.
    +
    +

    Rubicon Project FastLane Single Slot

    @@ -400,101 +1103,246 @@

    Rubicon Project FastLane Single Slot

    data-pos="atf" data-kw="amp-test" json='{"targeting":{"kw":"amp-test","age":"18-24","gender":"male","section":"amp"},"visitor":{"age":"18-24","gender":"male"},"inventory":{"section":"amp"}}'> -
    Ad Loading...
    -
    Ad Load Failed.
    +
    +
    -

    I-Mobile 320x50 banner

    - +

    Sharethrough

    + +
    +
    -

    GMOSSP 320x50 banner

    - +

    Sklik

    + +
    +
    -

    Yieldbot 300x250

    +

    SmartAdServer ad

    + +
    +
    +
    + +

    smartclip

    + +
    +
    +
    + +

    Sortable ad

    + type="sortable" + data-name="medrec" + data-site="ampproject.org"> +
    +
    -

    AdStir 320x50 banner

    - +

    SOVRN

    + +
    +
    -

    Colombia ad

    - +

    Swoop

    + +
    +
    -

    Sharethrough

    - Taboola responsive widget + + heights="(min-width:1907px) 39%, (min-width:1200px) 46%, (min-width:780px) 64%, (min-width:480px) 98%, (min-width:460px) 167%, 196%" + data-publisher="amp-demo" + data-mode="thumbnails-a" + data-placement="Ads Example" + data-article="auto"> +
    +
    +
    + +

    Teads

    + +
    +
    -

    E-Planning 320x50

    - +

    TripleLift

    + +
    +
    -

    MicroAd 320x50 banner

    - +

    Weborama

    + +
    +
    +
    + +

    Widespace 300x50 Ad

    + +
    +
    +
    + +

    Widespace 300x300 no-ad fallback

    + +
    +
    +
    + +

    Xlift native ad

    + +
    +
    -

    YahooJP YDN

    - Yahoo Display + +
    +
    +
    + +

    YahooJP YDN

    + +
    +
    +
    + +

    Yieldbot 300x250

    + +
    +
    +
    + +

    YIELD ONE

    + +
    +
    +
    + +

    Yieldmo

    + +
    +
    -

    Chargeads

    - +

    ValueCommerce

    + +
    +
    -

    Nend

    - +

    Webediads

    +
    It's a private ad network with strict ad targeting, hence very common to see a no-fill.
    + +
    +
    +

    ZEDO

    + +
    +
    +
    + +

    ZergNet

    + +
    +
    +
    + +

    Zucks

    + +
    +
    +
    diff --git a/examples/adsense.amp.html b/examples/adsense.amp.html new file mode 100644 index 000000000000..dcc9272a96d4 --- /dev/null +++ b/examples/adsense.amp.html @@ -0,0 +1,96 @@ + + + + + Adsense examples + + + + + + + + + + + + + This site uses cookies to personalize content. + Learn more. + + + +

    AdSense

    + +
    +
    +
    + + +
    should only show ad after notification get dismissed
    +
    +
    + +
    + +
    + +

    AdSense ad 2

    + + +
    +
    +
    + + + diff --git a/examples/alp.amp.html b/examples/alp.amp.html index 516cf19d00de..51c50641c490 100644 --- a/examples/alp.amp.html +++ b/examples/alp.amp.html @@ -8,10 +8,12 @@ + +

    ALP

    - +

    target=_blank

    + + diff --git a/examples/amp-fresh.amp.html b/examples/amp-fresh.amp.html new file mode 100644 index 000000000000..ebd360766c14 --- /dev/null +++ b/examples/amp-fresh.amp.html @@ -0,0 +1,22 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + +
    + hello +
    + + diff --git a/examples/amp-video.amp.html b/examples/amp-video.amp.html new file mode 100644 index 000000000000..5c92c1d7a92c --- /dev/null +++ b/examples/amp-video.amp.html @@ -0,0 +1,66 @@ + + + + + AMP #0 + + + + + + + + + +

    amp-video

    + + +
    + This is a placeholder +
    +
    + This is a fallback +
    +
    +

    Actions

    + + + + + +

    Autoplay

    + + +
    + + diff --git a/examples/amp-youtube.amp.html b/examples/amp-youtube.amp.html new file mode 100644 index 000000000000..ce70dfce9eb8 --- /dev/null +++ b/examples/amp-youtube.amp.html @@ -0,0 +1,58 @@ + + + + + AMP #0 + + + + + + + + + +

    amp-youtube

    + + + +

    Actions

    + + + + + +

    Autoplay

    + + +
    + + diff --git a/examples/ampcontext-creative.html b/examples/ampcontext-creative.html new file mode 100644 index 000000000000..dbeea5d128ff --- /dev/null +++ b/examples/ampcontext-creative.html @@ -0,0 +1,103 @@ + + + + + + + + + Test Ad using ampcontext.js to create window.context + + + + + + diff --git a/examples/analytics-notification.amp.html b/examples/analytics-notification.amp.html index 0e0874028594..67862dfbaa64 100644 --- a/examples/analytics-notification.amp.html +++ b/examples/analytics-notification.amp.html @@ -66,8 +66,8 @@ + data-show-if-href="https://example.com/api/show?timestamp=TIMESTAMP" + data-dismiss-href="https://example.com/api/echo/post"> This site uses cookies to personalize content. I accept diff --git a/examples/analytics-vendors.amp.html b/examples/analytics-vendors.amp.html new file mode 100644 index 000000000000..b704d1bc1af4 --- /dev/null +++ b/examples/analytics-vendors.amp.html @@ -0,0 +1,687 @@ + + + + + AMP Analytics + + + + + + + + + +
    +Container for analytics tags. Positioned far away from top to make sure that doesn't matter. + + + + + + + + + ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +

    AMP Analytics

    + + + Click here to generate an event + +

    +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam pellentesque augue quis elementum tempus. Pellentesque sit amet neque bibendum, sagittis purus vitae, pellentesque magna. Vestibulum non viverra metus, eget feugiat lacus. Nulla in maximus orci. Maecenas id turpis vel ipsum vestibulum bibendum ut sit amet magna. Nullam hendrerit ex at est eleifend, nec dignissim nibh rutrum. Aliquam quis tellus et nibh faucibus laoreet in eget turpis. Nam quam nisl, porttitor vel ex eget, dapibus placerat dui. Mauris commodo pellentesque leo, eu tempus quam. In hac habitasse platea dictumst. Suspendisse non ante finibus, luctus augue non, luctus orci. Vestibulum ornare lacinia aliquam. In sollicitudin vehicula vulputate. Sed mi elit, commodo nec sapien nec, pretium bibendum leo. Donec id justo tortor. Ut in mauris dapibus, laoreet metus vitae, dictum nisi. +

    +

    +Integer dapibus egestas arcu. Nunc vitae velit congue, placerat augue quis, suscipit nisi. Donec suscipit imperdiet turpis pharetra feugiat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus aliquam eleifend dolor, at lacinia orci semper vel. Nunc semper sem vel tincidunt posuere. Nunc lobortis velit vitae condimentum mollis. Morbi eu ullamcorper mauris. Pellentesque ac eros maximus, pulvinar sapien vitae, semper nisi. Curabitur imperdiet non mauris vitae sollicitudin. +

    +

    +Nam posuere velit euismod risus pulvinar, in sollicitudin sapien consectetur. Vestibulum nec ex odio. Quisque at elit nec nunc ultricies lacinia nec non lorem. Maecenas porttitor consequat mauris, vitae porttitor ligula pellentesque ut. Pellentesque rhoncus diam vel lacus lobortis imperdiet. Sed maximus dictum hendrerit. Vivamus ornare, purus in laoreet sagittis, est ante pretium mauris, vel vulputate arcu erat eget mauris. Suspendisse eu lorem metus. Aliquam tempus aliquet urna, vitae mollis lacus pretium vitae. Etiam semper gravida commodo. Maecenas at pulvinar quam. Nullam dolor ipsum, ornare a sollicitudin et, sodales porttitor neque. +

    +

    +Integer in felis at lacus mattis facilisis. Curabitur tincidunt, felis porttitor mollis finibus, tortor elit elementum dolor, vel vulputate lorem dui id ante. Vivamus in velit at lectus blandit gravida vitae quis arcu. Nam et magna magna. Fusce condimentum diam lacus, ac ullamcorper purus malesuada eu. Mauris ullamcorper elit et venenatis faucibus. Nullam lobortis molestie purus quis pellentesque. Sed at libero id nisi rhoncus tincidunt. Praesent vestibulum vehicula tristique. Etiam rutrum, nunc id porta interdum, nulla nisi molestie leo, at fermentum justo dolor at lorem. Duis in egestas sapien. +

    +

    +Donec pharetra molestie sollicitudin. Duis mattis eleifend rutrum. Quisque luctus tincidunt lacus, vitae lobortis nisi malesuada ac. Aliquam mattis leo vel elit rutrum, nec consequat massa vestibulum. Maecenas bibendum metus nec ante feugiat, eu faucibus orci mattis. Cras tristique sem non elit congue malesuada. Proin ornare, lacus et porttitor consequat, sapien urna rutrum diam, ac pellentesque ligula est eget nisi. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec ultrices sollicitudin eros a placerat. Proin eget pulvinar est. Donec posuere ultrices odio at ultrices. Suspendisse potenti. Phasellus id orci id purus porttitor consectetur a at erat. Nullam volutpat ultricies nisl id maximus. Morbi porta ex ante, et egestas odio ultricies consequat. +

    +

    +

      +
    • Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    • +
    • Aliquam in ex porta, imperdiet elit sit amet, condimentum diam.
    • +
    • Etiam fermentum nisi at porta pulvinar.
    • +
    +

    +

    +

      +
    • Proin mattis neque vel elit posuere molestie.
    • +
    • Integer tincidunt sem sed nunc auctor elementum.
    • +
    • Integer a felis in ipsum aliquet auctor sit amet a neque.
    • +
    +

    +

    +

      +
    • Sed suscipit dolor molestie, rhoncus quam ac, lacinia ex.
    • +
    • Curabitur et tellus vel justo ultrices aliquet sed id turpis.
    • +
    • Nam finibus risus at justo elementum bibendum.
    • +
    • In non lacus non urna congue feugiat at vel diam.
    • +
    +

    +

    +

      +
    • Integer hendrerit augue interdum dui venenatis, sit amet tristique mauris cursus.
    • +
    • Etiam quis eros viverra, tincidunt justo in, facilisis nunc.
    • +
    • Aliquam at lacus faucibus, congue lorem interdum, semper mauris.
    • +
    • Ut vulputate erat vel feugiat pharetra.
    • +
    • Morbi id augue id orci sagittis tempus.
    • +
    • Vestibulum varius libero ac dignissim sodales.
    • +
    +

    +

    +

      +
    • Aenean ac sem eget libero varius viverra sit amet vitae nunc.
    • +
    +

    + + diff --git a/examples/analytics.amp.html b/examples/analytics.amp.html index 1ae41b890336..6e2a7f8a7341 100644 --- a/examples/analytics.amp.html +++ b/examples/analytics.amp.html @@ -33,16 +33,17 @@ { "transport": {"beacon": false, "xhrpost": false}, "requests": { - "base": "https://example.com/?domain=${canonicalHost}&path=${canonicalPath}&title=${title}&time=${timestamp}&tz=${timezone}&pid=${pageViewId}&_=${random}", - "pageview": "${base}&name=${eventName}&type=${eventId}&screenSize=${screenWidth}x${screenHeight}", - "event": "${base}&name=${eventName}&scrollY=${scrollTop}&scrollX=${scrollLeft}&height=${availableScreenHeight}&width=${availableScreenWidth}&scrollBoundV=${verticalScrollBoundary}&scrollBoundH=${horizontalScrollBoundary}", - "visibility": "https://example.com/visibility?a=${maxContinuousTime}&b=${totalVisibleTime}&c=${firstSeenTime}&d=${lastSeenTime}&e=${fistVisibleTime}&f=${lastVisibleTime}&g=${minVisiblePercentage}&h=${maxVisiblePercentage}&i=${elementX}&j=${elementY}&k=${elementWidth}&l=${elementHeight}&m=${totalTime}&n=${loadTimeVisibility}&o=${backgroundedAtStart}&p=${backgrounded}" + "endpoint": "https://raw.githubusercontent.com/ampproject/amphtml/master/examples/img/ampicon.png", + "base": "${endpoint}?${type|default:foo}&path=${canonicalPath}", + "event": "${base}&scrollY=${scrollTop}&scrollX=${scrollLeft}&height=${availableScreenHeight}&width=${availableScreenWidth}&scrollBoundV=${verticalScrollBoundary}&scrollBoundH=${horizontalScrollBoundary}", + "visibility": "${base}&a=${maxContinuousVisibleTime}&b=${totalVisibleTime}&c=${firstSeenTime}&d=${lastSeenTime}&e=${fistVisibleTime}&f=${lastVisibleTime}&g=${minVisiblePercentage}&h=${maxVisiblePercentage}&i=${elementX}&j=${elementY}&k=${elementWidth}&l=${elementHeight}&m=${totalTime}&n=${loadTimeVisibility}&o=${backgroundedAtStart}&p=${backgrounded}&subTitle=${subTitle}", + "timer": "${base}&backgroundState=${backgroundState}" }, "vars": { "title": "Example Request" }, "extraUrlParams": { - "param1": "Interesting value", + "param1": "${random}", "platform": "AMP" }, "extraUrlParamsReplaceMap": { @@ -51,15 +52,21 @@ "triggers": { "defaultPageview": { "on": "visible", - "request": "pageview", + "request": "base", "vars": { - "eventName": "page-loaded", - "eventId": "42" + "type": "pageLoad" }, "extraUrlParams": { "param1": "Another value" } }, + "pageview": { + "on": "visible", + "request": "base", + "vars": { + "type": "${random}${title}" + } + }, "visibility": { "on": "visible", "request": "visibility", @@ -69,6 +76,21 @@ "visiblePercentageMax": 80, "totalTimeMin": 5000, "continuousTimeMin": 2000 + }, + "vars": { + "type": "visibility" + } + }, + "hidden": { + "on": "hidden", + "request": "visibility", + "visibilitySpec": { + "selector": "#anim-id", + "visiblePercentageMin": 20, + "continuousTimeMax": 5000 + }, + "vars": { + "type": "hidden" } }, "scrollPings": { @@ -79,506 +101,40 @@ "horizontalBoundaries": [100] }, "vars": { - "eventName": "scroll" + "type": "scroll" } - } - } -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+ - - - - - - - - - - - - - - - - +

    AMP Analytics

    - + Click here to generate an event

    @@ -633,10 +189,41 @@

    AMP Analytics

  • Aenean ac sem eget libero varius viverra sit amet vitae nunc.
  • - - - + + + + + + + - diff --git a/examples/analytics.config.json b/examples/analytics.config.json index e50f52ed18d9..0e8b4a5c0c94 100644 --- a/examples/analytics.config.json +++ b/examples/analytics.config.json @@ -1,6 +1,6 @@ { "requests": { - "event": "https://example.com?remote-test&title=${title}&r=${random}" + "event": "https://raw.githubusercontent.com/ampproject/amphtml/master/examples/img/ampicon.png?remote-test&title=${title}&r=${random}" }, "vars": { "title": "Example Request" @@ -10,6 +10,7 @@ "on": "visible", "request": "event" } - } + }, + "transport" : {"beacon": false, "xhrpost": false} } diff --git a/examples/animations.amp.html b/examples/animations.amp.html new file mode 100644 index 000000000000..023b2f7cb9e2 --- /dev/null +++ b/examples/animations.amp.html @@ -0,0 +1,51 @@ + + + + + Animations Examples + + + + + + + + + + + + + + + + + diff --git a/examples/apester-media.amp.html b/examples/apester-media.amp.html new file mode 100644 index 000000000000..184c1d6f726f --- /dev/null +++ b/examples/apester-media.amp.html @@ -0,0 +1,31 @@ + + + + + Apester Media Smart Unit + + + + + + + + +
    + + + + + + + + +
    + + + diff --git a/examples/article-access-laterpay.amp.html b/examples/article-access-laterpay.amp.html new file mode 100644 index 000000000000..7458f8933752 --- /dev/null +++ b/examples/article-access-laterpay.amp.html @@ -0,0 +1,324 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + + +
    + +
    +
    +

    Lorem Ipsum

    + +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +

    +
    + +
    + + + +
    + +
    +
    +
    + +
    + Oops... Something broke. +
    + +
    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus + luctus nunc ut elit cursus, et imperdiet diam vehicula. + Duis et nisi sed urna blandit bibendum et sit amet erat. + Suspendisse potenti. Curabitur consequat volutpat arcu nec + elementum. Etiam a turpis ac libero varius condimentum. + Maecenas sollicitudin felis aliquam tortor vulputate, + ac posuere velit semper. +

    +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. + Aliquam iaculis tincidunt quam sed maximus. Suspendisse faucibus + ornare sodales. Nullam id dolor vitae arcu consequat ornare a + et lectus. Sed tempus eget enim eget lobortis. + Mauris sem est, accumsan sed tincidunt ut, sagittis vel arcu. + Nullam in libero nisi. +

    + +

    + Sed pharetra semper fringilla. Nulla fringilla, neque eget + varius suscipit, mi turpis congue odio, quis dignissim nisi + nulla at erat. Duis non nibh vel erat vehicula hendrerit eget + vel velit. Donec congue augue magna, nec eleifend dui porttitor + sed. Cras orci quam, dignissim nec elementum ac, bibendum et purus. + Ut elementum mi eget felis ultrices tempus. Maecenas nec sodales + ex. Phasellus ultrices, purus non egestas ullamcorper, felis + lorem ultrices nibh, in tristique mauris justo sed ante. + Nunc commodo purus feugiat metus bibendum consequat. Duis + finibus urna ut ligula auctor, sed vehicula ex aliquam. + Sed sed augue auctor, porta turpis ultrices, cursus diam. + In venenatis aliquet porta. Sed volutpat fermentum quam, + ac molestie nulla porttitor ac. Donec porta risus ut enim + pellentesque, id placerat elit ornare. +

    +

    + Curabitur convallis, urna quis pulvinar feugiat, purus diam + posuere turpis, sit amet tincidunt purus justo et mi. Donec + sapien urna, aliquam ut lacinia quis, varius vitae ex. + Maecenas efficitur iaculis lorem, at imperdiet orci viverra + in. Nullam eu erat eu metus ultrices viverra a sit amet leo. + Pellentesque est felis, pulvinar mollis sollicitudin et, + suscipit eget massa. Nunc bibendum non nunc et consequat. + Quisque auctor est vel leo faucibus, non faucibus magna ultricies. + Vestibulum ante ipsum primis in faucibus orci luctus et ultrices + posuere cubilia Curae; Vestibulum tortor lacus, bibendum et + enim eu, vehicula placerat erat. Nullam gravida rhoncus accumsan. + Integer suscipit iaculis elit nec mollis. Vestibulum eget arcu + nec lectus finibus rutrum vel sed orci. +

    + +
    + + +
    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +
    +
    +
    + +

    + Cum sociis natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. Nulla et viverra turpis. Fusce + viverra enim eget elit blandit, in finibus enim blandit. Integer + fermentum eleifend felis non posuere. In vulputate et metus at + aliquam. Praesent a varius est. Quisque et tincidunt nisi. + Nam porta urna at turpis lacinia, sit amet mattis eros elementum. + Etiam vel mauris mattis, dignissim tortor in, pulvinar arcu. + In molestie sem elit, tincidunt venenatis tortor aliquet sodales. + Ut elementum velit fermentum felis volutpat sodales in non libero. + Aliquam erat volutpat. +

    + +

    + Morbi at velit vitae eros congue congue venenatis non dui. + Sed lacus sem, feugiat sed elementum sed, maximus sed lacus. + Integer accumsan magna in sagittis pharetra. Class aptent taciti + sociosqu ad litora torquent per conubia nostra, per inceptos + himenaeos. Suspendisse ac nisl efficitur ligula aliquam lacinia + eu in magna. Vestibulum non felis odio. Ut consectetur venenatis + felis aliquet maximus. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. +

    +
    +
    +
    +
    + +
    + +
    + + diff --git a/examples/article-access.amp.html b/examples/article-access.amp.html index 2a1f0013c300..c8a318b55c49 100644 --- a/examples/article-access.amp.html +++ b/examples/article-access.amp.html @@ -150,10 +150,11 @@ + - + diff --git a/examples/article-fixed-header.amp.html b/examples/article-fixed-header.amp.html new file mode 100644 index 000000000000..f1d47d6f2d2e --- /dev/null +++ b/examples/article-fixed-header.amp.html @@ -0,0 +1,461 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Medium App

    +

    Experience a richer experience on our mobile app!

    +
    +
    +
    + +
    +
    + +
    + + +
    + + + +
    + This is a slot fallback. +
    +
    + +
    +
    +
    + + +
    + + + + + + +
    +
    +

    Lorem Ipsum

    + +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +

    +
    + +
    + + + +
    +
    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus + luctus nunc ut elit cursus, et imperdiet diam vehicula. + Duis et nisi sed urna blandit bibendum et sit amet erat. + Suspendisse potenti. Curabitur consequat volutpat arcu nec + elementum. Etiam a turpis ac libero varius condimentum. + Maecenas sollicitudin felis aliquam tortor vulputate, + ac posuere velit semper. +

    +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. + Aliquam iaculis tincidunt quam sed maximus. Suspendisse faucibus + ornare sodales. Nullam id dolor vitae arcu consequat ornare a + et lectus. Sed tempus eget enim eget lobortis. + Mauris sem est, accumsan sed tincidunt ut, sagittis vel arcu. + Nullam in libero nisi. +

    + +
    + + +
    + +

    + Sed pharetra semper fringilla. Nulla fringilla, neque eget + varius suscipit, mi turpis congue odio, quis dignissim nisi + nulla at erat. Duis non nibh vel erat vehicula hendrerit eget + vel velit. Donec congue augue magna, nec eleifend dui porttitor + sed. Cras orci quam, dignissim nec elementum ac, bibendum et purus. + Ut elementum mi eget felis ultrices tempus. Maecenas nec sodales + ex. Phasellus ultrices, purus non egestas ullamcorper, felis + lorem ultrices nibh, in tristique mauris justo sed ante. + Nunc commodo purus feugiat metus bibendum consequat. Duis + finibus urna ut ligula auctor, sed vehicula ex aliquam. + Sed sed augue auctor, porta turpis ultrices, cursus diam. + In venenatis aliquet porta. Sed volutpat fermentum quam, + ac molestie nulla porttitor ac. Donec porta risus ut enim + pellentesque, id placerat elit ornare. +

    +

    + Curabitur convallis, urna quis pulvinar feugiat, purus diam + posuere turpis, sit amet tincidunt purus justo et mi. Donec + sapien urna, aliquam ut lacinia quis, varius vitae ex. + Maecenas efficitur iaculis lorem, at imperdiet orci viverra + in. Nullam eu erat eu metus ultrices viverra a sit amet leo. + Pellentesque est felis, pulvinar mollis sollicitudin et, + suscipit eget massa. Nunc bibendum non nunc et consequat. + Quisque auctor est vel leo faucibus, non faucibus magna ultricies. + Vestibulum ante ipsum primis in faucibus orci luctus et ultrices + posuere cubilia Curae; Vestibulum tortor lacus, bibendum et + enim eu, vehicula placerat erat. Nullam gravida rhoncus accumsan. + Integer suscipit iaculis elit nec mollis. Vestibulum eget arcu + nec lectus finibus rutrum vel sed orci. +

    + +
    + + +
    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +
    +
    +
    + +

    + Cum sociis natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. Nulla et viverra turpis. Fusce + viverra enim eget elit blandit, in finibus enim blandit. Integer + fermentum eleifend felis non posuere. In vulputate et metus at + aliquam. Praesent a varius est. Quisque et tincidunt nisi. + Nam porta urna at turpis lacinia, sit amet mattis eros elementum. + Etiam vel mauris mattis, dignissim tortor in, pulvinar arcu. + In molestie sem elit, tincidunt venenatis tortor aliquet sodales. + Ut elementum velit fermentum felis volutpat sodales in non libero. + Aliquam erat volutpat. +

    + +
    + + +
    + +

    + Morbi at velit vitae eros congue congue venenatis non dui. + Sed lacus sem, feugiat sed elementum sed, maximus sed lacus. + Integer accumsan magna in sagittis pharetra. Class aptent taciti + sociosqu ad litora torquent per conubia nostra, per inceptos + himenaeos. Suspendisse ac nisl efficitur ligula aliquam lacinia + eu in magna. Vestibulum non felis odio. Ut consectetur venenatis + felis aliquet maximus. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. +

    +
    +
    +
    +
    + +
    + +
    + + diff --git a/examples/article.amp.html b/examples/article.amp.html index ac3428eacb06..12c1979aa670 100644 --- a/examples/article.amp.html +++ b/examples/article.amp.html @@ -3,9 +3,10 @@ Lorem Ipsum | PublisherName - + - + + + + + + + - + + + + + + + +
    + +
    +
    Medium App
    +

    Experience a richer experience on our mobile app!

    +
    +
    + +
    +
    +
    +
    - +
    + + + +
    + This is a slot fallback. +
    +
    +
    @@ -161,8 +282,10 @@ src="img/hero@1x.jpg" srcset="img/hero@1x.jpg 1x, img/hero@2x.jpg 2x" layout="responsive" width="360" placeholder - alt="Curabitur convallis, urna quis pulvinar feugiat, purus diam posuere turpis, sit amet tincidunt purus justo et mi." - height="216" on="tap:headline-img-lightbox" role="img" tabindex="0"> + alt="Picture of two chairs on a lake shore with big pine trees" + title="Opens image in a lightbox" + role="button" on="tap:headline-img-lightbox" + height="216" tabindex="0">
    @@ -173,6 +296,7 @@ @@ -183,6 +307,10 @@
    +

    Lorem Ipsum

    -

    +

    Morbi at velit vitae eros congue congue venenatis non dui. Sed lacus sem, feugiat sed elementum sed, maximus sed lacus. Integer accumsan magna in sagittis pharetra. Class aptent taciti diff --git a/examples/auto-ads.amp.html b/examples/auto-ads.amp.html new file mode 100644 index 000000000000..213a44f491fb --- /dev/null +++ b/examples/auto-ads.amp.html @@ -0,0 +1,76 @@ + + + + + AMP #0 + + + + + + + + + +

    + +
    +

    Title of Page

    +
    +

    + Lorem ipsum dolor sit amet, has te dico epicuri, ad sumo constituam cum. Vix rebum partem graecis in, id legimus lobortis expetendis per. Zril tritani principes has in. Sea ei exerci ridens vituperatoribus. Id inermis invenire mei, ei iudico epicuri mea. Ad eos debet perfecto volutpat, suas error usu ut, est everti comprehensam no. +

    +

    + Dolor numquam consulatu ne cum, ei sed essent appellantur, eam et appetere partiendo. Vim mucius vituperatoribus ut. Consul virtute eum id, has no postulant dissentiet. Errem tibique copiosae ut has, ut labore nemore assentior per. +

    +

    + Essent signiferumque necessitatibus in mea. Sit minim petentium dissentiunt ei, ius inimicus mnesarchum ut. Errem maiestatis cu nec, dolor nusquam interesset ut qui. Mea in eripuit blandit complectitur. An eos recteque adversarium. +

    +

    + Lorem ipsum dolor sit amet, has te dico epicuri, ad sumo constituam cum. Vix rebum partem graecis in, id legimus lobortis expetendis per. Zril tritani principes has in. Sea ei exerci ridens vituperatoribus. Id inermis invenire mei, ei iudico epicuri mea. Ad eos debet perfecto volutpat, suas error usu ut, est everti comprehensam no. +

    +

    + Dolor numquam consulatu ne cum, ei sed essent appellantur, eam et appetere partiendo. Vim mucius vituperatoribus ut. Consul virtute eum id, has no postulant dissentiet. Errem tibique copiosae ut has, ut labore nemore assentior per. +

    +
    + + + diff --git a/examples/av/ForBiggerJoyrides.mp4 b/examples/av/ForBiggerJoyrides.mp4 new file mode 100644 index 000000000000..33f1dfe1a2d1 Binary files /dev/null and b/examples/av/ForBiggerJoyrides.mp4 differ diff --git a/examples/av/audio.mp3 b/examples/av/audio.mp3 new file mode 100644 index 000000000000..3dc4fd948878 Binary files /dev/null and b/examples/av/audio.mp3 differ diff --git a/examples/bind/basic.amp.html b/examples/bind/basic.amp.html new file mode 100644 index 000000000000..ff434b340a4e --- /dev/null +++ b/examples/bind/basic.amp.html @@ -0,0 +1,48 @@ + + + + + amp-bind: Basic + + + + + + + + + + + + + + +

    Basic example

    +

    After clicking the button below, this will read 'foo'

    +

    And this will read 'foobar'

    +

    This will read 'myStateValue1'

    + +

    This text will have have a red background color

    + +

    The image above will increase in size and change its src

    + +
    + +
    + + + + + + diff --git a/examples/bind/carousels.amp.html b/examples/bind/carousels.amp.html new file mode 100644 index 000000000000..32325d47d6e0 --- /dev/null +++ b/examples/bind/carousels.amp.html @@ -0,0 +1,87 @@ + + + + + amp-bind: Carousels + + + + + + + + + + + + + + + + +
    +

    Linked carousels

    +

    Selected slide: None

    + + + + + + + + + + + + +

    + +

    Without <amp-selector>

    + + + + + + + + + + + + +

    With <amp-selector>

    + + + + + + + + + + + + + + + diff --git a/examples/bind/ecommerce.amp.html b/examples/bind/ecommerce.amp.html new file mode 100644 index 000000000000..bc15df2e6c10 --- /dev/null +++ b/examples/bind/ecommerce.amp.html @@ -0,0 +1,103 @@ + + + + + amp-bind: E-commerce + + + + + + + + + + + + + + + +

    E-commerce

    + + + +

    COLOR: black

    + +

    SIZE: XS

    + + + + + + + + + + + + + + + + + + diff --git a/examples/bind/errors.amp.html b/examples/bind/errors.amp.html new file mode 100644 index 000000000000..93da83424148 --- /dev/null +++ b/examples/bind/errors.amp.html @@ -0,0 +1,55 @@ + + + + + amp-bind: Errors + + + + + + + + + + + + + +

    Error examples

    +

    This page lists the different types of runtime user errors that amp-bind can throw.

    +
    + +

    1. Binding to unsupported property

    +

    This paragraph element attempts to bind to "asdf".

    + +

    2. Malformed expressions

    +

    This expression is missing a closing parens.

    + +

    3. Invalid function invocation

    +

    This expression calls `substring` on a number.

    + +

    4.a Invalid expression result (validator)

    + This expression contains an invalid URL protocol. + +

    4.b Invalid expression result (css)

    + This expression contains an invalid [class] result (only string/array/null supported). + +

    5. Mismatched initial state

    +

    (Only with '#development=1')

    +

    This paragraph's initial `text` state doesn't match the evaluated expression result.

    + + + + + + diff --git a/examples/bind/performance.amp.html b/examples/bind/performance.amp.html new file mode 100644 index 000000000000..21aacfae2b96 --- /dev/null +++ b/examples/bind/performance.amp.html @@ -0,0 +1,5016 @@ + + + + + amp-bind: Performance + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/examples/bind/sandbox.amp.html b/examples/bind/sandbox.amp.html new file mode 100644 index 000000000000..5a1c5f42694e --- /dev/null +++ b/examples/bind/sandbox.amp.html @@ -0,0 +1,74 @@ + + + + + amp-bind: Sandbox + + + + + + + + + + + + + + + + + + + +

    Sandbox

    + + + +

    HTML appears below:

    +
    +

    1 + 1 = ?

    +
    + +
    + +

    1. Use text box above to create elements and bindings. Click "Parse" button to force amp-bind to parse them.

    + +
    + +
    + +

    2. Click "Evaluate" to reevaluate all expressions and apply updates to page.

    + +
    + +
    + + diff --git a/examples/bind/shirts/black.jpg b/examples/bind/shirts/black.jpg new file mode 100644 index 000000000000..a096770e7b60 Binary files /dev/null and b/examples/bind/shirts/black.jpg differ diff --git a/examples/bind/shirts/blue.jpg b/examples/bind/shirts/blue.jpg new file mode 100644 index 000000000000..2a816c473e99 Binary files /dev/null and b/examples/bind/shirts/blue.jpg differ diff --git a/examples/bind/shirts/brown.jpg b/examples/bind/shirts/brown.jpg new file mode 100644 index 000000000000..e0229061e561 Binary files /dev/null and b/examples/bind/shirts/brown.jpg differ diff --git a/examples/bind/shirts/dark-green.jpg b/examples/bind/shirts/dark-green.jpg new file mode 100644 index 000000000000..66aa437c52bd Binary files /dev/null and b/examples/bind/shirts/dark-green.jpg differ diff --git a/examples/bind/shirts/gray.jpg b/examples/bind/shirts/gray.jpg new file mode 100644 index 000000000000..4333848d768f Binary files /dev/null and b/examples/bind/shirts/gray.jpg differ diff --git a/examples/bind/shirts/light-gray.jpg b/examples/bind/shirts/light-gray.jpg new file mode 100644 index 000000000000..c59103b2fbe3 Binary files /dev/null and b/examples/bind/shirts/light-gray.jpg differ diff --git a/examples/bind/shirts/navy.jpg b/examples/bind/shirts/navy.jpg new file mode 100644 index 000000000000..65874d5928db Binary files /dev/null and b/examples/bind/shirts/navy.jpg differ diff --git a/examples/bind/shirts/white.jpg b/examples/bind/shirts/white.jpg new file mode 100644 index 000000000000..1bb4e4361ad7 Binary files /dev/null and b/examples/bind/shirts/white.jpg differ diff --git a/examples/bind/shirts/wine.jpg b/examples/bind/shirts/wine.jpg new file mode 100644 index 000000000000..1f61f5659005 Binary files /dev/null and b/examples/bind/shirts/wine.jpg differ diff --git a/examples/bind/xhr.amp.html b/examples/bind/xhr.amp.html new file mode 100644 index 000000000000..1af41e0f5553 --- /dev/null +++ b/examples/bind/xhr.amp.html @@ -0,0 +1,39 @@ + + + + + amp-bind: XHR + + + + + + + + + + + + + + +

    XHR

    +

    This text will be updated with XHR results.

    +
    + + + + + + + + diff --git a/examples/carousel.amp.html b/examples/carousel.amp.html index 0e0fab3a83f0..a536329bf636 100644 --- a/examples/carousel.amp.html +++ b/examples/carousel.amp.html @@ -22,6 +22,7 @@ + @@ -88,7 +89,7 @@ diff --git a/examples/csa.amp.html b/examples/csa.amp.html new file mode 100644 index 000000000000..3b587903b0ff --- /dev/null +++ b/examples/csa.amp.html @@ -0,0 +1,52 @@ + + + + + CSA example + + + + + + + + +
    +

    CSA Example

    +

    AdSense for Search ad

    + + + + +
    Page Content
    + +

    AdSense for Shopping ad

    + + +
    + + diff --git a/examples/csp.amp.html b/examples/csp.amp.html index 4731f0d3ddd4..484e127faf6a 100644 --- a/examples/csp.amp.html +++ b/examples/csp.amp.html @@ -8,6 +8,7 @@ + @@ -54,4 +55,3 @@

    Should show image

    Different font

    - diff --git a/examples/custom.ad.example.json b/examples/custom.ad.example.json new file mode 100644 index 000000000000..37564a73d442 --- /dev/null +++ b/examples/custom.ad.example.json @@ -0,0 +1,12 @@ +{ + "1": { + "src": "https://bachtrack.com/files/46901-banner-728x90px-uk-2016-5sec.gif", + "href":"https://bachtrack.com", + "info":"Info1" + }, + "2": { + "src":"https://bachtrack.com/files/42327-membership-200x200.gif", + "href":"http://onestoparts.com", + "info":"Info2" + } +} diff --git a/examples/custom.ad.example.single.json b/examples/custom.ad.example.single.json new file mode 100644 index 000000000000..ff71cbb4d207 --- /dev/null +++ b/examples/custom.ad.example.single.json @@ -0,0 +1,5 @@ +{ + "src": "https://bachtrack.com/files/46510-45th-hkaf-social-media-image-bachtrack-728x90.jpg", + "href":"https://bachtrack.com", + "info":"Info1" +} diff --git a/examples/doubleclick-multisize.amp.html b/examples/doubleclick-multisize.amp.html new file mode 100644 index 000000000000..41617a9200bc --- /dev/null +++ b/examples/doubleclick-multisize.amp.html @@ -0,0 +1,164 @@ + + + + + Multi-size Ad Requests with DoubleClick Examples + + + + + + + + + + +

    Multi-size Ad Requests with DoubleClick Examples

    + +

    Resize denied (above fold) - Creative should be centered

    + + + +

    Secondary sizes > primary size (above fold) - Should see 'fallback'

    + +
    fallback
    +
    + +

    Secondary sizes == primary size - Should render

    + +
    fallback
    +
    + + +

    Invalid multi-size attribute (above fold) - Should see 'fallback'

    + +
    fallback
    +
    + + + +
    + +

    Resize granted (beneath fold) - Creative should be closely wrapped by parent div

    + + + +

    Invalid multi-size attribute (beneath fold) - Should be empty

    + + + +

    One invalid and one valid multi-size attribute - Should render

    + + + + +

    Multi-size less than 2/3rds primary size & size-validation == false - Should render

    + + + +

    Multi-size less than 2/3rds primary size - Should be empty

    + + + +

    Multiple secondary sizes (all valid) - Should render

    + + + + +

    Responsive - Should render

    + + + +

    Size overrides - Should render with primary dimensions

    + + + + + +

    Secondary sizes > primary size (below fold) - Should be empty

    + + + + + + diff --git a/examples/everything.amp.html b/examples/everything.amp.html index 67193667d79e..585f78af6c4d 100644 --- a/examples/everything.amp.html +++ b/examples/everything.amp.html @@ -16,30 +16,12 @@ margin: 8px; } - .bordered { - border: 1px solid black; - } - - .lightbox1 { - background: #222; + amp-lightbox { + background-color: rgba(0, 0, 0, 0.9); } - .lightbox1-content { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - flex-direction: column; - flex-wrap: nowrap; - justify-content: center; - align-items: center; - } - .lightbox1 amp-img { - max-width: 80%; - max-height: 80%; - width: 100%; + .bordered { + border: 1px solid black; } .box1 { @@ -60,6 +42,18 @@ height: 40px; } + .ear { + background: lime; + border: 5px solid black; + opacity: 0.7; + position: absolute; + z-index: 999; + top: 0; + right: 0; + width: 20px; + height: 20px; + } + @media screen and (min-width: 380px) { .logo { top: auto; @@ -95,6 +89,14 @@ color: #f00; } + .lightbox-close-button { + background: white; + } + + .lightbox-text { + color: white; + } + #amp-iframe:target { border: 1px solid green; } @@ -110,8 +112,10 @@ background: rgba(0, 0, 0, 0.5); } + + @@ -123,12 +127,14 @@ + - + +
    @@ -194,7 +200,7 @@

    AMP #0

    Video

    Youtube

    Audio

    +

    Lightbox

    + +

    + +

    +

    + +

    + +
    + + + +

    Scrollable Lightbox

    + + +

    + +

    + +

    + +

    + +

    + +

    + + + + +
    + + + +

    Images

    - Responsive (w/srcset, w/lightbox) + Responsive (w/srcset, w/image-lightbox)

    + + on="tap:imagelightbox1" role="button" tabindex="0">

    - -
    - -
    -
    -

    Fixed

    @@ -342,6 +405,9 @@

    IFrame

    frameborder="0" src="https://www.google.com/maps/embed/v1/place?key=AIzaSyDG9YXIhKBhqclZizcSzJ0ROiE0qgVfwzI&q=Alameda,%20CA"> +
    + Go back +

    + + + + amp-experiment + + + + + + + + + + +

    Background color is user sticky; text color may change on a refresh

    + + + + + + + + + Dismiss this notification to see a border around title on your next visit. + + + + diff --git a/examples/forms.amp.html b/examples/forms.amp.html index 2ac4030c61e6..12cb6b56be3d 100644 --- a/examples/forms.amp.html +++ b/examples/forms.amp.html @@ -7,22 +7,39 @@ + + + + + -

    Subscribe to our weekly Newsletter (non-xhr)

    + + + + +

    Search (GET non-xhr same origin)

    +
    +
    + + +
    + + +

    Search (GET non-xhr cross origin - current host > httpbin.org)

    +
    +
    + + + + +
    + + +

    Search (GET xhr same origin)

    +
    +
    + + +
    - +
    + +
    + + +

    Search (GET xhr cross origin current host > httpbin.org)

    +
    + + + + +
    + +
    + +
    + + +

    Subscribe to our weekly Newsletter (POST xhr same origin)

    +
    +
    + + + +
    + +
    + +
    +
    + +
    + + + +

    Subscribe to our weekly Newsletter (xhr POST cross origin - current host > amp.localhost:8000)

    +
    +
    + + + + + +
    + +
    + +
    +
    + +
    + + +

    Subscribe to our weekly Newsletter (xhr + submit events)

    +
    This message will hide when the form is submitted
    +
    +
    + +
    -
    +
    There are some input that needs fixing. Please fix them.
    -
    +
    Everything looks good, you can submit now!
    +
    + Please hold on while we submit your answer! +
    +
    + +
    +
    + +
    + + + + + + + + + + +

    NOT ALLOWED (POST non-xhr - submission prevented)

    +
    +
    + + +
    -

    Subscribe to our weekly Newsletter (xhr)

    +

    Custom Validations

    +

    Show First Validation On Submit

    +
    + + + +
    + + +

    Show All Invalid Messages On Submit

    +
    +
    + + + +
    + + +

    Show All Invalid Messages On Submit (Summary)

    +
    +
    +
      +
    1. + Name is a required field. Please fill it out. +
    2. +
    3. + Please enter your first and last name separated by a space (e.g. Jane Miller) +
    4. +
    5. + We need your email to send you coolness! +
    6. +
    7. + That doesn't look like an email. Please fix it. +
    8. +
    +
    +
    + + + +
    + + +

    Show As You Go Messages On Submit

    +
    +
    + + + +
    + + +

    on=change and form.submit examples

    + +
    + + +
    +

    What is your favorite flightless bird?

    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    + +
    + +
    + + + +

    Using button[type=submit]

    +
    +
    + + + + +
    + +
    + +
    +
    + +
    + + + +

    amp-selector example

    +
    + + + + + + + + + + + +

    Redirect After POST (works only if running on localhost)

    +
    +
    + + +
    + + +

    Recursive Event-Action-Event-Action Test - Should only submit once

    +
    +
    + +
    @@ -95,13 +608,18 @@

    Subscribe to our weekly Newsletter (xhr)

    Please hold on while we submit your answer!
    -
    - Success! Thanks for subscribing! Please make sure to check your email - to confirm! +
    +
    -
    - Oops! We apologies something went wrong. Please try again later. +
    +
    + - \ No newline at end of file + diff --git a/examples/gfycat.amp.html b/examples/gfycat.amp.html new file mode 100644 index 000000000000..e4a81a04e44b --- /dev/null +++ b/examples/gfycat.amp.html @@ -0,0 +1,38 @@ + + + + + Gfycat examples + + + + + + + + +

    Gfycat

    +

    Responsive layout

    + + + +

    Fixed layout

    + + + +

    Autoplay disabled

    + + + + diff --git a/examples/img/ampicon.png b/examples/img/ampicon.png index a2e8b92ae879..fe726e3c56e8 100644 Binary files a/examples/img/ampicon.png and b/examples/img/ampicon.png differ diff --git a/examples/img/analytics.gif b/examples/img/analytics.gif new file mode 100644 index 000000000000..07e48b8c2abc Binary files /dev/null and b/examples/img/analytics.gif differ diff --git a/examples/img/cats-anim-placeholder.gif b/examples/img/cats-anim-placeholder.gif new file mode 100644 index 000000000000..e5ae50a30b22 Binary files /dev/null and b/examples/img/cats-anim-placeholder.gif differ diff --git a/examples/img/cats-anim.gif b/examples/img/cats-anim.gif new file mode 100644 index 000000000000..470343df19ec Binary files /dev/null and b/examples/img/cats-anim.gif differ diff --git a/examples/img/hero@1x.jpg b/examples/img/hero@1x.jpg index 407feffed44c..c3064ea39240 100644 Binary files a/examples/img/hero@1x.jpg and b/examples/img/hero@1x.jpg differ diff --git a/examples/img/hero@2x.jpg b/examples/img/hero@2x.jpg index 95a666161d54..0e421ab1e409 100644 Binary files a/examples/img/hero@2x.jpg and b/examples/img/hero@2x.jpg differ diff --git a/examples/img/sample.jpg b/examples/img/sample.jpg index 2a215e26c39e..79a5fbc5e160 100644 Binary files a/examples/img/sample.jpg and b/examples/img/sample.jpg differ diff --git a/examples/img/sea@1x.jpg b/examples/img/sea@1x.jpg index 097dca6f5cbd..9d946f1a25c4 100644 Binary files a/examples/img/sea@1x.jpg and b/examples/img/sea@1x.jpg differ diff --git a/examples/img/sea@2x.jpg b/examples/img/sea@2x.jpg index 13a9a5b017b2..eff4d1ea8a31 100644 Binary files a/examples/img/sea@2x.jpg and b/examples/img/sea@2x.jpg differ diff --git a/examples/inabox-gpt-pubads-impl.js b/examples/inabox-gpt-pubads-impl.js new file mode 100644 index 000000000000..6ca411f3b775 --- /dev/null +++ b/examples/inabox-gpt-pubads-impl.js @@ -0,0 +1,291 @@ +(function(){var window=this,document=this.document;var h,p=this,r=function(a){return void 0!==a},aa=function(){},ba=function(){throw Error("unimplemented abstract method");},ca=function(a){a.getInstance=function(){return a.ma?a.ma:a.ma=new a}},da=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&& +!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";else if("function"==b&&"undefined"==typeof a.call)return"object";return b},t=function(a){return"array"==da(a)},ea=function(a){var b=da(a);return"array"==b||"object"==b&&"number"==typeof a.length},u=function(a){return"string"==typeof a},fa=function(a){return"boolean"==typeof a},v=function(a){return"number"== +typeof a},ga=function(a){return"function"==da(a)},y=function(a){var b=typeof a;return"object"==b&&null!=a||"function"==b},ha="closure_uid_"+(1E9*Math.random()>>>0),ia=0,ja=function(a,b,c){return a.call.apply(a.bind,arguments)},ka=function(a,b,c){if(!a)throw Error();if(2")&&(a=a.replace(vc,">"));-1!=a.indexOf('"')&&(a=a.replace(wc,"""));-1!=a.indexOf("'")&&(a=a.replace(xc,"'"));-1!=a.indexOf("\x00")&&(a=a.replace(yc,"�"));return a},tc=/&/g,uc=//g,wc=/"/g,xc=/'/g,yc=/\x00/g,sc=/[\x00&<>"']/, +Cc=function(a){return-1!=a.indexOf("&")?"document"in p?Ac(a):Bc(a):a},Ac=function(a){var b={"&":"&","<":"<",">":">",""":'"'},c;c=p.document.createElement("div");return a.replace(Dc,function(a,e){var d=b[a];if(d)return d;"#"==e.charAt(0)&&(e=Number("0"+e.substr(1)),isNaN(e)||(d=String.fromCharCode(e)));d||(c.innerHTML=a+" ",d=c.firstChild.nodeValue.slice(0,-1));return b[a]=d})},Bc=function(a){return a.replace(/&([^;]+);/g,function(a,c){switch(c){case "amp":return"&";case "lt":return"<"; +case "gt":return">";case "quot":return'"';default:return"#"!=c.charAt(0)||(c=Number("0"+c.substr(1)),isNaN(c))?a:String.fromCharCode(c)}})},Dc=/&([^;\s<&]+);?/g,Ec={"\x00":"\\0","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r","\t":"\\t","\x0B":"\\x0B",'"':'\\"',"\\":"\\\\","<":"<"},Fc={"'":"\\'"},Gc=function(a){a=String(a);for(var b=['"'],c=0;ce))if(d in Fc)d=Fc[d];else if(d in Ec)d=Fc[d]=Ec[d];else{g=d.charCodeAt(0); +if(31g)e=d;else{if(256>g){if(e="\\x",16>g||256g&&(e+="0");e+=g.toString(16).toUpperCase()}d=Fc[d]=e}g=d}b[f]=g}b.push('"');return b.join("")},Hc=function(a){return null==a?"":String(a)},Ic=function(a,b){return ab?1:0};var nd=function(a,b){if(u(a))return u(b)&&1==b.length?a.indexOf(b,0):-1;for(var c=0;cb?null:u(a)?a.charAt(b):a[b]},rd=function(a,b,c){for(var d=a.length,e=u(a)?a.split(""):a,f=0;f=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)},zd=function(a){for(var b={},c=0,d=0;db?1:aparseFloat(be)){ae=String(Ae);break a}}ae=be} +var De=ae,ec={},Ee=function(a){return fc(a,function(){for(var b=0,c=rc(String(De)).split("."),d=rc(String(a)).split("."),e=Math.max(c.length,d.length),f=0;0==b&&f=a})};var ta=function(a,b){for(var c=[],d=0;df&&(a.o[b]=e);a.events.push({label:b,type:c,Lb:e,timestamp:d});return e}return 0};jg.prototype.w=function(a,b,c,d){b=gh(this,a,2,b,d);gh(this,a,3,c,b)};var gc=function(a){try{return!!a&&null!=a.location.href&&dc(a,"foo")}catch(b){return!1}},hc=function(a,b){var c=a.createElement("script");c.src=b;return(a=a.getElementsByTagName("script")[0])&&a.parentNode?(a.parentNode.insertBefore(c,a),c):null},ic=function(a,b){if(!(1E-4>Math.random())){var c=Math.random();if(c>2)+a.charCodeAt(d)&4294967295;return 0 +d)return null;e=a.indexOf("&",d);if(0>e||e>c)e=c;d+=b.length+1;return decodeURIComponent(a.substr(d,e-d).replace(/\+/g," "))};var Oc=function(a){if(a=/[-\w]+\.[-\w]+$/.exec(a)){a=a[0].toLowerCase();for(var b=0,c=0;c>>0;switch(b){case 1967261364:return 0;case 3147493546:return 1;case 1567346461:return 2;case 2183041838:return 3;case 763236279:return 4;case 1342279801:return 5;case 526831769:return 6;case 352806002:return 7;case 2755048925:return 8;case 3306848407:return 9;case 2207000920:return 10;case 484037040:return 11;case 3506871055:return 12;case 672143848:return 13;case 2528751226:return 14; +case 2744854768:return 15;case 3703278665:return 16;case 2014749173:return 17;case 133063824:return 18;case 2749334602:return 19;case 3131239845:return 20;case 2074086763:return 21;case 795772493:return 22;case 290857819:return 23;case 3035947606:return 24;case 2983138003:return 25;case 2197138676:return 26;case 4216016165:return 27;case 239803524:return 28;case 975993579:return 29;case 1794940339:return 30;case 1314429186:return 31;case 1643618937:return 32;case 497159982:return 33}}return-1},Pc= +function(a){if(!a.length)return 0;for(var b=[],c=0;33>=c;c++)b[c]=0;for(c=a.length-1;0<=c;c--){var d=Oc(a[c]);0<=d&&(b[33-d]=1)}return parseInt(b.join(""),2)};var Qc=function(a,b){this.j=a;this.l=b},Rc=function(a,b,c){this.url=a;this.Ga=b;this.Na=!!c;this.depth=v(void 0)?void 0:null},Tc=function(a){a=(this.l=a||p)||p;this.m=a.top==a?1:gc(a.top)?2:3;3!=this.m&&Date.parse(p.top.document.lastModified);this.j=Sc(this.l)},Uc=function(a,b){for(var c=0,d=(a=a.j[Math.max(a.j.length-1,0)].url||null)&&Lc(a.match(Kc)[3]||null),e=Math.min(b.length,26),f=0;fe)return"";a.j.sort(function(a,b){return a-b});d=null;c="";for(var f=0;f= +n.length){e-=n.length;b+=n;c=a.m;break}else a.u&&(c=e,n[c-1]==a.m&&--c,b+=n.substr(0,c),c=a.m,e=0);d=null==d?g:d}}f="";a.o&&null!=d&&(f=c+a.o+"="+(a.B||d));return b+f+""},$c=function(a){if(!a.o)return a.v;var b=1,c;for(c in a.l)b=c.length>b?c.length:b;return a.v-a.o.length-b-a.m.length-1},ad=function(a,b,c,d,e){var f=[];G(a,function(a,k){(a=cd(a,b,c,d,e))&&f.push(k+"="+a)});return f.join(b)},cd=function(a,b,c,d,e){if(null==a)return"";b=b||"&";c=c||",$";"string"==typeof c&&(c=c.split(""));if(a instanceof +Array){if(d=d||0,de?encodeURIComponent(ad(a,b,c,d,e+1)):"...";return encodeURIComponent(String(a))};var sn=function(a,b,c,d){this.o=a;this.l=b;this.m=c;this.j=d;this.u=Math.random()},ed=function(a,b,c,d,e){if((d?a.u:Math.random())<(e||a.j))try{var f;c instanceof Xc?f=c:(f=new Xc,G(c,function(a,b){var c=f,d=c.A++;a=Yc(b,a);c.j.push(d);c.l[d]=a}));var g=bd(f,a.o,a.l,a.m+b+"&");g&&dd(p,g)}catch(k){}},dd=function(a,b){a.google_image_requests||(a.google_image_requests=[]);var c=a.document.createElement("img");c.src=b;a.google_image_requests.push(c)};var fd=function(a,b,c){this.m=a;this.u=b;this.l=c;this.o=this.j},gd=function(a,b,c){this.message=a;this.j=b||"";this.l=c||-1},id=function(a,b){var c;try{c=b()}catch(f){var d=a.l;try{var e=hd(f),d=a.o.call(a,"osd::adp::reg",e,void 0,void 0)}catch(g){a.j("pAR",g)}if(!d)throw f;}finally{}return c},kd=function(a){var b=jd;return function(){for(var c=[],d=0;d");f=f.join("")}f=d.createElement(f);g&&(u(g)?f.className=g:t(g)?f.className=g.join(" "):Pe(f,g));2a.clientWidth||a.scrollHeight>a.clientHeight||"fixed"==c||"absolute"==c||"relative"==c))return a;return null},ff=function(a){var b=Le(a),c=new le(0,0),d;d=b?Le(b):document;d=!L||9<=Number(Be)||"CSS1Compat"== +Me(d).j.compatMode?d.documentElement:d.body;if(a==d)return c;a=df(a);d=Me(b).j;b=Re(d);d=Se(d);b=L&&Ee("10")&&d.pageYOffset!=b.scrollTop?new le(b.scrollLeft,b.scrollTop):new le(d.pageXOffset||b.scrollLeft,d.pageYOffset||b.scrollTop);c.x=a.left+b.x;c.y=a.top+b.y;return c},gf=function(a){"number"==typeof a&&(a=Math.round(a)+"px");return a},hf=function(a){var b=a.offsetWidth,c=a.offsetHeight,d=se&&!b&&!c;return r(b)&&!d||!a.getBoundingClientRect?new me(b,c):(a=df(a),new me(a.right-a.left,a.bottom-a.top))};var jf=function(a){var b="//tpc.googlesyndication.com/safeframe/1-0-5/html/container.html",c;c=a;for(var d=0;c!=c.parent;)d++,c=c.parent;(c=d)&&(b+="?n="+c);return(mc(a)?"https:":"http:")+b};var kf=!1,lf="",mf=function(a){a=a.match(/[\d]+/g);if(!a)return"";a.length=3;return a.join(".")}; +(function(){if(navigator.plugins&&navigator.plugins.length){var a=navigator.plugins["Shockwave Flash"];if(a&&(kf=!0,a.description)){lf=mf(a.description);return}if(navigator.plugins["Shockwave Flash 2.0"]){kf=!0;lf="2.0.0.11";return}}if(navigator.mimeTypes&&navigator.mimeTypes.length&&(a=navigator.mimeTypes["application/x-shockwave-flash"],kf=!(!a||!a.enabledPlugin))){lf=mf(a.enabledPlugin.description);return}try{var b=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");kf=!0;lf=mf(b.GetVariable("$version")); +return}catch(c){}try{b=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");kf=!0;lf="6.0.21";return}catch(c){}try{b=new ActiveXObject("ShockwaveFlash.ShockwaveFlash"),kf=!0,lf=mf(b.GetVariable("$version"))}catch(c){}})();var nf=kf,of=lf;var vf=function(a){return(a=a.exec(Id))?a[1]:""};(function(){if(pf)return vf(/Firefox\/([0-9.]+)/);if(L||pe||oe)return De;if(tf)return vf(/Chrome\/([0-9.]+)/);if(uf&&!(ne()||I("iPad")||I("iPod")))return vf(/Version\/([0-9.]+)/);if(qf||rf){var a=/Version\/(\S+).*Mobile\/(\S+)/.exec(Id);if(a)return a[1]+"."+a[2]}else if(sf)return(a=vf(/Android\s+([0-9.]+)/))?a:vf(/Version\/([0-9.]+)/);return""})();var wf=function(a){a=a||p;var b=a.context;if(!b)try{b=a.parent.context}catch(c){}try{if(b&&"pageViewId"in b&&"canonicalUrl"in b)return b}catch(c){}return null},xf=function(){var a=wf();return a&&a.initialIntersection?a.initialIntersection:null},yf=function(){var a=xf();return a&&y(a.rootBounds)?new me(a.rootBounds.width,a.rootBounds.height):null},zf=function(){var a=xf();return a&&y(a.rootBounds)?new le(a.rootBounds.left+a.boundingClientRect.left,a.rootBounds.top+a.boundingClientRect.top):null};var Af=function(a,b,c){b=b||K;a&&b.top!=b&&(b=b.top);try{return b.document&&!b.document.body?new me(-1,-1):c?(new me(b.innerWidth,b.innerHeight)).round():Qe(b||window).round()}catch(d){return new me(-12245933,-12245933)}};var Bf=Object.prototype.hasOwnProperty,Cf=function(a,b){for(var c in a)Bf.call(a,c)&&b.call(void 0,a[c],c,a)},Ef=function(){var a=Df();"google_onload_fired"in a||(a.google_onload_fired=!1,Vc(a,"load",function(){a.google_onload_fired=!0}))},Ff=function(a,b){var c=b-8;a.length>b&&(b=a.lastIndexOf("&",c),-1!==b?a=a.substring(0,b):(a=a.substring(0,c),a=a.replace(/%\w?$/,"")),a+="&trunc=1");return a},Gf=!!window.google_async_iframe_id,Hf=Gf&&window.parent||window,Df=function(){if(Gf&&!gc(Hf)){var a="."+ +ce.domain;try{for(;22*d||f>2*c)return!1}return!0},Nf=function(a,b){var c={};c.Eb=Kf(!1).Ga;var d;var e=c.Eb;d=e.location.href;if(e==e.top)d={url:d,Oa:!0};else{var f=!1,g=e.document;g&&g.referrer&& +(d=g.referrer,e.parent==e.top&&(f=!0));(e=e.location.ancestorOrigins)&&(e=e[e.length-1])&&-1==d.indexOf(e)&&(f=!1,d=e);d={url:d,Oa:f}}c.Fb=d;c.nb=Mf(Df(),b,a.google_ad_width,a.google_ad_height);a=c.nb;b=c.Fb.Oa;d=Df();d=d.top==d?0:gc(d.top)?1:2;f=4;a||1!=d?a||2!=d?a&&1==d?f=7:a&&2==d&&(f=8):f=6:f=5;b&&(f|=16);c.mb=""+f;return c};var Pf=function(a,b){var c=a.getPassbackPageUrl();if(""!=c)return c;a=b[M(a)];return null!=a?Of(a):null},Qf=function(a){var b=a;"about:blank"!=a&&(b=b.replace(//g,"%3E").replace(/"/g,"%22").replace(/'/g,"%27"),/^https?:\/\//.test(b)||(b="unknown:"+b));return b},Rf=/\+/g,Sf=function(a){var b=J.getInstance().get(6);return a||b?"https://"+J.getInstance().get(3):"http://"+J.getInstance().get(2)},Tf=function(){return J.getInstance().get(6)?"https://"+J.getInstance().get(33):"http://"+ +J.getInstance().get(33)},Uf=function(){var a=navigator.userAgent,b=a.indexOf("MSIE ");return-1==b?0:parseFloat(a.substring(b+5,a.indexOf(";",b)))},Vf=function(){var a=Id;return null!=a&&-1!=a.indexOf("MSIE ")&&-1==a.indexOf("IEMobile")},Xf=function(a,b){var c=0,d=[];a&&(d.push(a.getAdUnitPath()),d.push(Wf(a)),d.push(a.getSlotElementId()));if(b){a=[];for(var e=0;b&&25>e;b=b.parentNode,++e)a.push(9!==b.nodeType&&b.id||"");(b=a.join())&&d.push(b)}0= +b&&a.o&&(f=.5=b?1-b:0);c=a.j;var g=f*e.length;if((f=a.l)?c.j.hasOwnProperty(f)&&""==c.j[f]:1){var k;b:{try{var l=window.top.location.hash;if(l){var n=l.match(/\bdeid=([\d,]+)/);k=n&&n[1]||"";break b}}catch(m){}k=""}(e=(k=(k=k.match(new RegExp("\\b("+e.join("|")+")\\b")))&&k[0]||null)?k:ic(e,g))&&Td(c,e,f)}a.u=b;d&&b&&(Ud(a.j,a.l)||.5=b)&&!cg&&!O(a,ig.K)&&(d=jc(a.m),cg=O(a,ig.G)||Math.random()*b*d=a;a=b}return a},kg={K:"108809097",G:"108809098"},lg={K:"108809030",G:"108809031",Ta:"108809080"},mg={K:"108809113",Za:"108809114",Ya:"108809151"},ng={K:"108809124",G:"108809125"},og={K:"108809132",G:"108809133"},pg={K:"108809134",G:"108809135"},rg={K:"108809141",G:"108809140"},sg={K:"108809142",G:"108809143"},ig={G:"108809144",K:"108809167"},tg={K:"108809147",G:"108809148"},vg={Ua:"108809152",Qa:"108809153",Ha:"108809154",Ra:"108809155"},xg={K:"108809159",G:"108809160"}, +yg={K:"108809161",G:"108809162"},zg={K:"108809163",Va:"108809164",Sa:"108809165"},Ag={K:"108809166",G:"108809168"},fm={K:"21060002",G:"21060003"},ug={K:"21060004",G:"21060005"},wg={K:"21060006",G:"21060007"},Cg={K:"21060011",G:"21060012"},Zn={K:"21060015",G:"21060016"},Jg={K:"21060017",G:"21060018"},Ng={K:"21060019",G:"21060020"},Bg=function(a,b){for(var c=0;cc||1=b.length||(b=yd(b,0,3),b.push("__extra__")), +Q(a,"nw_id",b.join(",")))},fh=function(a,b){Q(a,"vrg","105");var c=document;b?(ch(a,dh(b)),Q(a,"nslots",eh(b).toString())):(ch(a,Zg(ah)),Q(a,"nslots",ah.length.toString()));b=Wd();0=b&&(window.GPT_jstiming.srt=c-b));try{c=null,window.chrome&&window.chrome.csi&&(c=Math.floor(window.chrome.csi().pageT)),null==c&& +window.gtbExternal&&(c=window.gtbExternal.pageT()),null==c&&window.external&&(c=window.external.pageT),c&&(window.GPT_jstiming.pt=c)}catch(e){}})();if(window.GPT_jstiming){window.GPT_jstiming.Ia={};window.GPT_jstiming.Ab=1;var lh=function(a,b,c){var d=a.t[b],e=a.t.start;if(d&&(e||c))return d=a.t[b][0],void 0!=c?e=c:e=e[0],Math.round(d-e)};window.GPT_jstiming.getTick=lh;var mh=function(a,b,c){var d="";window.GPT_jstiming.srt&&(d+="&srt="+window.GPT_jstiming.srt);window.GPT_jstiming.pt&&(d+="&tbsrt="+window.GPT_jstiming.pt);try{window.external&&window.external.tran?d+="&tran="+window.external.tran:window.gtbExternal&&window.gtbExternal.tran?d+= +"&tran="+window.gtbExternal.tran():window.chrome&&window.chrome.csi&&(d+="&tran="+window.chrome.csi().tran)}catch(q){}var e=window.chrome;if(e&&(e=e.loadTimes)){e().wasFetchedViaSpdy&&(d+="&p=s");if(e().wasNpnNegotiated){var d=d+"&npn=1",f=e().npnNegotiatedProtocol;f&&(d+="&npnv="+(encodeURIComponent||escape)(f))}e().wasAlternateProtocolAvailable&&(d+="&apa=1")}var g=a.t,k=g.start,e=[],f=[],l;for(l in g)if("start"!=l&&0!=l.indexOf("_")){var n=g[l][1];n?g[n]&&f.push(l+"."+lh(a,l,g[n][0])):k&&e.push(l+ +"."+lh(a,l))}if(b)for(var m in b)d+="&"+m+"="+b[m];(b=c)||(b="https:"==document.location.protocol?"https://csi.gstatic.com/csi":"http://csi.gstatic.com/csi");return[b,"?v=3","&s="+(window.GPT_jstiming.sn||"gpt")+"&action=",a.name,f.length?"&it="+f.join(","):"",d,"&rt=",e.join(",")].join("")},nh=function(a,b,c){a=mh(a,b,c);if(!a)return"";b=new Image;var d=window.GPT_jstiming.Ab++;window.GPT_jstiming.Ia[d]=b;b.onload=b.onerror=function(){window.GPT_jstiming&&delete window.GPT_jstiming.Ia[d]};b.src= +a;b=null;return a};window.GPT_jstiming.report=function(a,b,c){if("prerender"==document.webkitVisibilityState){var d=!1,e=function(){if(!d){b?b.prerender="1":b={prerender:"1"};var f;"prerender"==document.webkitVisibilityState?f=!1:(nh(a,b,c),f=!0);f&&(d=!0,document.removeEventListener("webkitvisibilitychange",e,!1))}};document.addEventListener("webkitvisibilitychange",e,!1);return""}return nh(a,b,c)}};var oh=function(){this.l=this.l;this.m=this.m};oh.prototype.l=!1;oh.prototype.u=function(){if(this.m)for(;this.m.length;)this.m.shift()()};var xh=function(a,b,c,d,e){oh.call(this);this.w=a;this.status=1;this.o=b;this.A=c;this.I=d;this.na=!!e;this.v=Math.random();this.B={};this.j=null;this.C=z(this.H,this)};A(xh,oh);xh.prototype.H=function(a){if(a.origin===this.A&&(this.na||a.source==this.o)){var b=null;try{b=qh(a.data)}catch(c){}if(y(b)&&(a=b.i,b.c===this.w&&a!=this.v&&(2!==this.status&&(this.status=2,yh(this),this.j&&(this.j(),this.j=null)),a=b.s,b=b.p,u(a)&&(u(b)||y(b))&&this.B.hasOwnProperty(a))))this.B[a](b)}}; +var yh=function(a){var b={};b.c=a.w;b.i=a.v;a.o.postMessage(th(b),a.A)};xh.prototype.D=function(){if(1===this.status){try{this.o.postMessage&&yh(this)}catch(a){}window.setTimeout(z(this.D,this),50)}};xh.prototype.connect=function(a){a&&(this.j=a);Vc(window,"message",this.C);this.I&&this.D()};var zh=function(a,b,c){a.B[b]=c},Ah=function(a,b,c){var d={};d.c=a.w;d.i=a.v;d.s=b;d.p=c;a.o.postMessage(th(d),a.A)};xh.prototype.u=function(){this.status=3;Wc(window,"message",this.C);xh.Fa.u.call(this)};re||se||L&&Ee(11);var Bh=function(){this.j=[]},Dh=function(a,b,c,d,e){a.j.push(new Ch(b,c,d,e))},Eh=function(a,b,c,d){Dh(a,b,c,d+"px",void 0)},Ch=function(a,b,c,d){this.m=a;this.j=(this.l=r(d)&&a.style&&a.style.getPropertyPriority)?String(b).replace(/([A-Z])/g,"-$1").toLowerCase():b;this.o=this.l?a.style.getPropertyValue(this.j):a.style[this.j];this.u=this.l?a.style.getPropertyPriority(this.j):null;this.l?(a.style.removeProperty(this.j),a.style.setProperty(this.j,c,d)):a.style[this.j]=c};var Fh=function(a){this.m=a;this.u=null;this.H=this.status=0;this.l=null;this.Y="sfchannel"+a};var Gh=function(a,b,c,d,e,f){this.m=Xe(a);this.l=Xe(b);this.o=c;this.j=Xe(d);this.u=e;this.v=f},Hh=function(a,b,c){var d=window.screenX||window.screenLeft||0,e=window.screenY||window.screenTop||0,f=window.outerWidth||document.documentElement.clientWidth||0,g=window.outerHeight||document.documentElement.clientHeight||0,d=b?new We(e,f+d,g+e,d):new We(e,f-d,g-e,d),k=ff(a);if("none"!=cf(a,"display"))e=hf(a);else{var e=a.style,f=e.display,g=e.visibility,l=e.position;e.visibility="hidden";e.position="absolute"; +e.display="inline";var n=hf(a);e.display=f;e.position=l;e.visibility=g;e=n}e=new Ye(k.x,k.y,e.width,e.height);f=Ze(e);g=String(cf(a,"zIndex"));if(b){c=window;b=c.document;var m;try{var q,w=c.document;q="CSS1Compat"==w.compatMode?w.documentElement:w.body;m=new me(q.clientWidth,q.clientHeight)}catch(U){m=new me(-12245933,-12245933)}c=new me(Math.max(m.width,Math.max(b.body.scrollWidth,b.documentElement.scrollWidth)),Math.max(m.height,Math.max(b.body.scrollHeight,b.documentElement.scrollHeight)));c= +new We(0,c.width,c.height,0);if(a){m=new We(0,Infinity,Infinity,0);w=Me(a);q=w.j.body;w=w.j.documentElement;a=ef(a);for(b=0;a&&100>b&&!(k=bf(a,"overflow")||(a.currentStyle?a.currentStyle.overflow:null)||a.style&&a.style.overflow,L&&0==a.clientWidth||se&&0==a.clientHeight&&a==q||a==q||a==w||"hidden"!=k);a=ef(a),b++)k=ff(a),l=new le(a.clientLeft,a.clientTop),k.x+=l.x,k.y+=l.y,m.top=Math.max(m.top,k.y),m.right=Math.min(m.right,k.x+a.clientWidth),m.bottom=Math.min(m.bottom,k.y+a.clientHeight),m.left= +Math.max(m.left,k.x);a=0<=m.top&&0<=m.left&&m.bottom>m.top&&m.right>m.left?m:null}else a=null;a=a?Ze(af($e(c),$e(a))):c}else if(c)m=c.boundingClientRect,a=k.y-m.top,m=k.x-m.left,a=new We(a,c.rootBounds.width+m,c.rootBounds.height+a,m);else{c=new We(0,Infinity,Infinity,0);m=Me(a);w=m.j.body;b=m.j.documentElement;for(q=Re(m.j);a=ef(a);)L&&0==a.clientWidth||se&&0==a.clientHeight&&a==w||a==w||a==b||"visible"==cf(a,"overflow")||(k=ff(a),l=new le(a.clientLeft,a.clientTop),k.x+=l.x,k.y+=l.y,c.top=Math.max(c.top, +k.y),c.right=Math.min(c.right,k.x+a.clientWidth),c.bottom=Math.min(c.bottom,k.y+a.clientHeight),c.left=Math.max(c.left,k.x));a=q.scrollLeft;q=q.scrollTop;c.left=Math.max(c.left,a);c.top=Math.max(c.top,q);m=Qe(Se(m.j)||window);c.right=Math.min(c.right,a+m.width);c.bottom=Math.min(c.bottom,q+m.height);a=0<=c.top&&0<=c.left&&c.bottom>c.top&&c.right>c.left?c:null}var x;null!=a&&(x=af($e(a),e));a=(c=(c=null!=x&&(0!=x.width||x.left+x.width!=a.left&&x.left!=a.right))&&(0!=x.height||x.top+x.height!=a.top&& +x.top!=a.bottom))?new We(Math.max(f.top-a.top,0),Math.max(a.right-f.right,0),Math.max(a.bottom-f.bottom,0),Math.max(f.left-a.left,0)):new We(0,0,0,0);m=c=0;x&&!(new me(x.width,x.height)).isEmpty()&&(c=x.width/e.width,m=x.height/e.height);return new Gh(d,f,g,a,c,m)},Ih=function(a){return th({windowCoords_t:a.m.top,windowCoords_r:a.m.right,windowCoords_b:a.m.bottom,windowCoords_l:a.m.left,frameCoords_t:a.l.top,frameCoords_r:a.l.right,frameCoords_b:a.l.bottom,frameCoords_l:a.l.left,styleZIndex:a.o,allowedExpansion_t:a.j.top, +allowedExpansion_r:a.j.right,allowedExpansion_b:a.j.bottom,allowedExpansion_l:a.j.left,xInView:a.u,yInView:a.v})};var Jh=function(){this.j={shared:{sf_ver:"1-0-5",ck_on:navigator.cookieEnabled?1:0,flash_ver:nf?of:"0"}}};var Kh=function(a,b){this.pa=a;this.qa=b};var Lh=function(a,b,c,d,e,f){var g=new Jh;this.o=a;this.j=b;this.l=c;this.permissions=d;this.m=g;this.u=e;this.na=f};var Mh=function(a){this.j=a};Mh.prototype.l=ba;var Nh=function(a,b){this.j=a;this.version=b};A(Nh,Mh);Nh.prototype.l=function(){return th({uid:this.j,version:this.version})};var Oh=function(a,b,c){this.j=a;this.o=b;this.m=c};A(Oh,Mh);Oh.prototype.l=function(){return th({uid:this.j,initialWidth:this.o,initialHeight:this.m})};var Ph=function(a,b){this.j=a;this.m=b};A(Ph,Mh);Ph.prototype.l=function(){return th({uid:this.j,description:this.m})};var Qh=function(a,b,c){this.j=a;this.m=b;this.push=c}; +A(Qh,Mh);Qh.prototype.l=function(){return th({uid:this.j,expand_t:this.m.top,expand_r:this.m.right,expand_b:this.m.bottom,expand_l:this.m.left,push:this.push})};var Rh=function(a){this.j=a};A(Rh,Mh);Rh.prototype.l=function(){return th({uid:this.j})};var Sh=function(a,b){this.j=a;this.o=b};A(Sh,Mh);Sh.prototype.l=function(){var a={uid:this.j,newGeometry:Ih(this.o)};return th(a)};var Th=function(a,b,c,d,e){Sh.call(this,a,c);this.u=b;this.m=d;this.push=e};A(Th,Sh); +Th.prototype.l=function(){var a={uid:this.j,success:this.u,newGeometry:Ih(this.o),expand_t:this.m.top,expand_r:this.m.right,expand_b:this.m.bottom,expand_l:this.m.left,push:this.push};return th(a)};var Uh=function(a,b,c){this.j=a;this.width=b;this.height=c};A(Uh,Mh);Uh.prototype.l=function(){return th({uid:this.j,width:this.width,height:this.height})};var Vh=1,Wh=!1,Zh=function(a){Fh.call(this,Vh++);this.A=a.ub;this.C=1==a.size;this.N=new Kh(a.permissions.pa&&!this.C,a.permissions.qa&&!this.C);this.v=a.Ca;this.V=window.location.protocol+"//"+window.location.host;this.W=!!a.na;this.T=window.location.protocol+"//tpc.googlesyndication.com";this.O=!!a.cb;this.Z=a.sandbox||!1;this.o=new Bh;Xh(this,a.Ca,a.size);this.D=a.fb||!1;this.u=this.R=Hh(a.Ca,this.D);this.X=a.Cb||[];this.j=Yh(this,a.kb,a.content,a.size,a.lb);this.M=null;this.I=z(this.L,this);this.J= +-1;this.w=0;this.B=null;!a.ob||"function"!==typeof IntersectionObserver||ue||te||(this.B=new IntersectionObserver(z(function(a){this.M=a[a.length-1];this.L()},this)));this.l=new xh(this.Y,this.j.contentWindow,this.T,!1);zh(this.l,"init_done",z(this.pb,this));zh(this.l,"register_done",z(this.zb,this));zh(this.l,"report_error",z(this.Bb,this));zh(this.l,"expand_request",z(this.eb,this));zh(this.l,"collapse_request",z(this.bb,this));zh(this.l,"creative_geometry_update",z(this.U,this));this.l.connect(z(this.xb, +this));var b=z(function(){this.j&&(this.j.name="",a.Pa&&a.Pa(),Wc(this.j,"load",b))},this);Vc(this.j,"load",b)};A(Zh,Fh); +var Xh=function(a,b,c){a.C?(b.style.width=gf("100%"),b.style.height=gf("auto")):(b.style.width=gf(c.width),b.style.height=gf(c.height))},Yh=function(a,b,c,d,e){var f=Me(a.v),g=a.X;c="1-0-5;"+(g.length?"e:"+g.join()+";":"")+c.length+";"+c;var g=new Lh(a.m,a.V,a.R,a.N,a.C,a.W),k=g.o,l=g.j,n=Ih(g.l),m;m=g.permissions;m=th({expandByOverlay:m.pa,expandByPush:m.qa,readCookie:!1,writeCookie:!1});g={uid:k,hostPeerName:l,initialGeometry:n,permissions:m,metadata:th(g.m.j),reportCreativeGeometry:g.u,isDifferentSourceWindow:g.na}; +g=th(g);c+=g;a.O&&d instanceof me&&(g=Me(a.v),Wh||(hc(g.j,"//pagead2.googlesyndication.com/pagead/expansion_embed.js?source=safeframe"),Wh=!0),g=Se(g.j),g.google_eas_queue=g.google_eas_queue||[],g.google_eas_queue.push({a:b,b:g.location.protocol+"//tpc.googlesyndication.com",c:d.width,d:d.height,e:"sf-gdn-exp-"+a.m,f:void 0,g:void 0,h:void 0,i:void 0}));a.C?(k=g=0,d="min-width:100%"):(g=d.width,k=d.height,d="");n=Se(f.j);l=jf(n);a.O&&(n=nc(n.location.href),l+="#"+[0\"":"about:blank";e={frameborder:0,style:"border:0;vertical-align:bottom;"+(d||""),allowTransparency:"true",src:e};b&&Sd(e, +b);f=f.l("IFRAME",e);a.Z&&(f.sandbox="allow-same-origin allow-forms allow-popups allow-scripts allow-pointer-lock allow-popups-to-escape-sandbox");a.v.appendChild(f);return f};h=Zh.prototype;h.xb=function(){this.B&&this.j?this.B.observe(this.j):(Vc(window,"resize",this.I),Vc(window,"scroll",this.I))}; +h.pb=function(a){try{if(0!=this.status)throw Error("Container already initialized");if(!u(a))throw Error("Could not parse serialized message");var b,c=qh(a);if(!(y(c)&&v(c.uid)&&u(c.version)))throw Error("Cannot parse JSON message");b=new Nh(c.uid,c.version);if(this.m!=b.j||"1-0-5"!=b.version)throw Error("Wrong source container");this.status=1}catch(d){this.A.error("Invalid INITIALIZE_DONE message. Reason: "+d.message)}}; +h.zb=function(a){try{if(1!=this.status)throw Error("Container not initialized");if(!u(a))throw Error("Could not parse serialized message");var b=qh(a);if(!(y(b)&&v(b.uid)&&v(b.initialWidth)&&v(b.initialHeight)))throw Error("Cannot parse JSON message");if(this.m!=(new Oh(b.uid,b.initialWidth,b.initialHeight)).j)throw Error("Wrong source container");this.status=2}catch(c){this.A.error("Invalid REGISTER_DONE message. Reason: "+c.message)}}; +h.Bb=function(a){try{if(!u(a))throw Error("Could not parse serialized message");var b,c=qh(a);if(!(y(c)&&v(c.uid)&&u(c.description)))throw Error("Cannot parse JSON message");b=new Ph(c.uid,c.description);if(this.m!=b.j)throw Error("Wrong source container");this.A.info("Ext reported an error. Description: "+b.m)}catch(d){this.A.error("Invalid REPORT_ERROR message. Reason: "+d.message)}}; +h.eb=function(a){try{if(2!=this.status)throw Error("Container is not registered");if(0!=this.H)throw Error("Container is not collapsed");if(!u(a))throw Error("Could not parse serialized message");var b,c=qh(a);if(!(y(c)&&v(c.uid)&&v(c.expand_t)&&v(c.expand_r)&&v(c.expand_b)&&v(c.expand_l)&&fa(c.push)))throw Error("Cannot parse JSON message");b=new Qh(c.uid,new We(c.expand_t,c.expand_r,c.expand_b,c.expand_l),c.push);if(this.m!=b.j)throw Error("Wrong source container");if(!(0<=b.m.top&&0<=b.m.left&& +0<=b.m.bottom&&0<=b.m.right))throw Error("Invalid expansion amounts");var d;if(d=b.push&&this.N.qa||!b.push&&this.N.pa){var e=b.m,f=b.push,g=this.u=Hh(this.j,this.D);if(e.top<=g.j.top&&e.right<=g.j.right&&e.bottom<=g.j.bottom&&e.left<=g.j.left){if(!f)for(var k=this.j.parentNode;k&&k.style;k=k.parentNode)Dh(this.o,k,"overflowX","visible","important"),Dh(this.o,k,"overflowY","visible","important");var l=this.u.l,n=this.u.l,m=Ze(new Ye(0,0,l.right-l.left,n.bottom-n.top));y(e)?(m.top-=e.top,m.right+= +e.right,m.bottom+=e.bottom,m.left-=e.left):(m.top-=e,m.right+=Number(void 0),m.bottom+=Number(void 0),m.left-=Number(void 0));Dh(this.o,this.v,"position","relative");Dh(this.o,this.j,"position","absolute");f?(Eh(this.o,this.v,"width",m.right-m.left),Eh(this.o,this.v,"height",m.bottom-m.top)):Dh(this.o,this.j,"zIndex","10000");Eh(this.o,this.j,"width",m.right-m.left);Eh(this.o,this.j,"height",m.bottom-m.top);Eh(this.o,this.j,"left",m.left);Eh(this.o,this.j,"top",m.top);this.H=2;this.u=Hh(this.j,this.D); +d=!0}else d=!1}a=d;Ah(this.l,"expand_response",(new Th(this.m,a,this.u,b.m,b.push)).l());if(!a)throw Error("Viewport or document body not large enough to expand into.");}catch(q){this.A.error("Invalid EXPAND_REQUEST message. Reason: "+q.message)}}; +h.bb=function(a){try{if(2!=this.status)throw Error("Container is not registered");if(2!=this.H)throw Error("Container is not expanded");if(!u(a))throw Error("Could not parse serialized message");var b=qh(a);if(!y(b)||!v(b.uid))throw Error("Cannot parse JSON message");if(this.m!=(new Rh(b.uid)).j)throw Error("Wrong source container");$h(this);Ah(this.l,"collapse_response",(new Sh(this.m,this.u)).l())}catch(c){this.A.error("Invalid COLLAPSE_REQUEST message. Reason: "+c.message)}}; +var $h=function(a){for(var b=a.o,c=b.j.length-1;0<=c;c--){var d=b.j[c];d.l?(d.m.style.removeProperty(d.j),d.m.style.setProperty(d.j,d.o,d.u)):d.m.style[d.j]=d.o}b.j.length=0;a.H=0;a.j&&(a.u=Hh(a.j,a.D))};Zh.prototype.L=function(){if(1==this.status||2==this.status)switch(this.w){case 0:ai(this);this.J=window.setTimeout(z(this.P,this),1E3);this.w=1;break;case 1:this.w=2;break;case 2:this.w=2}}; +Zh.prototype.U=function(a){try{if(!u(a))throw Error("Could not parse serialized message");var b,c=qh(a);if(!(y(c)&&v(c.uid)&&v(c.width)&&v(c.height)))throw Error("Cannot parse JSON message");b=new Uh(c.uid,c.width,c.height);if(this.m!=b.j)throw Error("Wrong source container");var d=String(b.height);this.C?d!=this.j.height&&(this.j.height=d,this.L()):this.A.error("Got CreativeGeometryUpdate message in non-fluidcontainer. The container is not resized.")}catch(e){this.A.error("Invalid CREATIVE_GEOMETRY_UPDATE message. Reason: "+ +e.message)}};Zh.prototype.P=function(){if(1==this.status||2==this.status)switch(this.w){case 1:this.w=0;break;case 2:ai(this),this.J=window.setTimeout(z(this.P,this),1E3),this.w=1}}; +var ai=function(a){a.u=Hh(a.j,a.D,a.M);a.M=null;Ah(a.l,"geometry_update",(new Sh(a.m,a.u)).l())},bi=function(a){if(100!=a.status){2==a.H&&$h(a);window.clearTimeout(a.J);a.J=-1;a.w=3;if(a.l){var b=a.l;b.l||(b.l=!0,b.u());a.l=null}a.B&&a.j?a.B.unobserve(a.j):(Wc(window,"resize",a.I),Wc(window,"scroll",a.I));if(b=a.j){var b=a.v,c;a:{c=a.j;var d;if(Je&&!(L&&Ee("9")&&!Ee("10")&&p.SVGElement&&c instanceof p.SVGElement)&&(d=c.parentElement)){c=d;break a}d=c.parentNode;c=y(d)&&1==d.nodeType?d:null}b=b==c}b&& +a.v.removeChild(a.j);a.j=null;a.v=null;a.B&&(a.B.disconnect(),a.B=null);a.status=100}};var ci=function(a,b,c,d,e){this.advertiserId=a;this.campaignId=b;this.creativeId=c;this.labelIds=d;this.lineItemId=e};var di=function(a){var b=ua(),c={};if(!a||!y(a))return null;var d=!1;G(a,function(e,f){switch(f){case "allowOverlayExpansion":fa(e)?c.allowOverlayExpansion=a.allowOverlayExpansion:(b.error(Zb("allowOverlayExpansion",a.allowOverlayExpansion),null,this),d=!0);break;case "allowPushExpansion":fa(e)?c.allowPushExpansion=a.allowPushExpansion:(b.error(Zb("allowPushExpansion",a.allowPushExpansion),null,this),d=!0);break;case "sandbox":!0===e?c.sandbox=a.sandbox:(b.error(Zb("sandbox",a.sandbox),null,this), +d=!0);break;default:b.j(Yb(f),null,this)}});return d?null:c},ei=function(a){for(var b={},c=0;cw;w++){if(0<=q.innerHTML.indexOf(e[n][2])){m= +q;break}q=q.parentNode}ak(e[n],m,f,c);e.splice(n,1);break}if(0n.length;)m[c].src&&n.unshift(m[c].src),c--;m=Uc(a,n)}else m=0;W(this,"icsg",m);if(a=a.l.document&&a.l.document.scripts?a.l.document.scripts:[]){m=[];for(n=a.length-1;0<=n;n--)(c=a[n])&& +null!=c.src&&m.push(Lc(c.src.match(Kc)[3]||null));a=Pc(m)}else a=0;0m&&(m=0,n=new P("gpt_negative_stack_trace",Tf(),J.getInstance().get(23)),fh(n,this.j),Q(n,"stackTrace",a.stack),bh(n)),W(this,"std",m));q.currentScript&&q.currentScript.text&&W(this,"csl",q.currentScript.text.length);if(Math.random()a.downlinkMax)<<1;a:{b=b.document.getElementsByTagName("script");for(a=1;a>21:b;return b},Mk=function(a,b){if(!a||"none"==a)return 1;a=String(a);"auto"==a&&(a=b,"www."==a.substring(0,4)&&(a=a.substring(4,a.length)));return Lk(a.toLowerCase())},Nk=/^\s*_ga=\s*1\.(\d+)[^.]*\.(.*?)\s*$/,Ok=/^[^=]+=\s*GA1\.(\d+)[^.]*\.(.*?)\s*$/;var Pk=function(a,b,c,d,e){vk.call(this,a,b,c,d,e)};A(Pk,vk);Pk.prototype.w=function(a,b,c){0var inDapIF=true;\x3c/script>\n"));if(0!=Uf()){var d;try{d=!!a.contentWindow.document}catch(U){d=!1}if(d){var e=b,f=Ll();try{var g=window.frames[a.name];a=e;var k="http://"+J.getInstance().get(1)+"/pagead/inject_object_div.js";if(6a.indexOf(k)){var l;b:{a=e;var k=document, +n=Uf(),m;if(!(m=0==n||isNaN(n)||7>n||9document.domain = \""+ +document.domain+'";'+w+"<\\/scr\\' + \\'ipt>\\');document.close(); };":'document.domain = "'+document.domain+'";'+w+"document.close();",a.src='javascript:\' + + + + + + + + + + + + + + + + + + +
    + +
    + +

    AMP #0

    + Go to iframe +

    + Quisque ultricies id augue a convallis. Vivamus euismod est quis tellus laoreet lacinia. In quam tellus, mollis nec porta eget, volutpat sit amet nibh. Duis ac odio sem. Sed consequat, ante gravida fringilla suscipit, libero libero ullamcorper metus, nec porta est elit at est. Curabitur vel diam ligula. Nulla bibendum malesuada odio. +

    +

    + + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. + +

    +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. +
    +

    + +

    + Top + Image0 + Footer +

    + +

    + + + + + + + + +

    +

    + + + + + + + + +

    +

    + + + +

    + +

    Video

    + + +

    Youtube

    + + + + + +

    Audio

    + + +

    Lightbox

    + +

    + +

    +

    + +

    + +
    + + + +

    Scrollable Lightbox

    + + +

    + +

    + +

    + +

    + +

    + +

    + + + + +
    + + + +

    Images

    +

    + Responsive (w/srcset, w/image-lightbox) +

    + + +

    + +

    + Fixed +

    + +

    + +

    + None +

    + +

    +

    + Lorem ipsum dolor sit amet. +

    + + +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    + +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    + +

    +

    + +

    +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +
    +

    Media query selection

    + + +

    +

    + +

    +

    + +

    +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    + +

    +

    + +

    +

    + +

    +

    Twitter

    + + + + + + + +

    + +

    +

    + Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit. +

    +

    Instagram

    + + + +

    IFrame

    + + +

    + + + +

    SVG

    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + diff --git a/examples/inabox.amp.html b/examples/inabox.amp.html new file mode 100644 index 000000000000..19f6edef5b77 --- /dev/null +++ b/examples/inabox.amp.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + +
    + + +
    + + + + + + diff --git a/examples/inabox.gpt.html b/examples/inabox.gpt.html new file mode 100644 index 000000000000..0d45107c5428 --- /dev/null +++ b/examples/inabox.gpt.html @@ -0,0 +1,33 @@ + + + + + + + AMP inabox for GPT demo + + + +

    Scroll down to see a GPT slot that tries to request that same AMP ad

    + +
    +
    + +
    +
    + + diff --git a/examples/inabox.host.html b/examples/inabox.host.html new file mode 100644 index 000000000000..dc7a6f5ca3c4 --- /dev/null +++ b/examples/inabox.host.html @@ -0,0 +1,96 @@ + + + + + + amp-inabox-host example + + + +

    Scroll down to see more ...

    +
    +

    Inabox nested in friendly iframe

    + +
    +

    A huge doc inabox

    + + + + + diff --git a/examples/live-blog-non-floating-button.amp.html b/examples/live-blog-non-floating-button.amp.html new file mode 100644 index 000000000000..186d55b1d880 --- /dev/null +++ b/examples/live-blog-non-floating-button.amp.html @@ -0,0 +1,258 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    +

    Lorem Ipsum

    + +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +

    +
    + +
    + + +
    +
    +

    + Bacon ipsum dolor amet +

    +
    + +
    +
    + Bacon ipsum dolor amet nostrud adipisicing pancetta beef ribs commodo swine porchetta esse ad kielbasa. Culpa aute prosciutto esse turkey tempor. Sunt pastrami t-bone short ribs turkey in sint ground round elit pork chop venison andouille meatloaf turducken. Cillum pancetta do, shankle chuck pariatur sirloin spare ribs eu cupim kevin sausage filet mignon in. Bacon aliqua flank aute nostrud. Sausage shank tail meatball, culpa ut consequat tongue sunt tempor occaecat jerky velit mollit bacon. Elit meatball andouille tri-tip, in laboris corned beef shank adipisicing qui. +
    +
    +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + + diff --git a/examples/live-blog.amp.html b/examples/live-blog.amp.html new file mode 100644 index 000000000000..848478d958bf --- /dev/null +++ b/examples/live-blog.amp.html @@ -0,0 +1,247 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + +
    + +
    + +
    +
    +
    +
    +

    Lorem Ipsum

    + +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +

    +
    + +
    + + +
    +
    +

    + Bacon ipsum dolor amet +

    +
    + +
    +
    + Bacon ipsum dolor amet nostrud adipisicing pancetta beef ribs commodo swine porchetta esse ad kielbasa. Culpa aute prosciutto esse turkey tempor. Sunt pastrami t-bone short ribs turkey in sint ground round elit pork chop venison andouille meatloaf turducken. Cillum pancetta do, shankle chuck pariatur sirloin spare ribs eu cupim kevin sausage filet mignon in. Bacon aliqua flank aute nostrud. Sausage shank tail meatball, culpa ut consequat tongue sunt tempor occaecat jerky velit mollit bacon. Elit meatball andouille tri-tip, in laboris corned beef shank adipisicing qui. +
    + + + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + + diff --git a/examples/live-list-update.amp.html b/examples/live-list-update.amp.html index e1ab697789c0..6ac78f965465 100644 --- a/examples/live-list-update.amp.html +++ b/examples/live-list-update.amp.html @@ -70,6 +70,24 @@ text-align: center; background: #3f51b5; } + + .pagination { + display: inline-block; + padding: 0; + } + + amp-live-list [pagination] nav { + display: flex; + align-items: center; + justify-content: center; + } + + .pagination li { + display: inline; + border: 1px solid black; + padding: 10px; + list-style-type: none; + } @@ -128,6 +146,13 @@
    +
    + +
    diff --git a/examples/loads-windowcontext-creative.html b/examples/loads-windowcontext-creative.html new file mode 100644 index 000000000000..50563fe45eb0 --- /dev/null +++ b/examples/loads-windowcontext-creative.html @@ -0,0 +1,30 @@ + + + + + window.context Example + + + + + + + +

    window.context example

    +

    Creative not loading? Make sure you put .max in the url for the page

    + +

    A4A + JS Creative

    + + +
    + + + diff --git a/examples/medium-manifest.json b/examples/medium-manifest.json new file mode 100644 index 000000000000..618bcc8f03fd --- /dev/null +++ b/examples/medium-manifest.json @@ -0,0 +1,10 @@ +{ + "prefer_related_applications": true, + "related_applications": [ + { + "platform": "play", + "id": "com.medium.reader", + "url": "android-app://com.medium.reader/https/medium.com/p/cb7f223fad86" + } + ] +} diff --git a/examples/metadata-examples/article-json-ld-twitter-card.amp.html b/examples/metadata-examples/article-json-ld-twitter-card.amp.html new file mode 100644 index 000000000000..175c8356cdd1 --- /dev/null +++ b/examples/metadata-examples/article-json-ld-twitter-card.amp.html @@ -0,0 +1,141 @@ + + + + + + + Lorem Ipsum + + + + + + + + + + + + + + + + + + + + +

    Lorem Ipsum

    + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec + odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla + quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent + mauris. Fusce nec tellus sed augue semper porta. Mauris massa. + Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad + litora torquent per conubia nostra, per inceptos himenaeos.

    +

    Curabitur sodales ligula in libero. Sed dignissim lacinia nunc. + Curabitur tortor. Pellentesque nibh. Aenean quam. In scelerisque sem at + dolor. Maecenas mattis. Sed convallis tristique sem. Proin ut ligula vel nunc + egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis, luctus non, + massa. Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum.

    +

    Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. + Quisque volutpat condimentum velit. Class aptent taciti sociosqu ad + litora torquent per conubia nostra, per inceptos himenaeos. Nam nec + ante. Sed lacinia, urna non tincidunt mattis, tortor neque adipiscing + diam, a cursus ipsum ante quis turpis. Nulla facilisi. Ut fringilla. + Suspendisse potenti. Nunc feugiat mi a tellus consequat imperdiet. + Vestibulum sapien. Proin quam.

    +
    +

    Quo usque tandem abutere, Catilina, patientia nostra? Quam diu etiam + furor iste tuus nos eludet? Quem ad finem sese effrenata iactabit + audacia?

    +

    CICERO

    +
    +

    Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. Sed + lectus. Integer euismod lacus luctus magna. Quisque cursus, metus vitae + pharetra auctor, sem massa mattis sem, at interdum magna augue eget diam. + Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere + cubilia Curae; Morbi lacinia molestie dui. Praesent blandit dolor. Sed + non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit + amet pede facilisis laoreet. Donec lacus nunc, viverra nec, blandit vel, + egestas et, augue. Vestibulum tincidunt malesuada tellus. Ut ultrices + ultrices enim.

    +

    Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. + Nulla facilisi. Integer lacinia sollicitudin massa. Cras metus. Sed + aliquet risus a tortor. Integer id quam. Morbi mi. Quisque nisl felis, + venenatis tristique, dignissim in, ultrices sit amet, augue. Proin + sodales libero eget ante. Nulla quam. Aenean laoreet. Vestibulum nisi + lectus, commodo ac, facilisis ac, ultricies eu, pede.

    + + diff --git a/examples/metadata-examples/article-json-ld.amp.html b/examples/metadata-examples/article-json-ld.amp.html index d8334a3b8989..3242184335d3 100644 --- a/examples/metadata-examples/article-json-ld.amp.html +++ b/examples/metadata-examples/article-json-ld.amp.html @@ -16,7 +16,7 @@ The canonical document should also have a corresponding tag within pointing at this AMP HTML file: - + It is possible that this AMP HTML document is the canonical document for this article, in which case, the canonical URL should point to this @@ -55,7 +55,7 @@ { "@context": "http://schema.org", "@type": "NewsArticle", - "mainEntityOfPage": "http://cdn.ampproject.org/article-metadata.html", + "mainEntityOfPage": "http://example.ampproject.org/article-metadata.html", "headline": "Lorem Ipsum", "datePublished": "1907-05-05T12:02:41Z", "dateModified": "1907-05-05T12:02:41Z", diff --git a/examples/metadata-examples/article-microdata.amp.html b/examples/metadata-examples/article-microdata.amp.html index 529558f434fc..870ea9efd3e6 100644 --- a/examples/metadata-examples/article-microdata.amp.html +++ b/examples/metadata-examples/article-microdata.amp.html @@ -16,7 +16,7 @@ The canonical document should also have a corresponding tag within pointing at this AMP HTML file: - + It is possible that this AMP HTML document is the canonical document for this article, in which case, the canonical URL should point to this diff --git a/examples/metadata-examples/recipe-json-ld.amp.html b/examples/metadata-examples/recipe-json-ld.amp.html index bb305200e610..3b0d25485ded 100644 --- a/examples/metadata-examples/recipe-json-ld.amp.html +++ b/examples/metadata-examples/recipe-json-ld.amp.html @@ -16,7 +16,7 @@ The canonical document should also have a corresponding tag within pointing at this AMP HTML file: - + It is possible that this AMP HTML document is the canonical document for this article, in which case, the canonical URL should point to this diff --git a/examples/metadata-examples/recipe-microdata.amp.html b/examples/metadata-examples/recipe-microdata.amp.html index 34faaf520f8f..3840cdd2ce07 100644 --- a/examples/metadata-examples/recipe-microdata.amp.html +++ b/examples/metadata-examples/recipe-microdata.amp.html @@ -12,15 +12,21 @@ diff --git a/examples/metadata-examples/review-json-ld.amp.html b/examples/metadata-examples/review-json-ld.amp.html index 02ac6064453a..535f77abcf82 100644 --- a/examples/metadata-examples/review-json-ld.amp.html +++ b/examples/metadata-examples/review-json-ld.amp.html @@ -12,15 +12,21 @@ diff --git a/examples/metadata-examples/review-microdata.amp.html b/examples/metadata-examples/review-microdata.amp.html index e084f9bbfad3..e338e83e7fa2 100644 --- a/examples/metadata-examples/review-microdata.amp.html +++ b/examples/metadata-examples/review-microdata.amp.html @@ -12,15 +12,21 @@ diff --git a/examples/metadata-examples/sports-article-json-ld.amp.html b/examples/metadata-examples/sports-article-json-ld.amp.html index 385cc48dcc9f..6aeb66b794b3 100644 --- a/examples/metadata-examples/sports-article-json-ld.amp.html +++ b/examples/metadata-examples/sports-article-json-ld.amp.html @@ -9,14 +9,14 @@ Lorem Ipsum - + +

    The Audience is programing

    A programming audience

    @@ -37,6 +43,7 @@ +

    The Audience is programing

    @@ -44,10 +51,10 @@

    The Audience is programing

    - + + + + + O2Player examples + + + + + + + + +

    O2Player

    + + + + + + + + + + + diff --git a/examples/ooyalaplayer.amp.html b/examples/ooyalaplayer.amp.html new file mode 100644 index 000000000000..727323ef84f2 --- /dev/null +++ b/examples/ooyalaplayer.amp.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + +
    + + + + + diff --git a/examples/openx.amp.html b/examples/openx.amp.html index 4e5df4a10963..ef31f6586466 100644 --- a/examples/openx.amp.html +++ b/examples/openx.amp.html @@ -7,6 +7,7 @@ +

    OpenX

    @@ -55,4 +56,4 @@

    Code

    json='{"dfp":{"targeting":{"sport":["rugby","cricket"]},"categoryExclusions":["health"],"tagForChildDirectedTreatment":1}}'> </amp-ad> - \ No newline at end of file + diff --git a/examples/playbuzz.amp.html b/examples/playbuzz.amp.html new file mode 100644 index 000000000000..3af381e6a80a --- /dev/null +++ b/examples/playbuzz.amp.html @@ -0,0 +1,72 @@ + + + + + playbuzz examples + + + + + + + + + + + + +
    + +

    Times.com

    +

    Some Article

    + + +

    orem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus felis arcu, vehicula sed pretium eget, pulvinar eu elit. Ut nibh nulla, pharetra at tristique at, pharetra et mi. Ut elementum quam eu feugiat varius. Vestibulum dui ligula, dapibus at suscipit venenatis, elementum vitae libero. Vivamus efficitur nibh et tortor semper, sit amet interdum augue tempor. Nulla vitae tempus dolor, ac molestie odio. Cras cursus, lectus ac consectetur scelerisque, mi arcu egestas velit, ac ultricies est mi aliquam enim. Aenean ac neque in arcu euismod ornare. Fusce sodales massa ac lectus finibus rutrum. Nunc laoreet nunc id rutrum vehicula. Sed tempor turpis posuere, auctor elit quis, ultricies ligula. Maecenas posuere pulvinar mi, ac congue nulla placerat nec.

    + + + +

    orem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus felis arcu, vehicula sed pretium eget, pulvinar eu elit. Ut nibh nulla, pharetra at tristique at, pharetra et mi. Ut elementum quam eu feugiat varius. Vestibulum dui ligula, dapibus at suscipit venenatis, elementum vitae libero. Vivamus efficitur nibh et tortor semper, sit amet interdum augue tempor. Nulla vitae tempus dolor, ac molestie odio. Cras cursus, lectus ac consectetur scelerisque, mi arcu egestas velit, ac ultricies est mi aliquam enim. Aenean ac neque in arcu euismod ornare. Fusce sodales massa ac lectus finibus rutrum. Nunc laoreet nunc id rutrum vehicula. Sed tempor turpis posuere, auctor elit quis, ultricies ligula. Maecenas posuere pulvinar mi, ac congue nulla placerat nec.

    + + + + +

    orem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus felis arcu, vehicula sed pretium eget, pulvinar eu elit. Ut nibh nulla, pharetra at tristique at, pharetra et mi. Ut elementum quam eu feugiat varius. Vestibulum dui ligula, dapibus at suscipit venenatis, elementum vitae libero. Vivamus efficitur nibh et tortor semper, sit amet interdum augue tempor. Nulla vitae tempus dolor, ac molestie odio. Cras cursus, lectus ac consectetur scelerisque, mi arcu egestas velit, ac ultricies est mi aliquam enim. Aenean ac neque in arcu euismod ornare. Fusce sodales massa ac lectus finibus rutrum. Nunc laoreet nunc id rutrum vehicula. Sed tempor turpis posuere, auctor elit quis, ultricies ligula. Maecenas posuere pulvinar mi, ac congue nulla placerat nec.

    + + + + + +

    orem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus felis arcu, vehicula sed pretium eget, pulvinar eu elit. Ut nibh nulla, pharetra at tristique at, pharetra et mi. Ut elementum quam eu feugiat varius. Vestibulum dui ligula, dapibus at suscipit venenatis, elementum vitae libero. Vivamus efficitur nibh et tortor semper, sit amet interdum augue tempor. Nulla vitae tempus dolor, ac molestie odio. Cras cursus, lectus ac consectetur scelerisque, mi arcu egestas velit, ac ultricies est mi aliquam enim. Aenean ac neque in arcu euismod ornare. Fusce sodales massa ac lectus finibus rutrum. Nunc laoreet nunc id rutrum vehicula. Sed tempor turpis posuere, auctor elit quis, ultricies ligula. Maecenas posuere pulvinar mi, ac congue nulla placerat nec.

    + + +
    + +
    Footer
    + + + diff --git a/examples/pwa/pwa-sw.js b/examples/pwa/pwa-sw.js new file mode 100644 index 000000000000..a71be4cebef9 --- /dev/null +++ b/examples/pwa/pwa-sw.js @@ -0,0 +1,33 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +self.addEventListener('install', event => { +}); + +self.addEventListener('fetch', event => { + // TODO(dvoytenko): use cache, implement one-behind. + if (event.request.url.indexOf('amp.max.html') != -1) { + // Override response with the shell unless the leaf document is explicitly + // requested. + if (event.request.mode === 'navigate') { + event.respondWith(fetch('/pwa')); + // Immediately start downloading the actual resource. + fetch(event.request.url); + } + } +}); diff --git a/examples/pwa/pwa.html b/examples/pwa/pwa.html new file mode 100644 index 000000000000..768e50d2bd6f --- /dev/null +++ b/examples/pwa/pwa.html @@ -0,0 +1,117 @@ + + + + + PWA + + + + + + + + + +
    + PWA Chronicles +
    + + + + + + diff --git a/examples/pwa/pwa.js b/examples/pwa/pwa.js new file mode 100644 index 000000000000..93c3d3e48e68 --- /dev/null +++ b/examples/pwa/pwa.js @@ -0,0 +1,363 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function log(args) { + var var_args = Array.prototype.slice.call(arguments, 0); + var_args.unshift('[SHELL]'); + console/*OK*/.log.apply(console, var_args); +} + + +class Shell { + + constructor(win) { + /** @private @const {!Window} */ + this.win = win; + + /** @private @const {!AmpViewer} */ + this.ampViewer_ = new AmpViewer(win, + win.document.getElementById('doc-container')); + + /** @private {string} */ + this.currentPage_ = win.location.pathname; + + win.addEventListener('popstate', this.handlePopState_.bind(this)); + win.document.documentElement.addEventListener('click', + this.handleNavigate_.bind(this)); + + log('Shell created'); + + if (this.currentPage_ && !isShellUrl(this.currentPage_)) { + this.navigateTo(this.currentPage_); + } else if (this.win.location.hash) { + const hashParams = parseQueryString(this.win.location.hash); + const href = hashParams['href']; + if (href) { + this.currentPage_ = href; + this.navigateTo(href); + } + } + + // Install service worker + this.registerServiceWorker_(); + } + + registerServiceWorker_() { + if ('serviceWorker' in navigator) { + log('Register service worker'); + navigator.serviceWorker.register('/pwa/pwa-sw.js').then(reg => { + log('Service worker registered: ', reg); + }).catch(err => { + log('Service worker registration failed: ', err); + }); + } + } + + unregisterServiceWorker_() { + if ('serviceWorker' in navigator) { + log('Register service worker'); + navigator.serviceWorker.getRegistration('/pwa/pwa-sw.js').then(reg => { + log('Service worker found: ', reg); + reg.unregister(); + log('Service worker unregistered'); + }); + } + } + + /** + */ + handleNavigate_(e) { + if (e.defaultPrevented) { + return false; + } + if (event.button) { + return false; + } + let a = event.target; + while (a) { + if (a.tagName == 'A' && a.href) { + break; + } + a = a.parentElement; + } + if (a) { + const url = new URL(a.href); + if (url.origin == this.win.location.origin && + url.pathname.indexOf('/pwa/') == 0 && + url.pathname.indexOf('amp.max.html') != -1) { + e.preventDefault(); + const newPage = url.pathname; + log('Internal link to: ', newPage); + if (newPage != this.currentPage_) { + this.navigateTo(newPage); + } + } + } + } + + /** + */ + handlePopState_() { + const newPage = this.win.location.pathname; + log('Pop state: ', newPage, this.currentPage_); + if (newPage != this.currentPage_) { + this.navigateTo(newPage); + } + } + + /** + * @param {string} path + * @return {!Promise} + */ + navigateTo(path) { + log('Navigate to: ', path); + const oldPage = this.currentPage_; + this.currentPage_ = path; + + // Update URL. + const push = !isShellUrl(path) && isShellUrl(oldPage); + if (path != this.win.location.pathname) { + if (push) { + this.win.history.pushState(null, '', path); + } else { + this.win.history.replaceState(null, '', path); + } + } + + if (isShellUrl(path)) { + log('Back to shell'); + this.ampViewer_.clear(); + return Promise.resolve(); + } + + // Fetch. + const url = this.resolveUrl_(path); + log('Fetch and render doc:', path, url); + return fetchDocument(url).then(doc => { + log('Fetch complete: ', doc); + this.ampViewer_.show(doc, url); + }); + } + + /** + * @param {string} url + * @return {string} + */ + resolveUrl_(url) { + if (!this.a_) { + this.a_ = this.win.document.createElement('a'); + } + this.a_.href = url; + return this.a_.href; + } +} + + +class AmpViewer { + + constructor(win, container) { + /** @private @const {!Window} */ + this.win = win; + /** @private @const {!Element} */ + this.container = container; + + win.AMP_SHADOW = true; + this.ampReadyPromise_ = new Promise(resolve => { + (window.AMP = window.AMP || []).push(resolve); + }); + this.ampReadyPromise_.then(AMP => { + log('AMP LOADED:', AMP); + }); + + /** @private @const {string} */ + this.baseUrl_ = null; + /** @private @const {?Element} */ + this.host_ = null; + /** @private @const {...} */ + this.amp_ = null; + + // Immediately install amp-shadow.js. + this.installScript_('/dist/amp-shadow.js'); + } + + /** + */ + clear() { + if (this.amp_) { + this.amp_.close(); + this.amp_ = null; + } + this.container.textContent = ''; + } + + /** + * @param {!Document} doc + * @param {string} url + */ + show(doc, url) { + log('Show document:', doc, url); + + // Cleanup the existing document if any. + this.clear(); + + this.baseUrl_ = url; + + this.host_ = this.win.document.createElement('div'); + this.host_.classList.add('amp-doc-host'); + + const hostTemplate = this.win.document.getElementById('amp-slot-template'); + if (hostTemplate) { + this.host_.appendChild(hostTemplate.content.cloneNode(true)); + } + + this.container.appendChild(this.host_); + + this.ampReadyPromise_.then(AMP => { + this.amp_ = AMP.attachShadowDoc(this.host_, doc, url, {}); + this.win.document.title = this.amp_.title || ''; + this.amp_.onMessage(this.onMessage_.bind(this)); + this.amp_.setVisibilityState('visible'); + }); + } + + /** + * @param {string} src + * @param {string=} customElement + * @param {string=} customTemplate + */ + installScript_(src, customElement, customTemplate) { + const doc = this.win.document; + const el = doc.createElement('script'); + el.setAttribute('src', src); + if (customElement) { + el.setAttribute('custom-element', customElement); + } + if (customTemplate) { + el.setAttribute('custom-template', customTemplate); + } + doc.head.appendChild(el); + log('- script added: ', src, el); + } + + /** + * @param {string} url + * @return {string} + */ + resolveUrl_(relativeUrlString) { + return new URL(relativeUrlString, this.baseUrl_).toString(); + } + + /** + * @param {string} url + * @return {string} + */ + getOrigin_(relativeUrlString) { + return new URL(relativeUrlString, this.baseUrl_).origin; + } + + /** + */ + onMessage_(type, data, rsvp) { + log('received message:', type, data, rsvp); + } +} + + +/** + * @param {string} url + * @return {boolean} + */ +function isShellUrl(url) { + return (url == '/pwa' || url == '/pwa/'); +} + + +/** + * @param {string} url + * @return {!Promise} + */ +function fetchDocument(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'document'; + xhr.setRequestHeader('Accept', 'text/html'); + xhr.onreadystatechange = () => { + if (xhr.readyState < /* STATUS_RECEIVED */ 2) { + return; + } + if (xhr.status < 100 || xhr.status > 599) { + xhr.onreadystatechange = null; + reject(new Error(`Unknown HTTP status ${xhr.status}`)); + return; + } + if (xhr.readyState == /* COMPLETE */ 4) { + if (xhr.responseXML) { + resolve(xhr.responseXML); + } else { + reject(new Error(`No xhr.responseXML`)); + } + } + }; + xhr.onerror = () => { + reject(new Error('Network failure')); + }; + xhr.onabort = () => { + reject(new Error('Request aborted')); + }; + xhr.send(); + }); +} + + +/** + * Parses the query string of an URL. This method returns a simple key/value + * map. If there are duplicate keys the latest value is returned. + * @param {string} queryString + * @return {!Object} + */ +function parseQueryString(queryString) { + const params = Object.create(null); + if (!queryString) { + return params; + } + if (queryString.indexOf('?') == 0 || queryString.indexOf('#') == 0) { + queryString = queryString.substr(1); + } + const pairs = queryString.split('&'); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i]; + const eqIndex = pair.indexOf('='); + let name; + let value; + if (eqIndex != -1) { + name = decodeURIComponent(pair.substring(0, eqIndex)).trim(); + value = decodeURIComponent(pair.substring(eqIndex + 1)).trim(); + } else { + name = decodeURIComponent(pair).trim(); + value = ''; + } + if (name) { + params[name] = value; + } + } + return params; +} + + +var shell = new Shell(window); diff --git a/examples/reddit.amp.html b/examples/reddit.amp.html new file mode 100644 index 000000000000..154bc5c6e3e2 --- /dev/null +++ b/examples/reddit.amp.html @@ -0,0 +1,36 @@ + + + + + Reddit examples + + + + + + + + +

    Reddit Embed

    + + + + + + diff --git a/examples/released.amp.html b/examples/released.amp.html index 3bf885c64281..a08dec5f7187 100644 --- a/examples/released.amp.html +++ b/examples/released.amp.html @@ -16,6 +16,7 @@ margin: 8px 0; } + @@ -78,7 +79,7 @@

    Audio

    Video

    + diff --git a/examples/share-tracking-with-url.amp.html b/examples/share-tracking-with-url.amp.html new file mode 100644 index 000000000000..f91128405135 --- /dev/null +++ b/examples/share-tracking-with-url.amp.html @@ -0,0 +1,19 @@ + + + + + Share tracking examples + + + + + + + + + + + + + + diff --git a/examples/share-tracking-without-url.amp.html b/examples/share-tracking-without-url.amp.html new file mode 100644 index 000000000000..9cd0944ffdeb --- /dev/null +++ b/examples/share-tracking-without-url.amp.html @@ -0,0 +1,18 @@ + + + + + Share tracking examples + + + + + + + + + + + + + diff --git a/examples/social-share.amp.html b/examples/social-share.amp.html index a0377ce2646f..9e607cc9c35c 100644 --- a/examples/social-share.amp.html +++ b/examples/social-share.amp.html @@ -21,19 +21,12 @@ .social-box { max-width: 300px; } - amp-social-share[type=baidu] { + amp-social-share[type="baidu"] { background-color: black; color: #fff; font-size: 32px; text-align: center; } - amp-social-share[type=whatsapp] { - background: #25d366; - text-align: center; - color: #0e5829; - font-size: 18px; - padding: 10px; - } @@ -42,6 +35,10 @@

    Social Share

    diff --git a/examples/soundcloud.amp.html b/examples/soundcloud.amp.html index 93b25ede178c..2713d682be8b 100644 --- a/examples/soundcloud.amp.html +++ b/examples/soundcloud.amp.html @@ -7,7 +7,7 @@ - + diff --git a/examples/sticky.ads.0.1.amp.html b/examples/sticky.ads.0.1.amp.html new file mode 100644 index 000000000000..35fcad9592ca --- /dev/null +++ b/examples/sticky.ads.0.1.amp.html @@ -0,0 +1,297 @@ + + + + + Sticky Ad Test + + + + + + + + + + + + +
    + +
    +
    + + +
    +
    +
    + +
    +
    + + +
    +
    +

    Lorem Ipsum

    + +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +

    +
    + +
    + + + +
    +
    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus + luctus nunc ut elit cursus, et imperdiet diam vehicula. + Duis et nisi sed urna blandit bibendum et sit amet erat. + Suspendisse potenti. Curabitur consequat volutpat arcu nec + elementum. Etiam a turpis ac libero varius condimentum. + Maecenas sollicitudin felis aliquam tortor vulputate, + ac posuere velit semper. +

    +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. + Aliquam iaculis tincidunt quam sed maximus. Suspendisse faucibus + ornare sodales. Nullam id dolor vitae arcu consequat ornare a + et lectus. Sed tempus eget enim eget lobortis. + Mauris sem est, accumsan sed tincidunt ut, sagittis vel arcu. + Nullam in libero nisi. +

    + +

    + Sed pharetra semper fringilla. Nulla fringilla, neque eget + varius suscipit, mi turpis congue odio, quis dignissim nisi + nulla at erat. Duis non nibh vel erat vehicula hendrerit eget + vel velit. Donec congue augue magna, nec eleifend dui porttitor + sed. Cras orci quam, dignissim nec elementum ac, bibendum et purus. + Ut elementum mi eget felis ultrices tempus. Maecenas nec sodales + ex. Phasellus ultrices, purus non egestas ullamcorper, felis + lorem ultrices nibh, in tristique mauris justo sed ante. + Nunc commodo purus feugiat metus bibendum consequat. Duis + finibus urna ut ligula auctor, sed vehicula ex aliquam. + Sed sed augue auctor, porta turpis ultrices, cursus diam. + In venenatis aliquet porta. Sed volutpat fermentum quam, + ac molestie nulla porttitor ac. Donec porta risus ut enim + pellentesque, id placerat elit ornare. +

    +

    + Curabitur convallis, urna quis pulvinar feugiat, purus diam + posuere turpis, sit amet tincidunt purus justo et mi. Donec + sapien urna, aliquam ut lacinia quis, varius vitae ex. + Maecenas efficitur iaculis lorem, at imperdiet orci viverra + in. Nullam eu erat eu metus ultrices viverra a sit amet leo. + Pellentesque est felis, pulvinar mollis sollicitudin et, + suscipit eget massa. Nunc bibendum non nunc et consequat. + Quisque auctor est vel leo faucibus, non faucibus magna ultricies. + Vestibulum ante ipsum primis in faucibus orci luctus et ultrices + posuere cubilia Curae; Vestibulum tortor lacus, bibendum et + enim eu, vehicula placerat erat. Nullam gravida rhoncus accumsan. + Integer suscipit iaculis elit nec mollis. Vestibulum eget arcu + nec lectus finibus rutrum vel sed orci. +

    + +
    + + +
    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. +
    +
    +
    + +

    + Cum sociis natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. Nulla et viverra turpis. Fusce + viverra enim eget elit blandit, in finibus enim blandit. Integer + fermentum eleifend felis non posuere. In vulputate et metus at + aliquam. Praesent a varius est. Quisque et tincidunt nisi. + Nam porta urna at turpis lacinia, sit amet mattis eros elementum. + Etiam vel mauris mattis, dignissim tortor in, pulvinar arcu. + In molestie sem elit, tincidunt venenatis tortor aliquet sodales. + Ut elementum velit fermentum felis volutpat sodales in non libero. + Aliquam erat volutpat. +

    + +

    + Morbi at velit vitae eros congue congue venenatis non dui. + Sed lacus sem, feugiat sed elementum sed, maximus sed lacus. + Integer accumsan magna in sagittis pharetra. Class aptent taciti + sociosqu ad litora torquent per conubia nostra, per inceptos + himenaeos. Suspendisse ac nisl efficitur ligula aliquam lacinia + eu in magna. Vestibulum non felis odio. Ut consectetur venenatis + felis aliquet maximus. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. +

    +
    +
    +
    +
    + +
    + +
    + + diff --git a/examples/sticky.ads.amp.html b/examples/sticky.ads.amp.html index 61406a917bf3..7c88bc8ac85e 100644 --- a/examples/sticky.ads.amp.html +++ b/examples/sticky.ads.amp.html @@ -144,16 +144,15 @@ background-color: #f4f4f4; } - .amp-sticky-ad-loaded { - background-color: rgba(255,255,255,1); - } - - + + + +
    - + + +
    -
    - - -
    - - - - - +
    @@ -239,15 +213,6 @@

    Lorem Ipsum

    Nullam in libero nisi.

    -
    - - -
    -

    Sed pharetra semper fringilla. Nulla fringilla, neque eget varius suscipit, mi turpis congue odio, quis dignissim nisi @@ -307,13 +272,6 @@

    Lorem Ipsum

    Aliquam erat volutpat.

    -
    - - -
    -

    Morbi at velit vitae eros congue congue venenatis non dui. Sed lacus sem, feugiat sed elementum sed, maximus sed lacus. diff --git a/examples/user-notification.amp.html b/examples/user-notification.amp.html index 4c763a77bf57..5b9ed75dc9d6 100644 --- a/examples/user-notification.amp.html +++ b/examples/user-notification.amp.html @@ -193,9 +193,10 @@ animation: fadeIn ease-in 1s 1 forwards; } - + + @@ -205,8 +206,8 @@ + data-show-if-href="https://example.com/api/show?timestamp=TIMESTAMP" + data-dismiss-href="https://example.com/api/echo/post"> This site uses cookies to personalize content. Learn more. @@ -215,8 +216,8 @@ + data-show-if-href="https://example.com/api/dont-show" + data-dismiss-href="https://example.com/api/echo/post"> notify 2 Learn more. @@ -233,8 +234,8 @@ layout=nodisplay id="amp-user-notification4" data-persist-dismissal="false" - data-show-if-href="/api/show?timestamp=TIMESTAMP" - data-dismiss-href="/api/echo/post"> + data-show-if-href="https://example.com/api/show?timestamp=TIMESTAMP" + data-dismiss-href="https://example.com/api/echo/post"> This notification should ALWAYS show. Learn more. @@ -244,8 +245,8 @@ layout=nodisplay id="amp-user-notification5" data-persist-dismissal="false" - data-show-if-href="/api/dont-show" - data-dismiss-href="/api/echo/post"> + data-show-if-href="https://example.com/api/dont-show" + data-dismiss-href="https://example.com/api/echo/post"> This notification should NEVER show. Learn more. diff --git a/examples/vega-data/bar.json b/examples/vega-data/bar.json new file mode 100644 index 000000000000..18c0280c9d99 --- /dev/null +++ b/examples/vega-data/bar.json @@ -0,0 +1,61 @@ +{ + "width": 400, + "height": 200, + "padding": {"top": 10, "left": 30, "bottom": 30, "right": 10}, + "data": [ + { + "name": "table", + "values": [ + {"x": 1, "y": 28}, {"x": 2, "y": 55}, + {"x": 3, "y": 43}, {"x": 4, "y": 91}, + {"x": 5, "y": 81}, {"x": 6, "y": 53}, + {"x": 7, "y": 19}, {"x": 8, "y": 87}, + {"x": 9, "y": 52}, {"x": 10, "y": 48}, + {"x": 11, "y": 24}, {"x": 12, "y": 49}, + {"x": 13, "y": 87}, {"x": 14, "y": 66}, + {"x": 15, "y": 17}, {"x": 16, "y": 27}, + {"x": 17, "y": 68}, {"x": 18, "y": 16}, + {"x": 19, "y": 49}, {"x": 20, "y": 15} + ] + } + ], + "scales": [ + { + "name": "x", + "type": "ordinal", + "range": "width", + "domain": {"data": "table", "field": "x"} + }, + { + "name": "y", + "type": "linear", + "range": "height", + "domain": {"data": "table", "field": "y"}, + "nice": true + } + ], + "axes": [ + {"type": "x", "scale": "x"}, + {"type": "y", "scale": "y"} + ], + "marks": [ + { + "type": "rect", + "from": {"data": "table"}, + "properties": { + "enter": { + "x": {"scale": "x", "field": "x"}, + "width": {"scale": "x", "band": true, "offset": -1}, + "y": {"scale": "y", "field": "y"}, + "y2": {"scale": "y", "value": 0} + }, + "update": { + "fill": {"value": "steelblue"} + }, + "hover": { + "fill": {"value": "red"} + } + } + } + ] +} \ No newline at end of file diff --git a/examples/vega-data/map.json b/examples/vega-data/map.json new file mode 100644 index 000000000000..8dcd6f2ef5d4 --- /dev/null +++ b/examples/vega-data/map.json @@ -0,0 +1,32 @@ +{ + "width": 740, + "height": 500, + "padding": 0, + "data": [ + { + "name": "world", + "url": "vega-data/world-110m.json", + "format": {"type": "topojson", "feature": "countries"}, + "transform": [{ + "type": "geopath", + "projection": "winkel3", + "scale": 170, + "translate": [350, 250] + }] + } + ], + "marks": [ + { + "type": "path", + "from": {"data": "world"}, + "properties": { + "enter": { + "stroke": {"value": "#fff"}, + "path": {"field": "layout_path"} + }, + "update": {"fill": {"value": "#ccc"}}, + "hover": {"fill": {"value": "pink"}} + } + } + ] +} \ No newline at end of file diff --git a/examples/vega-data/miserables.json b/examples/vega-data/miserables.json new file mode 100644 index 000000000000..a8b0faaa94c7 --- /dev/null +++ b/examples/vega-data/miserables.json @@ -0,0 +1 @@ +{"nodes":[{"name":"Myriel","group":1,"index":0},{"name":"Napoleon","group":1,"index":1},{"name":"Mlle.Baptistine","group":1,"index":2},{"name":"Mme.Magloire","group":1,"index":3},{"name":"CountessdeLo","group":1,"index":4},{"name":"Geborand","group":1,"index":5},{"name":"Champtercier","group":1,"index":6},{"name":"Cravatte","group":1,"index":7},{"name":"Count","group":1,"index":8},{"name":"OldMan","group":1,"index":9},{"name":"Labarre","group":2,"index":10},{"name":"Valjean","group":2,"index":11},{"name":"Marguerite","group":3,"index":12},{"name":"Mme.deR","group":2,"index":13},{"name":"Isabeau","group":2,"index":14},{"name":"Gervais","group":2,"index":15},{"name":"Tholomyes","group":3,"index":16},{"name":"Listolier","group":3,"index":17},{"name":"Fameuil","group":3,"index":18},{"name":"Blacheville","group":3,"index":19},{"name":"Favourite","group":3,"index":20},{"name":"Dahlia","group":3,"index":21},{"name":"Zephine","group":3,"index":22},{"name":"Fantine","group":3,"index":23},{"name":"Mme.Thenardier","group":4,"index":24},{"name":"Thenardier","group":4,"index":25},{"name":"Cosette","group":5,"index":26},{"name":"Javert","group":4,"index":27},{"name":"Fauchelevent","group":0,"index":28},{"name":"Bamatabois","group":2,"index":29},{"name":"Perpetue","group":3,"index":30},{"name":"Simplice","group":2,"index":31},{"name":"Scaufflaire","group":2,"index":32},{"name":"Woman1","group":2,"index":33},{"name":"Judge","group":2,"index":34},{"name":"Champmathieu","group":2,"index":35},{"name":"Brevet","group":2,"index":36},{"name":"Chenildieu","group":2,"index":37},{"name":"Cochepaille","group":2,"index":38},{"name":"Pontmercy","group":4,"index":39},{"name":"Boulatruelle","group":6,"index":40},{"name":"Eponine","group":4,"index":41},{"name":"Anzelma","group":4,"index":42},{"name":"Woman2","group":5,"index":43},{"name":"MotherInnocent","group":0,"index":44},{"name":"Gribier","group":0,"index":45},{"name":"Jondrette","group":7,"index":46},{"name":"Mme.Burgon","group":7,"index":47},{"name":"Gavroche","group":8,"index":48},{"name":"Gillenormand","group":5,"index":49},{"name":"Magnon","group":5,"index":50},{"name":"Mlle.Gillenormand","group":5,"index":51},{"name":"Mme.Pontmercy","group":5,"index":52},{"name":"Mlle.Vaubois","group":5,"index":53},{"name":"Lt.Gillenormand","group":5,"index":54},{"name":"Marius","group":8,"index":55},{"name":"BaronessT","group":5,"index":56},{"name":"Mabeuf","group":8,"index":57},{"name":"Enjolras","group":8,"index":58},{"name":"Combeferre","group":8,"index":59},{"name":"Prouvaire","group":8,"index":60},{"name":"Feuilly","group":8,"index":61},{"name":"Courfeyrac","group":8,"index":62},{"name":"Bahorel","group":8,"index":63},{"name":"Bossuet","group":8,"index":64},{"name":"Joly","group":8,"index":65},{"name":"Grantaire","group":8,"index":66},{"name":"MotherPlutarch","group":9,"index":67},{"name":"Gueulemer","group":4,"index":68},{"name":"Babet","group":4,"index":69},{"name":"Claquesous","group":4,"index":70},{"name":"Montparnasse","group":4,"index":71},{"name":"Toussaint","group":5,"index":72},{"name":"Child1","group":10,"index":73},{"name":"Child2","group":10,"index":74},{"name":"Brujon","group":4,"index":75},{"name":"Mme.Hucheloup","group":8,"index":76}],"links":[{"source":1,"target":0,"value":1},{"source":2,"target":0,"value":8},{"source":3,"target":0,"value":10},{"source":3,"target":2,"value":6},{"source":4,"target":0,"value":1},{"source":5,"target":0,"value":1},{"source":6,"target":0,"value":1},{"source":7,"target":0,"value":1},{"source":8,"target":0,"value":2},{"source":9,"target":0,"value":1},{"source":11,"target":10,"value":1},{"source":11,"target":3,"value":3},{"source":11,"target":2,"value":3},{"source":11,"target":0,"value":5},{"source":12,"target":11,"value":1},{"source":13,"target":11,"value":1},{"source":14,"target":11,"value":1},{"source":15,"target":11,"value":1},{"source":17,"target":16,"value":4},{"source":18,"target":16,"value":4},{"source":18,"target":17,"value":4},{"source":19,"target":16,"value":4},{"source":19,"target":17,"value":4},{"source":19,"target":18,"value":4},{"source":20,"target":16,"value":3},{"source":20,"target":17,"value":3},{"source":20,"target":18,"value":3},{"source":20,"target":19,"value":4},{"source":21,"target":16,"value":3},{"source":21,"target":17,"value":3},{"source":21,"target":18,"value":3},{"source":21,"target":19,"value":3},{"source":21,"target":20,"value":5},{"source":22,"target":16,"value":3},{"source":22,"target":17,"value":3},{"source":22,"target":18,"value":3},{"source":22,"target":19,"value":3},{"source":22,"target":20,"value":4},{"source":22,"target":21,"value":4},{"source":23,"target":16,"value":3},{"source":23,"target":17,"value":3},{"source":23,"target":18,"value":3},{"source":23,"target":19,"value":3},{"source":23,"target":20,"value":4},{"source":23,"target":21,"value":4},{"source":23,"target":22,"value":4},{"source":23,"target":12,"value":2},{"source":23,"target":11,"value":9},{"source":24,"target":23,"value":2},{"source":24,"target":11,"value":7},{"source":25,"target":24,"value":13},{"source":25,"target":23,"value":1},{"source":25,"target":11,"value":12},{"source":26,"target":24,"value":4},{"source":26,"target":11,"value":31},{"source":26,"target":16,"value":1},{"source":26,"target":25,"value":1},{"source":27,"target":11,"value":17},{"source":27,"target":23,"value":5},{"source":27,"target":25,"value":5},{"source":27,"target":24,"value":1},{"source":27,"target":26,"value":1},{"source":28,"target":11,"value":8},{"source":28,"target":27,"value":1},{"source":29,"target":23,"value":1},{"source":29,"target":27,"value":1},{"source":29,"target":11,"value":2},{"source":30,"target":23,"value":1},{"source":31,"target":30,"value":2},{"source":31,"target":11,"value":3},{"source":31,"target":23,"value":2},{"source":31,"target":27,"value":1},{"source":32,"target":11,"value":1},{"source":33,"target":11,"value":2},{"source":33,"target":27,"value":1},{"source":34,"target":11,"value":3},{"source":34,"target":29,"value":2},{"source":35,"target":11,"value":3},{"source":35,"target":34,"value":3},{"source":35,"target":29,"value":2},{"source":36,"target":34,"value":2},{"source":36,"target":35,"value":2},{"source":36,"target":11,"value":2},{"source":36,"target":29,"value":1},{"source":37,"target":34,"value":2},{"source":37,"target":35,"value":2},{"source":37,"target":36,"value":2},{"source":37,"target":11,"value":2},{"source":37,"target":29,"value":1},{"source":38,"target":34,"value":2},{"source":38,"target":35,"value":2},{"source":38,"target":36,"value":2},{"source":38,"target":37,"value":2},{"source":38,"target":11,"value":2},{"source":38,"target":29,"value":1},{"source":39,"target":25,"value":1},{"source":40,"target":25,"value":1},{"source":41,"target":24,"value":2},{"source":41,"target":25,"value":3},{"source":42,"target":41,"value":2},{"source":42,"target":25,"value":2},{"source":42,"target":24,"value":1},{"source":43,"target":11,"value":3},{"source":43,"target":26,"value":1},{"source":43,"target":27,"value":1},{"source":44,"target":28,"value":3},{"source":44,"target":11,"value":1},{"source":45,"target":28,"value":2},{"source":47,"target":46,"value":1},{"source":48,"target":47,"value":2},{"source":48,"target":25,"value":1},{"source":48,"target":27,"value":1},{"source":48,"target":11,"value":1},{"source":49,"target":26,"value":3},{"source":49,"target":11,"value":2},{"source":50,"target":49,"value":1},{"source":50,"target":24,"value":1},{"source":51,"target":49,"value":9},{"source":51,"target":26,"value":2},{"source":51,"target":11,"value":2},{"source":52,"target":51,"value":1},{"source":52,"target":39,"value":1},{"source":53,"target":51,"value":1},{"source":54,"target":51,"value":2},{"source":54,"target":49,"value":1},{"source":54,"target":26,"value":1},{"source":55,"target":51,"value":6},{"source":55,"target":49,"value":12},{"source":55,"target":39,"value":1},{"source":55,"target":54,"value":1},{"source":55,"target":26,"value":21},{"source":55,"target":11,"value":19},{"source":55,"target":16,"value":1},{"source":55,"target":25,"value":2},{"source":55,"target":41,"value":5},{"source":55,"target":48,"value":4},{"source":56,"target":49,"value":1},{"source":56,"target":55,"value":1},{"source":57,"target":55,"value":1},{"source":57,"target":41,"value":1},{"source":57,"target":48,"value":1},{"source":58,"target":55,"value":7},{"source":58,"target":48,"value":7},{"source":58,"target":27,"value":6},{"source":58,"target":57,"value":1},{"source":58,"target":11,"value":4},{"source":59,"target":58,"value":15},{"source":59,"target":55,"value":5},{"source":59,"target":48,"value":6},{"source":59,"target":57,"value":2},{"source":60,"target":48,"value":1},{"source":60,"target":58,"value":4},{"source":60,"target":59,"value":2},{"source":61,"target":48,"value":2},{"source":61,"target":58,"value":6},{"source":61,"target":60,"value":2},{"source":61,"target":59,"value":5},{"source":61,"target":57,"value":1},{"source":61,"target":55,"value":1},{"source":62,"target":55,"value":9},{"source":62,"target":58,"value":17},{"source":62,"target":59,"value":13},{"source":62,"target":48,"value":7},{"source":62,"target":57,"value":2},{"source":62,"target":41,"value":1},{"source":62,"target":61,"value":6},{"source":62,"target":60,"value":3},{"source":63,"target":59,"value":5},{"source":63,"target":48,"value":5},{"source":63,"target":62,"value":6},{"source":63,"target":57,"value":2},{"source":63,"target":58,"value":4},{"source":63,"target":61,"value":3},{"source":63,"target":60,"value":2},{"source":63,"target":55,"value":1},{"source":64,"target":55,"value":5},{"source":64,"target":62,"value":12},{"source":64,"target":48,"value":5},{"source":64,"target":63,"value":4},{"source":64,"target":58,"value":10},{"source":64,"target":61,"value":6},{"source":64,"target":60,"value":2},{"source":64,"target":59,"value":9},{"source":64,"target":57,"value":1},{"source":64,"target":11,"value":1},{"source":65,"target":63,"value":5},{"source":65,"target":64,"value":7},{"source":65,"target":48,"value":3},{"source":65,"target":62,"value":5},{"source":65,"target":58,"value":5},{"source":65,"target":61,"value":5},{"source":65,"target":60,"value":2},{"source":65,"target":59,"value":5},{"source":65,"target":57,"value":1},{"source":65,"target":55,"value":2},{"source":66,"target":64,"value":3},{"source":66,"target":58,"value":3},{"source":66,"target":59,"value":1},{"source":66,"target":62,"value":2},{"source":66,"target":65,"value":2},{"source":66,"target":48,"value":1},{"source":66,"target":63,"value":1},{"source":66,"target":61,"value":1},{"source":66,"target":60,"value":1},{"source":67,"target":57,"value":3},{"source":68,"target":25,"value":5},{"source":68,"target":11,"value":1},{"source":68,"target":24,"value":1},{"source":68,"target":27,"value":1},{"source":68,"target":48,"value":1},{"source":68,"target":41,"value":1},{"source":69,"target":25,"value":6},{"source":69,"target":68,"value":6},{"source":69,"target":11,"value":1},{"source":69,"target":24,"value":1},{"source":69,"target":27,"value":2},{"source":69,"target":48,"value":1},{"source":69,"target":41,"value":1},{"source":70,"target":25,"value":4},{"source":70,"target":69,"value":4},{"source":70,"target":68,"value":4},{"source":70,"target":11,"value":1},{"source":70,"target":24,"value":1},{"source":70,"target":27,"value":1},{"source":70,"target":41,"value":1},{"source":70,"target":58,"value":1},{"source":71,"target":27,"value":1},{"source":71,"target":69,"value":2},{"source":71,"target":68,"value":2},{"source":71,"target":70,"value":2},{"source":71,"target":11,"value":1},{"source":71,"target":48,"value":1},{"source":71,"target":41,"value":1},{"source":71,"target":25,"value":1},{"source":72,"target":26,"value":2},{"source":72,"target":27,"value":1},{"source":72,"target":11,"value":1},{"source":73,"target":48,"value":2},{"source":74,"target":48,"value":2},{"source":74,"target":73,"value":3},{"source":75,"target":69,"value":3},{"source":75,"target":68,"value":3},{"source":75,"target":25,"value":3},{"source":75,"target":48,"value":1},{"source":75,"target":41,"value":1},{"source":75,"target":70,"value":1},{"source":75,"target":71,"value":1},{"source":76,"target":64,"value":1},{"source":76,"target":65,"value":1},{"source":76,"target":66,"value":1},{"source":76,"target":63,"value":1},{"source":76,"target":62,"value":1},{"source":76,"target":48,"value":1},{"source":76,"target":58,"value":1}]} \ No newline at end of file diff --git a/examples/vega-data/nodes.json b/examples/vega-data/nodes.json new file mode 100644 index 000000000000..c6f6c7613478 --- /dev/null +++ b/examples/vega-data/nodes.json @@ -0,0 +1,110 @@ +{ + "padding": {"top":0, "bottom":0, "left":0, "right":0}, + + "signals": [ + { + "name": "hover", "init": null, + "streams": [ + { "type": "symbol:mouseover", + "expr": "datum._id" }, + { "type": "window:mouseup, symbol:mouseout", "expr": "null" } + ] + }, + { + "name": "grab", "init": null, + "streams": [ + { "type": "symbol:touchstart, symbol:mousedown", + "expr": "datum._id" }, + { "type": "touchend, window:mouseup", "expr": "null" } + ] + }, + { + "name": "active", "init": null, + "streams": [ + { "type": "hover, grab", "expr": "{id: grab || hover}" }, + { + "type": "symbol:touchmove, [symbol:mousedown, window:mouseup] > window:mousemove", + "expr": "{x: eventX(), y: eventY(), id: grab, update: true}" + } + ] + }, + { + "name": "dblclick", "init": null, "verbose": true, + "streams": [ + { "type": "symbol:dblclick", "expr": "datum._id" } + ] + } + ], + + "data": [ + { + "name": "fixed", + "modify": [ + {"type": "toggle", "signal": "dblclick", "field": "id"} + ] + }, + { + "name": "edges", + "url": "vega-data/miserables.json", + "format": {"type": "json", "property": "links"} + }, + { + "name": "nodes", + "url": "vega-data/miserables.json", + "format": {"type": "json", "property": "nodes"}, + "transform": [ + { + "type": "force", + "links": "edges", + "linkDistance": 30, + "linkStrength": 0.5, + "charge": -80, + "interactive": true, + "fixed": "fixed", + "active": {"signal": "active"} + } + ] + } + ], + + "marks": [ + { + "type": "path", + "from": { + "data": "edges", + "transform": [ + { "type": "lookup", "on": "nodes", + "keys": ["source", "target"], + "as": ["_source", "_target"] }, + { "type": "linkpath", "shape": "line" } + ] + }, + "properties": { + "update": { + "path": {"field": "layout_path"}, + "stroke": {"value": "#ccc"}, + "strokeWidth": {"value": 0.5} + } + } + }, + { + "type": "symbol", + "from": {"data": "nodes"}, + "properties": { + "enter": { + "fillOpacity": {"value": 0.3}, + "fill": {"value": "steelblue"} + }, + "update": { + "x": {"field": "layout_x"}, + "y": {"field": "layout_y"}, + "stroke": [ + { "test": "indata('fixed', datum._id, 'id')", + "value": "firebrick" }, + { "value": "steelblue" } + ] + } + } + } + ] +} diff --git a/examples/vega-data/unemployment.tsv b/examples/vega-data/unemployment.tsv new file mode 100644 index 000000000000..d1aca19c4821 --- /dev/null +++ b/examples/vega-data/unemployment.tsv @@ -0,0 +1,3219 @@ +id rate +1001 .097 +1003 .091 +1005 .134 +1007 .121 +1009 .099 +1011 .164 +1013 .167 +1015 .108 +1017 .186 +1019 .118 +1021 .099 +1023 .127 +1025 .17 +1027 .159 +1029 .104 +1031 .085 +1033 .114 +1035 .195 +1037 .14 +1039 .101 +1041 .097 +1043 .096 +1045 .093 +1047 .211 +1049 .143 +1051 .09 +1053 .129 +1055 .107 +1057 .128 +1059 .123 +1061 .1 +1063 .147 +1065 .127 +1067 .099 +1069 .089 +1071 .118 +1073 .107 +1075 .148 +1077 .105 +1079 .136 +1081 .086 +1083 .093 +1085 .185 +1087 .114 +1089 .075 +1091 .148 +1093 .152 +1095 .092 +1097 .111 +1099 .187 +1101 .102 +1103 .104 +1105 .198 +1107 .13 +1109 .087 +1111 .151 +1113 .126 +1115 .107 +1117 .076 +1119 .139 +1121 .136 +1123 .137 +1125 .09 +1127 .119 +1129 .151 +1131 .256 +1133 .175 +2013 .101 +2016 .084 +2020 .07 +2050 .148 +2060 .036 +2068 .034 +2070 .084 +2090 .069 +2100 .062 +2110 .057 +2122 .097 +2130 .061 +2150 .066 +2164 .059 +2170 .088 +2180 .121 +2185 .057 +2188 .132 +2201 .136 +2220 .059 +2232 .073 +2240 .081 +2261 .064 +2270 .204 +2280 .098 +2282 .063 +2290 .136 +4001 .148 +4003 .074 +4005 .077 +4007 .109 +4009 .144 +4011 .215 +4012 .089 +4013 .085 +4015 .102 +4017 .142 +4019 .084 +4021 .118 +4023 .172 +4025 .095 +4027 .242 +5001 .143 +5003 .091 +5005 .082 +5007 .053 +5009 .064 +5011 .078 +5013 .062 +5015 .043 +5017 .096 +5019 .063 +5021 .104 +5023 .059 +5025 .06 +5027 .081 +5029 .065 +5031 .059 +5033 .066 +5035 .099 +5037 .071 +5039 .076 +5041 .099 +5043 .091 +5045 .06 +5047 .059 +5049 .061 +5051 .066 +5053 .057 +5055 .082 +5057 .086 +5059 .073 +5061 .072 +5063 .078 +5065 .077 +5067 .092 +5069 .092 +5071 .061 +5073 .086 +5075 .079 +5077 .082 +5079 .082 +5081 .056 +5083 .077 +5085 .052 +5087 .053 +5089 .112 +5091 .045 +5093 .113 +5095 .074 +5097 .058 +5099 .087 +5101 .062 +5103 .071 +5105 .056 +5107 .088 +5109 .063 +5111 .075 +5113 .064 +5115 .062 +5117 .067 +5119 .06 +5121 .073 +5123 .094 +5125 .058 +5127 .063 +5129 .059 +5131 .062 +5133 .051 +5135 .078 +5137 .066 +5139 .095 +5141 .091 +5143 .05 +5145 .067 +5147 .091 +5149 .064 +6001 .113 +6003 .152 +6005 .121 +6007 .122 +6009 .143 +6011 .145 +6013 .112 +6015 .119 +6017 .112 +6019 .141 +6021 .138 +6023 .103 +6025 .301 +6027 .095 +6029 .139 +6031 .139 +6033 .147 +6035 .118 +6037 .127 +6039 .123 +6041 .08 +6043 .088 +6045 .101 +6047 .157 +6049 .111 +6051 .103 +6053 .1 +6055 .087 +6057 .109 +6059 .094 +6061 .113 +6063 .139 +6065 .147 +6067 .122 +6069 .125 +6071 .136 +6073 .102 +6075 .097 +6077 .155 +6079 .09 +6081 .09 +6083 .085 +6085 .118 +6087 .102 +6089 .147 +6091 .137 +6093 .135 +6095 .115 +6097 .099 +6099 .153 +6101 .151 +6103 .137 +6105 .159 +6107 .149 +6109 .127 +6111 .11 +6113 .109 +6115 .178 +8001 .081 +8003 .055 +8005 .069 +8007 .062 +8009 .034 +8011 .05 +8013 .055 +8014 .066 +8015 .051 +8017 .02 +8019 .07 +8021 .051 +8023 .084 +8025 .076 +8027 .052 +8029 .066 +8031 .077 +8033 .132 +8035 .059 +8037 .062 +8039 .06 +8041 .072 +8043 .077 +8045 .058 +8047 .06 +8049 .058 +8051 .044 +8053 .022 +8055 .072 +8057 .033 +8059 .067 +8061 .028 +8063 .029 +8065 .071 +8067 .047 +8069 .056 +8071 .07 +8073 .037 +8075 .046 +8077 .082 +8079 .053 +8081 .056 +8083 .063 +8085 .069 +8087 .049 +8089 .058 +8091 .041 +8093 .061 +8095 .026 +8097 .05 +8099 .05 +8101 .075 +8103 .039 +8105 .048 +8107 .06 +8109 .078 +8111 .053 +8113 .042 +8115 .033 +8117 .058 +8119 .067 +8121 .032 +8123 .075 +8125 .026 +9001 .078 +9003 .088 +9005 .079 +9007 .067 +9009 .089 +9011 .076 +9013 .067 +9015 .09 +10001 .079 +10003 .086 +10005 .073 +11001 .117 +12001 .071 +12003 .114 +12005 .089 +12007 .083 +12009 .111 +12011 .098 +12013 .082 +12015 .127 +12017 .121 +12019 .098 +12021 .131 +12023 .09 +12027 .117 +12029 .123 +12031 .112 +12033 .098 +12035 .162 +12037 .071 +12039 .096 +12041 .1 +12043 .1 +12045 .098 +12047 .113 +12049 .126 +12051 .168 +12053 .138 +12055 .116 +12057 .115 +12059 .072 +12061 .152 +12063 .072 +12065 .085 +12067 .072 +12069 .123 +12071 .139 +12073 .072 +12075 .121 +12077 .053 +12079 .117 +12081 .127 +12083 .133 +12085 .119 +12086 .113 +12087 .07 +12089 .107 +12091 .072 +12093 .133 +12095 .114 +12097 .128 +12099 .117 +12101 .125 +12103 .112 +12105 .127 +12107 .122 +12109 .09 +12111 .153 +12113 .094 +12115 .123 +12117 .106 +12119 .09 +12121 .098 +12123 .104 +12125 .084 +12127 .117 +12129 .072 +12131 .068 +12133 .096 +13001 .097 +13003 .126 +13005 .085 +13007 .091 +13009 .116 +13011 .067 +13013 .111 +13015 .133 +13017 .153 +13019 .124 +13021 .1 +13023 .097 +13025 .116 +13027 .087 +13029 .081 +13031 .09 +13033 .122 +13035 .127 +13037 .111 +13039 .091 +13043 .093 +13045 .108 +13047 .08 +13049 .106 +13051 .085 +13053 .146 +13055 .114 +13057 .095 +13059 .07 +13061 .082 +13063 .123 +13065 .11 +13067 .096 +13069 .168 +13071 .09 +13073 .07 +13075 .119 +13077 .1 +13079 .095 +13081 .121 +13083 .09 +13085 .101 +13087 .127 +13089 .107 +13091 .112 +13093 .101 +13095 .111 +13097 .114 +13099 .105 +13101 .067 +13103 .079 +13105 .122 +13107 .103 +13109 .089 +13111 .1 +13113 .084 +13115 .111 +13117 .086 +13119 .118 +13121 .107 +13123 .098 +13125 .111 +13127 .082 +13129 .131 +13131 .098 +13133 .111 +13135 .094 +13137 .094 +13139 .091 +13141 .177 +13143 .113 +13145 .072 +13147 .138 +13149 .124 +13151 .104 +13153 .073 +13155 .145 +13157 .109 +13159 .127 +13161 .135 +13163 .149 +13165 .196 +13167 .119 +13169 .086 +13171 .153 +13173 .084 +13175 .108 +13177 .079 +13179 .085 +13181 .103 +13183 .059 +13185 .082 +13187 .111 +13189 .121 +13191 .096 +13193 .13 +13195 .083 +13197 .099 +13199 .132 +13201 .087 +13205 .104 +13207 .102 +13209 .085 +13211 .098 +13213 .123 +13215 .092 +13217 .126 +13219 .062 +13221 .082 +13223 .107 +13225 .102 +13227 .108 +13229 .093 +13231 .118 +13233 .108 +13235 .071 +13237 .104 +13239 .118 +13241 .104 +13243 .13 +13245 .103 +13247 .12 +13249 .138 +13251 .137 +13253 .107 +13255 .154 +13257 .107 +13259 .107 +13261 .129 +13263 .098 +13265 .124 +13267 .087 +13269 .13 +13271 .162 +13273 .116 +13275 .093 +13277 .106 +13279 .098 +13281 .078 +13283 .119 +13285 .129 +13287 .13 +13289 .114 +13291 .09 +13293 .133 +13295 .096 +13297 .11 +13299 .107 +13301 .188 +13303 .14 +13305 .117 +13307 .091 +13309 .091 +13311 .095 +13313 .125 +13315 .116 +13317 .117 +13319 .107 +13321 .108 +15001 .108 +15003 .063 +15007 .096 +15009 .097 +16001 .091 +16003 .121 +16005 .084 +16007 .052 +16009 .121 +16011 .059 +16013 .077 +16015 .071 +16017 .095 +16019 .059 +16021 .12 +16023 .045 +16025 .11 +16027 .106 +16029 .06 +16031 .059 +16033 .041 +16035 .118 +16037 .039 +16039 .077 +16041 .04 +16043 .066 +16045 .104 +16047 .055 +16049 .083 +16051 .068 +16053 .059 +16055 .087 +16057 .059 +16059 .065 +16061 .052 +16063 .097 +16065 .055 +16067 .06 +16069 .055 +16071 .05 +16073 .041 +16075 .077 +16077 .067 +16079 .119 +16081 .051 +16083 .068 +16085 .114 +16087 .084 +17001 .079 +17003 .112 +17005 .093 +17007 .138 +17009 .045 +17011 .109 +17013 .095 +17015 .105 +17017 .073 +17019 .082 +17021 .099 +17023 .131 +17025 .114 +17027 .079 +17029 .091 +17031 .106 +17033 .105 +17035 .097 +17037 .092 +17039 .092 +17041 .091 +17043 .086 +17045 .107 +17047 .091 +17049 .079 +17051 .116 +17053 .104 +17055 .146 +17057 .125 +17059 .11 +17061 .092 +17063 .114 +17065 .093 +17067 .113 +17069 .128 +17071 .094 +17073 .086 +17075 .1 +17077 .073 +17079 .095 +17081 .1 +17083 .088 +17085 .081 +17087 .104 +17089 .099 +17091 .128 +17093 .104 +17095 .103 +17097 .1 +17099 .124 +17101 .103 +17103 .109 +17105 .107 +17107 .095 +17109 .077 +17111 .093 +17113 .074 +17115 .124 +17117 .105 +17119 .097 +17121 .119 +17123 .106 +17125 .143 +17127 .08 +17129 .078 +17131 .089 +17133 .078 +17135 .122 +17137 .084 +17139 .09 +17141 .119 +17143 .116 +17145 .119 +17147 .082 +17149 .081 +17151 .106 +17153 .119 +17155 .15 +17157 .094 +17159 .105 +17161 .095 +17163 .108 +17165 .112 +17167 .079 +17169 .066 +17171 .075 +17173 .1 +17175 .098 +17177 .116 +17179 .113 +17181 .108 +17183 .12 +17185 .103 +17187 .08 +17189 .08 +17191 .097 +17193 .086 +17195 .106 +17197 .099 +17199 .096 +17201 .155 +17203 .086 +18001 .134 +18003 .093 +18005 .088 +18007 .09 +18009 .133 +18011 .068 +18013 .078 +18015 .099 +18017 .105 +18019 .082 +18021 .096 +18023 .092 +18025 .105 +18027 .049 +18029 .089 +18031 .11 +18033 .124 +18035 .095 +18037 .08 +18039 .15 +18041 .134 +18043 .074 +18045 .116 +18047 .089 +18049 .112 +18051 .067 +18053 .111 +18055 .071 +18057 .061 +18059 .078 +18061 .078 +18063 .068 +18065 .12 +18067 .119 +18069 .111 +18071 .101 +18073 .087 +18075 .104 +18077 .095 +18079 .122 +18081 .074 +18083 .066 +18085 .11 +18087 .14 +18089 .094 +18091 .107 +18093 .111 +18095 .097 +18097 .084 +18099 .114 +18101 .063 +18103 .123 +18105 .056 +18107 .097 +18109 .075 +18111 .089 +18113 .145 +18115 .105 +18117 .092 +18119 .081 +18121 .083 +18123 .088 +18125 .087 +18127 .082 +18129 .07 +18131 .095 +18133 .082 +18135 .102 +18137 .091 +18139 .095 +18141 .104 +18143 .121 +18145 .088 +18147 .081 +18149 .126 +18151 .129 +18153 .093 +18155 .07 +18157 .085 +18159 .103 +18161 .086 +18163 .074 +18165 .108 +18167 .092 +18169 .12 +18171 .101 +18173 .069 +18175 .112 +18177 .109 +18179 .091 +18181 .094 +18183 .116 +19001 .051 +19003 .06 +19005 .093 +19007 .094 +19009 .051 +19011 .058 +19013 .058 +19015 .059 +19017 .05 +19019 .059 +19021 .055 +19023 .06 +19025 .054 +19027 .042 +19029 .058 +19031 .057 +19033 .067 +19035 .044 +19037 .084 +19039 .074 +19041 .062 +19043 .079 +19045 .077 +19047 .047 +19049 .051 +19051 .101 +19053 .066 +19055 .068 +19057 .082 +19059 .058 +19061 .06 +19063 .086 +19065 .085 +19067 .087 +19069 .07 +19071 .07 +19073 .063 +19075 .049 +19077 .056 +19079 .081 +19081 .09 +19083 .06 +19085 .044 +19087 .088 +19089 .086 +19091 .062 +19093 .058 +19095 .058 +19097 .077 +19099 .081 +19101 .087 +19103 .044 +19105 .063 +19107 .071 +19109 .056 +19111 .114 +19113 .065 +19115 .08 +19117 .06 +19119 .043 +19121 .058 +19123 .077 +19125 .057 +19127 .07 +19129 .043 +19131 .054 +19133 .071 +19135 .075 +19137 .087 +19139 .089 +19141 .053 +19143 .054 +19145 .087 +19147 .07 +19149 .047 +19151 .052 +19153 .062 +19155 .048 +19157 .066 +19159 .053 +19161 .047 +19163 .073 +19165 .039 +19167 .041 +19169 .045 +19171 .066 +19173 .067 +19175 .06 +19177 .08 +19179 .094 +19181 .058 +19183 .049 +19185 .064 +19187 .083 +19189 .091 +19191 .058 +19193 .057 +19195 .063 +19197 .084 +20001 .078 +20003 .079 +20005 .081 +20007 .051 +20009 .061 +20011 .065 +20013 .056 +20015 .072 +20017 .054 +20019 .084 +20021 .085 +20023 .037 +20025 .037 +20027 .042 +20029 .045 +20031 .057 +20033 .038 +20035 .076 +20037 .081 +20039 .033 +20041 .051 +20043 .088 +20045 .054 +20047 .044 +20049 .11 +20051 .036 +20053 .041 +20055 .043 +20057 .038 +20059 .071 +20061 .069 +20063 .035 +20065 .043 +20067 .042 +20069 .034 +20071 .045 +20073 .072 +20075 .042 +20077 .056 +20079 .074 +20081 .037 +20083 .042 +20085 .06 +20087 .067 +20089 .049 +20091 .068 +20093 .045 +20095 .063 +20097 .049 +20099 .078 +20101 .034 +20103 .073 +20105 .063 +20107 .084 +20109 .04 +20111 .057 +20113 .051 +20115 .063 +20117 .049 +20119 .042 +20121 .068 +20123 .058 +20125 .094 +20127 .068 +20129 .047 +20131 .043 +20133 .07 +20135 .04 +20137 .047 +20139 .07 +20141 .044 +20143 .065 +20145 .037 +20147 .06 +20149 .041 +20151 .053 +20153 .035 +20155 .063 +20157 .041 +20159 .051 +20161 .032 +20163 .073 +20165 .059 +20167 .046 +20169 .057 +20171 .032 +20173 .088 +20175 .051 +20177 .064 +20179 .032 +20181 .039 +20183 .043 +20185 .061 +20187 .034 +20189 .053 +20191 .09 +20193 .036 +20195 .036 +20197 .066 +20199 .059 +20201 .045 +20203 .035 +20205 .102 +20207 .089 +20209 .104 +21001 .101 +21003 .143 +21005 .109 +21007 .102 +21009 .116 +21011 .133 +21013 .125 +21015 .092 +21017 .091 +21019 .083 +21021 .11 +21023 .102 +21025 .1 +21027 .121 +21029 .108 +21031 .137 +21033 .109 +21035 .08 +21037 .107 +21039 .091 +21041 .124 +21043 .135 +21045 .102 +21047 .129 +21049 .114 +21051 .135 +21053 .09 +21055 .108 +21057 .13 +21059 .091 +21061 .128 +21063 .126 +21065 .129 +21067 .077 +21069 .122 +21071 .124 +21073 .091 +21075 .144 +21077 .111 +21079 .119 +21081 .101 +21083 .103 +21085 .164 +21087 .124 +21089 .098 +21091 .136 +21093 .1 +21095 .125 +21097 .111 +21099 .106 +21101 .098 +21103 .107 +21105 .092 +21107 .091 +21109 .175 +21111 .105 +21113 .088 +21115 .111 +21117 .1 +21119 .12 +21121 .115 +21123 .107 +21125 .101 +21127 .131 +21129 .123 +21131 .137 +21133 .12 +21135 .151 +21137 .121 +21139 .098 +21141 .104 +21143 .122 +21145 .091 +21147 .133 +21149 .106 +21151 .087 +21153 .214 +21155 .129 +21157 .107 +21159 .124 +21161 .111 +21163 .124 +21165 .147 +21167 .108 +21169 .158 +21171 .14 +21173 .126 +21175 .145 +21177 .107 +21179 .115 +21181 .125 +21183 .094 +21185 .086 +21187 .101 +21189 .112 +21191 .117 +21193 .125 +21195 .107 +21197 .167 +21199 .1 +21201 .096 +21203 .128 +21205 .086 +21207 .108 +21209 .091 +21211 .096 +21213 .118 +21215 .102 +21217 .102 +21219 .126 +21221 .16 +21223 .107 +21225 .108 +21227 .092 +21229 .127 +21231 .136 +21233 .088 +21235 .114 +21237 .139 +21239 .083 +22001 .071 +22003 .101 +22005 .067 +22007 .088 +22009 .081 +22011 .08 +22013 .105 +22015 .064 +22017 .079 +22019 .072 +22021 .107 +22023 .056 +22025 .103 +22027 .103 +22029 .113 +22031 .09 +22033 .068 +22035 .142 +22037 .08 +22039 .086 +22041 .113 +22043 .082 +22045 .076 +22047 .104 +22049 .078 +22051 .065 +22053 .064 +22055 .059 +22057 .049 +22059 .073 +22061 .078 +22063 .07 +22065 .093 +22067 .151 +22069 .084 +22071 .107 +22073 .076 +22075 .062 +22077 .079 +22079 .069 +22081 .096 +22083 .107 +22085 .081 +22087 .105 +22089 .065 +22091 .118 +22093 .1 +22095 .084 +22097 .083 +22099 .075 +22101 .086 +22103 .052 +22105 .081 +22107 .135 +22109 .052 +22111 .122 +22113 .072 +22115 .067 +22117 .096 +22119 .091 +22121 .074 +22123 .17 +22125 .082 +22127 .091 +23001 .084 +23003 .093 +23005 .065 +23007 .112 +23009 .068 +23011 .073 +23013 .07 +23015 .063 +23017 .109 +23019 .08 +23021 .116 +23023 .067 +23025 .107 +23027 .078 +23029 .104 +23031 .074 +24001 .075 +24003 .065 +24005 .077 +24009 .059 +24011 .088 +24013 .06 +24015 .086 +24017 .059 +24019 .109 +24021 .059 +24023 .069 +24025 .071 +24027 .054 +24029 .071 +24031 .053 +24033 .073 +24035 .066 +24037 .056 +24039 .095 +24041 .068 +24043 .094 +24045 .077 +24047 .075 +24510 .106 +25001 .08 +25003 .084 +25005 .118 +25007 .05 +25009 .101 +25011 .087 +25013 .105 +25015 .073 +25017 .081 +25019 .05 +25021 .085 +25023 .097 +25025 .093 +25027 .101 +26001 .167 +26003 .12 +26005 .132 +26007 .139 +26009 .147 +26011 .158 +26013 .243 +26015 .105 +26017 .123 +26019 .12 +26021 .135 +26023 .14 +26025 .124 +26027 .114 +26029 .139 +26031 .086 +26033 .116 +26035 .168 +26037 .094 +26039 .137 +26041 .115 +26043 .12 +26045 .103 +26047 .122 +26049 .158 +26051 .165 +26053 .133 +26055 .12 +26057 .137 +26059 .172 +26061 .102 +26063 .148 +26065 .116 +26067 .134 +26069 .164 +26071 .117 +26073 .085 +26075 .149 +26077 .112 +26079 .143 +26081 .117 +26083 .119 +26085 .175 +26087 .181 +26089 .09 +26091 .161 +26093 .136 +26095 .132 +26097 .061 +26099 .181 +26101 .126 +26103 .101 +26105 .129 +26107 .13 +26109 .125 +26111 .102 +26113 .15 +26115 .142 +26117 .176 +26119 .189 +26121 .16 +26123 .138 +26125 .156 +26127 .152 +26129 .127 +26131 .138 +26133 .153 +26135 .191 +26137 .147 +26139 .128 +26141 .16 +26143 .146 +26145 .129 +26147 .19 +26149 .139 +26151 .178 +26153 .127 +26155 .152 +26157 .163 +26159 .132 +26161 .093 +26163 .183 +26165 .183 +27001 .081 +27003 .079 +27005 .064 +27007 .072 +27009 .07 +27011 .045 +27013 .063 +27015 .055 +27017 .073 +27019 .07 +27021 .079 +27023 .061 +27025 .084 +27027 .036 +27029 .091 +27031 .047 +27033 .052 +27035 .074 +27037 .069 +27039 .061 +27041 .056 +27043 .076 +27045 .07 +27047 .076 +27049 .067 +27051 .068 +27053 .073 +27055 .071 +27057 .073 +27059 .083 +27061 .092 +27063 .047 +27065 .103 +27067 .055 +27069 .052 +27071 .073 +27073 .054 +27075 .075 +27077 .069 +27079 .086 +27081 .045 +27083 .049 +27085 .08 +27087 .06 +27089 .062 +27091 .069 +27093 .078 +27095 .105 +27097 .09 +27099 .055 +27101 .039 +27103 .061 +27105 .045 +27107 .051 +27109 .055 +27111 .063 +27113 .056 +27115 .091 +27117 .053 +27119 .044 +27121 .064 +27123 .074 +27125 .067 +27127 .061 +27129 .064 +27131 .075 +27133 .051 +27135 .05 +27137 .082 +27139 .068 +27141 .081 +27143 .065 +27145 .068 +27147 .077 +27149 .043 +27151 .066 +27153 .075 +27155 .062 +27157 .071 +27159 .088 +27161 .07 +27163 .068 +27165 .075 +27167 .047 +27169 .07 +27171 .082 +27173 .051 +28001 .08 +28003 .113 +28005 .086 +28007 .118 +28009 .126 +28011 .09 +28013 .097 +28015 .098 +28017 .132 +28019 .113 +28021 .15 +28023 .1 +28025 .178 +28027 .111 +28029 .099 +28031 .077 +28033 .072 +28035 .077 +28037 .09 +28039 .1 +28041 .099 +28043 .11 +28045 .075 +28047 .072 +28049 .079 +28051 .166 +28053 .102 +28055 .092 +28057 .096 +28059 .08 +28061 .102 +28063 .155 +28065 .106 +28067 .071 +28069 .103 +28071 .069 +28073 .066 +28075 .087 +28077 .102 +28079 .087 +28081 .098 +28083 .116 +28085 .088 +28087 .102 +28089 .063 +28091 .101 +28093 .112 +28095 .137 +28097 .127 +28099 .084 +28101 .079 +28103 .176 +28105 .08 +28107 .119 +28109 .083 +28111 .104 +28113 .098 +28115 .087 +28117 .115 +28119 .111 +28121 .056 +28123 .059 +28125 .09 +28127 .082 +28129 .082 +28131 .072 +28133 .118 +28135 .099 +28137 .103 +28139 .125 +28141 .101 +28143 .13 +28145 .088 +28147 .103 +28149 .091 +28151 .113 +28153 .094 +28155 .135 +28157 .101 +28159 .151 +28161 .129 +28163 .105 +29001 .065 +29003 .076 +29005 .079 +29007 .092 +29009 .081 +29011 .117 +29013 .108 +29015 .093 +29017 .088 +29019 .062 +29021 .087 +29023 .08 +29025 .086 +29027 .08 +29029 .079 +29031 .069 +29033 .098 +29035 .087 +29037 .097 +29039 .086 +29041 .105 +29043 .08 +29045 .113 +29047 .086 +29049 .087 +29051 .067 +29053 .084 +29055 .107 +29057 .101 +29059 .094 +29061 .087 +29063 .093 +29065 .094 +29067 .098 +29069 .111 +29071 .115 +29073 .108 +29075 .068 +29077 .083 +29079 .069 +29081 .078 +29083 .096 +29085 .123 +29087 .071 +29089 .083 +29091 .092 +29093 .087 +29095 .109 +29097 .084 +29099 .106 +29101 .08 +29103 .059 +29105 .11 +29107 .09 +29109 .078 +29111 .083 +29113 .113 +29115 .103 +29117 .078 +29119 .07 +29121 .079 +29123 .098 +29125 .087 +29127 .089 +29129 .079 +29131 .106 +29133 .097 +29135 .078 +29137 .11 +29139 .103 +29141 .113 +29143 .089 +29145 .079 +29147 .059 +29149 .09 +29151 .099 +29153 .076 +29155 .121 +29157 .072 +29159 .081 +29161 .071 +29163 .083 +29165 .08 +29167 .089 +29169 .068 +29171 .074 +29173 .08 +29175 .094 +29177 .083 +29179 .127 +29181 .095 +29183 .087 +29185 .103 +29186 .093 +29187 .109 +29189 .096 +29195 .079 +29197 .078 +29199 .122 +29201 .092 +29203 .096 +29205 .081 +29207 .09 +29209 .09 +29211 .064 +29213 .086 +29215 .088 +29217 .079 +29219 .112 +29221 .137 +29223 .095 +29225 .09 +29227 .084 +29229 .096 +29510 .111 +30001 .044 +30003 .102 +30005 .05 +30007 .062 +30009 .048 +30011 .039 +30013 .052 +30015 .042 +30017 .046 +30019 .042 +30021 .048 +30023 .065 +30025 .037 +30027 .047 +30029 .088 +30031 .055 +30033 .034 +30035 .072 +30037 .047 +30039 .082 +30041 .051 +30043 .058 +30045 .046 +30047 .083 +30049 .043 +30051 .047 +30053 .111 +30055 .032 +30057 .048 +30059 .078 +30061 .086 +30063 .056 +30065 .075 +30067 .061 +30069 .045 +30071 .048 +30073 .06 +30075 .04 +30077 .073 +30079 .031 +30081 .075 +30083 .046 +30085 .075 +30087 .073 +30089 .112 +30091 .043 +30093 .055 +30095 .052 +30097 .031 +30099 .04 +30101 .041 +30103 .051 +30105 .046 +30107 .043 +30109 .041 +30111 .047 +31001 .056 +31003 .036 +31005 .044 +31007 .035 +31009 .052 +31011 .039 +31013 .071 +31015 .036 +31017 .031 +31019 .035 +31021 .057 +31023 .039 +31025 .046 +31027 .032 +31029 .03 +31031 .025 +31033 .041 +31035 .043 +31037 .04 +31039 .035 +31041 .031 +31043 .055 +31045 .044 +31047 .048 +31049 .038 +31051 .037 +31053 .049 +31055 .05 +31057 .04 +31059 .044 +31061 .037 +31063 .03 +31065 .041 +31067 .059 +31069 .044 +31071 .025 +31073 .038 +31075 .031 +31077 .033 +31079 .044 +31081 .034 +31083 .036 +31085 .042 +31087 .066 +31089 .032 +31091 .04 +31093 .038 +31095 .046 +31097 .039 +31099 .038 +31101 .039 +31103 .044 +31105 .043 +31107 .036 +31109 .042 +31111 .04 +31113 .043 +31115 .032 +31117 .04 +31119 .042 +31121 .04 +31123 .041 +31125 .036 +31127 .054 +31129 .047 +31131 .05 +31133 .041 +31135 .031 +31137 .038 +31139 .036 +31141 .044 +31143 .042 +31145 .042 +31147 .057 +31149 .031 +31151 .04 +31153 .047 +31155 .045 +31157 .048 +31159 .037 +31161 .035 +31163 .03 +31165 .038 +31167 .036 +31169 .043 +31171 .038 +31173 .138 +31175 .029 +31177 .041 +31179 .038 +31181 .042 +31183 .032 +31185 .057 +32001 .099 +32003 .139 +32005 .126 +32007 .068 +32009 .083 +32011 .093 +32013 .086 +32015 .072 +32017 .094 +32019 .163 +32021 .105 +32023 .161 +32027 .106 +32029 .148 +32031 .131 +32033 .084 +32510 .128 +33001 .07 +33003 .054 +33005 .065 +33007 .078 +33009 .056 +33011 .075 +33013 .063 +33015 .075 +33017 .066 +33019 .065 +34001 .122 +34003 .084 +34005 .091 +34007 .109 +34009 .085 +34011 .126 +34013 .111 +34015 .1 +34017 .116 +34019 .069 +34021 .081 +34023 .092 +34025 .087 +34027 .076 +34029 .096 +34031 .117 +34033 .101 +34035 .079 +34037 .085 +34039 .098 +34041 .088 +35001 .075 +35003 .081 +35005 .068 +35006 .063 +35007 .08 +35009 .044 +35011 .043 +35013 .069 +35015 .06 +35017 .123 +35019 .075 +35021 .045 +35023 .076 +35025 .083 +35027 .051 +35028 .029 +35029 .134 +35031 .086 +35033 .131 +35035 .068 +35037 .059 +35039 .072 +35041 .044 +35043 .09 +35045 .083 +35047 .078 +35049 .065 +35051 .048 +35053 .05 +35055 .087 +35057 .088 +35059 .055 +35061 .085 +36001 .071 +36003 .079 +36005 .133 +36007 .085 +36009 .085 +36011 .08 +36013 .078 +36015 .089 +36017 .087 +36019 .09 +36021 .077 +36023 .085 +36025 .084 +36027 .082 +36029 .083 +36031 .078 +36033 .078 +36035 .091 +36037 .072 +36039 .084 +36041 .056 +36043 .076 +36045 .078 +36047 .11 +36049 .076 +36051 .075 +36053 .074 +36055 .083 +36057 .088 +36059 .072 +36061 .092 +36063 .088 +36065 .073 +36067 .079 +36069 .067 +36071 .082 +36073 .082 +36075 .093 +36077 .068 +36079 .07 +36081 .091 +36083 .077 +36085 .089 +36087 .074 +36089 .089 +36091 .064 +36093 .078 +36095 .076 +36097 .074 +36099 .073 +36101 .095 +36103 .075 +36105 .086 +36107 .079 +36109 .056 +36111 .081 +36113 .072 +36115 .074 +36117 .078 +36119 .074 +36121 .078 +36123 .06 +37001 .118 +37003 .136 +37005 .109 +37007 .148 +37009 .1 +37011 .079 +37013 .112 +37015 .105 +37017 .12 +37019 .105 +37021 .082 +37023 .141 +37025 .114 +37027 .152 +37029 .071 +37031 .076 +37033 .124 +37035 .136 +37037 .08 +37039 .142 +37041 .119 +37043 .108 +37045 .143 +37047 .125 +37049 .1 +37051 .091 +37053 .05 +37055 .068 +37057 .125 +37059 .121 +37061 .087 +37063 .08 +37065 .163 +37067 .095 +37069 .097 +37071 .133 +37073 .071 +37075 .134 +37077 .102 +37079 .102 +37081 .11 +37083 .131 +37085 .109 +37087 .085 +37089 .086 +37091 .094 +37093 .083 +37095 .059 +37097 .121 +37099 .078 +37101 .095 +37103 .109 +37105 .135 +37107 .114 +37109 .132 +37111 .141 +37113 .092 +37115 .093 +37117 .106 +37119 .11 +37121 .11 +37123 .133 +37125 .095 +37127 .119 +37129 .091 +37131 .107 +37133 .083 +37135 .063 +37137 .101 +37139 .091 +37141 .108 +37143 .103 +37145 .108 +37147 .1 +37149 .082 +37151 .11 +37153 .132 +37155 .115 +37157 .117 +37159 .128 +37161 .143 +37163 .083 +37165 .165 +37167 .119 +37169 .101 +37171 .118 +37173 .091 +37175 .085 +37177 .088 +37179 .1 +37181 .13 +37183 .083 +37185 .127 +37187 .117 +37189 .069 +37191 .088 +37193 .129 +37195 .119 +37197 .093 +37199 .109 +38001 .027 +38003 .034 +38005 .053 +38007 .013 +38009 .029 +38011 .027 +38013 .022 +38015 .028 +38017 .037 +38019 .025 +38021 .041 +38023 .034 +38025 .036 +38027 .038 +38029 .051 +38031 .028 +38033 .025 +38035 .035 +38037 .032 +38039 .036 +38041 .027 +38043 .034 +38045 .043 +38047 .027 +38049 .029 +38051 .031 +38053 .022 +38055 .034 +38057 .033 +38059 .03 +38061 .044 +38063 .029 +38065 .031 +38067 .051 +38069 .04 +38071 .032 +38073 .06 +38075 .033 +38077 .043 +38079 .097 +38081 .065 +38083 .037 +38085 .043 +38087 .012 +38089 .028 +38091 .021 +38093 .032 +38095 .026 +38097 .035 +38099 .045 +38101 .028 +38103 .038 +38105 .02 +39001 .138 +39003 .102 +39005 .114 +39007 .126 +39009 .086 +39011 .101 +39013 .092 +39015 .119 +39017 .091 +39019 .132 +39021 .114 +39023 .099 +39025 .094 +39027 .139 +39029 .125 +39031 .123 +39033 .132 +39035 .085 +39037 .099 +39039 .117 +39041 .067 +39043 .095 +39045 .082 +39047 .117 +39049 .082 +39051 .121 +39053 .095 +39055 .065 +39057 .094 +39059 .108 +39061 .089 +39063 .091 +39065 .112 +39067 .106 +39069 .109 +39071 .153 +39073 .104 +39075 .063 +39077 .128 +39079 .105 +39081 .127 +39083 .087 +39085 .079 +39087 .084 +39089 .089 +39091 .112 +39093 .091 +39095 .113 +39097 .085 +39099 .118 +39101 .102 +39103 .075 +39105 .152 +39107 .078 +39109 .11 +39111 .115 +39113 .11 +39115 .146 +39117 .1 +39119 .119 +39121 .14 +39123 .107 +39125 .115 +39127 .128 +39129 .103 +39131 .147 +39133 .092 +39135 .111 +39137 .09 +39139 .118 +39141 .115 +39143 .108 +39145 .121 +39147 .12 +39149 .119 +39151 .11 +39153 .096 +39155 .135 +39157 .102 +39159 .08 +39161 .132 +39163 .113 +39165 .085 +39167 .094 +39169 .089 +39171 .141 +39173 .1 +39175 .112 +40001 .076 +40003 .055 +40005 .084 +40007 .033 +40009 .059 +40011 .057 +40013 .052 +40015 .061 +40017 .055 +40019 .057 +40021 .056 +40023 .076 +40025 .035 +40027 .052 +40029 .088 +40031 .051 +40033 .04 +40035 .055 +40037 .081 +40039 .049 +40041 .06 +40043 .046 +40045 .045 +40047 .045 +40049 .059 +40051 .074 +40053 .042 +40055 .079 +40057 .069 +40059 .043 +40061 .084 +40063 .107 +40065 .048 +40067 .086 +40069 .07 +40071 .081 +40073 .043 +40075 .062 +40077 .104 +40079 .087 +40081 .067 +40083 .056 +40085 .054 +40087 .056 +40089 .116 +40091 .082 +40093 .045 +40095 .075 +40097 .081 +40099 .047 +40101 .081 +40103 .077 +40105 .093 +40107 .089 +40109 .061 +40111 .085 +40113 .074 +40115 .062 +40117 .082 +40119 .063 +40121 .065 +40123 .051 +40125 .065 +40127 .089 +40129 .045 +40131 .072 +40133 .092 +40135 .087 +40137 .084 +40139 .038 +40141 .056 +40143 .068 +40145 .066 +40147 .059 +40149 .063 +40151 .042 +40153 .065 +41001 .085 +41003 .075 +41005 .104 +41007 .082 +41009 .126 +41011 .117 +41013 .161 +41015 .112 +41017 .135 +41019 .14 +41021 .056 +41023 .099 +41025 .15 +41027 .066 +41029 .115 +41031 .129 +41033 .133 +41035 .12 +41037 .101 +41039 .115 +41041 .094 +41043 .136 +41045 .093 +41047 .103 +41049 .074 +41051 .109 +41053 .093 +41055 .076 +41057 .082 +41059 .078 +41061 .102 +41063 .087 +41065 .08 +41067 .096 +41069 .067 +41071 .11 +42001 .074 +42003 .072 +42005 .08 +42007 .084 +42009 .109 +42011 .091 +42013 .074 +42015 .076 +42017 .073 +42019 .071 +42021 .088 +42023 .167 +42025 .1 +42027 .056 +42029 .063 +42031 .083 +42033 .098 +42035 .082 +42037 .082 +42039 .101 +42041 .068 +42043 .081 +42045 .079 +42047 .129 +42049 .092 +42051 .091 +42053 .102 +42055 .086 +42057 .143 +42059 .078 +42061 .102 +42063 .078 +42065 .095 +42067 .076 +42069 .083 +42071 .075 +42073 .089 +42075 .07 +42077 .093 +42079 .092 +42081 .093 +42083 .1 +42085 .113 +42087 .104 +42089 .09 +42091 .07 +42093 .059 +42095 .088 +42097 .094 +42099 .081 +42101 .11 +42103 .086 +42105 .103 +42107 .101 +42109 .086 +42111 .085 +42113 .079 +42115 .078 +42117 .078 +42119 .088 +42121 .087 +42123 .073 +42125 .079 +42127 .066 +42129 .078 +42131 .069 +42133 .084 +44001 .111 +44003 .119 +44005 .1 +44007 .135 +44009 .092 +45001 .148 +45003 .094 +45005 .225 +45007 .125 +45009 .181 +45011 .19 +45013 .087 +45015 .107 +45017 .144 +45019 .09 +45021 .162 +45023 .211 +45025 .168 +45027 .161 +45029 .142 +45031 .13 +45033 .172 +45035 .103 +45037 .107 +45039 .129 +45041 .117 +45043 .125 +45045 .102 +45047 .137 +45049 .161 +45051 .109 +45053 .107 +45055 .104 +45057 .179 +45059 .117 +45061 .157 +45063 .083 +45065 .157 +45067 .21 +45069 .202 +45071 .117 +45073 .142 +45075 .187 +45077 .106 +45079 .095 +45081 .094 +45083 .122 +45085 .139 +45087 .206 +45089 .154 +45091 .141 +46003 .04 +46005 .03 +46007 .06 +46009 .049 +46011 .032 +46013 .031 +46015 .027 +46017 .172 +46019 .039 +46021 .045 +46023 .048 +46025 .047 +46027 .037 +46029 .058 +46031 .056 +46033 .03 +46035 .043 +46037 .053 +46039 .048 +46041 .104 +46043 .031 +46045 .027 +46047 .049 +46049 .031 +46051 .035 +46053 .031 +46055 .036 +46057 .051 +46059 .028 +46061 .03 +46063 .023 +46065 .027 +46067 .039 +46069 .036 +46071 .062 +46073 .026 +46075 .024 +46077 .051 +46079 .058 +46081 .038 +46083 .039 +46085 .057 +46087 .045 +46089 .05 +46091 .047 +46093 .04 +46095 .068 +46097 .057 +46099 .047 +46101 .061 +46103 .044 +46105 .039 +46107 .032 +46109 .048 +46111 .038 +46113 .126 +46115 .034 +46117 .021 +46119 .024 +46121 .084 +46123 .029 +46125 .044 +46127 .053 +46129 .04 +46135 .053 +46137 .078 +47001 .097 +47003 .121 +47005 .128 +47007 .137 +47009 .093 +47011 .093 +47013 .131 +47015 .108 +47017 .164 +47019 .098 +47021 .094 +47023 .115 +47025 .111 +47027 .126 +47029 .122 +47031 .102 +47033 .134 +47035 .108 +47037 .092 +47039 .142 +47041 .102 +47043 .098 +47045 .14 +47047 .108 +47049 .13 +47051 .104 +47053 .157 +47055 .14 +47057 .135 +47059 .143 +47061 .13 +47063 .125 +47065 .087 +47067 .183 +47069 .127 +47071 .111 +47073 .103 +47075 .18 +47077 .181 +47079 .131 +47081 .124 +47083 .116 +47085 .119 +47087 .127 +47089 .118 +47091 .122 +47093 .081 +47095 .107 +47097 .189 +47099 .146 +47101 .149 +47103 .069 +47105 .095 +47107 .13 +47109 .127 +47111 .112 +47113 .11 +47115 .121 +47117 .163 +47119 .119 +47121 .136 +47123 .161 +47125 .09 +47127 .097 +47129 .116 +47131 .104 +47133 .12 +47135 .176 +47137 .134 +47139 .114 +47141 .097 +47143 .127 +47145 .089 +47147 .101 +47149 .095 +47151 .184 +47153 .128 +47155 .092 +47157 .102 +47159 .128 +47161 .111 +47163 .088 +47165 .097 +47167 .116 +47169 .12 +47171 .103 +47173 .107 +47175 .142 +47177 .125 +47179 .086 +47181 .129 +47183 .13 +47185 .135 +47187 .075 +47189 .09 +48001 .094 +48003 .076 +48005 .089 +48007 .073 +48009 .065 +48011 .051 +48013 .079 +48015 .084 +48017 .052 +48019 .068 +48021 .08 +48023 .051 +48025 .103 +48027 .071 +48029 .072 +48031 .058 +48033 .062 +48035 .086 +48037 .078 +48039 .089 +48041 .062 +48043 .051 +48045 .059 +48047 .099 +48049 .072 +48051 .074 +48053 .063 +48055 .082 +48057 .095 +48059 .063 +48061 .108 +48063 .096 +48065 .067 +48067 .125 +48069 .053 +48071 .107 +48073 .097 +48075 .064 +48077 .074 +48079 .072 +48081 .086 +48083 .069 +48085 .078 +48087 .059 +48089 .066 +48091 .066 +48093 .061 +48095 .082 +48097 .065 +48099 .087 +48101 .056 +48103 .095 +48105 .097 +48107 .07 +48109 .045 +48111 .044 +48113 .087 +48115 .087 +48117 .058 +48119 .084 +48121 .077 +48123 .083 +48125 .06 +48127 .11 +48129 .07 +48131 .125 +48133 .084 +48135 .092 +48137 .067 +48139 .086 +48141 .098 +48143 .071 +48145 .095 +48147 .09 +48149 .057 +48151 .064 +48153 .069 +48155 .057 +48157 .083 +48159 .073 +48161 .067 +48163 .084 +48165 .068 +48167 .085 +48169 .057 +48171 .049 +48173 .052 +48175 .079 +48177 .061 +48179 .089 +48181 .087 +48183 .08 +48185 .1 +48187 .067 +48189 .064 +48191 .089 +48193 .06 +48195 .055 +48197 .072 +48199 .101 +48201 .085 +48203 .087 +48205 .043 +48207 .052 +48209 .068 +48211 .03 +48213 .085 +48215 .116 +48217 .085 +48219 .07 +48221 .076 +48223 .065 +48225 .103 +48227 .075 +48229 .065 +48231 .085 +48233 .08 +48235 .054 +48237 .064 +48239 .076 +48241 .119 +48243 .056 +48245 .108 +48247 .1 +48249 .1 +48251 .091 +48253 .082 +48255 .105 +48257 .088 +48259 .06 +48261 .061 +48263 .062 +48265 .06 +48267 .054 +48269 .054 +48271 .089 +48273 .077 +48275 .064 +48277 .078 +48279 .066 +48281 .061 +48283 .099 +48285 .064 +48287 .069 +48289 .079 +48291 .112 +48293 .072 +48295 .061 +48297 .076 +48299 .073 +48301 .115 +48303 .057 +48305 .068 +48307 .091 +48309 .071 +48311 .078 +48313 .087 +48315 .118 +48317 .05 +48319 .054 +48321 .112 +48323 .136 +48325 .071 +48327 .09 +48329 .062 +48331 .102 +48333 .06 +48335 .087 +48337 .078 +48339 .079 +48341 .053 +48343 .156 +48345 .053 +48347 .07 +48349 .084 +48351 .124 +48353 .064 +48355 .077 +48357 .063 +48359 .055 +48361 .111 +48363 .091 +48365 .075 +48367 .084 +48369 .048 +48371 .117 +48373 .103 +48375 .066 +48377 .178 +48379 .085 +48381 .051 +48383 .058 +48385 .061 +48387 .105 +48389 .14 +48391 .07 +48393 .062 +48395 .087 +48397 .078 +48399 .101 +48401 .091 +48403 .159 +48405 .108 +48407 .1 +48409 .098 +48411 .078 +48413 .111 +48415 .072 +48417 .043 +48419 .081 +48421 .049 +48423 .083 +48425 .073 +48427 .178 +48429 .069 +48431 .046 +48433 .048 +48435 .078 +48437 .067 +48439 .082 +48441 .061 +48443 .078 +48445 .077 +48447 .056 +48449 .077 +48451 .07 +48453 .07 +48455 .088 +48457 .105 +48459 .081 +48461 .056 +48463 .089 +48465 .096 +48467 .074 +48469 .079 +48471 .078 +48473 .09 +48475 .085 +48477 .069 +48479 .091 +48481 .079 +48483 .056 +48485 .081 +48487 .055 +48489 .139 +48491 .078 +48493 .069 +48495 .09 +48497 .096 +48499 .084 +48501 .071 +48503 .064 +48505 .126 +48507 .163 +49001 .042 +49003 .056 +49005 .042 +49007 .065 +49009 .027 +49011 .056 +49013 .069 +49015 .062 +49017 .049 +49019 .052 +49021 .063 +49023 .068 +49025 .044 +49027 .04 +49029 .053 +49031 .038 +49033 .028 +49035 .06 +49037 .086 +49039 .056 +49041 .057 +49043 .058 +49045 .064 +49047 .066 +49049 .054 +49051 .06 +49053 .078 +49055 .042 +49057 .07 +50001 .058 +50003 .068 +50005 .07 +50007 .057 +50009 .078 +50011 .062 +50013 .07 +50015 .066 +50017 .055 +50019 .083 +50021 .087 +50023 .061 +50025 .063 +50027 .058 +51001 .061 +51003 .05 +51005 .087 +51007 .07 +51009 .07 +51011 .067 +51013 .042 +51015 .058 +51017 .054 +51019 .062 +51021 .075 +51023 .062 +51025 .114 +51027 .088 +51029 .078 +51031 .066 +51033 .082 +51035 .113 +51036 .087 +51037 .091 +51041 .068 +51043 .061 +51045 .072 +51047 .075 +51049 .07 +51051 .083 +51053 .083 +51057 .077 +51059 .047 +51061 .05 +51063 .071 +51065 .058 +51067 .078 +51069 .073 +51071 .085 +51073 .055 +51075 .067 +51077 .104 +51079 .058 +51081 .091 +51083 .115 +51085 .066 +51087 .072 +51089 .134 +51091 .059 +51093 .063 +51095 .046 +51097 .075 +51099 .075 +51101 .064 +51103 .073 +51105 .071 +51107 .047 +51109 .081 +51111 .091 +51113 .06 +51115 .048 +51117 .105 +51119 .064 +51121 .063 +51125 .062 +51127 .069 +51131 .066 +51133 .065 +51135 .079 +51137 .076 +51139 .098 +51141 .099 +51143 .103 +51145 .06 +51147 .084 +51149 .073 +51153 .053 +51155 .108 +51157 .056 +51159 .072 +51161 .059 +51163 .056 +51165 .053 +51167 .096 +51169 .099 +51171 .076 +51173 .102 +51175 .077 +51177 .051 +51179 .051 +51181 .073 +51183 .095 +51185 .079 +51187 .066 +51191 .082 +51193 .071 +51195 .067 +51197 .097 +51199 .05 +51510 .048 +51515 .089 +51520 .099 +51530 .087 +51540 .062 +51550 .064 +51570 .082 +51580 .091 +51590 .13 +51595 .118 +51600 .054 +51610 .073 +51620 .107 +51630 .091 +51640 .103 +51650 .077 +51660 .064 +51670 .104 +51678 .092 +51680 .077 +51683 .068 +51685 .054 +51690 .2 +51700 .073 +51710 .084 +51720 .055 +51730 .138 +51735 .05 +51740 .084 +51750 .085 +51760 .1 +51770 .086 +51775 .063 +51790 .073 +51800 .066 +51810 .059 +51820 .082 +51830 .135 +51840 .079 +53001 .065 +53003 .081 +53005 .058 +53007 .065 +53009 .085 +53011 .127 +53013 .084 +53015 .128 +53017 .066 +53019 .113 +53021 .062 +53023 .053 +53025 .073 +53027 .117 +53029 .081 +53031 .074 +53033 .088 +53035 .071 +53037 .073 +53039 .081 +53041 .12 +53043 .074 +53045 .094 +53047 .074 +53049 .107 +53051 .121 +53053 .088 +53055 .047 +53057 .091 +53059 .094 +53061 .101 +53063 .083 +53065 .105 +53067 .071 +53069 .114 +53071 .055 +53073 .078 +53075 .047 +53077 .068 +54001 .092 +54003 .085 +54005 .094 +54007 .08 +54009 .111 +54011 .066 +54013 .121 +54015 .119 +54017 .074 +54019 .086 +54021 .065 +54023 .112 +54025 .083 +54027 .078 +54029 .118 +54031 .097 +54033 .071 +54035 .129 +54037 .066 +54039 .071 +54041 .075 +54043 .106 +54045 .098 +54047 .127 +54049 .065 +54051 .087 +54053 .121 +54055 .069 +54057 .079 +54059 .107 +54061 .046 +54063 .065 +54065 .091 +54067 .091 +54069 .078 +54071 .075 +54073 .102 +54075 .113 +54077 .07 +54079 .063 +54081 .08 +54083 .085 +54085 .088 +54087 .13 +54089 .078 +54091 .079 +54093 .109 +54095 .108 +54097 .089 +54099 .077 +54101 .104 +54103 .123 +54105 .114 +54107 .088 +54109 .115 +55001 .083 +55003 .083 +55005 .071 +55007 .062 +55009 .071 +55011 .057 +55013 .091 +55015 .067 +55017 .066 +55019 .076 +55021 .071 +55023 .081 +55025 .054 +55027 .084 +55029 .066 +55031 .065 +55033 .058 +55035 .06 +55037 .081 +55039 .078 +55041 .081 +55043 .061 +55045 .078 +55047 .075 +55049 .062 +55051 .094 +55053 .072 +55055 .08 +55057 .085 +55059 .098 +55061 .073 +55063 .059 +55065 .06 +55067 .085 +55069 .096 +55071 .085 +55073 .077 +55075 .102 +55077 .083 +55078 .127 +55079 .093 +55081 .064 +55083 .091 +55085 .074 +55087 .075 +55089 .072 +55091 .054 +55093 .061 +55095 .084 +55097 .058 +55099 .094 +55101 .093 +55103 .075 +55105 .111 +55107 .101 +55109 .068 +55111 .07 +55113 .071 +55115 .082 +55117 .083 +55119 .096 +55121 .064 +55123 .066 +55125 .071 +55127 .077 +55129 .078 +55131 .081 +55133 .072 +55135 .082 +55137 .082 +55139 .07 +55141 .074 +56001 .036 +56003 .082 +56005 .057 +56007 .068 +56009 .059 +56011 .049 +56013 .074 +56015 .053 +56017 .058 +56019 .067 +56021 .063 +56023 .065 +56025 .073 +56027 .045 +56029 .051 +56031 .062 +56033 .066 +56035 .045 +56037 .074 +56039 .052 +56041 .072 +56043 .059 +56045 .059 +72001 .217 +72003 .179 +72005 .178 +72007 .182 +72009 .192 +72011 .183 +72013 .167 +72015 .227 +72017 .201 +72019 .196 +72021 .136 +72023 .135 +72025 .158 +72027 .156 +72029 .212 +72031 .133 +72033 .183 +72035 .181 +72037 .176 +72039 .236 +72041 .173 +72043 .231 +72045 .234 +72047 .177 +72049 .116 +72051 .152 +72053 .178 +72054 .215 +72055 .199 +72057 .22 +72059 .226 +72061 .106 +72063 .163 +72065 .165 +72067 .164 +72069 .208 +72071 .191 +72073 .219 +72075 .186 +72077 .191 +72079 .186 +72081 .175 +72083 .181 +72085 .192 +72087 .204 +72089 .202 +72091 .192 +72093 .199 +72095 .259 +72097 .179 +72099 .18 +72101 .216 +72103 .232 +72105 .189 +72107 .228 +72109 .236 +72111 .202 +72113 .159 +72115 .168 +72117 .179 +72119 .171 +72121 .189 +72123 .252 +72125 .178 +72127 .119 +72129 .185 +72131 .182 +72133 .257 +72135 .129 +72137 .134 +72139 .111 +72141 .258 +72143 .163 +72145 .176 +72147 .277 +72149 .198 +72151 .241 +72153 .16 diff --git a/examples/vega-data/us-10m.json b/examples/vega-data/us-10m.json new file mode 100644 index 000000000000..a1ce852de6f2 --- /dev/null +++ b/examples/vega-data/us-10m.json @@ -0,0 +1 @@ +{"type":"Topology","transform":{"scale":[0.0036000360003600037,0.0016925586033320111],"translate":[-180,-85.60903777459777]},"objects":{"land":{"type":"MultiPolygon","arcs":[[[0]],[[1]],[[2]],[[3]],[[4]],[[5]],[[6]],[[7,8,9]],[[10,11]],[[12]],[[13]],[[14]],[[15]],[[16]],[[17]],[[18]],[[19]],[[20]],[[21]],[[22]],[[23]],[[24]],[[25]],[[26]],[[27]],[[28]],[[29,30]],[[31]],[[32]],[[33]],[[34]],[[35]],[[36]],[[37]],[[38]],[[39]],[[40]],[[41]],[[42,43]],[[44]],[[45]],[[46]],[[47,48,49,50]],[[51]],[[52]],[[53]],[[54]],[[55]],[[56]],[[57]],[[58]],[[59]],[[60]],[[61]],[[62,63]],[[64]],[[65]],[[66]],[[67]],[[68]],[[69]],[[70]],[[71]],[[72]],[[73]],[[74]],[[75]],[[76,77]],[[78]],[[79]],[[80]],[[81]],[[82]],[[83]],[[84]],[[85]],[[86]],[[87]],[[88]],[[89]],[[90,91]],[[92]],[[93]],[[94]],[[95]],[[96]],[[97]],[[98]],[[99]],[[100]],[[101]],[[102]],[[103]],[[104]],[[105]],[[106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221]],[[222,223]],[[224]],[[225]],[[226]],[[227]],[[228]],[[229]],[[230,231,232,233]],[[234]],[[235]],[[236]],[[237]],[[238]],[[239]],[[240]],[[241]],[[242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477],[478,479,480,481,482,483,484]],[[485]],[[486]],[[487]],[[488]],[[489]],[[490]],[[491]],[[492]],[[493]],[[494]],[[495]],[[496]],[[497]],[[498]]]},"countries":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[499,500,501,502,503,504]],"id":4},{"type":"MultiPolygon","arcs":[[[505,506,352,507]],[[354,508,509]]],"id":24},{"type":"Polygon","arcs":[[510,511,414,512,513,514]],"id":8},{"type":"Polygon","arcs":[[312,515,314,516,517]],"id":784},{"type":"MultiPolygon","arcs":[[[518,11]],[[519,520,521,166,522,168,523,524]]],"id":32},{"type":"Polygon","arcs":[[525,526,527,528,529]],"id":51},{"type":"MultiPolygon","arcs":[[[0]],[[1]],[[2]],[[3]],[[4]],[[5]],[[6]],[[530,531]]],"id":10},{"type":"Polygon","arcs":[[13]],"id":260},{"type":"MultiPolygon","arcs":[[[14]],[[24]]],"id":36},{"type":"Polygon","arcs":[[532,533,534,535,536,537,538]],"id":40},{"type":"MultiPolygon","arcs":[[[539,-528]],[[484,540,479,541,-526,542,543]]],"id":31},{"type":"Polygon","arcs":[[544,545,546]],"id":108},{"type":"Polygon","arcs":[[547,548,549,550,437]],"id":56},{"type":"Polygon","arcs":[[551,552,553,554,366]],"id":204},{"type":"Polygon","arcs":[[555,556,557,-553,558,559]],"id":854},{"type":"Polygon","arcs":[[560,561,289,562]],"id":50},{"type":"Polygon","arcs":[[563,404,564,565,566,567]],"id":100},{"type":"MultiPolygon","arcs":[[[71]],[[73]],[[74]]],"id":44},{"type":"Polygon","arcs":[[568,569,570]],"id":70},{"type":"Polygon","arcs":[[571,572,573,574,575]],"id":112},{"type":"Polygon","arcs":[[576,145,577]],"id":84},{"type":"Polygon","arcs":[[578,579,580,581,-525]],"id":68},{"type":"Polygon","arcs":[[-521,582,-581,583,584,585,586,587,588,164,589]],"id":76},{"type":"Polygon","arcs":[[48,590]],"id":96},{"type":"Polygon","arcs":[[591,592]],"id":64},{"type":"Polygon","arcs":[[593,594,595,596]],"id":72},{"type":"Polygon","arcs":[[597,598,599,600,601,602,603]],"id":140},{"type":"MultiPolygon","arcs":[[[84]],[[85]],[[86]],[[87]],[[88]],[[96]],[[97]],[[99]],[[101]],[[103]],[[604,107,605,109,606,111,607,113,608,115,609,117,610,199,611,201,612,215,613,217,614,219,615,221]],[[616,223]],[[224]],[[225]],[[226]],[[227]],[[229]],[[230,617,232,618]],[[235]],[[237]],[[238]],[[240]],[[241]],[[485]],[[486]],[[488]],[[489]],[[490]],[[496]],[[497]]],"id":124},{"type":"Polygon","arcs":[[-536,619,620,621]],"id":756},{"type":"MultiPolygon","arcs":[[[-519,622,623,624]],[[-524,169,625,171,626,-579]]],"id":152},{"type":"MultiPolygon","arcs":[[[64]],[[627,274,628,276,629,278,630,280,631,632,633,634,635,-593,636,637,638,639,-503,640,641,642,643,644,645]]],"id":156},{"type":"Polygon","arcs":[[369,646,647,648,-556,649]],"id":384},{"type":"Polygon","arcs":[[650,651,652,359,653,654,655,656,-604,657]],"id":120},{"type":"Polygon","arcs":[[658,659,-545,660,661,662,663,-508,353,-510,664,-602,665]],"id":180},{"type":"Polygon","arcs":[[-509,355,666,-658,-603,-665]],"id":178},{"type":"Polygon","arcs":[[667,174,668,155,669,-585,670]],"id":170},{"type":"Polygon","arcs":[[178,671,151,672]],"id":188},{"type":"Polygon","arcs":[[70]],"id":192},{"type":"Polygon","arcs":[[77,673]],"id":-99},{"type":"Polygon","arcs":[[76,-674]],"id":196},{"type":"Polygon","arcs":[[-538,674,675,676]],"id":203},{"type":"Polygon","arcs":[[445,677,-675,-537,-622,678,679,-549,680,441,681]],"id":276},{"type":"Polygon","arcs":[[337,682,683,684]],"id":262},{"type":"MultiPolygon","arcs":[[[92]],[[-682,442,685,444]]],"id":208},{"type":"Polygon","arcs":[[62,686]],"id":214},{"type":"Polygon","arcs":[[687,688,689,690,691,384,692,693]],"id":12},{"type":"Polygon","arcs":[[173,-668,694]],"id":218},{"type":"Polygon","arcs":[[333,695,696,390,697]],"id":818},{"type":"Polygon","arcs":[[698,699,700,336,-685]],"id":232},{"type":"Polygon","arcs":[[431,701,433,702,427,703,429,704]],"id":724},{"type":"Polygon","arcs":[[450,705,706]],"id":233},{"type":"Polygon","arcs":[[-684,707,708,709,710,711,712,-699]],"id":231},{"type":"Polygon","arcs":[[713,452,714,715,455,716,717]],"id":246},{"type":"MultiPolygon","arcs":[[[18]],[[19]],[[20]]],"id":242},{"type":"Polygon","arcs":[[12]],"id":238},{"type":"MultiPolygon","arcs":[[[718,719,163,-589]],[[82]],[[720,-679,-621,721,426,-703,434,722,436,-551]]],"id":250},{"type":"Polygon","arcs":[[356,723,-651,-667]],"id":266},{"type":"MultiPolygon","arcs":[[[724,90]],[[725,726,727,728,729,730,731,732]]],"id":826},{"type":"Polygon","arcs":[[400,733,-543,-530,734]],"id":268},{"type":"Polygon","arcs":[[368,-650,-560,735]],"id":288},{"type":"Polygon","arcs":[[736,737,374,738,739,740,-648]],"id":324},{"type":"Polygon","arcs":[[741,377]],"id":270},{"type":"Polygon","arcs":[[375,742,-739]],"id":624},{"type":"Polygon","arcs":[[357,-652,-724]],"id":226},{"type":"MultiPolygon","arcs":[[[78]],[[407,743,409,744,411,745,413,-512,746,-566,747]]],"id":300},{"type":"Polygon","arcs":[[498]],"id":304},{"type":"Polygon","arcs":[[185,748,-578,146,749,750]],"id":320},{"type":"Polygon","arcs":[[161,751,-587,752]],"id":328},{"type":"Polygon","arcs":[[182,753,754,-750,147,755,149,756]],"id":340},{"type":"Polygon","arcs":[[757,-571,758,417,759,419,760,761]],"id":191},{"type":"Polygon","arcs":[[-687,63]],"id":332},{"type":"Polygon","arcs":[[-533,762,763,764,765,-762,766]],"id":348},{"type":"MultiPolygon","arcs":[[[26]],[[767,30]],[[31]],[[32]],[[35]],[[36]],[[39]],[[40]],[[768,43]],[[44]],[[45]],[[769,50]],[[46]]],"id":360},{"type":"Polygon","arcs":[[-639,770,-637,-592,-636,771,-563,290,772,292,773,294,774,296,775]],"id":356},{"type":"Polygon","arcs":[[91,-725]],"id":372},{"type":"Polygon","arcs":[[776,-505,777,300,778,302,779,780,781,-540,-527,-542,480]],"id":364},{"type":"Polygon","arcs":[[782,783,784,785,786,-781,787]],"id":368},{"type":"Polygon","arcs":[[100]],"id":352},{"type":"Polygon","arcs":[[788,789,-698,391,790,791,792]],"id":376},{"type":"MultiPolygon","arcs":[[[79]],[[80]],[[793,421,794,423,795,425,-722,-620,-535]]],"id":380},{"type":"Polygon","arcs":[[61]],"id":388},{"type":"Polygon","arcs":[[796,-785,797,332,-790,798,-793]],"id":400},{"type":"MultiPolygon","arcs":[[[75]],[[81]],[[83]]],"id":392},{"type":"Polygon","arcs":[[799,800,482,801,-643,802]],"id":398},{"type":"Polygon","arcs":[[342,803,804,805,-710,806]],"id":404},{"type":"Polygon","arcs":[[-803,-642,807,808]],"id":417},{"type":"Polygon","arcs":[[809,810,811,283]],"id":116},{"type":"Polygon","arcs":[[265,812,267,813]],"id":410},{"type":"Polygon","arcs":[[-515,814,815,816]],"id":-99},{"type":"Polygon","arcs":[[304,817,-783]],"id":414},{"type":"Polygon","arcs":[[818,819,-634,820,-811]],"id":418},{"type":"Polygon","arcs":[[-791,392,821]],"id":422},{"type":"Polygon","arcs":[[370,822,372,823,-737,-647]],"id":430},{"type":"Polygon","arcs":[[824,-694,825,388,826,-697,827,828]],"id":434},{"type":"Polygon","arcs":[[52]],"id":144},{"type":"Polygon","arcs":[[829]],"id":426},{"type":"Polygon","arcs":[[830,448,831,-572,832]],"id":440},{"type":"Polygon","arcs":[[-680,-721,-550]],"id":442},{"type":"Polygon","arcs":[[449,-707,833,-573,-832]],"id":428},{"type":"Polygon","arcs":[[-692,834,835,836,837,383]],"id":504},{"type":"Polygon","arcs":[[838,839]],"id":498},{"type":"Polygon","arcs":[[23]],"id":450},{"type":"Polygon","arcs":[[840,-577,-749,186,841,188,842,190,843,192,844,194,845]],"id":484},{"type":"Polygon","arcs":[[-817,846,-567,-747,-511]],"id":807},{"type":"Polygon","arcs":[[847,-689,848,-557,-649,-741,849]],"id":466},{"type":"Polygon","arcs":[[287,-561,-772,-635,-820,850]],"id":104},{"type":"Polygon","arcs":[[416,-759,-570,851,-815,-514,852]],"id":499},{"type":"Polygon","arcs":[[853,-645]],"id":496},{"type":"Polygon","arcs":[[854,344,855,856,347,857,858,859,860,861,862]],"id":508},{"type":"Polygon","arcs":[[863,379,864,-690,-848]],"id":478},{"type":"Polygon","arcs":[[-863,865,866]],"id":454},{"type":"MultiPolygon","arcs":[[[285,867]],[[-770,47,-591,49]]],"id":458},{"type":"Polygon","arcs":[[351,-507,868,-595,869]],"id":516},{"type":"Polygon","arcs":[[17]],"id":540},{"type":"Polygon","arcs":[[-558,-849,-688,-825,870,-656,871,-554]],"id":562},{"type":"Polygon","arcs":[[361,872,363,873,365,-555,-872,-655]],"id":566},{"type":"Polygon","arcs":[[179,874,181,-757,150,-672]],"id":558},{"type":"Polygon","arcs":[[-681,-548,438,875,440]],"id":528},{"type":"MultiPolygon","arcs":[[[876,-718,877,457,878,459,879,461]],[[487]],[[492]],[[493]]],"id":578},{"type":"Polygon","arcs":[[-771,-638]],"id":524},{"type":"MultiPolygon","arcs":[[[15]],[[16]]],"id":554},{"type":"MultiPolygon","arcs":[[[880,319,881,882,883,-517,315,884,317]],[[-516,313]]],"id":512},{"type":"Polygon","arcs":[[-640,-776,297,885,299,-778,-504]],"id":586},{"type":"Polygon","arcs":[[175,886,177,-673,152,887,154,-669]],"id":591},{"type":"Polygon","arcs":[[-627,172,-695,-671,-584,-580]],"id":604},{"type":"MultiPolygon","arcs":[[[51]],[[54]],[[55]],[[56]],[[57]],[[58]],[[59]]],"id":608},{"type":"MultiPolygon","arcs":[[[37]],[[38]],[[-769,42]],[[41]]],"id":598},{"type":"Polygon","arcs":[[-678,446,888,-833,-576,889,890,-676]],"id":616},{"type":"Polygon","arcs":[[60]],"id":630},{"type":"Polygon","arcs":[[262,891,264,-814,892,269,893,271,894,273,-628,895]],"id":408},{"type":"Polygon","arcs":[[-705,430]],"id":620},{"type":"Polygon","arcs":[[-582,-583,-520]],"id":600},{"type":"Polygon","arcs":[[-799,-789]],"id":275},{"type":"Polygon","arcs":[[308,896,310,897]],"id":634},{"type":"Polygon","arcs":[[898,-840,899,403,-564,900,-765]],"id":642},{"type":"MultiPolygon","arcs":[[[89]],[[-889,447,-831]],[[102]],[[104]],[[105]],[[228]],[[234]],[[236]],[[239]],[[901,243,902,245,903,247,904,249,905,251,906,253,907,255,908,257,909,259,910,261,-896,-646,-854,-644,-802,483,-544,-734,401,911,-574,-834,-706,451,-714,-877,912,913,914,915,464,916,466,917,468,918,470,919,920,473,921,475,922,477]],[[491]],[[494]],[[495]]],"id":643},{"type":"Polygon","arcs":[[923,-546,-660,924]],"id":646},{"type":"Polygon","arcs":[[-691,-865,380,-835]],"id":732},{"type":"Polygon","arcs":[[925,329,926,331,-798,-784,-818,305,927,307,-898,311,-518,-884,928]],"id":682},{"type":"Polygon","arcs":[[-599,929,-828,-696,334,-700,-713,930,931,932]],"id":729},{"type":"Polygon","arcs":[[-711,-806,933,-666,-601,934,-932,935]],"id":728},{"type":"Polygon","arcs":[[378,-864,-850,-740,-743,376,-742]],"id":686},{"type":"MultiPolygon","arcs":[[[25]],[[27]],[[28]],[[33]],[[34]]],"id":90},{"type":"Polygon","arcs":[[373,-738,-824]],"id":694},{"type":"Polygon","arcs":[[184,-751,-755,936]],"id":222},{"type":"Polygon","arcs":[[-708,-683,338,937,340,938]],"id":-99},{"type":"Polygon","arcs":[[-807,-709,-939,341]],"id":706},{"type":"Polygon","arcs":[[-568,-847,-816,-852,-569,-758,-766,-901]],"id":688},{"type":"Polygon","arcs":[[162,-720,939,-588,-752]],"id":740},{"type":"Polygon","arcs":[[-891,940,-763,-539,-677]],"id":703},{"type":"Polygon","arcs":[[-534,-767,-761,420,-794]],"id":705},{"type":"Polygon","arcs":[[-878,-717,456]],"id":752},{"type":"Polygon","arcs":[[941,-859]],"id":748},{"type":"Polygon","arcs":[[-797,-792,-822,393,942,-786]],"id":760},{"type":"Polygon","arcs":[[-871,-829,-930,-598,-657]],"id":148},{"type":"Polygon","arcs":[[-736,-559,-552,367]],"id":768},{"type":"Polygon","arcs":[[284,-868,286,-851,-819,-810]],"id":764},{"type":"Polygon","arcs":[[-808,-641,-502,943]],"id":762},{"type":"Polygon","arcs":[[-777,481,-801,944,-500]],"id":795},{"type":"Polygon","arcs":[[29,-768]],"id":626},{"type":"Polygon","arcs":[[53]],"id":780},{"type":"Polygon","arcs":[[-693,385,945,387,-826]],"id":788},{"type":"MultiPolygon","arcs":[[[399,-735,-529,-782,-787,-943,394,946,947,397,948]],[[949,-748,-565,405]]],"id":792},{"type":"Polygon","arcs":[[72]],"id":158},{"type":"Polygon","arcs":[[-804,343,-855,-867,950,-663,951,-661,-547,-924,952]],"id":834},{"type":"Polygon","arcs":[[-925,-659,-934,-805,-953]],"id":800},{"type":"Polygon","arcs":[[-912,402,-900,-839,-899,-764,-941,-890,-575]],"id":804},{"type":"Polygon","arcs":[[-590,165,-522]],"id":858},{"type":"MultiPolygon","arcs":[[[65]],[[66]],[[67]],[[68]],[[69]],[[118,953,120,954,122,955,124,956,126,957,128,958,130,959,132,960,134,961,136,962,138,963,140,964,142,-846,195,965,966,967,968,969,-611]],[[93]],[[95]],[[98]],[[-613,202,970,204,971,206,972,208,973,210,974,212,975,214]]],"id":840},{"type":"Polygon","arcs":[[-945,-800,-809,-944,-501]],"id":860},{"type":"Polygon","arcs":[[156,976,158,977,160,-753,-586,-670]],"id":862},{"type":"Polygon","arcs":[[282,-812,-821,-633]],"id":704},{"type":"MultiPolygon","arcs":[[[21]],[[22]]],"id":548},{"type":"Polygon","arcs":[[321,978,323,979,325,980,327,-929,-883,981]],"id":887},{"type":"Polygon","arcs":[[982,350,-870,-594,983,-860,-942,-858,348],[-830]],"id":710},{"type":"Polygon","arcs":[[-866,-862,984,-596,-869,-506,-664,-951]],"id":894},{"type":"Polygon","arcs":[[-984,-597,-985,-861]],"id":716}]}},"arcs":[[[33289,2723],[-582,81],[-621,-35],[-348,197],[0,23],[-152,174],[625,-23],[599,-58],[207,243],[147,208],[288,-243],[-82,-301],[-81,-266]],[[5242,3530],[-364,208],[-163,209],[-11,35],[-180,162],[169,220],[517,-93],[277,-185],[212,-209],[76,-266],[-533,-81]],[[35977,2708],[-658,35],[-365,197],[49,243],[593,162],[239,197],[174,254],[126,220],[168,209],[180,243],[141,0],[414,127],[419,-127],[342,-255],[120,-359],[33,-254],[11,-301],[-430,-186],[-452,-150],[-522,-139],[-582,-116]],[[16602,6806],[-386,47],[-278,208],[60,197],[332,-104],[359,-93],[332,104],[-158,-208],[-261,-151]],[[15547,6934],[-164,23],[-359,58],[-381,162],[202,127],[277,-139],[425,-231]],[[23277,7733],[-217,46],[-337,-23],[-343,23],[-376,-35],[-283,116],[-146,243],[174,104],[353,-81],[403,-46],[305,-81],[304,69],[163,-335]],[[30256,7743],[-364,11],[136,232],[-327,-81],[-310,-81],[-212,174],[-16,243],[305,231],[190,70],[321,-23],[82,301],[16,219],[-6,475],[158,278],[256,93],[147,-220],[65,-220],[120,-267],[92,-254],[76,-267],[33,-266],[-49,-231],[-76,-220],[-326,-81],[-311,-116]],[[794,704],[78,49],[94,61],[81,52],[41,26]],[[1088,892],[41,-1],[29,-10]],[[1158,881],[402,-246],[352,246],[63,34],[816,104],[265,-138],[130,-71],[419,-196],[789,-151],[625,-185],[1072,-139],[800,162],[1181,-116],[669,-185],[734,174],[773,162],[60,278],[-1094,23],[-898,139],[-234,231],[-745,128],[49,266],[103,243],[104,220],[-55,243],[-462,162],[-212,209],[-430,185],[675,-35],[642,93],[402,-197],[495,173],[457,220],[223,197],[-98,243],[-359,162],[-408,174],[-571,35],[-500,81],[-539,58],[-180,220],[-359,185],[-217,208],[-87,672],[136,-58],[250,-185],[457,58],[441,81],[228,-255],[441,58],[370,127],[348,162],[315,197],[419,58],[-11,220],[-97,220],[81,208],[359,104],[163,-196],[425,115],[321,151],[397,12],[375,57],[376,139],[299,128],[337,127],[218,-35],[190,-46],[414,81],[370,-104],[381,11],[364,81],[375,-57],[414,-58],[386,23],[403,-12],[413,-11],[381,23],[283,174],[337,92],[349,-127],[331,104],[300,208],[179,-185],[98,-208],[180,-197],[288,174],[332,-220],[375,-70],[321,-162],[392,35],[354,104],[418,-23],[376,-81],[381,-104],[147,254],[-180,197],[-136,209],[-359,46],[-158,220],[-60,220],[-98,440],[213,-81],[364,-35],[359,35],[327,-93],[283,-174],[119,-208],[376,-35],[359,81],[381,116],[342,70],[283,-139],[370,46],[239,451],[224,-266],[321,-104],[348,58],[228,-232],[365,-23],[337,-69],[332,-128],[218,220],[108,209],[278,-232],[381,58],[283,-127],[190,-197],[370,58],[288,127],[283,151],[337,81],[392,69],[354,81],[272,127],[163,186],[65,254],[-32,244],[-87,231],[-98,232],[-87,231],[-71,209],[-16,231],[27,232],[130,220],[109,243],[44,231],[-55,255],[-32,232],[136,266],[152,173],[180,220],[190,186],[223,173],[109,255],[152,162],[174,151],[267,34],[174,186],[196,115],[228,70],[202,150],[157,186],[218,69],[163,-151],[-103,-196],[-283,-174],[-120,-127],[-206,92],[-229,-58],[-190,-139],[-202,-150],[-136,-174],[-38,-231],[17,-220],[130,-197],[-190,-139],[-261,-46],[-153,-197],[-163,-185],[-174,-255],[-44,-220],[98,-243],[147,-185],[229,-139],[212,-185],[114,-232],[60,-220],[82,-232],[130,-196],[82,-220],[38,-544],[81,-220],[22,-232],[87,-231],[-38,-313],[-152,-243],[-163,-197],[-370,-81],[-125,-208],[-169,-197],[-419,-220],[-370,-93],[-348,-127],[-376,-128],[-223,-243],[-446,-23],[-489,23],[-441,-46],[-468,0],[87,-232],[424,-104],[311,-162],[174,-208],[-310,-185],[-479,58],[-397,-151],[-17,-243],[-11,-232],[327,-196],[60,-220],[353,-220],[588,-93],[500,-162],[398,-185],[506,-186],[690,-92],[681,-162],[473,-174],[517,-197],[272,-278],[136,-220],[337,209],[457,173],[484,186],[577,150],[495,162],[691,12],[680,-81],[560,-139],[180,255],[386,173],[702,12],[550,127],[522,128],[577,81],[614,104],[430,150],[-196,209],[-119,208],[0,220],[-539,-23],[-571,-93],[-544,0],[-77,220],[39,440],[125,128],[397,138],[468,139],[337,174],[337,174],[251,231],[380,104],[376,81],[190,47],[430,23],[408,81],[343,116],[337,139],[305,139],[386,185],[245,197],[261,173],[82,232],[-294,139],[98,243],[185,185],[288,116],[305,139],[283,185],[217,232],[136,277],[202,163],[331,-35],[136,-197],[332,-23],[11,220],[142,231],[299,-58],[71,-220],[331,-34],[360,104],[348,69],[315,-34],[120,-243],[305,196],[283,105],[315,81],[310,81],[283,139],[310,92],[240,128],[168,208],[207,-151],[288,81],[202,-277],[157,-209],[316,116],[125,232],[283,162],[365,-35],[108,-220],[229,220],[299,69],[326,23],[294,-11],[310,-70],[300,-34],[130,-197],[180,-174],[304,104],[327,24],[315,0],[310,11],[278,81],[294,70],[245,162],[261,104],[283,58],[212,162],[152,324],[158,197],[288,-93],[109,-208],[239,-139],[289,46],[196,-208],[206,-151],[283,139],[98,255],[250,104],[289,197],[272,81],[326,116],[218,127],[228,139],[218,127],[261,-69],[250,208],[180,162],[261,-11],[229,139],[54,208],[234,162],[228,116],[278,93],[256,46],[244,-35],[262,-58],[223,-162],[27,-254],[245,-197],[168,-162],[332,-70],[185,-162],[229,-162],[266,-35],[223,116],[240,243],[261,-127],[272,-70],[261,-69],[272,-46],[277,0],[229,-614],[-11,-150],[-33,-267],[-266,-150],[-218,-220],[38,-232],[310,12],[-38,-232],[-141,-220],[-131,-243],[212,-185],[321,-58],[321,104],[153,232],[92,220],[153,185],[174,174],[70,208],[147,289],[174,58],[316,24],[277,69],[283,93],[136,231],[82,220],[190,220],[272,151],[234,115],[153,197],[157,104],[202,93],[277,-58],[250,58],[272,69],[305,-34],[201,162],[142,393],[103,-162],[131,-278],[234,-115],[266,-47],[267,70],[283,-46],[261,-12],[174,58],[234,-35],[212,-127],[250,81],[300,0],[255,81],[289,-81],[185,197],[141,196],[191,163],[348,439],[179,-81],[212,-162],[185,-208],[354,-359],[272,-12],[256,0],[299,70],[299,81],[229,162],[190,174],[310,23],[207,127],[218,-116],[141,-185],[196,-185],[305,23],[190,-150],[332,-151],[348,-58],[288,47],[218,185],[185,185],[250,46],[251,-81],[288,-58],[261,93],[250,0],[245,-58],[256,-58],[250,104],[299,93],[283,23],[316,0],[255,58],[251,46],[76,290],[11,243],[174,-162],[49,-266],[92,-244],[115,-196],[234,-105],[315,35],[365,12],[250,35],[364,0],[262,11],[364,-23],[310,-46],[196,-186],[-54,-220],[179,-173],[299,-139],[310,-151],[360,-104],[375,-92],[283,-93],[315,-12],[180,197],[245,-162],[212,-185],[245,-139],[337,-58],[321,-69],[136,-232],[316,-139],[212,-208],[310,-93],[321,12],[299,-35],[332,12],[332,-47],[310,-81],[288,-139],[289,-116],[195,-173],[-32,-232],[-147,-208],[-125,-266],[-98,-209],[-131,-243],[-364,-93],[-163,-208],[-360,-127],[-125,-232],[-190,-220],[-201,-185],[-115,-243],[-70,-220],[-28,-266],[6,-220],[158,-232],[60,-220],[130,-208],[517,-81],[109,-255],[-501,-93],[-424,-127],[-528,-23],[-234,-336],[-49,-278],[-119,-220],[-147,-220],[370,-196],[141,-244],[239,-219],[338,-197],[386,-186],[419,-185],[636,-185],[142,-289],[800,-128],[53,-45],[208,-175],[767,151],[636,-186],[-99504,-147],[245,344],[501,-185],[32,21]],[[31400,18145],[-92,-239],[-238,-183],[-301,67],[-202,177],[-291,86],[-350,330],[-283,317],[-383,662],[229,-124],[390,-395],[369,-212],[143,271],[90,405],[256,244],[198,-70]],[[30935,19481],[106,-274],[139,-443],[361,-355],[389,-147],[-125,-296],[-264,-29],[-141,208]],[[33139,19680],[-139,266],[333,354],[236,-148],[167,237],[222,-266],[-83,-207],[-375,-177],[-125,207],[-236,-266]],[[69095,21172],[-7,314],[41,244],[19,121],[179,-186],[263,-74],[9,-112],[-77,-269],[-427,-38]],[[90796,24799],[-57,32],[-171,19],[-171,505],[-38,390],[-160,515],[7,271],[181,-52],[269,-204],[151,81],[217,113],[166,-39],[20,-702],[-95,-203],[-29,-476],[-97,162],[-193,-412]],[[97036,23023],[-256,13],[-180,194],[-302,42],[-46,217],[149,438],[349,583],[179,111],[200,225],[238,310],[167,306],[123,441],[106,149],[41,330],[195,273],[61,-251],[63,-244],[198,239],[80,-249],[0,-249],[-103,-274],[-182,-435],[-142,-238],[103,-284],[-214,-7],[-238,-223],[-75,-387],[-157,-597],[-219,-264],[-138,-169]],[[98677,25949],[-48,155],[-116,85],[160,486],[-91,326],[-299,236],[8,214],[201,206],[47,455],[-13,382],[-113,396],[8,104],[-133,244],[-218,523],[-117,418],[104,46],[151,-328],[216,-153],[78,-526],[202,-622],[5,403],[126,-161],[41,-447],[224,-192],[188,-48],[158,226],[141,-69],[-67,-524],[-85,-345],[-212,12],[-74,-179],[26,-254],[-41,-110],[-105,-319],[-138,-404],[-214,-236]],[[96316,37345],[-153,160],[-199,266],[-179,313],[-184,416],[-38,201],[119,-9],[156,-201],[122,-200],[89,-166],[228,-366],[144,-272],[-105,-142]],[[99425,39775],[-153,73],[-27,260],[107,203],[126,-74],[69,98],[96,-171],[-46,-308],[-172,-81]],[[99645,40529],[-36,220],[139,121],[88,33],[163,184],[0,-289],[-177,-145],[-177,-124]],[[0,40798],[0,289],[57,27],[-34,-284],[-23,-32]],[[96531,40773],[-93,259],[10,158],[175,-339],[-92,-78]],[[96463,41280],[-75,74],[-58,-32],[-39,163],[-6,453],[133,-182],[45,-476]],[[62613,35454],[-160,151],[-220,211],[-77,312],[-18,524],[-98,471],[-26,425],[50,426],[128,102],[1,197],[133,447],[25,377],[-65,280],[-52,372],[-23,544],[97,331],[38,375],[138,22],[155,121],[103,107],[122,7],[158,337],[229,364],[83,297],[-38,253],[118,-71],[153,410],[6,356],[92,264],[96,-254],[74,-251],[69,-390],[45,-711],[72,-276],[-28,-284],[-49,-174],[-94,347],[-53,-175],[53,-438],[-24,-250],[-77,-137],[-18,-500],[-109,-689],[-137,-814],[-172,-1120],[-106,-821],[-125,-685],[-226,-140],[-243,-250]],[[90643,27516],[-230,262],[-170,104],[43,308],[-152,-112],[-243,-428],[-240,160],[-158,94],[-159,42],[-269,171],[-179,364],[-52,449],[-64,298],[-137,240],[-267,71],[91,287],[-67,438],[-136,-408],[-247,-109],[146,327],[42,341],[107,289],[-22,438],[-226,-504],[-174,-202],[-106,-470],[-217,243],[9,313],[-174,429],[-147,221],[52,137],[-356,358],[-195,17],[-267,287],[-498,-56],[-359,-211],[-317,-197],[-265,39],[-294,-303],[-241,-137],[-53,-309],[-103,-240],[-236,-15],[-174,-52],[-246,107],[-199,-64],[-191,-27],[-165,-315],[-81,26],[-140,-167],[-133,-187],[-203,23],[-186,0],[-295,377],[-149,113],[6,338],[138,81],[47,134],[-10,212],[34,411],[-31,350],[-147,598],[-45,337],[12,336],[-111,385],[-7,174],[-123,235],[-35,463],[-158,467],[-39,252],[122,-255],[-93,548],[137,-171],[83,-229],[-5,303],[-138,465],[-26,186],[-65,177],[31,341],[56,146],[38,295],[-29,346],[114,425],[21,-450],[118,406],[225,198],[136,252],[212,217],[126,46],[77,-73],[219,220],[168,66],[42,129],[74,54],[153,-14],[292,173],[151,262],[71,316],[163,300],[13,236],[7,321],[194,502],[117,-510],[119,118],[-99,279],[87,287],[122,-128],[34,449],[152,291],[67,233],[140,101],[4,165],[122,-69],[5,148],[122,85],[134,80],[205,-271],[155,-350],[173,-4],[177,-56],[-59,325],[133,473],[126,155],[-44,147],[121,338],[168,208],[142,-70],[234,111],[-5,302],[-204,195],[148,86],[184,-147],[148,-242],[234,-151],[79,60],[172,-182],[162,169],[105,-51],[65,113],[127,-292],[-74,-316],[-105,-239],[-96,-20],[32,-236],[-81,-295],[-99,-291],[20,-166],[221,-327],[214,-189],[143,-204],[201,-350],[78,1],[145,-151],[43,-183],[265,-200],[183,202],[55,317],[56,262],[34,324],[85,470],[-39,286],[20,171],[-32,339],[37,445],[53,120],[-43,197],[67,313],[52,325],[7,168],[104,222],[78,-289],[19,-371],[70,-71],[11,-249],[101,-300],[21,-335],[-10,-214],[100,-464],[179,223],[92,-250],[133,-231],[-29,-262],[60,-506],[42,-295],[70,-72],[75,-505],[-27,-307],[90,-400],[301,-309],[197,-281],[186,-257],[-37,-143],[159,-371],[108,-639],[111,130],[113,-256],[68,91],[48,-626],[197,-363],[129,-226],[217,-478],[78,-475],[7,-337],[-19,-365],[132,-502],[-16,-523],[-48,-274],[-75,-527],[6,-339],[-55,-423],[-123,-538],[-205,-290],[-102,-458],[-93,-292],[-82,-510],[-107,-294],[-70,-442],[-36,-407],[14,-187],[-159,-205],[-311,-22],[-257,-242],[-127,-229],[-168,-254]],[[95110,44183],[-194,4],[-106,363],[166,-142],[56,-22],[78,-203]],[[83414,44519],[-368,414],[259,116],[146,-180],[97,-180],[-17,-159],[-117,-11]],[[94572,44733],[-170,60],[-58,91],[17,235],[183,-93],[91,-124],[45,-155],[-108,-14]],[[94868,44799],[-206,512],[-57,353],[94,0],[100,-473],[111,-283],[-42,-109]],[[84713,45326],[32,139],[239,133],[194,20],[87,74],[105,-74],[-102,-160],[-289,-258],[-233,-170]],[[84746,45030],[-181,-441],[-238,-130],[-33,71],[25,201],[119,360],[275,235]],[[82576,45238],[-149,5],[95,340],[153,5],[74,209],[100,-158],[172,48],[69,-251],[-321,-119],[-193,-79]],[[83681,45301],[-370,73],[0,216],[220,123],[174,-177],[185,45],[249,216],[-41,-328],[-417,-168]],[[94421,45535],[-218,251],[-152,212],[-104,197],[41,60],[128,-142],[228,-272],[65,-187],[12,-119]],[[93704,46205],[-121,134],[-114,243],[14,99],[166,-250],[111,-193],[-56,-33]],[[81823,45409],[-306,238],[-251,-16],[-288,44],[-260,106],[-322,225],[-204,59],[-116,-74],[-506,243],[-48,254],[-255,44],[191,564],[337,-35],[224,-231],[115,-45],[38,-210],[533,-59],[61,244],[515,-284],[101,-383],[417,-108],[341,-351],[-317,-225]],[[87280,46506],[-27,445],[49,212],[58,200],[63,-173],[0,-282],[-143,-402]],[[93221,46491],[-120,227],[-122,375],[-59,450],[38,57],[30,-175],[84,-134],[135,-375],[131,-200],[-39,-166],[-78,-59]],[[91733,46847],[-148,1],[-228,171],[-158,165],[23,183],[249,-86],[152,46],[42,283],[40,15],[27,-314],[158,45],[78,202],[155,211],[-30,348],[166,11],[56,-97],[-5,-327],[-93,-361],[-146,-48],[-44,-166],[-152,-144],[-142,-138]],[[85242,48340],[-192,108],[-54,254],[281,29],[69,-195],[-104,-196]],[[86342,48300],[-234,244],[-232,49],[-157,-39],[-192,21],[65,325],[344,24],[305,-172],[101,-452]],[[92451,47764],[-52,348],[-65,229],[-126,193],[-158,252],[-200,174],[77,143],[150,-166],[94,-130],[117,-142],[111,-248],[106,-189],[33,-307],[-87,-157]],[[89166,49043],[482,-407],[513,-338],[192,-302],[154,-297],[43,-349],[462,-365],[68,-313],[-256,-64],[62,-393],[248,-388],[180,-627],[159,20],[-11,-262],[215,-100],[-84,-111],[295,-249],[-30,-171],[-184,-41],[-69,153],[-238,66],[-281,89],[-216,377],[-158,325],[-144,517],[-362,259],[-235,-169],[-170,-195],[35,-436],[-218,-203],[-155,99],[-288,25]],[[89175,45193],[-247,485],[-282,118],[-69,-168],[-352,-18],[118,481],[175,164],[-72,642],[-134,496],[-538,500],[-229,50],[-417,546],[-82,-287],[-107,-52],[-63,216],[-1,257],[-212,290],[299,213],[198,-11],[-23,156],[-407,1],[-110,352],[-248,109],[-117,293],[374,143],[142,192],[446,-242],[44,-220],[78,-955],[287,-354],[232,627],[319,356],[247,1],[238,-206],[206,-212],[298,-113]],[[83276,47228],[-119,173],[79,544],[-43,570],[-117,4],[-86,405],[115,387],[40,469],[139,891],[58,243],[237,439],[217,-174],[350,-82],[319,25],[275,429],[48,-132],[-223,-587],[-209,-113],[-267,115],[-463,-29],[-243,-85],[-39,-447],[248,-526],[150,268],[518,201],[-22,-272],[-121,86],[-121,-347],[-245,-229],[263,-757],[-50,-203],[249,-682],[-2,-388],[-148,-173],[-109,207],[134,484],[-273,-229],[-69,164],[36,228],[-200,346],[21,576],[-186,-179],[24,-689],[11,-846],[-176,-85]],[[85582,50048],[-112,374],[-82,755],[56,472],[92,215],[20,-322],[164,-52],[26,-241],[-15,-517],[-143,58],[-42,-359],[114,-312],[-78,-71]],[[79085,47110],[-234,494],[-356,482],[-119,358],[-210,481],[-138,443],[-212,827],[-244,493],[-81,508],[-103,461],[-250,372],[-145,506],[-209,330],[-290,652],[-24,300],[178,-24],[430,-114],[246,-577],[215,-401],[153,-246],[263,-635],[283,-9],[233,-405],[161,-495],[211,-270],[-111,-482],[159,-205],[100,-15],[47,-412],[97,-330],[204,-52],[135,-374],[-70,-735],[-11,-914],[-308,-12]],[[80461,51765],[204,-202],[214,110],[56,500],[119,112],[333,128],[199,467],[137,374]],[[81723,53254],[110,221],[236,323]],[[82069,53798],[214,411],[140,462],[112,2],[143,-299],[13,-257],[183,-165],[231,-177],[-20,-232],[-186,-29],[50,-289],[-205,-201]],[[82744,53024],[-158,-533],[204,-560],[-48,-272],[312,-546],[-329,-70],[-93,-403],[12,-535],[-267,-404],[-7,-589],[-107,-903],[-41,210],[-316,-266],[-110,361],[-198,34],[-139,189],[-330,-212],[-101,285],[-182,-32],[-229,68],[-43,793],[-138,164],[-134,505],[-38,517],[32,548],[165,392]],[[84832,53877],[-327,343],[-78,428],[84,280],[-176,280],[-87,-245],[-131,23],[-205,-330],[-46,173],[109,498],[175,166],[151,223],[98,-268],[212,162],[45,264],[196,15],[-16,457],[225,-280],[23,-297],[20,-218],[28,-392],[16,-332],[-94,-540],[-102,602],[-130,-300],[89,-435],[-79,-277]],[[72318,54106],[-132,470],[-49,849],[126,959],[192,-328],[129,-416],[134,-616],[-42,-615],[-116,-168],[-242,-135]],[[32841,56488],[-50,53],[81,163],[-6,233],[160,77],[58,-21],[-11,-440],[-232,-65]],[[84165,55910],[-171,409],[57,158],[70,165],[30,367],[153,35],[-44,-398],[205,570],[-26,-563],[-100,-195],[-87,-373],[-87,-175]],[[82548,55523],[136,414],[200,364],[167,409],[146,587],[49,-482],[-183,-325],[-146,-406],[-369,-561]],[[83889,56748],[-10,275],[20,301],[-43,282],[166,-183],[177,1],[-5,-247],[-129,-251],[-176,-178]],[[84666,56567],[-11,416],[-84,31],[-43,357],[163,-47],[-4,224],[-169,451],[266,-13],[77,-220],[78,-660],[-214,157],[5,-199],[68,-364],[-132,-133]],[[83683,57791],[-119,295],[-142,450],[238,-22],[97,-213],[-74,-510]],[[84465,57987],[-216,290],[-103,310],[-71,-217],[-177,354],[-253,-87],[-138,130],[14,244],[87,151],[-83,136],[-36,-213],[-137,340],[-41,257],[-11,566],[112,-195],[29,925],[90,535],[169,-1],[171,-168],[85,153],[26,-150],[-46,-245],[95,-423],[-73,-491],[-164,-196],[-43,-476],[62,-471],[147,-65],[123,70],[347,-328],[-27,-321],[91,-142],[-29,-272]],[[31337,61183],[-16,253],[40,86],[227,-3],[142,-52],[50,-118],[-71,-149],[-209,4],[-163,-21]],[[28554,61038],[-156,95],[-159,215],[34,135],[116,41],[64,-20],[187,-53],[147,-142],[46,-161],[-195,-11],[-84,-99]],[[30080,62227],[34,101],[217,-3],[165,-152],[73,15],[50,-209],[152,11],[-9,-176],[124,-21],[136,-217],[-103,-240],[-132,128],[-127,-25],[-92,28],[-50,-107],[-106,-37],[-43,144],[-92,-85],[-111,-405],[-71,94],[-14,170]],[[30081,61241],[-185,100],[-131,-41],[-169,43],[-130,-110],[-149,184],[24,190],[256,-82],[210,-47],[100,131],[-127,256],[2,226],[-175,92],[62,163],[170,-26],[241,-93]],[[80409,61331],[-228,183],[-8,509],[137,267],[304,166],[159,-14],[62,-226],[-122,-260],[-64,-341],[-240,-284]],[[6753,61756],[-69,84],[8,165],[-46,216],[14,65],[48,97],[-19,116],[16,55],[21,-11],[107,-100],[49,-51],[45,-79],[71,-207],[-7,-33],[-108,-126],[-89,-92],[-41,-99]],[[6551,62734],[-47,125],[-32,48],[-3,37],[27,50],[99,-56],[73,-90],[-23,-71],[-94,-43]],[[6447,63028],[-149,17],[21,72],[137,-26],[-9,-63]],[[6192,63143],[-19,8],[-97,21],[-35,133],[-11,24],[74,82],[23,-38],[80,-196],[-15,-34]],[[5704,63509],[-93,107],[14,43],[43,58],[64,-12],[5,-138],[-33,-58]],[[28401,62311],[186,329],[-113,154],[-179,39],[-96,171],[-66,336],[-157,-23],[-259,159],[-83,124],[-362,91],[-97,115],[104,148],[-273,30],[-199,-307],[-115,-8],[-40,-144],[-138,-65],[-118,56],[146,183],[60,213],[126,131],[142,116],[210,56],[67,65],[240,-42],[219,-7],[261,-201],[110,-216],[260,66],[98,-138],[235,-366],[173,-267],[92,8],[165,-120],[-20,-167],[205,-24],[210,-242],[-33,-138],[-185,-75],[-187,-29],[-191,46],[-398,-57]],[[28394,64588],[-70,340],[-104,171],[60,375],[84,-23],[97,-491],[1,-343],[-68,-29]],[[83540,63560],[-146,499],[-32,438],[163,581],[223,447],[127,-176],[-49,-357],[-167,-947],[-119,-485]],[[28080,66189],[-19,219],[130,47],[184,-18],[8,-153],[-303,-95]],[[28563,65870],[-51,75],[4,309],[-124,234],[-1,67],[220,-265],[-48,-420]],[[86948,69902],[-181,168],[2,281],[154,352],[158,-68],[114,248],[204,-127],[35,-203],[-156,-357],[-114,189],[-143,-137],[-73,-346]],[[59437,71293],[8,-48],[-285,-240],[-136,77],[-64,237],[132,22]],[[59092,71341],[19,3],[40,143],[200,-8],[253,176],[-188,-251],[21,-111]],[[56867,71211],[3,98],[-339,115],[52,251],[152,-199],[216,34],[207,-42],[-7,-103],[151,71],[-35,-175],[-400,-50]],[[54194,72216],[-213,222],[-141,64],[-387,300],[38,304],[325,-54],[284,64],[211,51],[-100,-465],[41,-183],[-58,-303]],[[52446,73567],[-105,156],[-11,713],[-64,338],[153,-30],[139,183],[166,-419],[-39,-782],[-126,38],[-113,-197]],[[86301,68913],[-135,229],[69,533],[-176,172],[-113,405],[263,182],[145,371],[280,306],[203,403],[553,177],[297,-121],[291,1050],[185,-282],[408,591],[158,229],[174,723],[-47,664],[117,374],[295,108],[152,-819],[-9,-479],[-256,-595],[4,-610],[-104,-472],[48,-296],[-145,-416],[-355,-278],[-488,-36],[-396,-675],[-186,227],[-12,442],[-483,-130],[-329,-279],[-325,-11],[282,-435],[-186,-1004],[-179,-248]],[[52563,75028],[-126,120],[-64,398],[56,219],[179,226],[47,-507],[-92,-456]],[[88876,75140],[-39,587],[138,455],[296,33],[81,817],[83,460],[326,-615],[213,-198],[195,-126],[197,250],[62,-663],[-412,-162],[-244,-587],[-436,404],[-152,-646],[-308,-9]],[[32535,77739],[-353,250],[-69,198],[105,183],[97,-288],[202,-79],[257,16],[-137,-242],[-102,-38]],[[32696,79581],[-360,186],[-258,279],[96,49],[365,-148],[284,-247],[8,-108],[-135,-11]],[[15552,79158],[-456,269],[-84,209],[-248,207],[-50,168],[-286,107],[-107,321],[24,137],[291,-129],[171,-89],[261,-63],[94,-204],[138,-280],[277,-244],[115,-327],[-140,-82]],[[35133,78123],[-183,111],[60,484],[-77,75],[-322,-513],[-166,21],[196,277],[-267,144],[-298,-35],[-539,18],[-43,175],[173,208],[-121,160],[234,356],[287,941],[172,336],[241,204],[129,-26],[-54,-160],[-148,-372],[-184,-517],[181,199],[187,-126],[-98,-206],[247,-162],[128,144],[277,-182],[-86,-433],[194,101],[36,-313],[86,-367],[-117,-520],[-125,-22]],[[13561,81409],[-111,1],[-167,270],[-103,272],[-140,184],[-51,260],[16,188],[131,-76],[267,47],[-84,-671],[242,-475]],[[89469,77738],[-51,496],[31,575],[-32,638],[64,446],[13,790],[-163,581],[24,808],[257,271],[-110,274],[123,83],[73,-391],[96,-569],[-7,-581],[114,-597],[280,-1046],[-411,195],[-171,-854],[271,-605],[-8,-413],[-211,356],[-182,-457]],[[47896,83153],[233,24],[298,-365],[-149,-406]],[[48278,82406],[46,-422],[-210,-528],[-493,-349],[-393,89],[225,617],[-145,601],[378,463],[210,276]],[[53358,82957],[-291,333],[-39,246],[408,195],[88,-296],[-166,-478]],[[7221,84100],[-142,152],[-43,277],[252,210],[148,90],[185,-40],[117,-183],[-240,-281],[-277,-225]],[[48543,80097],[-148,118],[407,621],[249,127],[-436,99],[-79,235],[291,183],[-152,319],[52,387],[414,-54],[40,343],[-190,372],[-337,104],[-66,160],[101,264],[-92,163],[-149,-279],[-17,569],[-140,301],[101,611],[216,480],[222,-47],[335,49],[-297,-639],[283,81],[304,-3],[-72,-481],[-250,-530],[287,-38],[270,-759],[190,-95],[171,-673],[79,-233],[337,-113],[-34,-378],[-142,-173],[111,-305],[-250,-310],[-371,6],[-473,-163],[-130,116],[-183,-276],[-257,67],[-195,-226]],[[3835,85884],[-182,110],[-168,161],[274,101],[220,-54],[27,-226],[-171,-92]],[[27873,86994],[-123,50],[-73,176],[13,41],[107,177],[114,-13],[70,-121],[-108,-310]],[[26925,87305],[-196,13],[-61,160],[207,273],[381,-6],[-6,-114],[-325,-326]],[[2908,87788],[-211,128],[-106,107],[-245,-34],[-66,52],[17,223],[171,-113],[173,61],[225,-156],[276,-79],[-23,-64],[-211,-125]],[[26243,87832],[-95,346],[-377,-57],[242,292],[35,465],[95,542],[201,-49],[51,-259],[143,91],[161,-155],[304,-203],[318,-184],[25,-281],[204,46],[199,-196],[-247,-186],[-432,142],[-156,266],[-275,-314],[-396,-306]],[[44817,88095],[-365,87],[-775,187],[273,261],[-605,289],[492,114],[-12,174],[-583,137],[188,385],[421,87],[433,-400],[422,321],[349,-167],[453,315],[461,-42],[-64,-382],[314,-403],[-361,-451],[-801,-405],[-240,-107]],[[28614,90223],[-69,289],[118,331],[255,82],[217,-163],[3,-253],[-32,-82],[-180,-174],[-312,-30]],[[1957,88542],[-260,17],[-212,206],[-369,172],[-62,257],[-283,96],[-315,-76],[-151,207],[60,219],[-333,-140],[126,-278],[-158,-251],[0,2354],[681,-451],[728,-588],[-24,-367],[187,-147],[-64,429],[754,-88],[544,-553],[-276,-257],[-455,-61],[-7,-578],[-111,-122]],[[23258,91203],[-374,179],[-226,-65],[-380,266],[245,183],[194,256],[295,-168],[166,-106],[84,-112],[169,-226],[-173,-207]],[[99694,92399],[-49,187],[354,247],[0,-404],[-305,-30]],[[0,92429],[0,404],[36,24],[235,-1],[402,-169],[-24,-81],[-286,-141],[-363,-36]],[[26228,91219],[16,648],[394,-45]],[[26638,91822],[411,-87],[373,-293],[17,-293],[-207,-315],[196,-316],[-36,-288],[-544,-413],[-386,-91],[-287,178],[-83,-297],[-268,-498]],[[25824,89109],[-81,-258],[-322,-400]],[[25421,88451],[-397,-39],[-220,-250],[-18,-384],[-323,-74],[-340,-479],[-301,-665],[-108,-466]],[[23714,86094],[-15,-686],[408,-99]],[[24107,85309],[125,-553],[130,-448],[388,117],[517,-256],[277,-225],[199,-279]],[[25743,83665],[348,-162],[294,-249]],[[26385,83254],[459,-34],[302,-58],[-45,-511],[86,-594],[201,-661],[414,-561],[214,192],[150,607],[-145,934],[-196,311],[445,276],[314,415],[154,411]],[[28738,83981],[-22,395],[-189,502]],[[28527,84878],[-338,445],[328,619],[-121,535],[-93,922],[194,137],[476,-161],[286,-57],[230,155],[258,-200],[342,-343],[85,-229],[495,-45],[-8,-496],[92,-747],[254,-92],[201,-348],[402,328],[266,652],[184,274],[216,-527],[362,-754],[307,-709],[-112,-371],[370,-333],[250,-338],[442,-152],[179,-189],[110,-500],[216,-78],[112,-223],[20,-664],[-202,-222],[-199,-207],[-458,-210],[-349,-486],[-470,-96],[-594,125],[-417,4],[-287,-41],[-233,-424],[-354,-262],[-401,-782],[-320,-545],[236,97],[446,776],[583,493]],[[31513,79609],[416,59],[245,-290]],[[32174,79378],[-262,-397],[88,-637],[91,-446],[361,-295],[459,86],[278,664],[19,-429],[180,-214],[-344,-387],[-615,-351],[-276,-239],[-310,-426],[-211,44],[-11,500],[483,488],[-445,-19],[-309,-72]],[[31350,77248],[48,-194],[-296,-286],[-286,-204],[-293,-175]],[[30523,76389],[-159,-386],[-35,-98]],[[30329,75905],[-3,-313],[92,-313],[115,-15],[-29,216],[83,-131],[-22,-169],[-188,-96]],[[30377,75084],[-133,12],[-205,-104]],[[30039,74992],[-121,-29],[-162,-29],[-231,-171],[408,111],[82,-112],[-389,-177],[-177,-1],[8,72],[-84,-164],[82,-27],[-60,-424],[-203,-455],[-20,152]],[[29172,73738],[-61,31],[-91,147]],[[29020,73916],[57,-318]],[[29077,73598],[66,-106],[8,-222]],[[29151,73270],[-89,-230],[-157,-472],[-25,24],[86,402]],[[28966,72994],[-142,226],[-33,490]],[[28791,73710],[-53,-255],[59,-375]],[[28797,73080],[-175,88],[183,-186]],[[28805,72982],[12,-562],[79,-41],[29,-204],[39,-591],[-176,-439],[-288,-175],[-182,-346],[-139,-38],[-141,-217],[-39,-199],[-305,-383],[-157,-281],[-131,-351],[-43,-419],[50,-411],[92,-505],[124,-418],[1,-256],[132,-685],[-9,-398],[-12,-230],[-69,-361]],[[27672,65472],[-83,-74],[-137,71]],[[27452,65469],[-44,259]],[[27408,65728],[-106,136],[-147,508]],[[27155,66372],[-129,452],[-42,231],[57,393],[-77,325],[-217,494]],[[26747,68267],[-108,91],[-281,-269]],[[26358,68089],[-49,30]],[[26309,68119],[-135,276],[-174,146]],[[26000,68541],[-314,-75],[-247,66],[-212,-41]],[[25227,68491],[-118,-83],[54,-166]],[[25163,68242],[-5,-240],[59,-117],[-53,-77],[-103,87],[-104,-112],[-202,18]],[[24755,67801],[-207,313],[-242,-74]],[[24306,68040],[-202,137],[-173,-42],[-234,-138],[-253,-438],[-276,-255],[-152,-282],[-63,-266],[-3,-407],[14,-284],[52,-201]],[[23016,65864],[1,-1],[-1,-1],[-107,-516]],[[22909,65346],[-49,-426],[-20,-791],[-27,-289],[48,-322],[86,-288],[56,-458],[184,-440],[65,-337],[109,-291],[295,-157],[114,-247],[244,165],[212,60],[208,106],[175,101],[176,241],[67,345],[22,496],[48,173],[188,155],[294,137],[246,-21],[169,50],[66,-125],[-9,-285],[-149,-351],[-66,-360],[51,-103],[-42,-255],[-69,-461],[-71,152],[-58,-10]],[[25472,61510],[1,-87],[53,-3],[-5,-160],[-45,-256],[24,-91],[-29,-212],[18,-56],[-32,-299],[-55,-156],[-50,-19],[-55,-205]],[[25297,59966],[90,-107],[24,88],[82,-75]],[[25493,59872],[29,-23],[61,104],[79,8],[26,-48],[43,29],[129,-53]],[[25860,59889],[128,16],[90,65]],[[26078,59970],[32,66],[89,-31],[66,-40],[73,14],[55,51],[127,-82],[44,-13],[85,-110],[80,-132],[101,-91],[73,-162]],[[26903,59440],[-24,-57],[-14,-132],[29,-216],[-64,-202],[-30,-237],[-9,-261],[15,-152],[7,-266],[-43,-58],[-26,-253],[19,-156],[-56,-151],[12,-159],[43,-97]],[[26762,57043],[70,-321],[108,-238],[130,-252]],[[27070,56232],[100,-212]],[[27170,56020],[-6,-125],[111,-27]],[[27275,55868],[26,48],[77,-145],[136,42],[119,150],[168,119],[95,176],[153,-34],[-10,-58],[155,-21],[124,-102],[90,-177],[105,-164]],[[28513,55702],[143,-18],[209,412],[114,63],[3,195],[51,500],[159,274],[175,11],[22,123],[218,-49],[218,298],[109,132],[134,285],[98,-36],[73,-156],[-54,-199]],[[30185,57537],[-8,-139],[-163,-69],[91,-268],[-3,-309]],[[30102,56752],[-123,-343],[105,-469]],[[30084,55940],[120,38],[62,427],[-86,208],[-14,447],[346,241],[-38,278],[97,186],[100,-415],[195,-9],[180,-330],[11,-195],[249,-6],[297,61],[159,-264]],[[31762,56607],[213,-73],[155,184]],[[32130,56718],[4,149],[344,35],[333,9],[-236,-175],[95,-279],[222,-44],[210,-291],[45,-473],[144,13],[109,-139]],[[33400,55523],[183,-217],[171,-385],[8,-304],[105,-14],[149,-289],[109,-205]],[[34125,54109],[333,-119],[30,107],[225,43],[298,-159]],[[35011,53981],[95,-65],[204,-140],[294,-499],[46,-242]],[[35650,53035],[95,28],[69,-327],[155,-1033],[149,-97],[7,-408],[-208,-487],[86,-178],[491,-92],[10,-593],[211,388],[349,-212],[462,-361],[135,-346],[-45,-327],[323,182],[540,-313],[415,23],[411,-489],[355,-662],[214,-170],[237,-24],[101,-186],[94,-752],[46,-358],[-110,-977],[-142,-385],[-391,-822],[-177,-668],[-206,-513],[-69,-11],[-78,-435],[20,-1107],[-77,-910],[-30,-390],[-88,-233],[-49,-790],[-282,-771],[-47,-610],[-225,-256],[-65,-355],[-302,2],[-437,-227],[-195,-263],[-311,-173],[-327,-470],[-235,-586],[-41,-441],[46,-326],[-51,-597],[-63,-289],[-195,-325],[-308,-1040],[-244,-468],[-189,-277],[-127,-562],[-183,-337]],[[35174,30629],[-121,-372],[-313,-328],[-205,118],[-151,-63],[-256,253],[-189,-19],[-169,327]],[[33770,30545],[-19,-308],[353,-506],[-38,-408],[173,-257],[-14,-289],[-267,-757],[-412,-317],[-557,-123],[-305,59],[59,-352],[-57,-442],[51,-298],[-167,-208],[-284,-82],[-267,216],[-108,-155],[39,-587],[188,-178],[152,186],[82,-307],[-255,-183],[-223,-367],[-41,-595],[-66,-316],[-262,-2],[-218,-302],[-80,-443]],[[31227,23224],[274,-433],[265,-119]],[[31766,22672],[-96,-531],[-328,-333],[-180,-692],[-254,-234],[-113,-276],[89,-614],[185,-342],[-117,30]],[[30952,19680],[-247,4],[-134,-145],[-250,-213],[-45,-552],[-118,-14],[-313,192],[-318,412],[-346,338],[-87,374],[79,346],[-140,393],[-36,1007],[119,568],[293,457],[-422,172],[265,522],[94,982],[309,-208],[145,1224],[-186,157],[-87,-738],[-175,83],[87,845],[95,1095],[127,404]],[[29661,27385],[-79,576],[-23,666]],[[29559,28627],[117,19],[170,954],[192,945],[118,881],[-64,885],[83,487],[-34,730],[163,721],[50,1143],[89,1227],[87,1321],[-20,967],[-58,832]],[[30452,39739],[-279,340],[-24,242],[-551,593],[-498,646],[-214,365],[-115,488],[46,170],[-236,775],[-274,1090],[-262,1177],[-114,269],[-87,435],[-216,386],[-198,239],[90,264],[-134,563],[86,414],[221,373]],[[27693,48568],[148,442],[-60,258],[-106,-275],[-166,259],[56,167],[-47,536],[97,89],[52,368],[105,381],[-20,241],[153,126],[190,236]],[[28095,51396],[-37,183],[103,44],[-12,296],[65,214],[138,40],[117,371],[106,310],[-102,141],[52,343],[-62,540],[59,155],[-44,500],[-112,315]],[[28366,54848],[-93,170],[-59,319],[68,158],[-70,40]],[[28212,55535],[-52,195],[-138,165]],[[28022,55895],[-122,-38],[-56,-205],[-112,-149],[-61,-20],[-27,-123],[132,-321],[-75,-76],[-40,-87],[-130,-30],[-48,353],[-36,-101],[-92,35],[-56,238],[-114,39],[-72,69],[-119,-1],[-8,-128],[-32,89]],[[26954,55439],[-151,131],[-56,124],[32,103],[-11,130],[-77,142],[-109,116],[-95,76],[-19,173],[-73,105],[18,-172],[-55,-141],[-64,164],[-89,58],[-38,120],[2,179],[36,187],[-78,83],[64,114]],[[26191,57131],[-96,186],[-130,238],[-61,200],[-117,185],[-140,267]],[[25647,58207],[31,92],[46,-89]],[[25724,58210],[21,41]],[[25745,58251],[-48,185]],[[25697,58436],[-84,52],[-31,-140]],[[25582,58348],[-161,9],[-100,57],[-115,117],[-154,37],[-79,127]],[[24973,58695],[-142,103],[-174,11],[-127,117],[-149,244]],[[24381,59170],[-314,636]],[[24067,59806],[-144,192],[-226,154]],[[23697,60152],[-156,-43],[-223,-223],[-140,-58],[-196,156],[-208,112],[-260,271],[-208,83],[-314,275],[-233,282],[-70,158],[-155,35],[-284,187],[-116,270],[-299,335],[-139,373],[-66,288],[93,57],[-29,169],[64,153],[1,204],[-93,266],[-25,235],[-94,298],[-244,587],[-280,462],[-135,368],[-238,241],[-51,145],[42,365]],[[19641,66203],[-142,137],[-164,288]],[[19335,66628],[-69,412],[-149,48],[-162,311],[-130,288],[-12,184],[-149,446],[-99,452],[5,227]],[[18570,68996],[-201,235],[-93,-26]],[[18276,69205],[-159,163],[-44,-240],[46,-284],[27,-444],[95,-243],[206,-407],[46,-139],[42,-42],[37,-203],[49,8],[56,-381],[85,-150],[59,-210],[174,-300],[92,-550],[83,-259],[77,-277],[15,-311],[134,-20],[112,-268],[100,-264],[-6,-106],[-117,-217],[-49,3],[-74,359]],[[19362,64423],[-182,337],[-200,286]],[[18980,65046],[-142,150],[9,432],[-42,320],[-132,183],[-191,264],[-37,-76],[-70,154],[-171,143],[-164,343],[20,44],[115,-33],[103,221],[10,266],[-214,422],[-163,163],[-102,369],[-103,388],[-129,472],[-113,531]],[[17464,69802],[-46,302],[-180,340],[-130,71],[-30,169],[-156,30],[-100,159],[-258,59]],[[16564,70932],[-70,95],[-34,324]],[[16460,71351],[-270,594],[-231,821],[10,137],[-123,195],[-215,495],[-38,482],[-148,323],[61,489],[-10,507],[-89,453],[109,557],[67,1072],[-50,792],[-88,506],[-80,274],[33,115],[402,-200],[148,-558]],[[15948,78405],[68,156],[-44,485],[-94,484]],[[15878,79530],[-38,1],[-537,581],[-199,255]],[[15104,80367],[-503,245],[-155,523],[40,362]],[[14486,81497],[-356,252],[-48,476],[-336,429],[-6,304]],[[13740,82958],[-153,223],[-245,188],[-78,515],[-358,478],[-150,558],[-267,38],[-441,15],[-326,170],[-574,613],[-266,112],[-486,211]],[[10396,86079],[-385,-50],[-546,271]],[[9465,86300],[-330,252],[-309,-125],[58,-411],[-154,-38],[-321,-123],[-245,-199]],[[8164,85656],[-307,-126],[-40,348]],[[7817,85878],[125,580],[295,182],[-76,148],[-354,-329],[-190,-394],[-400,-420],[203,-287],[-262,-424]],[[7158,84934],[-299,-247],[-278,-181]],[[6581,84506],[-69,-261],[-434,-305],[-87,-278],[-325,-252],[-191,45],[-259,-165],[-282,-201],[-231,-197],[-477,-169],[-43,99],[304,276],[271,182],[296,324],[345,66],[137,243],[385,353],[62,119],[205,208],[48,448],[141,349],[-320,-179],[-90,102],[-150,-215],[-181,300],[-75,-212],[-104,294],[-278,-236],[-170,0],[-24,352]],[[4985,85596],[50,217],[-179,210]],[[4856,86023],[-361,-113],[-235,277],[-190,142],[-1,334],[-214,252],[108,340],[226,330],[99,303],[225,43],[191,-94],[224,285],[201,-51],[212,183],[-52,270],[-155,106],[205,228],[-170,-7],[-295,-128],[-85,-131],[-219,131],[-392,-67],[-407,142],[-117,238],[-351,343],[390,247],[620,289],[228,0]],[[4541,89915],[-38,-295],[586,22]],[[5089,89642],[-225,366]],[[4864,90008],[-342,226],[-197,295]],[[4325,90529],[-267,252],[-381,187],[155,309],[493,19],[350,270],[66,287],[284,281],[271,68],[526,262],[256,-40],[427,315],[421,-124],[201,-266],[123,114],[469,-35],[-16,-136],[425,-101],[283,59],[585,-186],[534,-56],[214,-77],[370,96],[421,-177],[302,-83]],[[10837,91767],[518,-142]],[[11355,91625],[438,-284],[289,-55]],[[12082,91286],[244,247],[336,184],[413,-72],[416,259],[455,148],[191,-245],[207,138],[62,278],[192,-63],[470,-530],[369,401]],[[15437,92031],[38,-448],[341,96]],[[15816,91679],[105,173],[337,-34],[424,-248],[650,-217],[383,-100],[272,38]],[[17987,91291],[375,-300],[-391,-293]],[[17971,90698],[502,-127],[750,70],[236,103],[296,-354],[302,299],[-283,251],[179,202],[338,27],[223,59],[224,-141],[279,-321],[310,47],[491,-266],[431,94],[405,-14],[-32,367],[247,103],[431,-200],[-2,-559],[177,471],[223,-16],[126,594],[-298,364],[-324,239],[22,653],[329,429],[366,-95],[281,-261],[378,-666],[-247,-290],[517,-120],[-1,-604],[371,463],[332,-380],[-83,-438],[269,-399],[290,427],[202,510]],[[19722,91216],[-824,-103],[-374,-41]],[[18524,91072],[-151,279],[-379,161],[-246,-66],[-343,468],[185,62],[429,101],[392,-26],[362,103],[-537,138],[-594,-47],[-394,12],[-146,217],[644,237],[-428,-9],[-485,156],[233,443],[193,235],[744,359],[284,-114],[-139,-277],[618,179],[386,-298],[314,302],[254,-194],[227,-580],[140,244],[-197,606],[244,86],[276,-94],[311,-239],[175,-575],[86,-417],[466,-293],[502,-279],[-31,-260],[-456,-48],[178,-227],[-94,-217],[-503,93],[-478,160],[-322,-36],[-522,-201]],[[20728,93568],[-434,413],[95,83],[372,24],[211,-130],[-244,-390]],[[27920,93557],[-80,36],[-306,313],[12,213],[133,39],[636,-63],[479,-325],[25,-163],[-296,17],[-299,13],[-304,-80]],[[31620,87170],[-753,236],[-596,343],[-337,287],[97,167],[-414,304],[-405,286],[5,-171],[-803,-94],[-235,203],[183,435],[522,10],[571,76],[-92,211],[96,294],[360,576],[-77,261],[-107,203],[-425,286],[-563,201],[178,150],[-294,367],[-245,34],[-219,201],[-149,-175],[-503,-76],[-1011,132],[-588,174],[-450,89],[-231,207],[290,270],[-394,2],[-88,599],[213,528],[286,241],[717,158],[-204,-382],[219,-369],[256,477],[704,242],[477,-611],[-42,-387],[550,172],[263,235],[616,-299],[383,-282],[36,-258],[515,134],[290,-376],[670,-234],[242,-238],[263,-553],[-510,-275],[654,-386],[441,-130],[400,-543],[437,-39],[-87,-414],[-487,-687],[-342,253],[-437,568],[-359,-74],[-35,-338],[292,-344],[377,-272],[114,-157],[181,-584],[-96,-425],[-350,160],[-697,473],[393,-509],[289,-357],[45,-206]],[[22678,92689],[-268,50],[-192,225],[-690,456],[5,189],[567,-73],[-306,386],[329,286],[331,-124],[496,75],[72,-172],[-259,-283],[420,-254],[-50,-532],[-455,-229]],[[89468,93831],[-569,66],[-49,31],[263,234],[348,54],[394,-226],[34,-155],[-421,-4]],[[23814,93133],[-317,22],[-173,519],[4,294],[145,251],[276,161],[579,-20],[530,-144],[-415,-526],[-331,-115],[-298,-442]],[[15808,92470],[-147,259],[-641,312]],[[15020,93041],[93,193],[218,489]],[[15331,93723],[241,388],[-272,362],[939,93],[397,-123],[709,-33],[270,-171],[298,-249],[-349,-149],[-681,-415],[-344,-414]],[[16539,93012],[0,-248],[-731,-294]],[[91548,94707],[-444,53],[-516,233],[66,192],[518,-89],[697,-155],[-321,-234]],[[23845,94650],[-403,44],[-337,155],[148,266],[399,159],[243,-208],[101,-187],[-151,-229]],[[88598,94662],[-550,384],[149,406],[366,111],[734,-26],[1004,-313],[-219,-439],[-1023,16],[-461,-139]],[[22275,94831],[-298,94],[5,345],[-455,-46],[-18,457],[299,-18],[419,201],[390,-34],[22,77],[212,-273],[9,-303],[-127,-440],[-458,-60]],[[18404,94533],[-35,193],[577,261],[-1255,-70],[-389,106],[379,577],[262,165],[782,-199],[493,-350],[485,-45],[-397,565],[255,215],[286,-68],[94,-282],[109,-210],[247,99],[291,-26],[49,-289],[-169,-281],[-940,-91],[-701,-256],[-423,-14]],[[65817,92311],[-907,77],[-74,262],[-503,158],[-40,320],[284,126],[-10,323],[551,503],[-255,73],[665,518],[-75,268],[621,312],[917,380],[925,110],[475,220],[541,76],[193,-233],[-187,-184],[-984,-293],[-848,-282],[-863,-562],[-414,-577],[-435,-568],[56,-491],[531,-484],[-164,-52]],[[25514,94532],[-449,73],[-738,190],[-96,325],[-34,293],[-279,258],[-574,72],[-322,183],[104,242],[573,-37],[308,-190],[547,1],[240,-194],[-64,-222],[319,-134],[177,-140],[374,-26],[406,-50],[441,128],[566,51],[451,-42],[298,-223],[62,-244],[-174,-157],[-414,-127],[-355,72],[-797,-91],[-570,-11]],[[16250,95423],[-377,128],[472,442],[570,383],[426,-9],[381,87],[-38,-454],[-214,-205],[-259,-29],[-517,-252],[-444,-91]],[[81143,94175],[250,112],[142,-379]],[[81535,93908],[122,153],[444,93],[892,-97],[67,-276],[1162,-88],[15,451]],[[84237,94144],[590,-103],[443,3]],[[85270,94044],[449,-312],[128,-378],[-165,-247],[349,-465],[437,-240],[268,620],[446,-266],[473,159],[538,-182],[204,166],[455,-83],[-201,549],[367,256],[2509,-384],[236,-351],[727,-451],[1122,112],[553,-98],[231,-244],[-33,-432],[342,-168],[372,121],[492,15],[525,-116],[526,66],[484,-526],[344,189],[-224,378]],[[97224,91732],[123,263],[886,-166]],[[98233,91829],[578,36],[799,-282],[389,-258],[0,-2354],[-2,-3],[-357,-260],[-360,44],[250,-315],[166,-487],[128,-159],[32,-244],[-71,-157],[-518,129],[-777,-445],[-247,-69],[-425,-415],[-403,-362],[-102,-269],[-397,409],[-724,-464]],[[96192,85904],[-126,220],[-268,-254]],[[95798,85870],[-371,81],[-90,-388],[-333,-572],[10,-239],[316,-132],[-37,-860],[-258,-22],[-119,-494],[116,-255]],[[95032,82989],[-486,-301],[-96,-675]],[[94450,82013],[-415,-144],[-83,-600],[-400,-551],[-103,407],[-119,862],[-155,1313],[134,819],[234,353]],[[93543,84472],[15,276],[431,132]],[[93989,84880],[496,744],[479,608],[499,471],[223,833],[-337,-50],[-167,-487]],[[95182,86999],[-705,-648],[-227,726]],[[94250,87077],[-717,-201],[-696,-990],[230,-362],[-620,-154],[-430,-61],[20,427],[-431,90],[-344,-291],[-850,102]],[[90412,85637],[-913,-175],[-900,-1153]],[[88599,84309],[-1065,-1394],[438,-74],[136,-370],[270,-132]],[[88378,82339],[178,296],[305,-39]],[[88861,82596],[401,-650]],[[89262,81946],[9,-502],[-217,-591]],[[89054,80853],[-23,-705],[-126,-945],[-418,-855],[-94,-409],[-377,-688],[-374,-682],[-179,-349],[-370,-346],[-175,-8],[-175,287],[-373,-432],[-43,-197]],[[86327,75524],[-106,36]],[[86221,75560],[-120,-201],[-83,-201]],[[86018,75158],[10,-424],[-143,-130],[-50,-105],[-104,-174],[-185,-97],[-121,-159],[-9,-256],[-32,-65],[111,-96],[157,-259]],[[85652,73393],[240,-697],[68,-383],[3,-681],[-105,-325],[-252,-113],[-222,-245],[-250,-51],[-31,322]],[[85103,71220],[52,443],[-123,615]],[[85032,72278],[206,99],[-190,506]],[[85048,72883],[-135,113],[-34,-112]],[[84879,72884],[-81,-49],[-10,112],[-72,54],[-75,94]],[[84641,73095],[77,260],[65,69]],[[84783,73424],[-25,108],[71,319]],[[84829,73851],[-18,97],[-163,64]],[[84648,74012],[-131,158]],[[84517,74170],[-388,-171],[-204,-277],[-300,-161],[148,274],[-58,230],[220,397],[-147,310],[-242,-209],[-314,-411],[-171,-381],[-272,-29],[-142,-275],[147,-400],[227,-97],[9,-265]],[[83030,72705],[220,-172],[311,421]],[[83561,72954],[247,-230],[179,-15]],[[83987,72709],[46,-310],[-394,-165]],[[83639,72234],[-130,-319],[-270,-296],[-142,-414]],[[83097,71205],[299,-324],[109,-582]],[[83505,70299],[169,-541],[189,-454],[-5,-439],[-174,-161],[66,-315],[164,-184],[-43,-481],[-71,-468],[-155,-53],[-203,-640],[-225,-775],[-258,-705],[-382,-545],[-386,-498],[-313,-68],[-170,-262],[-96,192],[-157,-294],[-388,-296],[-294,-90],[-95,-624],[-154,-35],[-73,429],[66,228]],[[80517,63220],[-373,190],[-131,-97]],[[80013,63313],[-371,-505],[-231,-558],[-61,-410],[212,-623],[260,-772],[252,-365],[169,-475],[127,-1093],[-37,-1039],[-232,-389],[-318,-381],[-227,-492],[-346,-550],[-101,378],[78,401],[-206,335]],[[78981,56775],[-233,87],[-112,307],[-141,611]],[[78495,57780],[-249,271],[-238,-11],[41,464],[-245,-3],[-22,-650],[-150,-863],[-90,-522],[19,-428],[181,-18],[113,-539],[50,-512],[155,-338],[168,-69],[144,-306]],[[78372,54256],[64,-56],[164,-356],[116,-396],[16,-398],[-29,-269],[27,-203],[20,-349],[98,-163],[109,-523],[-5,-199],[-197,-40],[-263,438],[-329,469],[-32,301],[-161,395],[-38,489],[-100,322],[30,431],[-61,250]],[[77801,54399],[-110,227],[-47,292],[-148,334],[-135,280],[-45,-347],[-53,328],[30,369],[82,566]],[[77375,56448],[-27,439],[86,452],[-94,350],[23,644],[-113,306],[-90,707],[-50,746],[-121,490],[-183,-297],[-315,-421],[-156,53],[-172,138],[96,732],[-58,554],[-218,681],[34,213],[-163,76],[-197,481]],[[75657,62792],[-79,309],[-16,301],[-53,284]],[[75509,63686],[-116,344],[-256,23],[25,-243],[-87,-329],[-118,120],[-41,-108],[-78,65],[-108,53]],[[74730,63611],[-39,-216],[-189,7],[-343,-122],[16,-445],[-148,-349],[-400,-398],[-311,-695],[-209,-373]],[[73107,61020],[-276,-386],[-1,-272]],[[72830,60362],[-138,-146]],[[72692,60216],[-250,-212],[-130,-31]],[[72312,59973],[-84,-450],[58,-769],[15,-490],[-118,-561],[-1,-1004],[-144,-29],[-126,-450],[84,-195]],[[71996,56025],[-253,-167],[-93,-402]],[[71650,55456],[-112,-170],[-263,552],[-128,827],[-107,596],[-97,279],[-148,568],[-69,739],[-48,369],[-253,811],[-115,1145],[-83,756],[1,716],[-54,553],[-404,-353],[-196,70],[-362,716],[133,214],[-82,232],[-326,501]],[[68937,64577],[-203,150]],[[68734,64727],[-83,425],[-215,449]],[[68436,65601],[-512,-111],[-451,-11],[-391,-83]],[[67082,65396],[-523,179]],[[66559,65575],[-302,136],[-314,76]],[[65943,65787],[-118,725],[-133,105],[-214,-106],[-280,-286],[-339,196],[-281,454],[-267,168],[-186,561],[-205,788],[-149,-96],[-177,196]],[[63594,68492],[-103,-231],[-165,29]],[[63326,68290],[58,-261],[-25,-135],[89,-445]],[[63448,67449],[109,-510],[137,-135],[47,-207]],[[63741,66597],[190,-248],[16,-244]],[[63947,66105],[-27,-197],[35,-199],[80,-165],[37,-194],[41,-145]],[[64113,65205],[-18,430],[75,310],[76,64]],[[64246,66009],[84,-186],[5,-345]],[[64335,65478],[-61,-348]],[[64274,65130],[53,-226]],[[64327,64904],[49,29],[11,-162],[217,93],[230,-15],[168,-18],[190,400],[207,379],[176,364]],[[65575,65974],[80,201],[35,-51],[-26,-244],[-37,-108]],[[65627,65772],[38,-466]],[[65665,65306],[125,-404],[155,-214]],[[65945,64688],[204,-78],[164,-107]],[[66313,64503],[125,-339],[75,-196],[100,-75],[-1,-132],[-101,-352],[-44,-166],[-117,-189],[-104,-404],[-126,31],[-58,-141],[-44,-300],[34,-395],[-26,-72],[-128,2],[-174,-221],[-27,-288],[-63,-125],[-173,5],[-109,-149]],[[65352,60997],[1,-239],[-134,-164]],[[65219,60594],[-153,56],[-186,-199]],[[64880,60451],[-128,-33],[-201,-159]],[[64551,60259],[-54,-263],[-6,-201],[-277,-249],[-444,-276],[-249,-417]],[[63521,58853],[-122,-32],[-83,34]],[[63316,58855],[-163,-245]],[[63153,58610],[-177,-113],[-233,-31]],[[62743,58466],[-70,-34],[-61,-156],[-73,-43]],[[62539,58233],[-42,-150],[-138,13]],[[62359,58096],[-89,-80],[-192,30],[-72,345],[8,323],[-46,174],[-54,437],[-80,243],[56,29],[-29,270],[34,114],[-12,257]],[[61883,60238],[-36,253],[-84,177]],[[61763,60668],[-22,236],[-143,212],[-148,495],[-79,482],[-192,406],[-124,97],[-184,563],[-32,411],[12,350],[-159,655],[-130,231],[-150,122],[-92,339],[15,133]],[[60335,65400],[-77,307],[-81,131]],[[60177,65838],[-108,440],[-170,476],[-141,406],[-139,-3],[44,325],[12,206],[34,236]],[[59709,67924],[-9,86]],[[59700,68010],[-78,-238],[-60,-446],[-75,-308],[-65,-103],[-93,191],[-125,263],[-198,847],[-29,-53],[115,-624],[171,-594],[210,-920],[102,-321],[90,-334],[249,-654],[-55,-103],[9,-384],[323,-530],[49,-121]],[[60240,63578],[90,-580],[-61,-107],[40,-608],[102,-706],[106,-145],[152,-219]],[[60669,61213],[161,-683],[77,-543]],[[60907,59987],[152,-288],[379,-558],[154,-336],[151,-341],[87,-203],[136,-178]],[[61966,58083],[66,-183],[-9,-245],[-158,-142],[119,-161]],[[61984,57352],[91,-109]],[[62075,57243],[54,-244],[125,-248]],[[62254,56751],[138,-2],[262,151],[302,70],[245,184],[138,39],[99,108],[158,20]],[[63596,57321],[89,12],[128,88],[147,59],[132,202],[105,2],[6,-163],[-25,-344],[1,-310],[-59,-214],[-78,-639],[-134,-659],[-172,-755],[-238,-866],[-237,-661],[-327,-806],[-278,-479],[-415,-586],[-259,-450],[-304,-715],[-64,-312],[-63,-140]],[[61551,49585],[-195,-236],[-68,-246],[-104,-44],[-40,-416],[-89,-238],[-54,-393],[-112,-195]],[[60889,47817],[-128,-728],[16,-335],[178,-216],[8,-153],[-76,-357],[16,-180],[-18,-282],[97,-370],[115,-583],[101,-129]],[[61198,44484],[45,-265],[-11,-588],[34,-519],[11,-923],[49,-290],[-83,-422],[-108,-410],[-177,-366],[-254,-225],[-313,-287],[-313,-634],[-107,-108],[-194,-420],[-115,-136],[-23,-421],[132,-448],[54,-346],[4,-177],[49,29],[-8,-579]],[[59870,36949],[-45,-274],[65,-102]],[[59890,36573],[-41,-246],[-116,-210]],[[59733,36117],[-229,-199],[-334,-320],[-122,-219],[24,-248],[71,-40],[-24,-311]],[[59119,34780],[-70,-430],[-32,-491],[-72,-267],[-190,-298],[-54,-86],[-118,-300],[-77,-303],[-158,-424],[-314,-609],[-196,-355]],[[57838,31217],[-209,-269],[-291,-229]],[[57338,30719],[-141,-31],[-36,-164],[-169,88],[-138,-113],[-301,114],[-168,-72],[-115,31],[-286,-233],[-238,-94],[-171,-223],[-127,-14],[-117,210],[-94,11],[-120,264],[-13,-82],[-37,159],[2,346],[-90,396],[89,108],[-7,453],[-182,553],[-139,501],[-1,1],[-199,768]],[[54540,33696],[-207,446],[-108,432],[-62,575],[-68,428],[-93,910],[-7,707],[-35,322],[-108,243],[-144,489],[-146,708],[-60,371],[-226,577],[-17,453]],[[53259,40357],[-26,372],[38,519],[96,541],[15,254],[90,532],[66,243],[159,386],[90,263],[29,438],[-15,335],[-83,211],[-74,358],[-68,355],[15,122],[85,235],[-84,570],[-57,396],[-139,374],[26,115]],[[53422,46976],[-39,183]],[[53383,47159],[-74,444]],[[53309,47603],[-228,626]],[[53081,48229],[-285,596],[-184,488],[-169,610],[9,196],[61,189],[67,430],[56,438]],[[52636,51176],[-52,90],[96,663]],[[52680,51929],[40,467],[-108,390]],[[52612,52786],[-127,100],[-56,265]],[[52429,53151],[-71,85],[3,163]],[[52361,53399],[-289,-213]],[[52072,53186],[-105,32],[-107,-133]],[[51860,53085],[-222,13],[-149,370],[-91,427]],[[51398,53895],[-197,390],[-209,-8]],[[50992,54277],[-245,1]],[[50747,54278],[-229,-69]],[[50518,54209],[-224,-126]],[[50294,54083],[-436,-346],[-154,-203],[-250,-171],[-248,168]],[[49206,53531],[-126,-7],[-194,116],[-178,-7],[-329,-103],[-193,-170],[-275,-217],[-54,15]],[[47857,53158],[-73,-5],[-286,282]],[[47498,53435],[-252,450],[-237,323]],[[47009,54208],[-187,381]],[[46822,54589],[-75,44],[-200,238],[-144,316],[-49,216],[-34,437]],[[46320,55840],[-122,349],[-108,232],[-71,76],[-69,118],[-32,261],[-41,130],[-80,97]],[[45797,57103],[-149,247],[-117,39],[-63,166],[1,90],[-84,125],[-18,127]],[[45367,57897],[-46,453]],[[45321,58350],[36,262]],[[45357,58612],[-115,460],[-138,210],[122,112],[134,415],[66,304]],[[45426,60113],[-24,318],[78,291],[34,557],[-30,583],[-34,294],[28,295],[-72,281],[-146,255]],[[45260,62987],[12,249]],[[45272,63236],[13,274],[106,161],[91,308],[-18,200],[96,417],[155,376],[93,95],[74,344],[6,315],[100,365],[185,216],[177,603],[144,235]],[[46494,67145],[259,66],[219,403],[139,158]],[[47111,67772],[232,493],[-70,735],[106,508],[37,312],[179,399],[278,270],[206,244],[186,612],[87,362],[205,-2],[167,-251],[264,41],[288,-131],[121,-6]],[[49397,71358],[267,323],[300,102],[175,244],[268,180],[471,105],[459,48],[140,-87],[262,232],[297,5],[113,-137],[190,35]],[[52339,72408],[302,239],[195,-71],[-9,-299],[236,217],[20,-113]],[[53083,72381],[-139,-289],[-2,-274]],[[52942,71818],[96,-147],[-36,-511],[-183,-297],[53,-322],[143,-10],[70,-281],[106,-92]],[[53191,70158],[326,-204],[117,51],[232,-98],[368,-264],[130,-526],[250,-114],[391,-248],[296,-293],[136,153],[133,272],[-65,452],[87,288],[200,277],[192,80],[375,-121],[95,-264],[104,-2],[88,-101]],[[56646,69496],[276,-69],[68,-196]],[[56990,69231],[369,10],[268,-156],[275,-175],[129,-92],[214,188],[114,169],[245,49],[198,-75],[75,-293],[65,193],[222,-140],[217,-33],[137,149]],[[59518,69025],[80,194],[-19,34],[74,276],[56,446],[40,149],[8,6]],[[59757,70130],[99,482],[138,416],[5,21]],[[59999,71049],[-26,452],[68,243]],[[60041,71744],[-102,268],[105,222],[-169,-51],[-233,136],[-191,-340],[-421,-66],[-225,317],[-300,20],[-64,-245]],[[58441,72005],[-192,-71],[-268,315]],[[57981,72249],[-303,-10],[-165,587]],[[57513,72826],[-203,328],[135,459],[-176,283],[308,565],[428,23],[117,449],[529,-78],[334,383],[324,167],[459,13]],[[59768,75418],[485,-416],[399,-229]],[[60652,74773],[323,91],[239,-53],[328,309]],[[61542,75120],[42,252],[-70,403],[-160,218],[-154,68],[-102,181]],[[61098,76242],[-354,499],[-317,223],[-240,347],[202,95],[231,494],[-156,234],[410,241],[-8,129],[-249,-95]],[[60617,78409],[-222,-48],[-185,-191],[-260,-31],[-239,-220],[16,-368],[136,-142],[284,35],[-55,-210],[-304,-103],[-377,-342],[-154,121],[61,277],[-304,173],[50,113],[265,197],[-80,135],[-432,149],[-19,221],[-257,-73],[-103,-325],[-215,-437]],[[58223,77340],[6,-152],[-135,-128],[-84,56],[-78,-713]],[[57932,76403],[-144,-245],[-101,-422],[89,-337]],[[57776,75399],[33,-228],[243,-190],[-51,-145],[-330,-33],[-118,-182],[-232,-319]],[[57321,74302],[-87,275],[3,122]],[[57237,74699],[-169,17],[-145,56],[-336,-154],[192,-332],[-141,-96]],[[56638,74190],[-154,0],[-147,304]],[[56337,74494],[-52,-130],[62,-353],[139,-277]],[[56486,73734],[-105,-130],[155,-272]],[[56536,73332],[137,-171],[4,-334],[-257,157],[82,-302],[-176,-62],[105,-521]],[[56431,72099],[-184,-7],[-228,257],[-104,472]],[[55915,72821],[-49,393],[-108,272],[-143,337],[-18,168]],[[55597,73991],[-48,41],[-5,130],[-154,199],[-24,281],[23,403],[38,184]],[[55427,75229],[-46,93],[-59,46]],[[55322,75368],[-78,192],[-120,118]],[[55124,75678],[-261,218],[-161,213],[-254,176]],[[54448,76285],[-233,435],[56,44]],[[54271,76764],[-127,248],[-5,200],[-179,93],[-85,-255],[-82,198],[6,205],[10,9]],[[53809,77462],[62,54]],[[53871,77516],[-221,86],[-226,-210],[15,-293],[-34,-168],[91,-301],[261,-298],[140,-488],[309,-476],[217,3],[68,-130],[-78,-118]],[[54413,75123],[249,-213],[204,-179]],[[54866,74731],[238,-308],[29,-111],[-52,-211],[-154,276],[-242,97],[-116,-382],[200,-219],[-33,-309],[-116,-35],[-148,-506],[-116,-46],[1,181],[57,317],[60,126],[-108,342],[-85,298],[-115,74],[-82,255],[-179,107],[-120,238],[-206,38],[-217,267],[-254,384]],[[53108,75604],[-189,341],[-86,584]],[[52833,76529],[-138,68],[-226,195],[-128,-80],[-161,-274],[-115,-43]],[[52065,76395],[-252,-334],[-548,160],[-404,-192],[-32,-355]],[[50829,75674],[15,-344],[-263,-393],[-356,-125],[-25,-199],[-171,-327],[-107,-481],[108,-338],[-160,-263],[-60,-384],[-210,-118]],[[49600,72702],[-197,-455],[-352,-8]],[[49051,72239],[-265,11],[-174,-209],[-106,-223],[-136,49],[-103,199],[-79,340],[-259,92]],[[47929,72498],[-112,-153],[-146,83],[-143,-65],[42,462],[-26,363],[-124,55],[-67,224],[22,386],[111,215],[20,239],[58,355],[-6,250],[-56,212],[-12,200]],[[47490,75324],[14,420],[-114,257],[393,426]],[[47783,76427],[340,-107],[373,4]],[[48496,76324],[296,-101],[230,31],[449,-19]],[[49471,76235],[144,354],[53,1177],[-287,620],[-205,299]],[[49176,78685],[-424,228],[-28,430]],[[48724,79343],[360,129],[466,-152],[-88,669],[263,-254],[646,461],[84,484],[243,119]],[[50698,80799],[222,117]],[[50920,80916],[143,162]],[[51063,81078],[244,870],[380,247]],[[51687,82195],[231,-17]],[[51918,82178],[54,125],[232,32],[52,-130],[188,291],[-63,222],[-13,335]],[[52368,83053],[-113,328],[-8,604],[46,159]],[[52293,84144],[80,178],[244,36]],[[52617,84358],[98,163],[223,167],[-9,-304],[-82,-192],[33,-166],[151,-89],[-68,-223],[-83,64],[-200,-425],[76,-288]],[[52756,83065],[4,-228],[281,-138],[-3,-210],[283,111],[156,162],[313,-233],[132,-189]],[[53922,82340],[189,174],[434,273],[350,200],[277,-100],[21,-144],[268,-7]],[[55461,82736],[63,260],[383,191]],[[55907,83187],[-59,497]],[[55848,83684],[10,445],[136,371],[262,202],[221,-442],[223,12],[53,453]],[[56753,84725],[32,349],[-102,-75],[-176,210],[-24,340],[351,164],[350,86],[301,-97],[287,17]],[[57772,85719],[316,327],[-291,280]],[[57797,86326],[-504,-47],[-489,-216],[-452,-125]],[[56352,85938],[-161,322],[-269,195],[62,581]],[[55984,87036],[-135,534],[133,344]],[[55982,87914],[252,371],[635,640],[185,124],[-28,250],[-387,279]],[[56639,89578],[-478,-167],[-269,-413],[43,-361],[-441,-475],[-537,-509],[-202,-832],[198,-416],[265,-328],[-255,-666],[-289,-138],[-106,-992],[-157,-554],[-337,57],[-158,-468],[-321,-27],[-89,558],[-232,671],[-211,835]],[[53063,85353],[-187,363],[-548,-684]],[[52328,85032],[-370,-138],[-385,301]],[[51573,85195],[-99,635]],[[51474,85830],[-88,1364],[256,380]],[[51642,87574],[733,496],[549,609],[508,824],[668,1141],[465,444],[763,741],[610,259],[457,-31],[423,489],[506,-26],[499,118],[869,-433],[-358,-158],[305,-371]],[[58639,91676],[286,206],[456,-358],[761,-140],[1050,-668],[213,-281],[18,-393],[-308,-311],[-454,-157],[-1240,449],[-204,-75],[453,-433],[36,-878],[358,-180],[217,-153],[36,286]],[[60317,88590],[-174,263],[183,215]],[[60326,89068],[672,-368]],[[60998,88700],[234,144],[-187,433]],[[61045,89277],[647,578],[256,-34],[260,-206],[161,406],[-231,352],[136,353],[-204,367],[777,-190],[158,-331],[-351,-73]],[[62654,90499],[2,-328],[218,-203]],[[62874,89968],[429,128],[68,377]],[[63371,90473],[581,282],[969,507]],[[64921,91262],[209,-29],[-273,-359],[344,-61],[199,202],[521,16],[412,245],[317,-356],[315,391],[-291,343],[145,195],[820,-179],[385,-185],[1006,-675],[186,309],[-282,313],[-8,125],[-335,58],[92,280],[-149,461],[-8,189],[512,535]],[[69038,93080],[182,537],[207,116]],[[69427,93733],[735,-156],[58,-328]],[[70220,93249],[-263,-479],[173,-189],[89,-413],[-63,-809],[307,-362],[-120,-395],[-544,-839],[318,-87],[110,213],[306,151],[74,293],[240,281],[-162,336],[130,390],[-304,49],[-67,328]],[[70444,91717],[222,594],[-361,481]],[[70305,92792],[497,398],[-64,421],[139,13],[145,-328],[-109,-570],[297,-108],[-127,426],[465,233],[577,31],[513,-337],[-247,492],[-28,630]],[[72363,94093],[484,119],[668,-26]],[[73515,94186],[602,77],[-226,309],[321,388],[319,16],[540,293],[734,79],[93,162],[729,55],[227,-133],[624,314],[510,-10],[77,255],[265,252],[656,242],[476,-191],[-378,-146],[629,-90],[75,-292],[254,143],[812,-7],[626,-289],[223,-221],[-69,-307],[-307,-175],[-730,-328],[-209,-175],[345,-83],[410,-149]],[[63720,73858],[-47,-207],[-102,-138]],[[63571,73513],[7,-293]],[[63578,73220],[88,-436],[263,-123],[193,-296],[395,-102],[434,156],[27,139]],[[64978,72558],[-52,417],[40,618],[-216,200],[71,405],[-184,34],[61,498],[262,-145],[244,189],[-202,355],[-80,338],[-224,-151],[-28,-433],[-87,383]],[[64583,75266],[-15,144],[68,246],[-53,206],[-322,202],[-125,530],[-154,150],[-9,192],[270,-56],[11,432],[236,96],[243,-88],[50,576],[-50,365],[-278,-28],[-236,144],[-321,-260],[-259,-124]],[[63639,77993],[-127,-350],[-269,-97],[-276,-610],[252,-561],[-27,-398],[303,-696]],[[63495,75281],[146,-311],[141,-419],[130,-28],[85,-159],[-228,-47],[-49,-459]],[[23807,96363],[-521,38],[-74,165],[559,-9],[195,-109],[-33,-68],[-126,-17]],[[18874,96315],[-411,191],[224,188],[406,60],[392,-92],[-93,-177],[-518,-170]],[[56247,96336],[-490,137],[191,152],[-167,189],[575,119],[110,-222],[401,-134],[-620,-241]],[[19199,96904],[-461,1],[5,84],[285,177],[149,-27],[361,-120],[-339,-115]],[[22969,96575],[-226,138],[-119,221],[-22,245],[360,-24],[162,-39],[332,-205],[-76,-214],[-411,-122]],[[22313,96609],[-453,66],[-457,192],[-619,21],[268,176],[-335,142],[-21,227],[546,-81],[751,-215],[212,-281],[108,-247]],[[77621,96617],[507,776],[229,66],[208,-38],[704,-336],[-82,-240],[-1566,-228]],[[54420,95937],[-598,361],[252,210],[-416,170],[-541,499],[-216,463],[757,212],[152,-207],[396,8],[105,202],[408,20],[350,-206],[915,-440],[-699,-233],[-155,-435],[-243,-111],[-132,-490],[-335,-23]],[[56395,97491],[-819,98],[-50,163],[-398,11],[-304,271],[858,165],[403,-142],[281,177],[702,-148],[545,-207],[-412,-318],[-806,-70]],[[63218,97851],[-301,140],[158,185],[-618,18],[542,107],[422,8],[57,-160],[159,142],[262,97],[412,-129],[-107,-90],[-373,-78],[-250,-45],[-39,-97],[-324,-98]],[[77154,97111],[-773,170],[-462,226],[-213,423],[-379,117],[722,404],[600,133],[540,-297],[640,-572],[-69,-531],[-606,-73]],[[24776,96791],[-575,76],[-299,240],[4,215],[220,157],[-508,-4],[-306,196],[-176,268],[193,262],[192,180],[285,42],[-122,135],[646,30],[355,-315],[468,-127],[455,-112],[220,-390],[334,-190],[-381,-176],[-513,-445],[-492,-42]],[[27622,95587],[-726,163],[-816,-91],[-414,71],[-525,31],[-35,284],[514,133],[-137,427],[170,41],[742,-255],[-379,379],[-450,113],[225,229],[492,141],[79,206],[-392,231],[-118,304],[759,-26],[220,-64],[433,216],[-625,68],[-972,-38],[-491,201],[-232,239],[-324,173],[-61,202],[413,112],[324,19],[545,96],[409,220],[344,-30],[300,-166],[211,319],[367,95],[498,65],[849,24],[148,-63],[802,100],[601,-38],[602,-37],[742,-47],[597,-75],[508,-161],[-12,-157],[-678,-257],[-672,-119],[-251,-133],[605,3],[-656,-358],[-452,-167],[-476,-483],[-573,-98],[-177,-120],[-841,-64],[383,-74],[-192,-105],[230,-292],[-264,-202],[-429,-167],[-132,-232],[-388,-176],[39,-134],[475,23],[6,-144],[-742,-355]],[[37559,86051],[-410,482],[-556,3],[-269,324],[-186,577],[-481,735],[-141,385],[-38,530],[-384,546],[100,435],[-186,208],[275,691],[418,220],[110,247],[58,461],[-318,-209],[-151,-88],[-249,-84],[-341,193],[-19,401],[109,314],[258,9],[567,-157],[-478,375],[-249,202],[-276,-83],[-232,147],[310,550],[-169,220],[-220,409],[-335,626],[-353,230],[3,247],[-745,346],[-590,43],[-743,-24],[-677,-44],[-323,188],[-482,372],[729,186],[559,31],[-1188,154],[-627,241],[39,229],[1051,285],[1018,284],[107,214],[-750,213],[243,235],[961,413],[404,63],[-115,265],[658,156],[854,93],[853,5],[303,-184],[737,325],[663,-221],[390,-46],[577,-192],[-660,318],[38,253],[932,353],[975,-27],[354,218],[982,57],[2219,-74],[1737,-469],[-513,-227],[-1062,-26],[-1496,-58],[140,-105],[984,65],[836,-204],[540,181],[231,-212],[-305,-344],[707,220],[1348,229],[833,-114],[156,-253],[-1132,-420],[-157,-136],[-888,-102],[643,-28],[-324,-431],[-224,-383],[9,-658],[333,-386],[-434,-24],[-457,-187],[513,-313],[65,-502],[-297,-55],[360,-508],[-617,-42],[322,-241],[-91,-208],[-391,-91],[-388,-2],[348,-400],[4,-263],[-549,244],[-143,-158],[375,-148],[364,-361],[105,-476],[-495,-114],[-214,228],[-344,340],[95,-401],[-322,-311],[732,-25],[383,-32],[-745,-515],[-755,-466],[-813,-204],[-306,-2],[-288,-228],[-386,-624],[-597,-414],[-192,-24],[-370,-145],[-399,-138],[-238,-365],[-4,-415],[-141,-388],[-453,-472],[112,-462],[-125,-488],[-142,-577],[-391,-36]],[[67002,71642],[284,-224],[209,79],[58,268],[219,89],[157,180],[55,472],[234,114],[44,211],[131,-158],[84,-19]],[[68477,72654],[154,-4],[210,-124]],[[68841,72526],[85,-72],[201,189],[93,-114],[90,271],[166,-12],[43,86],[29,239],[120,205],[150,-134],[-30,-181],[84,-28],[-26,-496],[110,-194],[97,125],[123,58],[173,265],[192,-44],[286,-1]],[[70827,72688],[50,-169]],[[70877,72519],[-162,-67],[-141,-109],[-319,-68],[-298,-124],[-163,-258],[66,-250],[32,-294],[-139,-248],[12,-227],[-76,-213],[-265,18],[110,-390],[-177,-150],[-118,-356],[15,-355],[-108,-166],[-103,55],[-212,-77],[-31,-166],[-207,1],[-154,-334],[-10,-503],[-361,-246],[-194,52],[-56,-129],[-166,75],[-278,-88],[-465,301]],[[66909,68203],[252,536],[-23,380],[-210,100],[-22,375],[-91,472],[119,323],[-121,87],[76,430],[113,736]],[[56642,44124],[29,-184],[-32,-286],[49,-277],[-41,-222],[24,-203],[-579,7],[-13,-1880],[188,-483],[181,-369]],[[56448,40227],[-510,-241],[-673,83],[-192,284],[-1126,-26],[-42,-41],[-166,267],[-180,17],[-166,-100],[-134,-113]],[[53422,46976],[115,79],[80,-11],[98,71],[820,-8],[68,-440],[80,-354],[64,-191],[106,-309],[184,47],[91,83],[154,-83],[42,148],[69,344],[172,23],[15,103],[142,2],[-24,-213],[337,5],[5,-372],[56,-228],[-41,-356],[21,-363],[93,-219],[-15,-703],[68,54],[121,-15],[172,89],[127,-35]],[[53309,47603],[112,255],[84,100],[104,-203]],[[53609,47755],[-101,-124],[-45,-152],[-9,-258],[-71,-62]],[[55719,75309],[-35,-201],[39,-254],[115,-144]],[[55838,74710],[-5,-155],[-91,-85],[-16,-192],[-129,-287]],[[55427,75229],[-47,93]],[[55380,75322],[-18,188],[120,291],[18,-111],[75,52]],[[55575,75742],[59,-159],[66,-60],[19,-214]],[[65575,65974],[52,-202]],[[65665,65306],[-142,-3],[-23,-384],[50,-82],[-126,-117],[-1,-241],[-81,-245],[-7,-238]],[[65335,63996],[-56,-125],[-835,298],[-106,599],[-11,136]],[[31400,18145],[-168,16],[-297,1],[0,1319]],[[32587,37434],[511,-964],[227,-89],[339,-437],[286,-231],[40,-261],[-273,-898],[280,-160],[312,-91],[220,95],[252,453],[45,521]],[[34826,35372],[138,114],[139,-341],[-6,-472],[-234,-326],[-186,-241],[-314,-573],[-370,-806]],[[33993,32727],[-70,-473],[-74,-607],[3,-588],[-61,-132],[-21,-382]],[[31227,23224],[273,-433],[266,-119]],[[30952,19680],[-257,93],[-672,79],[-115,344],[6,443],[-185,-38],[-98,214],[-24,626],[213,260],[88,375],[-33,299],[148,504],[101,782],[-30,347],[122,112],[-30,223],[-129,118],[92,248],[-126,224],[-65,682],[112,120],[-47,720],[65,605],[75,527],[166,215],[-84,576],[-1,543],[210,386],[-7,494],[159,576],[1,544],[-72,108],[-128,1020],[171,607],[-27,572],[100,537],[182,555],[196,367],[-83,232],[58,190],[-9,985],[302,291],[96,614],[-34,148]],[[31359,37147],[231,534],[364,-144],[163,-427],[109,475],[316,-24],[45,-127]],[[62492,74950],[57,-155],[106,-103],[-56,-148],[148,-202],[-78,-189],[118,-160],[124,-97],[7,-410]],[[62918,73486],[-101,-17]],[[62817,73469],[-113,342],[1,91],[-123,-2],[-82,159],[-58,-16]],[[62442,74043],[-109,172],[-207,147],[27,288],[-47,208]],[[62106,74858],[386,92]],[[1088,892],[38,-7],[32,-4]],[[1158,881],[402,-246],[352,246],[63,34],[816,104],[265,-138],[130,-71],[419,-196],[789,-151],[625,-185],[1072,-139],[800,162],[1181,-116],[669,-185],[734,174],[773,162],[60,278],[-1094,23],[-898,139],[-234,231],[-745,128],[49,266],[103,243],[104,220],[-55,243],[-462,162],[-212,209],[-430,185],[675,-35],[642,93],[402,-197],[495,173],[457,220],[223,197],[-98,243],[-359,162],[-408,174],[-571,35],[-500,81],[-539,58],[-180,220],[-359,185],[-217,208],[-87,672],[136,-58],[250,-185],[457,58],[441,81],[228,-255],[441,58],[370,127],[348,162],[315,197],[419,58],[-11,220],[-97,220],[81,208],[359,104],[163,-196],[425,115],[321,151],[397,12],[375,57],[376,139],[299,128],[337,127],[218,-35],[190,-46],[414,81],[370,-104],[381,11],[364,81],[375,-57],[414,-58],[386,23],[403,-12],[413,-11],[381,23],[283,174],[337,92],[349,-127],[331,104],[300,208],[179,-185],[98,-208],[180,-197],[288,174],[332,-220],[375,-70],[321,-162],[392,35],[354,104],[418,-23],[376,-81],[381,-104],[147,254],[-180,197],[-136,209],[-359,46],[-158,220],[-60,220],[-98,440],[213,-81],[364,-35],[359,35],[327,-93],[283,-174],[119,-208],[376,-35],[359,81],[381,116],[342,70],[283,-139],[370,46],[239,451],[224,-266],[321,-104],[348,58],[228,-232],[365,-23],[337,-69],[332,-128],[218,220],[108,209],[278,-232],[381,58],[283,-127],[190,-197],[370,58],[288,127],[283,151],[337,81],[392,69],[354,81],[272,127],[163,186],[65,254],[-32,244],[-87,231],[-98,232],[-87,231],[-71,209],[-16,231],[27,232],[130,220],[109,243],[44,231],[-55,255],[-32,232],[136,266],[152,173],[180,220],[190,186],[223,173],[109,255],[152,162],[174,151],[267,34],[174,186],[196,115],[228,70],[202,150],[157,186],[218,69],[163,-151],[-103,-196],[-283,-174],[-120,-127],[-206,92],[-229,-58],[-190,-139],[-202,-150],[-136,-174],[-38,-231],[17,-220],[130,-197],[-190,-139],[-261,-46],[-153,-197],[-163,-185],[-174,-255],[-44,-220],[98,-243],[147,-185],[229,-139],[212,-185],[114,-232],[60,-220],[82,-232],[130,-196],[82,-220],[38,-544],[81,-220],[22,-232],[87,-231],[-38,-313],[-152,-243],[-163,-197],[-370,-81],[-125,-208],[-169,-197],[-419,-220],[-370,-93],[-348,-127],[-376,-128],[-223,-243],[-446,-23],[-489,23],[-441,-46],[-468,0],[87,-232],[424,-104],[311,-162],[174,-208],[-310,-185],[-479,58],[-397,-151],[-17,-243],[-11,-232],[327,-196],[60,-220],[353,-220],[588,-93],[500,-162],[398,-185],[506,-186],[690,-92],[681,-162],[473,-174],[517,-197],[272,-278],[136,-220],[337,209],[457,173],[484,186],[577,150],[495,162],[691,12],[680,-81],[560,-139],[180,255],[386,173],[702,12],[550,127],[522,128],[577,81],[614,104],[430,150],[-196,209],[-119,208],[0,220],[-539,-23],[-571,-93],[-544,0],[-77,220],[39,440],[125,128],[397,138],[468,139],[337,174],[337,174],[251,231],[380,104],[376,81],[190,47],[430,23],[408,81],[343,116],[337,139],[305,139],[386,185],[245,197],[261,173],[82,232],[-294,139],[98,243],[185,185],[288,116],[305,139],[283,185],[217,232],[136,277],[202,163],[331,-35],[136,-197],[332,-23],[11,220],[142,231],[299,-58],[71,-220],[331,-34],[360,104],[348,69],[315,-34],[120,-243],[305,196],[283,105],[315,81],[310,81],[283,139],[310,92],[240,128],[168,208],[207,-151],[288,81],[202,-277],[157,-209],[316,116],[125,232],[283,162],[365,-35],[108,-220],[229,220],[299,69],[326,23],[294,-11],[310,-70],[300,-34],[130,-197],[180,-174],[304,104],[327,24],[315,0],[310,11],[278,81],[294,70],[245,162],[261,104],[283,58],[212,162],[152,324],[158,197],[288,-93],[109,-208],[239,-139],[289,46],[196,-208],[206,-151],[283,139],[98,255],[250,104],[289,197],[272,81],[326,116],[218,127],[228,139],[218,127],[261,-69],[250,208],[180,162],[261,-11],[229,139],[54,208],[234,162],[228,116],[278,93],[256,46],[244,-35],[262,-58],[223,-162],[27,-254],[245,-197],[168,-162],[332,-70],[185,-162],[229,-162],[266,-35],[223,116],[240,243],[261,-127],[272,-70],[261,-69],[272,-46],[277,0],[229,-614],[-11,-150],[-33,-267],[-266,-150],[-218,-220],[38,-232],[310,12],[-38,-232],[-141,-220],[-131,-243],[212,-185],[321,-58],[321,104],[153,232],[92,220],[153,185],[174,174],[70,208],[147,289],[174,58],[316,24],[277,69],[283,93],[136,231],[82,220],[190,220],[272,151],[234,115],[153,197],[157,104],[202,93],[277,-58],[250,58],[272,69],[305,-34],[201,162],[142,393],[103,-162],[131,-278],[234,-115],[266,-47],[267,70],[283,-46],[261,-12],[174,58],[234,-35],[212,-127],[250,81],[300,0],[255,81],[289,-81],[185,197],[141,196],[191,163],[348,439],[179,-81],[212,-162],[185,-208],[354,-359],[272,-12],[256,0],[299,70],[299,81],[229,162],[190,174],[310,23],[207,127],[218,-116],[141,-185],[196,-185],[305,23],[190,-150],[332,-151],[348,-58],[288,47],[218,185],[185,185],[250,46],[251,-81],[288,-58],[261,93],[250,0],[245,-58],[256,-58],[250,104],[299,93],[283,23],[316,0],[255,58],[251,46],[76,290],[11,243],[174,-162],[49,-266],[92,-244],[115,-196],[234,-105],[315,35],[365,12],[250,35],[364,0],[262,11],[364,-23],[310,-46],[196,-186],[-54,-220],[179,-173],[299,-139],[310,-151],[360,-104],[375,-92],[283,-93],[315,-12],[180,197],[245,-162],[212,-185],[245,-139],[337,-58],[321,-69],[136,-232],[316,-139],[212,-208],[310,-93],[321,12],[299,-35],[332,12],[332,-47],[310,-81],[288,-139],[289,-116],[195,-173],[-32,-232],[-147,-208],[-125,-266],[-98,-209],[-131,-243],[-364,-93],[-163,-208],[-360,-127],[-125,-232],[-190,-220],[-201,-185],[-115,-243],[-70,-220],[-28,-266],[6,-220],[158,-232],[60,-220],[130,-208],[517,-81],[109,-255],[-501,-93],[-424,-127],[-528,-23],[-234,-336],[-49,-278],[-119,-220],[-147,-220],[370,-196],[141,-244],[239,-219],[338,-197],[386,-186],[419,-185],[636,-185],[142,-289],[800,-128],[53,-45],[208,-175],[767,151],[636,-186],[-99504,-147],[245,344],[501,-185],[32,21],[294,188]],[[54716,79012],[-21,-241],[-156,-2],[53,-128],[-92,-380]],[[54500,78261],[-53,-100],[-243,-14],[-140,-134],[-229,45]],[[53835,78058],[-398,153],[-62,205],[-274,-102],[-32,-113],[-169,84]],[[52900,78285],[-142,16],[-125,108],[42,145],[-10,104]],[[52665,78658],[83,33],[141,-164],[39,156],[245,-25],[199,106],[133,-18],[87,-121],[26,100],[-40,385],[100,75],[98,272]],[[53776,79457],[206,-190],[157,242],[98,44],[215,-180],[131,30],[128,-111]],[[54711,79292],[-23,-75],[28,-205]],[[62817,73469],[-190,78],[-141,273],[-44,223]],[[63720,73858],[-48,-207],[-101,-138]],[[63578,73220],[-69,-29],[-173,309],[95,292],[-82,174],[-104,-44],[-327,-436]],[[62492,74950],[68,96],[207,-169],[149,-36],[38,70],[-136,319],[72,82]],[[62890,75312],[78,-20],[191,-359],[122,-40],[48,150],[166,238]],[[58149,47921],[-17,713],[-70,268]],[[58062,48902],[169,-46],[85,336],[147,-38]],[[58463,49154],[16,-233],[60,-134],[3,-192],[-69,-124],[-108,-308],[-101,-214],[-115,-28]],[[50920,80916],[204,-47],[257,123],[176,-258],[153,-138]],[[51710,80596],[-32,-400]],[[51678,80196],[-72,-22],[-30,-331]],[[51576,79843],[-243,269],[-143,-46],[-194,279],[-129,237],[-129,10],[-40,207]],[[50518,54209],[-69,407],[13,1357],[-56,122],[-11,290],[-96,207],[-85,174],[35,311]],[[50249,57077],[96,67],[56,258],[136,56],[61,176]],[[50598,57634],[93,173],[100,2],[212,-340]],[[51003,57469],[-11,-197],[62,-350],[-54,-238],[29,-159],[-135,-366],[-86,-181],[-52,-372],[7,-376],[-16,-952]],[[49214,56277],[-190,152],[-130,-22],[-97,-149],[-125,125],[-49,195],[-125,129]],[[48498,56707],[-18,343],[76,250],[-7,200],[221,490],[41,405],[76,144],[134,-79],[116,120],[38,152],[216,265],[53,184],[259,246],[153,84],[70,-114],[178,3]],[[50104,59400],[-22,-286],[37,-269],[156,-386],[9,-286],[320,-134],[-6,-405]],[[50249,57077],[-243,13]],[[50006,57090],[-128,47],[-90,-96],[-123,43],[-482,-27],[-7,-336],[38,-444]],[[75742,63602],[-6,-424],[-97,90],[18,-476]],[[75657,62792],[-79,308],[-16,301],[-53,285]],[[74730,63611],[-43,486],[-96,444],[47,356],[-171,159],[62,215],[173,220],[-200,313],[98,401],[220,-255],[133,-30],[24,-410],[265,-81],[257,8],[160,-101],[-128,-500],[-124,-34],[-86,-336],[152,-306],[46,377],[76,2],[147,-937]],[[56293,76715],[80,-243],[108,43],[213,-92],[408,-31],[138,150],[327,138],[202,-215],[163,-62]],[[57776,75399],[-239,79],[-283,-186]],[[57254,75292],[-3,-294],[-252,-56],[-196,206],[-222,-162],[-206,17]],[[56375,75003],[-20,391],[-139,189]],[[56216,75583],[46,84],[-30,70],[47,188],[105,185],[-135,255],[-24,216],[68,134]],[[55279,77084],[100,2],[-69,-260],[134,-227],[-41,-278],[-65,-27]],[[55338,76294],[-52,-53],[-90,-138],[-41,-325]],[[55155,75778],[-246,224],[-105,247],[-106,130],[-127,221],[-61,183],[-136,277],[59,245],[99,-136],[60,123],[130,13],[239,-98],[192,8],[126,-131]],[[56523,82432],[268,-4],[302,223],[64,333],[228,190],[-26,264]],[[57359,83438],[169,100],[298,228]],[[57826,83766],[293,-149],[39,-146],[146,70],[272,-141],[27,-277],[-60,-159],[174,-387],[113,-108],[-16,-107],[187,-104],[80,-157],[-108,-129],[-224,20],[-54,-55],[66,-196],[68,-379]],[[58829,81362],[-239,-35],[-85,-129],[-18,-298],[-111,57],[-250,-28],[-73,138],[-104,-103],[-105,86],[-218,12],[-310,141],[-281,47],[-215,-14],[-152,-160],[-133,-23]],[[56535,81053],[-6,263],[-85,274],[166,121],[2,235],[-77,225],[-12,261]],[[25238,61101],[-2,87],[33,27],[51,-70],[99,357],[53,8]],[[25297,59966],[-83,0],[22,667],[2,468]],[[31359,37147],[-200,-81],[-109,814],[-150,663],[88,572],[-146,250],[-37,426],[-136,402]],[[30669,40193],[175,638],[-119,496],[63,199],[-49,219],[108,295],[6,503],[13,415],[60,200],[-240,951]],[[30686,44109],[206,-50],[143,13],[62,179],[243,239],[147,222],[363,100],[-29,-443],[34,-227],[-23,-396],[302,-529],[311,-98],[109,-220],[188,-117],[115,-172],[175,6],[161,-175],[12,-342],[55,-172],[3,-255],[-81,-10],[107,-688],[533,-24],[-41,-342],[30,-233],[151,-166],[66,-367],[-49,-465],[-77,-259],[27,-337],[-87,-122]],[[33842,38659],[-4,182],[-259,302],[-258,9],[-484,-172],[-133,-520],[-7,-318],[-110,-708]],[[34826,35372],[54,341],[38,350],[0,325],[-100,107],[-104,-96],[-103,26],[-33,228],[-26,541],[-52,177],[-187,160],[-114,-116],[-293,113],[18,802],[-82,329]],[[30686,44109],[-157,-102],[-126,68],[18,898],[-228,-348],[-245,15],[-105,315],[-184,34],[59,254],[-155,359],[-115,532],[73,108],[0,250],[168,171],[-28,319],[71,206],[20,275],[318,402],[227,114],[37,89],[251,-28]],[[30585,48040],[125,1620],[6,256],[-43,339],[-123,215],[1,430],[156,97],[56,-61],[9,226],[-162,61],[-4,370],[541,-13],[92,203],[77,-187],[55,-349],[52,73]],[[31423,51320],[153,-312],[216,38],[54,181],[206,138],[115,97],[32,250],[198,168],[-15,124],[-235,51],[-39,372],[12,396],[-125,153],[52,55],[206,-76],[221,-148],[80,140],[200,92],[310,221],[102,225],[-37,167]],[[33129,53652],[145,26],[64,-136],[-36,-259],[96,-90],[63,-274],[-77,-209],[-44,-502],[71,-299],[20,-274],[171,-277],[137,-29],[30,116],[88,25],[126,104],[90,157],[154,-50],[67,21]],[[34294,51702],[151,-48],[25,120],[-46,118],[28,171],[112,-53],[131,61],[159,-125]],[[34854,51946],[121,-122],[86,160],[62,-25],[38,-166],[133,42],[107,224],[85,436],[164,540]],[[35174,30629],[-77,334],[122,280],[-160,402],[-218,327],[-286,379],[-103,-18],[-279,457],[-180,-63]],[[82069,53798],[-13,-291],[-16,-377],[-133,19],[-58,-202],[-126,307]],[[75471,66988],[113,-189],[-20,-363],[-227,-17],[-234,39],[-175,-92],[-252,224],[-6,119]],[[74670,66709],[184,439],[150,150],[198,-137],[147,-14],[122,-159]],[[58175,37528],[-393,-435],[-249,-442],[-93,-393],[-83,-222],[-152,-47],[-48,-283],[-28,-184],[-178,-138],[-226,29],[-133,166],[-117,71],[-135,-137],[-68,-283],[-132,-177],[-139,-264],[-199,-60],[-62,207],[26,360],[-165,562],[-75,88]],[[55526,35946],[0,1725],[274,20],[8,2105],[207,19],[428,207],[106,-243],[177,231],[85,2],[156,133]],[[56967,40145],[50,-44]],[[57017,40101],[107,-473],[56,-105],[87,-342],[315,-649],[119,-64],[0,-208],[82,-375],[215,-90],[177,-267]],[[54244,54965],[229,44],[52,152],[46,-11],[69,-134],[350,226],[118,230],[145,207],[-28,208],[78,54],[269,-36],[261,273],[201,645],[141,239],[176,101]],[[56351,57163],[31,-253],[160,-369],[1,-241],[-45,-246],[18,-184],[96,-170]],[[56612,55700],[212,-258]],[[56824,55442],[152,-239],[2,-192],[187,-308],[116,-255],[70,-355],[208,-234],[44,-187]],[[57603,53672],[-91,-63],[-178,14],[-209,62],[-104,-51],[-41,-143],[-90,-18],[-110,125],[-309,-295],[-127,60],[-38,-46],[-83,-357],[-207,115],[-203,59],[-177,218],[-229,200],[-149,-190],[-108,-300],[-25,-412]],[[55125,52650],[-178,33],[-188,99],[-166,-313],[-146,-550]],[[54447,51919],[-29,172],[-12,269],[-127,190],[-103,305],[-23,212],[-132,309],[23,176],[-28,249],[21,458],[67,107],[140,599]],[[26228,91219],[16,649],[394,-46]],[[25824,89109],[-81,-259],[-322,-399]],[[23714,86094],[-16,-686],[409,-99]],[[25743,83665],[348,-163],[294,-248]],[[28738,83981],[-23,395],[-188,502]],[[31513,79609],[415,58],[246,-289]],[[31350,77248],[-181,334],[0,805],[-123,171],[-187,-100],[-92,155],[-212,-446],[-84,-460],[-99,-269],[-118,-91],[-89,-30],[-28,-146],[-512,0],[-422,-4],[-125,-109],[-294,-425],[-34,-46],[-89,-231],[-255,1],[-273,-3],[-125,-93],[44,-116],[25,-181],[-5,-60],[-363,-293],[-286,-93],[-323,-316],[-70,0],[-94,93],[-31,85],[6,61],[61,207],[131,325],[81,349],[-56,514],[-59,536],[-290,277],[35,105],[-41,73],[-76,0],[-56,93],[-14,140],[-54,-61],[-75,18],[17,59],[-65,58],[-27,155],[-216,189],[-224,197],[-272,229],[-261,214],[-248,-167],[-91,-6],[-342,154],[-225,-77],[-269,183],[-284,94],[-194,36],[-86,100],[-49,325],[-94,-3],[-1,-227],[-575,0],[-951,0],[-944,0],[-833,0],[-834,0],[-819,0],[-847,0],[-273,0],[-825,0],[-788,0]],[[15104,80367],[-503,244],[-155,523],[40,363]],[[13740,82958],[154,285],[-7,373],[-473,376],[-284,674],[-173,424],[-255,266],[-187,242],[-147,306],[-279,-192],[-270,-330],[-247,388],[-194,259],[-271,164],[-273,17],[1,3364],[2,2193]],[[11355,91625],[438,-285],[289,-54]],[[15437,92031],[38,-449],[341,97]],[[17987,91291],[374,-300],[-390,-293]],[[19722,91216],[-704,-88],[-494,-56]],[[15020,93041],[119,250],[192,432]],[[16539,93012],[0,-257],[-731,-285]],[[52900,78285],[-22,-242],[-122,-100],[-206,75],[-60,-239],[-132,-19],[-48,94],[-156,-200],[-134,-28],[-120,126]],[[51900,77752],[-95,259],[-133,-92],[5,267],[203,332],[-9,150],[126,-54],[77,101]],[[52074,78715],[236,-4],[57,128],[298,-181]],[[31400,18145],[-92,-239],[-238,-183]],[[31070,17723],[-137,19],[-164,48]],[[30769,17790],[-202,177],[-291,86],[-350,330],[-283,317],[-383,662],[229,-124],[390,-395],[369,-212],[143,271],[90,405],[256,244],[198,-70]],[[29661,27385],[-80,576],[-22,666]],[[30452,39739],[143,151],[74,303]],[[86288,75628],[-179,348],[-111,-331],[-429,-254],[44,-312],[-241,22],[-131,185],[-191,-419],[-306,-318],[-227,-379]],[[83030,72705],[220,-173],[311,422]],[[83987,72709],[45,-310],[-393,-165]],[[83097,71205],[299,-325],[109,-581]],[[80517,63220],[-373,189],[-131,-96]],[[80013,63313],[-280,154],[-132,240],[44,340],[-254,108],[-134,222],[-236,-315],[-271,-68],[-221,3],[-149,-145]],[[78380,63852],[-144,-86],[42,-676],[-148,16],[-25,139]],[[78105,63245],[-9,244],[-203,-172],[-121,109],[-206,222],[81,490],[-176,115],[-66,544],[-293,-98],[33,701],[263,493],[11,487],[-8,452],[-121,141],[-93,348],[-162,-44]],[[77035,67277],[-300,89],[94,248],[-130,367],[-198,-249],[-233,145],[-321,-376],[-252,-439],[-224,-74]],[[74670,66709],[-23,465],[-170,-124]],[[74477,67050],[-324,57],[-314,136],[-225,259],[-216,117],[-93,284],[-157,84],[-280,385],[-223,182],[-115,-141]],[[72530,68413],[-386,413],[-273,374],[-78,651],[200,-79],[9,301],[-111,303],[28,482],[-298,692]],[[71621,71550],[-457,239],[-82,454],[-205,276]],[[70827,72688],[-42,337],[10,230],[-169,134],[-91,-59],[-70,546]],[[70465,73876],[79,136],[-39,138],[266,279],[192,116],[294,-80],[105,378],[356,70],[99,234],[438,320],[39,134]],[[72294,75601],[-22,337],[190,154],[-250,1026],[550,236],[143,131],[200,1058],[551,-194],[155,267],[13,592],[230,56],[212,393]],[[74266,79657],[109,49]],[[74375,79706],[73,-413],[233,-313],[396,-222],[192,-476],[-107,-690],[100,-256],[330,-101],[374,-83],[336,-368],[171,-66],[127,-544],[163,-351],[306,14],[574,-133],[369,82],[274,-88],[411,-359],[336,1],[123,-184],[324,318],[448,205],[417,22],[324,208],[200,316],[194,199],[-45,195],[-89,227],[146,381],[156,-53],[286,-120],[277,313],[423,229],[204,391],[195,168],[404,78],[219,-66],[30,210],[-251,413],[-223,189],[-214,-219],[-274,92],[-157,-74],[-72,241],[197,590],[135,446]],[[82410,80055],[333,-223],[392,373],[-3,260],[251,627],[155,189],[-4,326],[-152,141],[229,294],[345,106],[369,16],[415,-176],[244,-217],[172,-596],[104,-254],[97,-363],[103,-579],[483,-189],[329,-420],[112,-555],[423,-1],[240,233],[459,175],[-146,-532],[-107,-216],[-96,-647],[-186,-575],[-338,104],[-238,-208],[73,-506],[-40,-698],[-142,-16],[2,-300]],[[47857,53158],[22,487],[26,74],[-8,233],[-118,247],[-88,40],[-81,162],[60,262],[-28,286],[13,172]],[[47655,55121],[44,0],[17,258],[-22,114],[27,82],[103,71],[-69,473],[-64,245],[23,200],[55,46]],[[47769,56610],[36,54],[77,-89],[215,-5],[51,172],[48,-11],[80,67],[43,-253],[65,74],[114,88]],[[49214,56277],[74,-841],[-117,-496],[-73,-667],[121,-509],[-13,-233]],[[53632,51919],[-35,32],[-164,-76],[-169,79],[-132,-38]],[[53132,51916],[-452,13]],[[52680,51929],[40,466],[-108,391]],[[52429,53151],[-72,85],[4,163]],[[52361,53399],[71,418],[132,570],[81,6],[165,345],[105,10],[156,-243],[191,199],[26,246],[63,238],[43,299],[148,243],[56,414],[59,132],[39,307],[74,377],[234,457],[14,196],[31,107],[-110,235]],[[53939,57955],[9,188],[78,34]],[[54026,58177],[111,-378],[18,-392],[-10,-393],[151,-537],[-155,6],[-78,-42],[-127,60],[-60,-279],[164,-345],[121,-100],[39,-245],[87,-407],[-43,-160]],[[54447,51919],[-20,-319],[-220,140],[-225,156],[-350,23]],[[58564,52653],[-16,-691],[111,-80],[-89,-210],[-107,-157],[-106,-308],[-59,-274],[-15,-475],[-65,-225],[-2,-446]],[[58216,49787],[-80,-165],[-10,-351],[-38,-46],[-26,-323]],[[58149,47921],[50,-544],[-27,-307]],[[58172,47070],[55,-343],[161,-330]],[[58388,46397],[150,-745]],[[58538,45652],[-109,60],[-373,-99],[-75,-71],[-79,-377],[62,-261],[-49,-699],[-34,-593],[75,-105],[194,-230],[76,107],[23,-637],[-212,5],[-114,325],[-103,252],[-213,82],[-62,310],[-170,-187],[-222,83],[-93,268],[-176,55],[-131,-15],[-15,184],[-96,15]],[[53609,47755],[73,-60],[95,226],[152,-6],[17,-167],[104,-105],[164,370],[161,289],[71,189],[-10,486],[121,574],[127,304],[183,285],[32,189],[7,216],[45,205],[-14,335],[34,524],[55,368],[83,316],[16,357]],[[57603,53672],[169,-488],[124,-71],[75,99],[128,-39],[155,125],[66,-252],[244,-393]],[[53081,48229],[212,326],[-105,391],[95,148],[187,73],[23,261],[148,-283],[245,-25],[85,279],[36,393],[-31,461],[-131,350],[120,684],[-69,117],[-207,-48],[-78,305],[21,258]],[[29063,50490],[-119,140],[-137,195],[-79,-94],[-235,82],[-68,255],[-52,-10],[-278,338]],[[28366,54848],[36,287],[89,-43],[52,176],[-64,348],[34,86]],[[30185,57537],[-178,-99],[-71,-295],[-107,-169],[-81,-220],[-34,-422],[-77,-345],[144,-40],[35,-271],[62,-130],[21,-238],[-33,-219],[10,-123],[69,-49],[66,-207],[357,57],[161,-75],[196,-508],[112,63],[200,-32],[158,68],[99,-102],[-50,-318],[-62,-199],[-22,-423],[56,-393],[79,-175],[9,-133],[-140,-294],[100,-130],[74,-207],[85,-589]],[[30585,48040],[-139,314],[-83,14],[179,602],[-213,276],[-166,-51],[-101,103],[-153,-157],[-207,74],[-163,620],[-129,152],[-89,279],[-184,280],[-74,-56]],[[26191,57131],[42,76],[183,-156],[63,77],[89,-50],[46,-121],[82,-40],[66,126]],[[27070,56232],[-107,-53],[1,-238],[58,-88],[-41,-70],[10,-107],[-23,-120],[-14,-117]],[[59437,71293],[-30,21],[-53,-45],[-42,12],[-14,-22],[-5,59],[-20,37],[-54,6],[-75,-51],[-52,31]],[[53776,79457],[-157,254],[-141,142],[-30,249],[-49,176],[202,129],[103,147],[200,114],[70,113],[73,-68],[124,62]],[[54171,80775],[132,-191],[207,-51],[-17,-163],[151,-122],[41,153],[191,-66],[26,-185],[207,-36],[127,-291]],[[55236,79823],[-82,-1],[-43,-106],[-64,-26],[-18,-134],[-54,-28],[-7,-55],[-95,-61],[-123,10],[-39,-130]],[[53922,82340],[64,-300],[-77,-158],[101,-210],[69,-316],[-22,-204],[114,-377]],[[52074,78715],[35,421],[140,404],[-400,109],[-131,155]],[[51718,79804],[16,259],[-56,133]],[[51710,80596],[-47,619],[167,0],[70,222],[69,541],[-51,200]],[[52368,83053],[210,-78],[178,90]],[[61984,57352],[-102,-317]],[[61882,57035],[-62,106],[-67,-42],[-155,10],[-4,180],[-22,163],[94,277],[98,261]],[[61764,57990],[119,-51],[83,144]],[[52293,84144],[80,177],[244,37]],[[30081,61241],[5,161],[-71,177],[68,99],[21,228],[-24,321]],[[53333,64447],[-952,-1126],[-804,-1161],[-392,-263]],[[51185,61897],[-308,-58],[-3,376],[-129,96],[-173,169],[-66,277],[-937,1289],[-937,1289]],[[48632,65335],[-1045,1431]],[[47587,66766],[6,114],[-1,40]],[[47592,66920],[-2,700],[449,436],[277,90],[227,159],[107,295],[324,234],[12,438],[161,51],[126,219],[363,99],[51,230],[-73,125],[-96,624],[-17,359],[-104,379]],[[52339,72408],[-57,-303],[44,-563],[-65,-487],[-171,-330],[24,-445],[227,-352],[3,-143],[171,-238],[118,-1061]],[[52633,68486],[90,-522],[15,-274],[-49,-482],[21,-270],[-36,-323],[24,-371],[-110,-247],[164,-431],[11,-253],[99,-330],[130,109],[219,-275],[122,-370]],[[29063,50490],[38,-449],[-86,-384],[-303,-619],[-334,-233],[-170,-514],[-53,-398],[-157,-243],[-116,298],[-113,64],[-114,-47],[-8,216],[79,141],[-33,246]],[[60240,63578],[-1102,0],[-1077,0],[-1117,0]],[[56944,63578],[0,2175],[0,2101],[-83,476],[71,365],[-43,253],[101,283]],[[59518,69025],[182,-1015]],[[61764,57990],[-95,191],[-114,346],[-124,190],[-71,204],[-242,237],[-191,7],[-67,124],[-163,-139],[-168,268],[-87,-441],[-323,124]],[[60119,59101],[-30,236],[120,868],[27,393],[88,181],[204,97],[141,337]],[[60669,61213],[161,-684],[77,-542]],[[47783,76427],[340,-106],[373,3]],[[49471,76235],[111,-230],[511,-268],[101,127],[313,-267],[322,77]],[[49600,72702],[-197,-454],[-352,-9]],[[47929,72498],[-23,195],[103,222],[38,161],[-96,175],[77,388],[-111,355],[120,48],[11,280],[45,86],[3,461],[129,160],[-78,296],[-162,21],[-47,-75],[-164,0],[-70,289],[-113,-86],[-101,-150]],[[57772,85719],[42,-103],[-198,-341],[83,-551],[-120,-187]],[[57579,84537],[-229,1],[-239,219],[-121,73],[-237,-105]],[[61882,57035],[-61,-209],[103,-325],[102,-285],[106,-210],[909,-702],[233,4]],[[63274,55308],[-785,-1773],[-362,-26],[-247,-417],[-178,-11],[-76,-186]],[[61626,52895],[-190,0],[-112,200],[-254,-247],[-82,-247],[-185,47],[-62,68],[-65,-16],[-87,6],[-352,502],[-193,0],[-95,194],[0,332],[-145,99]],[[59804,53833],[-164,643],[-127,137],[-48,236],[-141,288],[-171,42],[95,337],[147,14],[42,181]],[[59437,55711],[-4,531]],[[59433,56242],[82,618],[132,166],[28,241],[119,451],[168,293],[112,582],[45,508]],[[57942,91385],[-41,-414],[425,-394],[-256,-445],[323,-673],[-187,-506],[250,-440],[-113,-385],[411,-405],[-105,-301],[-258,-341],[-594,-755]],[[56352,85938],[-161,323],[-269,193],[62,582]],[[55984,87036],[-135,533],[133,345]],[[56639,89578],[-93,230],[-8,910],[-433,402],[-371,289]],[[55734,91409],[167,156],[309,-312],[362,29],[298,-143],[265,262],[137,433],[431,200],[356,-235],[-117,-414]],[[34854,51946],[70,252],[24,269],[48,253],[-107,349]],[[34889,53069],[-22,404],[144,508]],[[51576,79843],[62,-52],[80,13]],[[51900,77752],[-11,-167],[82,-222],[-97,-180],[72,-457],[151,-75],[-32,-256]],[[49176,78685],[-424,227],[-28,431]],[[52636,51176],[94,35],[404,-6],[-2,711]],[[48278,82406],[-210,122],[-172,-9],[57,317],[-57,317]],[[49420,83612],[22,-62],[248,-697]],[[49690,82853],[190,-95],[171,-673],[79,-233],[337,-113],[-34,-378],[-142,-173],[111,-305],[-250,-310],[-371,6],[-473,-163],[-130,116],[-183,-276],[-257,67],[-195,-226],[-148,118],[407,621],[249,127]],[[49051,80963],[-2,1],[-434,98]],[[48615,81062],[-79,235],[291,183],[-152,319],[52,387]],[[48727,82186],[413,-54],[1,0]],[[49141,82132],[40,343]],[[49181,82475],[-186,364],[-4,8]],[[48991,82847],[-337,104],[-66,160],[101,264],[-92,163],[-149,-279],[-17,569],[-140,301],[101,611],[216,480],[222,-47],[335,49],[-297,-639],[283,81],[304,-3],[-72,-481],[-250,-530],[287,-38]],[[61098,76242],[34,70],[235,-101],[409,-96],[378,-283],[48,-110],[169,93],[259,-124],[85,-242],[175,-137]],[[62106,74858],[-268,290],[-296,-28]],[[50006,57090],[-20,-184],[116,-305],[-1,-429],[27,-466],[69,-215],[-61,-532],[22,-294],[74,-375],[62,-207]],[[47655,55121],[-78,15],[-57,-238],[-78,3],[-55,126],[19,237],[-116,362],[-73,-67],[-59,-13]],[[47158,55546],[-77,-34],[3,217],[-44,155],[9,171],[-60,249],[-78,211],[-222,1],[-65,-112],[-76,-13],[-48,-128],[-32,-163],[-148,-260]],[[45797,57103],[123,288],[84,-11],[73,99],[61,1],[44,78],[-24,196],[31,62],[5,200]],[[46194,58016],[134,-6],[200,-144],[61,13],[21,66],[151,-47],[40,33]],[[46801,57931],[16,-216],[44,1],[73,78],[46,-19],[77,-150],[119,-48],[76,128],[90,79],[67,83],[55,-15],[62,-130],[33,-163],[114,-248],[-57,-152],[-11,-192],[59,58],[35,-69],[-15,-176],[85,-170]],[[45357,58612],[302,17],[63,140],[88,9],[110,-145],[86,-3],[92,99],[56,-170],[-120,-133],[-121,11],[-119,124],[-103,-136],[-50,-5],[-67,-83],[-253,13]],[[45367,57897],[147,96],[92,-19],[75,67],[513,-25]],[[56638,74190],[-154,-1],[-147,305]],[[56486,73734],[-105,-129],[155,-273]],[[56431,72099],[-184,-8],[-228,257],[-104,473]],[[55838,74710],[182,53],[106,129],[150,-12],[46,103],[53,20]],[[57254,75292],[135,-157],[-86,-369],[-66,-67]],[[24381,59170],[7,172],[32,138],[-39,111],[133,481],[357,2],[7,201],[-45,36],[-31,128],[-103,136],[-103,198],[125,1],[1,333],[259,1],[257,-7]],[[25493,59872],[-127,-225],[-131,-166],[-20,-113],[22,-116],[-58,-150]],[[25179,59102],[-65,-37],[15,-69],[-52,-66],[-95,-149],[-9,-86]],[[34125,54109],[-44,-532],[-169,-154],[15,-139],[-51,-305],[123,-429],[89,-1],[37,-333],[169,-514]],[[33129,53652],[-188,448],[75,163],[-5,273],[171,95],[69,110],[-95,220],[24,215],[220,347]],[[25697,58436],[-84,51]],[[25613,58487],[19,237],[-38,64],[-57,42],[-122,-70],[-10,79],[-84,95],[-60,118],[-82,50]],[[25860,59889],[128,15],[90,66]],[[26903,59440],[-95,12],[-38,-81],[-97,-77],[-70,0],[-61,-76],[-56,27],[-47,90],[-29,-17],[-36,-141],[-27,5],[-4,-121],[-97,-163],[-51,-70],[-29,-74],[-82,120],[-60,-158],[-58,4],[-65,-14],[6,-290],[-41,-5],[-35,-135],[-86,-25]],[[55230,77704],[67,-229],[89,-169],[-107,-222]],[[55155,75778],[-31,-100]],[[54448,76285],[-233,434],[56,45]],[[53809,77462],[194,-20],[51,100],[94,-97],[109,-11],[-1,165],[97,60],[27,239],[221,157]],[[54601,78055],[88,-73],[208,-253],[229,-114],[104,89]],[[54716,79012],[141,-151],[103,-65],[233,73],[22,118],[111,18],[135,92],[30,-38],[130,74],[66,139],[91,36],[297,-180],[59,61]],[[56134,79189],[155,-161],[19,-159]],[[56308,78869],[-170,-123],[-131,-401],[-168,-401],[-223,-111]],[[55616,77833],[-173,26],[-213,-155]],[[54601,78055],[-54,200],[-47,6]],[[84713,45326],[28,-117],[5,-179]],[[89166,49043],[5,-1925],[4,-1925]],[[80461,51765],[47,-395],[190,-334],[179,121],[177,-43],[162,299],[133,52],[263,-166],[226,126],[143,822],[107,205],[96,672],[319,0],[241,-100]],[[72530,68413],[-176,-268],[-108,-553],[269,-224],[262,-289],[362,-332],[381,-76],[160,-301],[215,-56],[334,-138],[231,10],[32,234],[-36,375],[21,255]],[[77035,67277],[20,-224],[-97,-108],[23,-364],[-199,107],[-359,-408],[8,-338],[-153,-496],[-14,-288],[-124,-487],[-217,135],[-11,-612],[-63,-201],[30,-251],[-137,-140]],[[73107,61020],[-276,-387],[-1,-271]],[[72692,60216],[-251,-212],[-129,-31]],[[71996,56025],[-253,-168],[-93,-401]],[[68937,64577],[185,395],[612,-2],[-56,507],[-156,300],[-31,455],[-182,265],[306,619],[323,-45],[290,620],[174,599],[270,593],[-4,421],[236,342],[-224,292],[-96,400],[-99,517],[137,255],[421,-144],[310,88],[268,496]],[[64978,72558],[244,114],[197,338],[186,-17],[122,110],[197,-55],[308,-299],[221,-65],[318,-523],[207,-21],[24,-498]],[[66909,68203],[137,-310],[112,-357],[266,-260],[7,-520],[133,-96],[23,-272],[-400,-305],[-105,-687]],[[66559,65575],[-303,136],[-313,76]],[[63594,68492],[-104,-231]],[[63490,68261],[-153,311],[-3,314],[-89,0],[46,428],[-143,449],[-340,324],[-193,562],[65,461],[139,204],[-21,345],[-182,177],[-180,705]],[[62436,72541],[-152,473],[55,183],[-87,678],[190,168]],[[63326,68290],[-187,49],[-204,-567]],[[62935,67772],[-516,47],[-784,1188],[-413,414],[-335,160]],[[60887,69581],[-112,720]],[[60775,70301],[615,614],[105,715],[-26,431],[152,146],[142,369]],[[61763,72576],[119,92],[324,-77],[97,-150],[133,100]],[[63490,68261],[-164,29]],[[59873,69719],[-100,82],[-58,-394],[69,-66],[-71,-81],[-12,-156],[131,80]],[[59832,69184],[7,-230],[-139,-944]],[[59757,70130],[93,-1],[25,104],[75,8]],[[59950,70241],[4,-242],[-38,-90],[6,-4]],[[59922,69905],[-49,-186]],[[53835,78058],[-31,-291],[67,-251]],[[54413,75123],[249,-214],[204,-178]],[[53108,75604],[-189,340],[-86,585]],[[59922,69905],[309,-234],[544,630]],[[60887,69581],[-53,-89],[-556,-296],[277,-591],[-92,-101],[-46,-197],[-212,-82],[-66,-213],[-120,-182],[-310,94]],[[59832,69184],[41,173],[0,362]],[[69711,75551],[-159,-109],[-367,-412],[-121,-422],[-104,-4],[-76,280],[-353,19],[-57,484],[-135,4],[21,593],[-333,431],[-476,-46],[-326,-86],[-265,533],[-227,223],[-431,423],[-52,51],[-715,-349],[11,-2178]],[[65546,74986],[-142,-29],[-195,463],[-188,166],[-315,-123],[-123,-197]],[[63639,77993],[-142,96],[29,304],[-177,395],[-207,-17],[-235,401],[160,448],[-81,120],[222,649],[285,-342],[35,431],[573,643],[434,15],[612,-409],[329,-239],[295,249],[440,12],[356,-306],[80,175],[391,-25],[69,280],[-450,406],[267,288],[-52,161],[266,153],[-200,405],[127,202],[1039,205],[136,146],[695,218],[250,245],[499,-127],[88,-612],[290,144],[356,-202],[-23,-322],[267,33],[696,558],[-102,-185],[355,-457],[620,-1500],[148,309],[383,-340],[399,151],[154,-106],[133,-341],[194,-115],[119,-251],[358,79],[147,-361]],[[72294,75601],[-171,87],[-140,212],[-412,62],[-461,16],[-100,-65],[-396,248],[-158,-122],[-43,-349],[-457,204],[-183,-84],[-62,-259]],[[60889,47817],[-399,590],[-19,343],[-1007,1203],[-47,65]],[[59417,50018],[-3,627],[80,239],[137,391],[101,431],[-123,678],[-32,296],[-132,411]],[[59445,53091],[171,352],[188,390]],[[61626,52895],[-243,-670],[3,-2152],[165,-488]],[[70465,73876],[-526,-89],[-343,192],[-301,-46],[26,340],[303,-98],[101,182]],[[69725,74357],[212,-58],[355,425],[-329,311],[-198,-147],[-205,223],[234,382],[-83,58]],[[78495,57780],[-66,713],[178,492],[359,112],[261,-84]],[[79227,59013],[229,-232],[126,407],[246,-217]],[[79828,58971],[64,-394],[-34,-708],[-467,-455],[122,-358],[-292,-43],[-240,-238]],[[85103,71220],[51,443],[-122,615]],[[85048,72883],[17,54],[124,-21],[108,266],[197,29],[118,39],[40,143]],[[55575,75742],[52,132]],[[55627,75874],[66,43],[38,196],[50,33],[40,-84],[52,-36],[36,-94],[46,-28],[54,-110],[39,4],[-31,-144],[-33,-71],[9,-44]],[[55993,75539],[-62,-23],[-164,-91],[-13,-121],[-35,5]],[[63448,67449],[-196,-16],[-69,282],[-248,57]],[[79227,59013],[90,266],[12,500],[-224,515],[-18,583],[-211,480],[-210,40],[-56,-205],[-163,-17],[-83,104],[-293,-353],[-6,530],[68,623],[-188,27],[-16,355],[-120,182]],[[77809,62643],[59,218],[237,384]],[[78380,63852],[162,-466],[125,-537],[342,-5],[108,-515],[-178,-155],[-80,-212],[333,-353],[231,-699],[175,-520],[210,-411],[70,-418],[-50,-590]],[[59999,71049],[125,-31],[45,-231],[-151,-223],[-68,-323]],[[47498,53435],[-252,449],[-237,324]],[[46822,54589],[66,189],[15,172],[126,320],[129,276]],[[54125,64088],[-197,-220],[-156,324],[-439,255]],[[52633,68486],[136,137],[24,250],[-30,244],[191,228],[86,189],[135,170],[16,454]],[[56646,69496],[276,-70],[68,-195]],[[56944,63578],[0,-1180],[-320,-2],[-3,-248]],[[56621,62148],[-1108,1131],[-1108,1132],[-280,-323]],[[57708,32474],[-209,454],[148,374],[151,232],[130,120],[121,-182],[96,-178],[-85,-288],[-47,-192],[-155,-93],[-51,-188],[-99,-59]],[[56314,82678],[-23,150],[30,162],[-123,94],[-291,103]],[[55848,83684],[318,181],[466,-38],[273,59],[39,-123],[148,-38],[267,-287]],[[56523,82432],[-67,182],[-142,64]],[[57579,84537],[134,-136],[24,-287],[89,-348]],[[47592,66920],[-42,0],[7,-317],[-172,-19],[-90,-134],[-126,0],[-100,76],[-234,-63],[-91,-460],[-86,-44],[-131,-745],[-386,-637],[-92,-816],[-114,-265],[-33,-213],[-625,-48],[-5,1]],[[45272,63236],[13,274],[106,161],[91,308],[-18,200],[96,417],[155,376],[93,95],[74,344],[6,315],[100,365],[185,216],[177,603]],[[46350,66910],[5,8],[139,227]],[[46494,67145],[259,65],[218,404],[140,158]],[[57394,79070],[66,87],[185,58],[204,-184],[115,-22],[125,-159],[-20,-200],[101,-97],[40,-247],[97,-150],[-19,-88],[52,-60],[-74,-44],[-164,18],[-27,81],[-58,-47],[20,-106],[-76,-188],[-49,-203],[-70,-64]],[[57842,77455],[-50,270],[30,252],[-9,259],[-160,352],[-89,249],[-86,175],[-84,58]],[[23016,65864],[-107,-518],[-49,-426],[-20,-791],[-27,-289],[48,-322],[86,-288],[56,-458],[184,-440],[65,-337],[109,-291],[295,-157],[114,-247],[244,165],[212,60],[208,106],[175,101],[176,241],[67,345],[22,496],[48,173],[188,155],[294,137],[246,-21],[169,50],[66,-125],[-9,-285],[-149,-351],[-66,-360],[51,-103],[-42,-255],[-69,-461],[-71,152],[-58,-10]],[[24067,59806],[-144,191],[-226,155]],[[19641,66203],[-142,138],[-164,287]],[[18570,68996],[-201,234],[-93,-25]],[[19362,64423],[-181,337],[-201,286]],[[17464,69802],[316,46],[353,64],[-26,-116],[419,-287],[634,-416],[552,4],[221,0],[0,244],[481,0],[102,-210],[142,-186],[165,-260],[92,-309],[69,-325],[144,-178],[230,-177],[175,467],[227,11],[196,-236],[139,-404],[96,-346],[164,-337],[61,-414],[78,-277],[217,-184],[197,-130],[108,18]],[[55993,75539],[95,35],[128,9]],[[46619,59216],[93,107],[47,348],[88,14],[194,-165],[157,117],[107,-39],[42,131],[1114,9],[62,414],[-48,73],[-134,2550],[-134,2550],[425,10]],[[51185,61897],[1,-1361],[-152,-394],[-24,-364],[-247,-94],[-379,-51],[-102,-210],[-178,-23]],[[46801,57931],[13,184],[-24,229],[-104,166],[-54,338],[-13,368]],[[77809,62643],[-159,-137],[-162,-256],[-196,-26],[-127,-639],[-117,-107],[134,-519],[177,-431],[113,-390],[-101,-514],[-96,-109],[66,-296],[185,-470],[32,-330],[-4,-274],[108,-539],[-152,-551],[-135,-607]],[[55338,76294],[74,-101],[40,-82],[91,-63],[106,-123],[-22,-51]],[[55380,75322],[-58,46]],[[74375,79706],[292,102],[530,509],[423,278],[242,-182],[289,-8],[186,-276],[277,-22],[402,-148],[270,411],[-113,348],[288,612],[311,-244],[252,-69],[327,-152],[53,-443],[394,-248],[263,109],[351,78],[279,-78],[272,-284],[168,-302],[258,6],[350,-96],[255,146],[366,98],[407,416],[166,-63],[146,-198],[331,49]],[[59599,43773],[209,48],[334,-166],[73,74],[193,16],[99,177],[167,-10],[303,230],[221,342]],[[59870,36949],[-45,-275],[65,-101]],[[59890,36573],[-41,-245],[-116,-211]],[[59119,34780],[-211,5]],[[58908,34785],[-24,261],[-41,265]],[[58843,35311],[-23,212],[49,659],[-72,419],[-133,832]],[[58664,37433],[292,671],[74,426],[42,53],[31,348],[-45,175],[12,442],[54,409],[0,748],[-145,190],[-132,43],[-60,146],[-128,125],[-232,-12],[-18,220]],[[58409,41417],[-26,421],[843,487]],[[59226,42325],[159,-284],[77,54],[110,-149],[16,-237],[-59,-274],[21,-417],[181,-365],[85,410],[120,124],[-24,760],[-116,427],[-100,191],[-97,-9],[-77,768],[77,449]],[[46619,59216],[-184,405],[-168,435],[-184,157],[-133,173],[-155,-6],[-135,-129],[-138,51],[-96,-189]],[[45260,62987],[60,197],[1088,-4],[-53,853],[68,304],[261,53],[-9,1512],[911,-31],[1,895]],[[59226,42325],[-147,153],[85,549],[87,205],[-53,490],[56,479],[47,160],[-71,501],[-131,264]],[[59099,45126],[273,-110],[55,-164],[95,-275],[77,-804]],[[77801,54399],[48,105],[227,-258],[22,-304],[183,71],[91,243]],[[56448,40227],[228,134],[180,-34],[109,-133],[2,-49]],[[55526,35946],[0,-2182],[-248,-302],[-149,-43],[-175,112],[-125,43],[-47,252],[-109,162],[-133,-292]],[[54125,64088],[68,-919],[104,-153],[4,-188],[116,-203],[-60,-254],[-107,-1199],[-15,-769],[-354,-557],[-120,-778],[115,-219],[0,-380],[178,-13],[-28,-279]],[[53939,57955],[-52,-13],[-188,647],[-65,24],[-217,-331],[-215,173],[-150,34],[-80,-83],[-163,18],[-164,-252],[-141,-14],[-337,305],[-131,-145],[-142,10],[-104,223],[-279,221],[-298,-70],[-72,-128],[-39,-340],[-80,-238],[-19,-527]],[[52072,53186],[-105,31],[-107,-132]],[[51398,53895],[-197,389],[-209,-7]],[[25647,58207],[31,91],[46,-88]],[[51063,81078],[244,869],[380,248]],[[58639,91676],[-473,-237],[-224,-54]],[[55734,91409],[-172,-24],[-41,-389],[-523,95],[-74,-329],[-267,2],[-183,-421],[-278,-655],[-431,-831],[101,-202],[-97,-234],[-275,10],[-180,-554],[17,-784],[177,-300],[-92,-694],[-231,-405],[-122,-341]],[[52328,85032],[-371,-138],[-384,301]],[[51474,85830],[-88,1363],[256,381]],[[65352,60997],[1,-238],[-134,-165]],[[64880,60451],[-128,-34]],[[64752,60417],[-91,413],[-217,975]],[[64444,61805],[833,591],[185,1182],[-127,418]],[[65945,64688],[203,-78],[165,-107]],[[68734,64727],[-83,424],[-215,450]],[[28212,55535],[-52,196],[-138,164]],[[27170,56020],[-6,-126],[111,-26]],[[55461,82736],[342,-67],[511,9]],[[56535,81053],[139,-515],[-29,-166],[-138,-69],[-252,-491],[71,-266],[-60,35]],[[56266,79581],[-264,227],[-200,-84],[-131,61],[-165,-127],[-140,210],[-114,-81],[-16,36]],[[86221,75560],[-120,-200],[-83,-202]],[[85048,72883],[-135,112],[-34,-111]],[[84641,73095],[76,260],[66,69]],[[84829,73851],[-18,96],[-163,65]],[[86288,75628],[39,-104]],[[64246,66009],[84,-185],[5,-346]],[[64274,65130],[-77,-42],[-84,117]],[[56308,78869],[120,127],[172,-65],[178,-3],[129,-144],[95,91],[205,56],[69,139],[118,0]],[[57842,77455],[124,-109],[131,95],[126,-101]],[[56293,76715],[-51,103],[65,99],[-69,74],[-87,-133],[-162,172],[-22,244],[-169,139],[-31,188],[-151,232]],[[81143,94175],[251,112],[141,-379]],[[84237,94144],[590,-104],[443,4]],[[97224,91732],[123,262],[886,-165]],[[96192,85904],[-126,219],[-268,-253]],[[95032,82989],[-486,-302],[-96,-674]],[[93543,84472],[14,276],[432,132]],[[95182,86999],[-705,-649],[-227,727]],[[90412,85637],[-914,-175],[-899,-1153]],[[88378,82339],[178,295],[305,-38]],[[89262,81946],[9,-503],[-217,-590]],[[60617,78409],[9,262],[143,165],[269,43],[44,197],[-62,326],[113,310],[-3,173],[-410,192],[-162,-6],[-172,277],[-213,-94],[-352,208],[6,116],[-99,256],[-222,29],[-23,183],[70,120],[-178,334],[-288,-57],[-84,30],[-70,-134],[-104,23]],[[58639,91676],[286,206],[456,-358],[761,-140],[1050,-668],[213,-281],[18,-393],[-308,-311],[-454,-157],[-1240,449],[-204,-75],[453,-433]],[[59670,89515],[18,-274],[18,-604]],[[59706,88637],[358,-180],[217,-153],[36,286]],[[60317,88590],[-168,254],[177,224]],[[60998,88700],[233,144],[-186,433]],[[62654,90499],[1,-328],[219,-203]],[[63371,90473],[580,282],[970,507]],[[69038,93080],[183,537],[206,116]],[[69427,93733],[736,-156],[57,-328]],[[70444,91717],[222,593],[-361,482]],[[72363,94093],[483,119],[669,-26]],[[58449,49909],[110,-333],[-16,-348],[-80,-74]],[[58216,49787],[67,-60],[166,182]],[[61883,60238],[-37,252],[-83,178]],[[60335,65400],[-77,306],[-81,132]],[[63741,66597],[190,-249],[16,-243]],[[64444,61805],[-801,-226],[-259,-266],[-199,-620],[-130,-99],[-70,197],[-106,-30],[-269,60],[-50,59],[-321,-14],[-75,-53],[-114,153],[-74,-290],[28,-249],[-121,-189]],[[56351,57163],[3,143],[-102,174],[-3,343],[-58,228],[-98,-34],[28,217],[72,246],[-32,245],[92,181],[-58,138],[73,365],[127,435],[240,-41],[-14,2345]],[[59433,56242],[1,-71]],[[59434,56171],[-39,12],[5,294],[-33,203],[-143,233],[-34,426],[34,436],[-129,41],[-19,-132],[-167,-30],[67,-173],[23,-355],[-152,-324],[-138,-426],[-144,-61],[-233,345],[-105,-122],[-29,-172],[-143,-112],[-9,-122],[-277,0],[-38,122],[-200,20],[-100,-101],[-77,51],[-143,344],[-48,163],[-200,-81],[-76,-274],[-72,-528],[-95,-111],[-85,-65]],[[56635,55672],[-23,28]],[[59445,53091],[-171,-272],[-195,1],[-224,-138],[-176,132],[-115,-161]],[[56824,55442],[-189,230]],[[59434,56171],[3,-460]],[[25613,58487],[-31,-139]],[[62075,57243],[54,-245],[125,-247]],[[63596,57321],[-2,-9],[-1,-244],[0,-596],[0,-308],[-125,-363],[-194,-493]],[[34889,53069],[109,-351],[-49,-254],[-24,-270],[-71,-248]],[[56266,79581],[-77,-154],[-55,-238]],[[58908,34785],[-56,-263],[-163,-63],[-166,320],[-2,204],[76,222],[26,172],[80,42],[140,-108]],[[60041,71744],[74,129],[75,130],[15,329],[91,-115],[306,165],[147,-112],[229,2],[320,222],[149,-10],[316,92]],[[68841,72526],[156,598],[-60,440],[-204,140],[72,261],[232,-28],[132,326],[89,380],[371,137],[-58,-274],[40,-164],[114,15]],[[65546,74986],[313,8],[-45,297],[237,204],[234,343],[374,-312],[30,-471],[106,-121],[301,27],[93,-108],[137,-609],[317,-408],[181,-278],[291,-289],[369,-253],[-7,-362]],[[53083,72381],[-139,-290],[-2,-273]],[[58441,72005],[-192,-70],[-268,314]],[[57981,72249],[-303,-11],[-165,588]],[[59768,75418],[485,-417],[399,-228]],[[57321,74302],[-87,276],[3,121]],[[59099,45126],[-157,177],[-177,100],[-111,99],[-116,150]],[[58388,46397],[-161,331],[-55,342]],[[58449,49909],[98,71],[304,-7],[566,45]],[[30523,76389],[-147,-351],[-47,-133]],[[30377,75084],[-133,11],[-205,-103]],[[29172,73738],[-61,30],[-91,148]],[[29077,73598],[69,-105],[5,-223]],[[28966,72994],[-142,225],[-33,491]],[[28797,73080],[-183,93],[191,-191]],[[27672,65472],[-83,-75],[-137,72]],[[27408,65728],[-105,136],[-148,508]],[[26747,68267],[-108,90],[-281,-268]],[[26309,68119],[-135,275],[-174,147]],[[25227,68491],[-114,-92],[50,-157]],[[24755,67801],[-207,312],[-242,-73]],[[16564,70932],[-71,95],[-33,324]],[[16460,71351],[-270,594],[-231,821],[10,137],[-123,195],[-215,495],[-38,482],[-148,323],[61,489],[-10,507],[-89,453],[109,557]],[[15516,76404],[34,536],[33,536]],[[15583,77476],[-50,792],[-88,506],[-80,274],[33,115],[402,-200],[148,-558]],[[15948,78405],[69,156],[-45,484],[-94,485]],[[10396,86079],[-385,-51],[-546,272]],[[8164,85656],[-308,-126],[-39,348]],[[7158,84934],[-299,-248],[-278,-180]],[[4985,85596],[50,216],[-179,211]],[[4541,89915],[-38,-296],[586,23]],[[4864,90008],[-342,225],[-197,296]],[[30102,56752],[-123,-344],[105,-468]],[[31762,56607],[213,-74],[155,185]],[[63521,58853],[-122,-33],[-83,35]],[[63153,58610],[-177,-114],[-233,-30]],[[62539,58233],[-43,-150],[-137,13]],[[64752,60417],[-201,-158]],[[57838,31217],[-210,-269],[-290,-229]],[[58175,37528],[113,-7],[134,-100],[94,71],[148,-59]],[[58409,41417],[-210,-81],[-159,-235],[-33,-205],[-100,-46],[-241,-486],[-154,-383],[-94,-13],[-90,68],[-311,65]]]} \ No newline at end of file diff --git a/examples/vega-data/world-110m.json b/examples/vega-data/world-110m.json new file mode 100644 index 000000000000..a1ce852de6f2 --- /dev/null +++ b/examples/vega-data/world-110m.json @@ -0,0 +1 @@ +{"type":"Topology","transform":{"scale":[0.0036000360003600037,0.0016925586033320111],"translate":[-180,-85.60903777459777]},"objects":{"land":{"type":"MultiPolygon","arcs":[[[0]],[[1]],[[2]],[[3]],[[4]],[[5]],[[6]],[[7,8,9]],[[10,11]],[[12]],[[13]],[[14]],[[15]],[[16]],[[17]],[[18]],[[19]],[[20]],[[21]],[[22]],[[23]],[[24]],[[25]],[[26]],[[27]],[[28]],[[29,30]],[[31]],[[32]],[[33]],[[34]],[[35]],[[36]],[[37]],[[38]],[[39]],[[40]],[[41]],[[42,43]],[[44]],[[45]],[[46]],[[47,48,49,50]],[[51]],[[52]],[[53]],[[54]],[[55]],[[56]],[[57]],[[58]],[[59]],[[60]],[[61]],[[62,63]],[[64]],[[65]],[[66]],[[67]],[[68]],[[69]],[[70]],[[71]],[[72]],[[73]],[[74]],[[75]],[[76,77]],[[78]],[[79]],[[80]],[[81]],[[82]],[[83]],[[84]],[[85]],[[86]],[[87]],[[88]],[[89]],[[90,91]],[[92]],[[93]],[[94]],[[95]],[[96]],[[97]],[[98]],[[99]],[[100]],[[101]],[[102]],[[103]],[[104]],[[105]],[[106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221]],[[222,223]],[[224]],[[225]],[[226]],[[227]],[[228]],[[229]],[[230,231,232,233]],[[234]],[[235]],[[236]],[[237]],[[238]],[[239]],[[240]],[[241]],[[242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477],[478,479,480,481,482,483,484]],[[485]],[[486]],[[487]],[[488]],[[489]],[[490]],[[491]],[[492]],[[493]],[[494]],[[495]],[[496]],[[497]],[[498]]]},"countries":{"type":"GeometryCollection","geometries":[{"type":"Polygon","arcs":[[499,500,501,502,503,504]],"id":4},{"type":"MultiPolygon","arcs":[[[505,506,352,507]],[[354,508,509]]],"id":24},{"type":"Polygon","arcs":[[510,511,414,512,513,514]],"id":8},{"type":"Polygon","arcs":[[312,515,314,516,517]],"id":784},{"type":"MultiPolygon","arcs":[[[518,11]],[[519,520,521,166,522,168,523,524]]],"id":32},{"type":"Polygon","arcs":[[525,526,527,528,529]],"id":51},{"type":"MultiPolygon","arcs":[[[0]],[[1]],[[2]],[[3]],[[4]],[[5]],[[6]],[[530,531]]],"id":10},{"type":"Polygon","arcs":[[13]],"id":260},{"type":"MultiPolygon","arcs":[[[14]],[[24]]],"id":36},{"type":"Polygon","arcs":[[532,533,534,535,536,537,538]],"id":40},{"type":"MultiPolygon","arcs":[[[539,-528]],[[484,540,479,541,-526,542,543]]],"id":31},{"type":"Polygon","arcs":[[544,545,546]],"id":108},{"type":"Polygon","arcs":[[547,548,549,550,437]],"id":56},{"type":"Polygon","arcs":[[551,552,553,554,366]],"id":204},{"type":"Polygon","arcs":[[555,556,557,-553,558,559]],"id":854},{"type":"Polygon","arcs":[[560,561,289,562]],"id":50},{"type":"Polygon","arcs":[[563,404,564,565,566,567]],"id":100},{"type":"MultiPolygon","arcs":[[[71]],[[73]],[[74]]],"id":44},{"type":"Polygon","arcs":[[568,569,570]],"id":70},{"type":"Polygon","arcs":[[571,572,573,574,575]],"id":112},{"type":"Polygon","arcs":[[576,145,577]],"id":84},{"type":"Polygon","arcs":[[578,579,580,581,-525]],"id":68},{"type":"Polygon","arcs":[[-521,582,-581,583,584,585,586,587,588,164,589]],"id":76},{"type":"Polygon","arcs":[[48,590]],"id":96},{"type":"Polygon","arcs":[[591,592]],"id":64},{"type":"Polygon","arcs":[[593,594,595,596]],"id":72},{"type":"Polygon","arcs":[[597,598,599,600,601,602,603]],"id":140},{"type":"MultiPolygon","arcs":[[[84]],[[85]],[[86]],[[87]],[[88]],[[96]],[[97]],[[99]],[[101]],[[103]],[[604,107,605,109,606,111,607,113,608,115,609,117,610,199,611,201,612,215,613,217,614,219,615,221]],[[616,223]],[[224]],[[225]],[[226]],[[227]],[[229]],[[230,617,232,618]],[[235]],[[237]],[[238]],[[240]],[[241]],[[485]],[[486]],[[488]],[[489]],[[490]],[[496]],[[497]]],"id":124},{"type":"Polygon","arcs":[[-536,619,620,621]],"id":756},{"type":"MultiPolygon","arcs":[[[-519,622,623,624]],[[-524,169,625,171,626,-579]]],"id":152},{"type":"MultiPolygon","arcs":[[[64]],[[627,274,628,276,629,278,630,280,631,632,633,634,635,-593,636,637,638,639,-503,640,641,642,643,644,645]]],"id":156},{"type":"Polygon","arcs":[[369,646,647,648,-556,649]],"id":384},{"type":"Polygon","arcs":[[650,651,652,359,653,654,655,656,-604,657]],"id":120},{"type":"Polygon","arcs":[[658,659,-545,660,661,662,663,-508,353,-510,664,-602,665]],"id":180},{"type":"Polygon","arcs":[[-509,355,666,-658,-603,-665]],"id":178},{"type":"Polygon","arcs":[[667,174,668,155,669,-585,670]],"id":170},{"type":"Polygon","arcs":[[178,671,151,672]],"id":188},{"type":"Polygon","arcs":[[70]],"id":192},{"type":"Polygon","arcs":[[77,673]],"id":-99},{"type":"Polygon","arcs":[[76,-674]],"id":196},{"type":"Polygon","arcs":[[-538,674,675,676]],"id":203},{"type":"Polygon","arcs":[[445,677,-675,-537,-622,678,679,-549,680,441,681]],"id":276},{"type":"Polygon","arcs":[[337,682,683,684]],"id":262},{"type":"MultiPolygon","arcs":[[[92]],[[-682,442,685,444]]],"id":208},{"type":"Polygon","arcs":[[62,686]],"id":214},{"type":"Polygon","arcs":[[687,688,689,690,691,384,692,693]],"id":12},{"type":"Polygon","arcs":[[173,-668,694]],"id":218},{"type":"Polygon","arcs":[[333,695,696,390,697]],"id":818},{"type":"Polygon","arcs":[[698,699,700,336,-685]],"id":232},{"type":"Polygon","arcs":[[431,701,433,702,427,703,429,704]],"id":724},{"type":"Polygon","arcs":[[450,705,706]],"id":233},{"type":"Polygon","arcs":[[-684,707,708,709,710,711,712,-699]],"id":231},{"type":"Polygon","arcs":[[713,452,714,715,455,716,717]],"id":246},{"type":"MultiPolygon","arcs":[[[18]],[[19]],[[20]]],"id":242},{"type":"Polygon","arcs":[[12]],"id":238},{"type":"MultiPolygon","arcs":[[[718,719,163,-589]],[[82]],[[720,-679,-621,721,426,-703,434,722,436,-551]]],"id":250},{"type":"Polygon","arcs":[[356,723,-651,-667]],"id":266},{"type":"MultiPolygon","arcs":[[[724,90]],[[725,726,727,728,729,730,731,732]]],"id":826},{"type":"Polygon","arcs":[[400,733,-543,-530,734]],"id":268},{"type":"Polygon","arcs":[[368,-650,-560,735]],"id":288},{"type":"Polygon","arcs":[[736,737,374,738,739,740,-648]],"id":324},{"type":"Polygon","arcs":[[741,377]],"id":270},{"type":"Polygon","arcs":[[375,742,-739]],"id":624},{"type":"Polygon","arcs":[[357,-652,-724]],"id":226},{"type":"MultiPolygon","arcs":[[[78]],[[407,743,409,744,411,745,413,-512,746,-566,747]]],"id":300},{"type":"Polygon","arcs":[[498]],"id":304},{"type":"Polygon","arcs":[[185,748,-578,146,749,750]],"id":320},{"type":"Polygon","arcs":[[161,751,-587,752]],"id":328},{"type":"Polygon","arcs":[[182,753,754,-750,147,755,149,756]],"id":340},{"type":"Polygon","arcs":[[757,-571,758,417,759,419,760,761]],"id":191},{"type":"Polygon","arcs":[[-687,63]],"id":332},{"type":"Polygon","arcs":[[-533,762,763,764,765,-762,766]],"id":348},{"type":"MultiPolygon","arcs":[[[26]],[[767,30]],[[31]],[[32]],[[35]],[[36]],[[39]],[[40]],[[768,43]],[[44]],[[45]],[[769,50]],[[46]]],"id":360},{"type":"Polygon","arcs":[[-639,770,-637,-592,-636,771,-563,290,772,292,773,294,774,296,775]],"id":356},{"type":"Polygon","arcs":[[91,-725]],"id":372},{"type":"Polygon","arcs":[[776,-505,777,300,778,302,779,780,781,-540,-527,-542,480]],"id":364},{"type":"Polygon","arcs":[[782,783,784,785,786,-781,787]],"id":368},{"type":"Polygon","arcs":[[100]],"id":352},{"type":"Polygon","arcs":[[788,789,-698,391,790,791,792]],"id":376},{"type":"MultiPolygon","arcs":[[[79]],[[80]],[[793,421,794,423,795,425,-722,-620,-535]]],"id":380},{"type":"Polygon","arcs":[[61]],"id":388},{"type":"Polygon","arcs":[[796,-785,797,332,-790,798,-793]],"id":400},{"type":"MultiPolygon","arcs":[[[75]],[[81]],[[83]]],"id":392},{"type":"Polygon","arcs":[[799,800,482,801,-643,802]],"id":398},{"type":"Polygon","arcs":[[342,803,804,805,-710,806]],"id":404},{"type":"Polygon","arcs":[[-803,-642,807,808]],"id":417},{"type":"Polygon","arcs":[[809,810,811,283]],"id":116},{"type":"Polygon","arcs":[[265,812,267,813]],"id":410},{"type":"Polygon","arcs":[[-515,814,815,816]],"id":-99},{"type":"Polygon","arcs":[[304,817,-783]],"id":414},{"type":"Polygon","arcs":[[818,819,-634,820,-811]],"id":418},{"type":"Polygon","arcs":[[-791,392,821]],"id":422},{"type":"Polygon","arcs":[[370,822,372,823,-737,-647]],"id":430},{"type":"Polygon","arcs":[[824,-694,825,388,826,-697,827,828]],"id":434},{"type":"Polygon","arcs":[[52]],"id":144},{"type":"Polygon","arcs":[[829]],"id":426},{"type":"Polygon","arcs":[[830,448,831,-572,832]],"id":440},{"type":"Polygon","arcs":[[-680,-721,-550]],"id":442},{"type":"Polygon","arcs":[[449,-707,833,-573,-832]],"id":428},{"type":"Polygon","arcs":[[-692,834,835,836,837,383]],"id":504},{"type":"Polygon","arcs":[[838,839]],"id":498},{"type":"Polygon","arcs":[[23]],"id":450},{"type":"Polygon","arcs":[[840,-577,-749,186,841,188,842,190,843,192,844,194,845]],"id":484},{"type":"Polygon","arcs":[[-817,846,-567,-747,-511]],"id":807},{"type":"Polygon","arcs":[[847,-689,848,-557,-649,-741,849]],"id":466},{"type":"Polygon","arcs":[[287,-561,-772,-635,-820,850]],"id":104},{"type":"Polygon","arcs":[[416,-759,-570,851,-815,-514,852]],"id":499},{"type":"Polygon","arcs":[[853,-645]],"id":496},{"type":"Polygon","arcs":[[854,344,855,856,347,857,858,859,860,861,862]],"id":508},{"type":"Polygon","arcs":[[863,379,864,-690,-848]],"id":478},{"type":"Polygon","arcs":[[-863,865,866]],"id":454},{"type":"MultiPolygon","arcs":[[[285,867]],[[-770,47,-591,49]]],"id":458},{"type":"Polygon","arcs":[[351,-507,868,-595,869]],"id":516},{"type":"Polygon","arcs":[[17]],"id":540},{"type":"Polygon","arcs":[[-558,-849,-688,-825,870,-656,871,-554]],"id":562},{"type":"Polygon","arcs":[[361,872,363,873,365,-555,-872,-655]],"id":566},{"type":"Polygon","arcs":[[179,874,181,-757,150,-672]],"id":558},{"type":"Polygon","arcs":[[-681,-548,438,875,440]],"id":528},{"type":"MultiPolygon","arcs":[[[876,-718,877,457,878,459,879,461]],[[487]],[[492]],[[493]]],"id":578},{"type":"Polygon","arcs":[[-771,-638]],"id":524},{"type":"MultiPolygon","arcs":[[[15]],[[16]]],"id":554},{"type":"MultiPolygon","arcs":[[[880,319,881,882,883,-517,315,884,317]],[[-516,313]]],"id":512},{"type":"Polygon","arcs":[[-640,-776,297,885,299,-778,-504]],"id":586},{"type":"Polygon","arcs":[[175,886,177,-673,152,887,154,-669]],"id":591},{"type":"Polygon","arcs":[[-627,172,-695,-671,-584,-580]],"id":604},{"type":"MultiPolygon","arcs":[[[51]],[[54]],[[55]],[[56]],[[57]],[[58]],[[59]]],"id":608},{"type":"MultiPolygon","arcs":[[[37]],[[38]],[[-769,42]],[[41]]],"id":598},{"type":"Polygon","arcs":[[-678,446,888,-833,-576,889,890,-676]],"id":616},{"type":"Polygon","arcs":[[60]],"id":630},{"type":"Polygon","arcs":[[262,891,264,-814,892,269,893,271,894,273,-628,895]],"id":408},{"type":"Polygon","arcs":[[-705,430]],"id":620},{"type":"Polygon","arcs":[[-582,-583,-520]],"id":600},{"type":"Polygon","arcs":[[-799,-789]],"id":275},{"type":"Polygon","arcs":[[308,896,310,897]],"id":634},{"type":"Polygon","arcs":[[898,-840,899,403,-564,900,-765]],"id":642},{"type":"MultiPolygon","arcs":[[[89]],[[-889,447,-831]],[[102]],[[104]],[[105]],[[228]],[[234]],[[236]],[[239]],[[901,243,902,245,903,247,904,249,905,251,906,253,907,255,908,257,909,259,910,261,-896,-646,-854,-644,-802,483,-544,-734,401,911,-574,-834,-706,451,-714,-877,912,913,914,915,464,916,466,917,468,918,470,919,920,473,921,475,922,477]],[[491]],[[494]],[[495]]],"id":643},{"type":"Polygon","arcs":[[923,-546,-660,924]],"id":646},{"type":"Polygon","arcs":[[-691,-865,380,-835]],"id":732},{"type":"Polygon","arcs":[[925,329,926,331,-798,-784,-818,305,927,307,-898,311,-518,-884,928]],"id":682},{"type":"Polygon","arcs":[[-599,929,-828,-696,334,-700,-713,930,931,932]],"id":729},{"type":"Polygon","arcs":[[-711,-806,933,-666,-601,934,-932,935]],"id":728},{"type":"Polygon","arcs":[[378,-864,-850,-740,-743,376,-742]],"id":686},{"type":"MultiPolygon","arcs":[[[25]],[[27]],[[28]],[[33]],[[34]]],"id":90},{"type":"Polygon","arcs":[[373,-738,-824]],"id":694},{"type":"Polygon","arcs":[[184,-751,-755,936]],"id":222},{"type":"Polygon","arcs":[[-708,-683,338,937,340,938]],"id":-99},{"type":"Polygon","arcs":[[-807,-709,-939,341]],"id":706},{"type":"Polygon","arcs":[[-568,-847,-816,-852,-569,-758,-766,-901]],"id":688},{"type":"Polygon","arcs":[[162,-720,939,-588,-752]],"id":740},{"type":"Polygon","arcs":[[-891,940,-763,-539,-677]],"id":703},{"type":"Polygon","arcs":[[-534,-767,-761,420,-794]],"id":705},{"type":"Polygon","arcs":[[-878,-717,456]],"id":752},{"type":"Polygon","arcs":[[941,-859]],"id":748},{"type":"Polygon","arcs":[[-797,-792,-822,393,942,-786]],"id":760},{"type":"Polygon","arcs":[[-871,-829,-930,-598,-657]],"id":148},{"type":"Polygon","arcs":[[-736,-559,-552,367]],"id":768},{"type":"Polygon","arcs":[[284,-868,286,-851,-819,-810]],"id":764},{"type":"Polygon","arcs":[[-808,-641,-502,943]],"id":762},{"type":"Polygon","arcs":[[-777,481,-801,944,-500]],"id":795},{"type":"Polygon","arcs":[[29,-768]],"id":626},{"type":"Polygon","arcs":[[53]],"id":780},{"type":"Polygon","arcs":[[-693,385,945,387,-826]],"id":788},{"type":"MultiPolygon","arcs":[[[399,-735,-529,-782,-787,-943,394,946,947,397,948]],[[949,-748,-565,405]]],"id":792},{"type":"Polygon","arcs":[[72]],"id":158},{"type":"Polygon","arcs":[[-804,343,-855,-867,950,-663,951,-661,-547,-924,952]],"id":834},{"type":"Polygon","arcs":[[-925,-659,-934,-805,-953]],"id":800},{"type":"Polygon","arcs":[[-912,402,-900,-839,-899,-764,-941,-890,-575]],"id":804},{"type":"Polygon","arcs":[[-590,165,-522]],"id":858},{"type":"MultiPolygon","arcs":[[[65]],[[66]],[[67]],[[68]],[[69]],[[118,953,120,954,122,955,124,956,126,957,128,958,130,959,132,960,134,961,136,962,138,963,140,964,142,-846,195,965,966,967,968,969,-611]],[[93]],[[95]],[[98]],[[-613,202,970,204,971,206,972,208,973,210,974,212,975,214]]],"id":840},{"type":"Polygon","arcs":[[-945,-800,-809,-944,-501]],"id":860},{"type":"Polygon","arcs":[[156,976,158,977,160,-753,-586,-670]],"id":862},{"type":"Polygon","arcs":[[282,-812,-821,-633]],"id":704},{"type":"MultiPolygon","arcs":[[[21]],[[22]]],"id":548},{"type":"Polygon","arcs":[[321,978,323,979,325,980,327,-929,-883,981]],"id":887},{"type":"Polygon","arcs":[[982,350,-870,-594,983,-860,-942,-858,348],[-830]],"id":710},{"type":"Polygon","arcs":[[-866,-862,984,-596,-869,-506,-664,-951]],"id":894},{"type":"Polygon","arcs":[[-984,-597,-985,-861]],"id":716}]}},"arcs":[[[33289,2723],[-582,81],[-621,-35],[-348,197],[0,23],[-152,174],[625,-23],[599,-58],[207,243],[147,208],[288,-243],[-82,-301],[-81,-266]],[[5242,3530],[-364,208],[-163,209],[-11,35],[-180,162],[169,220],[517,-93],[277,-185],[212,-209],[76,-266],[-533,-81]],[[35977,2708],[-658,35],[-365,197],[49,243],[593,162],[239,197],[174,254],[126,220],[168,209],[180,243],[141,0],[414,127],[419,-127],[342,-255],[120,-359],[33,-254],[11,-301],[-430,-186],[-452,-150],[-522,-139],[-582,-116]],[[16602,6806],[-386,47],[-278,208],[60,197],[332,-104],[359,-93],[332,104],[-158,-208],[-261,-151]],[[15547,6934],[-164,23],[-359,58],[-381,162],[202,127],[277,-139],[425,-231]],[[23277,7733],[-217,46],[-337,-23],[-343,23],[-376,-35],[-283,116],[-146,243],[174,104],[353,-81],[403,-46],[305,-81],[304,69],[163,-335]],[[30256,7743],[-364,11],[136,232],[-327,-81],[-310,-81],[-212,174],[-16,243],[305,231],[190,70],[321,-23],[82,301],[16,219],[-6,475],[158,278],[256,93],[147,-220],[65,-220],[120,-267],[92,-254],[76,-267],[33,-266],[-49,-231],[-76,-220],[-326,-81],[-311,-116]],[[794,704],[78,49],[94,61],[81,52],[41,26]],[[1088,892],[41,-1],[29,-10]],[[1158,881],[402,-246],[352,246],[63,34],[816,104],[265,-138],[130,-71],[419,-196],[789,-151],[625,-185],[1072,-139],[800,162],[1181,-116],[669,-185],[734,174],[773,162],[60,278],[-1094,23],[-898,139],[-234,231],[-745,128],[49,266],[103,243],[104,220],[-55,243],[-462,162],[-212,209],[-430,185],[675,-35],[642,93],[402,-197],[495,173],[457,220],[223,197],[-98,243],[-359,162],[-408,174],[-571,35],[-500,81],[-539,58],[-180,220],[-359,185],[-217,208],[-87,672],[136,-58],[250,-185],[457,58],[441,81],[228,-255],[441,58],[370,127],[348,162],[315,197],[419,58],[-11,220],[-97,220],[81,208],[359,104],[163,-196],[425,115],[321,151],[397,12],[375,57],[376,139],[299,128],[337,127],[218,-35],[190,-46],[414,81],[370,-104],[381,11],[364,81],[375,-57],[414,-58],[386,23],[403,-12],[413,-11],[381,23],[283,174],[337,92],[349,-127],[331,104],[300,208],[179,-185],[98,-208],[180,-197],[288,174],[332,-220],[375,-70],[321,-162],[392,35],[354,104],[418,-23],[376,-81],[381,-104],[147,254],[-180,197],[-136,209],[-359,46],[-158,220],[-60,220],[-98,440],[213,-81],[364,-35],[359,35],[327,-93],[283,-174],[119,-208],[376,-35],[359,81],[381,116],[342,70],[283,-139],[370,46],[239,451],[224,-266],[321,-104],[348,58],[228,-232],[365,-23],[337,-69],[332,-128],[218,220],[108,209],[278,-232],[381,58],[283,-127],[190,-197],[370,58],[288,127],[283,151],[337,81],[392,69],[354,81],[272,127],[163,186],[65,254],[-32,244],[-87,231],[-98,232],[-87,231],[-71,209],[-16,231],[27,232],[130,220],[109,243],[44,231],[-55,255],[-32,232],[136,266],[152,173],[180,220],[190,186],[223,173],[109,255],[152,162],[174,151],[267,34],[174,186],[196,115],[228,70],[202,150],[157,186],[218,69],[163,-151],[-103,-196],[-283,-174],[-120,-127],[-206,92],[-229,-58],[-190,-139],[-202,-150],[-136,-174],[-38,-231],[17,-220],[130,-197],[-190,-139],[-261,-46],[-153,-197],[-163,-185],[-174,-255],[-44,-220],[98,-243],[147,-185],[229,-139],[212,-185],[114,-232],[60,-220],[82,-232],[130,-196],[82,-220],[38,-544],[81,-220],[22,-232],[87,-231],[-38,-313],[-152,-243],[-163,-197],[-370,-81],[-125,-208],[-169,-197],[-419,-220],[-370,-93],[-348,-127],[-376,-128],[-223,-243],[-446,-23],[-489,23],[-441,-46],[-468,0],[87,-232],[424,-104],[311,-162],[174,-208],[-310,-185],[-479,58],[-397,-151],[-17,-243],[-11,-232],[327,-196],[60,-220],[353,-220],[588,-93],[500,-162],[398,-185],[506,-186],[690,-92],[681,-162],[473,-174],[517,-197],[272,-278],[136,-220],[337,209],[457,173],[484,186],[577,150],[495,162],[691,12],[680,-81],[560,-139],[180,255],[386,173],[702,12],[550,127],[522,128],[577,81],[614,104],[430,150],[-196,209],[-119,208],[0,220],[-539,-23],[-571,-93],[-544,0],[-77,220],[39,440],[125,128],[397,138],[468,139],[337,174],[337,174],[251,231],[380,104],[376,81],[190,47],[430,23],[408,81],[343,116],[337,139],[305,139],[386,185],[245,197],[261,173],[82,232],[-294,139],[98,243],[185,185],[288,116],[305,139],[283,185],[217,232],[136,277],[202,163],[331,-35],[136,-197],[332,-23],[11,220],[142,231],[299,-58],[71,-220],[331,-34],[360,104],[348,69],[315,-34],[120,-243],[305,196],[283,105],[315,81],[310,81],[283,139],[310,92],[240,128],[168,208],[207,-151],[288,81],[202,-277],[157,-209],[316,116],[125,232],[283,162],[365,-35],[108,-220],[229,220],[299,69],[326,23],[294,-11],[310,-70],[300,-34],[130,-197],[180,-174],[304,104],[327,24],[315,0],[310,11],[278,81],[294,70],[245,162],[261,104],[283,58],[212,162],[152,324],[158,197],[288,-93],[109,-208],[239,-139],[289,46],[196,-208],[206,-151],[283,139],[98,255],[250,104],[289,197],[272,81],[326,116],[218,127],[228,139],[218,127],[261,-69],[250,208],[180,162],[261,-11],[229,139],[54,208],[234,162],[228,116],[278,93],[256,46],[244,-35],[262,-58],[223,-162],[27,-254],[245,-197],[168,-162],[332,-70],[185,-162],[229,-162],[266,-35],[223,116],[240,243],[261,-127],[272,-70],[261,-69],[272,-46],[277,0],[229,-614],[-11,-150],[-33,-267],[-266,-150],[-218,-220],[38,-232],[310,12],[-38,-232],[-141,-220],[-131,-243],[212,-185],[321,-58],[321,104],[153,232],[92,220],[153,185],[174,174],[70,208],[147,289],[174,58],[316,24],[277,69],[283,93],[136,231],[82,220],[190,220],[272,151],[234,115],[153,197],[157,104],[202,93],[277,-58],[250,58],[272,69],[305,-34],[201,162],[142,393],[103,-162],[131,-278],[234,-115],[266,-47],[267,70],[283,-46],[261,-12],[174,58],[234,-35],[212,-127],[250,81],[300,0],[255,81],[289,-81],[185,197],[141,196],[191,163],[348,439],[179,-81],[212,-162],[185,-208],[354,-359],[272,-12],[256,0],[299,70],[299,81],[229,162],[190,174],[310,23],[207,127],[218,-116],[141,-185],[196,-185],[305,23],[190,-150],[332,-151],[348,-58],[288,47],[218,185],[185,185],[250,46],[251,-81],[288,-58],[261,93],[250,0],[245,-58],[256,-58],[250,104],[299,93],[283,23],[316,0],[255,58],[251,46],[76,290],[11,243],[174,-162],[49,-266],[92,-244],[115,-196],[234,-105],[315,35],[365,12],[250,35],[364,0],[262,11],[364,-23],[310,-46],[196,-186],[-54,-220],[179,-173],[299,-139],[310,-151],[360,-104],[375,-92],[283,-93],[315,-12],[180,197],[245,-162],[212,-185],[245,-139],[337,-58],[321,-69],[136,-232],[316,-139],[212,-208],[310,-93],[321,12],[299,-35],[332,12],[332,-47],[310,-81],[288,-139],[289,-116],[195,-173],[-32,-232],[-147,-208],[-125,-266],[-98,-209],[-131,-243],[-364,-93],[-163,-208],[-360,-127],[-125,-232],[-190,-220],[-201,-185],[-115,-243],[-70,-220],[-28,-266],[6,-220],[158,-232],[60,-220],[130,-208],[517,-81],[109,-255],[-501,-93],[-424,-127],[-528,-23],[-234,-336],[-49,-278],[-119,-220],[-147,-220],[370,-196],[141,-244],[239,-219],[338,-197],[386,-186],[419,-185],[636,-185],[142,-289],[800,-128],[53,-45],[208,-175],[767,151],[636,-186],[-99504,-147],[245,344],[501,-185],[32,21]],[[31400,18145],[-92,-239],[-238,-183],[-301,67],[-202,177],[-291,86],[-350,330],[-283,317],[-383,662],[229,-124],[390,-395],[369,-212],[143,271],[90,405],[256,244],[198,-70]],[[30935,19481],[106,-274],[139,-443],[361,-355],[389,-147],[-125,-296],[-264,-29],[-141,208]],[[33139,19680],[-139,266],[333,354],[236,-148],[167,237],[222,-266],[-83,-207],[-375,-177],[-125,207],[-236,-266]],[[69095,21172],[-7,314],[41,244],[19,121],[179,-186],[263,-74],[9,-112],[-77,-269],[-427,-38]],[[90796,24799],[-57,32],[-171,19],[-171,505],[-38,390],[-160,515],[7,271],[181,-52],[269,-204],[151,81],[217,113],[166,-39],[20,-702],[-95,-203],[-29,-476],[-97,162],[-193,-412]],[[97036,23023],[-256,13],[-180,194],[-302,42],[-46,217],[149,438],[349,583],[179,111],[200,225],[238,310],[167,306],[123,441],[106,149],[41,330],[195,273],[61,-251],[63,-244],[198,239],[80,-249],[0,-249],[-103,-274],[-182,-435],[-142,-238],[103,-284],[-214,-7],[-238,-223],[-75,-387],[-157,-597],[-219,-264],[-138,-169]],[[98677,25949],[-48,155],[-116,85],[160,486],[-91,326],[-299,236],[8,214],[201,206],[47,455],[-13,382],[-113,396],[8,104],[-133,244],[-218,523],[-117,418],[104,46],[151,-328],[216,-153],[78,-526],[202,-622],[5,403],[126,-161],[41,-447],[224,-192],[188,-48],[158,226],[141,-69],[-67,-524],[-85,-345],[-212,12],[-74,-179],[26,-254],[-41,-110],[-105,-319],[-138,-404],[-214,-236]],[[96316,37345],[-153,160],[-199,266],[-179,313],[-184,416],[-38,201],[119,-9],[156,-201],[122,-200],[89,-166],[228,-366],[144,-272],[-105,-142]],[[99425,39775],[-153,73],[-27,260],[107,203],[126,-74],[69,98],[96,-171],[-46,-308],[-172,-81]],[[99645,40529],[-36,220],[139,121],[88,33],[163,184],[0,-289],[-177,-145],[-177,-124]],[[0,40798],[0,289],[57,27],[-34,-284],[-23,-32]],[[96531,40773],[-93,259],[10,158],[175,-339],[-92,-78]],[[96463,41280],[-75,74],[-58,-32],[-39,163],[-6,453],[133,-182],[45,-476]],[[62613,35454],[-160,151],[-220,211],[-77,312],[-18,524],[-98,471],[-26,425],[50,426],[128,102],[1,197],[133,447],[25,377],[-65,280],[-52,372],[-23,544],[97,331],[38,375],[138,22],[155,121],[103,107],[122,7],[158,337],[229,364],[83,297],[-38,253],[118,-71],[153,410],[6,356],[92,264],[96,-254],[74,-251],[69,-390],[45,-711],[72,-276],[-28,-284],[-49,-174],[-94,347],[-53,-175],[53,-438],[-24,-250],[-77,-137],[-18,-500],[-109,-689],[-137,-814],[-172,-1120],[-106,-821],[-125,-685],[-226,-140],[-243,-250]],[[90643,27516],[-230,262],[-170,104],[43,308],[-152,-112],[-243,-428],[-240,160],[-158,94],[-159,42],[-269,171],[-179,364],[-52,449],[-64,298],[-137,240],[-267,71],[91,287],[-67,438],[-136,-408],[-247,-109],[146,327],[42,341],[107,289],[-22,438],[-226,-504],[-174,-202],[-106,-470],[-217,243],[9,313],[-174,429],[-147,221],[52,137],[-356,358],[-195,17],[-267,287],[-498,-56],[-359,-211],[-317,-197],[-265,39],[-294,-303],[-241,-137],[-53,-309],[-103,-240],[-236,-15],[-174,-52],[-246,107],[-199,-64],[-191,-27],[-165,-315],[-81,26],[-140,-167],[-133,-187],[-203,23],[-186,0],[-295,377],[-149,113],[6,338],[138,81],[47,134],[-10,212],[34,411],[-31,350],[-147,598],[-45,337],[12,336],[-111,385],[-7,174],[-123,235],[-35,463],[-158,467],[-39,252],[122,-255],[-93,548],[137,-171],[83,-229],[-5,303],[-138,465],[-26,186],[-65,177],[31,341],[56,146],[38,295],[-29,346],[114,425],[21,-450],[118,406],[225,198],[136,252],[212,217],[126,46],[77,-73],[219,220],[168,66],[42,129],[74,54],[153,-14],[292,173],[151,262],[71,316],[163,300],[13,236],[7,321],[194,502],[117,-510],[119,118],[-99,279],[87,287],[122,-128],[34,449],[152,291],[67,233],[140,101],[4,165],[122,-69],[5,148],[122,85],[134,80],[205,-271],[155,-350],[173,-4],[177,-56],[-59,325],[133,473],[126,155],[-44,147],[121,338],[168,208],[142,-70],[234,111],[-5,302],[-204,195],[148,86],[184,-147],[148,-242],[234,-151],[79,60],[172,-182],[162,169],[105,-51],[65,113],[127,-292],[-74,-316],[-105,-239],[-96,-20],[32,-236],[-81,-295],[-99,-291],[20,-166],[221,-327],[214,-189],[143,-204],[201,-350],[78,1],[145,-151],[43,-183],[265,-200],[183,202],[55,317],[56,262],[34,324],[85,470],[-39,286],[20,171],[-32,339],[37,445],[53,120],[-43,197],[67,313],[52,325],[7,168],[104,222],[78,-289],[19,-371],[70,-71],[11,-249],[101,-300],[21,-335],[-10,-214],[100,-464],[179,223],[92,-250],[133,-231],[-29,-262],[60,-506],[42,-295],[70,-72],[75,-505],[-27,-307],[90,-400],[301,-309],[197,-281],[186,-257],[-37,-143],[159,-371],[108,-639],[111,130],[113,-256],[68,91],[48,-626],[197,-363],[129,-226],[217,-478],[78,-475],[7,-337],[-19,-365],[132,-502],[-16,-523],[-48,-274],[-75,-527],[6,-339],[-55,-423],[-123,-538],[-205,-290],[-102,-458],[-93,-292],[-82,-510],[-107,-294],[-70,-442],[-36,-407],[14,-187],[-159,-205],[-311,-22],[-257,-242],[-127,-229],[-168,-254]],[[95110,44183],[-194,4],[-106,363],[166,-142],[56,-22],[78,-203]],[[83414,44519],[-368,414],[259,116],[146,-180],[97,-180],[-17,-159],[-117,-11]],[[94572,44733],[-170,60],[-58,91],[17,235],[183,-93],[91,-124],[45,-155],[-108,-14]],[[94868,44799],[-206,512],[-57,353],[94,0],[100,-473],[111,-283],[-42,-109]],[[84713,45326],[32,139],[239,133],[194,20],[87,74],[105,-74],[-102,-160],[-289,-258],[-233,-170]],[[84746,45030],[-181,-441],[-238,-130],[-33,71],[25,201],[119,360],[275,235]],[[82576,45238],[-149,5],[95,340],[153,5],[74,209],[100,-158],[172,48],[69,-251],[-321,-119],[-193,-79]],[[83681,45301],[-370,73],[0,216],[220,123],[174,-177],[185,45],[249,216],[-41,-328],[-417,-168]],[[94421,45535],[-218,251],[-152,212],[-104,197],[41,60],[128,-142],[228,-272],[65,-187],[12,-119]],[[93704,46205],[-121,134],[-114,243],[14,99],[166,-250],[111,-193],[-56,-33]],[[81823,45409],[-306,238],[-251,-16],[-288,44],[-260,106],[-322,225],[-204,59],[-116,-74],[-506,243],[-48,254],[-255,44],[191,564],[337,-35],[224,-231],[115,-45],[38,-210],[533,-59],[61,244],[515,-284],[101,-383],[417,-108],[341,-351],[-317,-225]],[[87280,46506],[-27,445],[49,212],[58,200],[63,-173],[0,-282],[-143,-402]],[[93221,46491],[-120,227],[-122,375],[-59,450],[38,57],[30,-175],[84,-134],[135,-375],[131,-200],[-39,-166],[-78,-59]],[[91733,46847],[-148,1],[-228,171],[-158,165],[23,183],[249,-86],[152,46],[42,283],[40,15],[27,-314],[158,45],[78,202],[155,211],[-30,348],[166,11],[56,-97],[-5,-327],[-93,-361],[-146,-48],[-44,-166],[-152,-144],[-142,-138]],[[85242,48340],[-192,108],[-54,254],[281,29],[69,-195],[-104,-196]],[[86342,48300],[-234,244],[-232,49],[-157,-39],[-192,21],[65,325],[344,24],[305,-172],[101,-452]],[[92451,47764],[-52,348],[-65,229],[-126,193],[-158,252],[-200,174],[77,143],[150,-166],[94,-130],[117,-142],[111,-248],[106,-189],[33,-307],[-87,-157]],[[89166,49043],[482,-407],[513,-338],[192,-302],[154,-297],[43,-349],[462,-365],[68,-313],[-256,-64],[62,-393],[248,-388],[180,-627],[159,20],[-11,-262],[215,-100],[-84,-111],[295,-249],[-30,-171],[-184,-41],[-69,153],[-238,66],[-281,89],[-216,377],[-158,325],[-144,517],[-362,259],[-235,-169],[-170,-195],[35,-436],[-218,-203],[-155,99],[-288,25]],[[89175,45193],[-247,485],[-282,118],[-69,-168],[-352,-18],[118,481],[175,164],[-72,642],[-134,496],[-538,500],[-229,50],[-417,546],[-82,-287],[-107,-52],[-63,216],[-1,257],[-212,290],[299,213],[198,-11],[-23,156],[-407,1],[-110,352],[-248,109],[-117,293],[374,143],[142,192],[446,-242],[44,-220],[78,-955],[287,-354],[232,627],[319,356],[247,1],[238,-206],[206,-212],[298,-113]],[[83276,47228],[-119,173],[79,544],[-43,570],[-117,4],[-86,405],[115,387],[40,469],[139,891],[58,243],[237,439],[217,-174],[350,-82],[319,25],[275,429],[48,-132],[-223,-587],[-209,-113],[-267,115],[-463,-29],[-243,-85],[-39,-447],[248,-526],[150,268],[518,201],[-22,-272],[-121,86],[-121,-347],[-245,-229],[263,-757],[-50,-203],[249,-682],[-2,-388],[-148,-173],[-109,207],[134,484],[-273,-229],[-69,164],[36,228],[-200,346],[21,576],[-186,-179],[24,-689],[11,-846],[-176,-85]],[[85582,50048],[-112,374],[-82,755],[56,472],[92,215],[20,-322],[164,-52],[26,-241],[-15,-517],[-143,58],[-42,-359],[114,-312],[-78,-71]],[[79085,47110],[-234,494],[-356,482],[-119,358],[-210,481],[-138,443],[-212,827],[-244,493],[-81,508],[-103,461],[-250,372],[-145,506],[-209,330],[-290,652],[-24,300],[178,-24],[430,-114],[246,-577],[215,-401],[153,-246],[263,-635],[283,-9],[233,-405],[161,-495],[211,-270],[-111,-482],[159,-205],[100,-15],[47,-412],[97,-330],[204,-52],[135,-374],[-70,-735],[-11,-914],[-308,-12]],[[80461,51765],[204,-202],[214,110],[56,500],[119,112],[333,128],[199,467],[137,374]],[[81723,53254],[110,221],[236,323]],[[82069,53798],[214,411],[140,462],[112,2],[143,-299],[13,-257],[183,-165],[231,-177],[-20,-232],[-186,-29],[50,-289],[-205,-201]],[[82744,53024],[-158,-533],[204,-560],[-48,-272],[312,-546],[-329,-70],[-93,-403],[12,-535],[-267,-404],[-7,-589],[-107,-903],[-41,210],[-316,-266],[-110,361],[-198,34],[-139,189],[-330,-212],[-101,285],[-182,-32],[-229,68],[-43,793],[-138,164],[-134,505],[-38,517],[32,548],[165,392]],[[84832,53877],[-327,343],[-78,428],[84,280],[-176,280],[-87,-245],[-131,23],[-205,-330],[-46,173],[109,498],[175,166],[151,223],[98,-268],[212,162],[45,264],[196,15],[-16,457],[225,-280],[23,-297],[20,-218],[28,-392],[16,-332],[-94,-540],[-102,602],[-130,-300],[89,-435],[-79,-277]],[[72318,54106],[-132,470],[-49,849],[126,959],[192,-328],[129,-416],[134,-616],[-42,-615],[-116,-168],[-242,-135]],[[32841,56488],[-50,53],[81,163],[-6,233],[160,77],[58,-21],[-11,-440],[-232,-65]],[[84165,55910],[-171,409],[57,158],[70,165],[30,367],[153,35],[-44,-398],[205,570],[-26,-563],[-100,-195],[-87,-373],[-87,-175]],[[82548,55523],[136,414],[200,364],[167,409],[146,587],[49,-482],[-183,-325],[-146,-406],[-369,-561]],[[83889,56748],[-10,275],[20,301],[-43,282],[166,-183],[177,1],[-5,-247],[-129,-251],[-176,-178]],[[84666,56567],[-11,416],[-84,31],[-43,357],[163,-47],[-4,224],[-169,451],[266,-13],[77,-220],[78,-660],[-214,157],[5,-199],[68,-364],[-132,-133]],[[83683,57791],[-119,295],[-142,450],[238,-22],[97,-213],[-74,-510]],[[84465,57987],[-216,290],[-103,310],[-71,-217],[-177,354],[-253,-87],[-138,130],[14,244],[87,151],[-83,136],[-36,-213],[-137,340],[-41,257],[-11,566],[112,-195],[29,925],[90,535],[169,-1],[171,-168],[85,153],[26,-150],[-46,-245],[95,-423],[-73,-491],[-164,-196],[-43,-476],[62,-471],[147,-65],[123,70],[347,-328],[-27,-321],[91,-142],[-29,-272]],[[31337,61183],[-16,253],[40,86],[227,-3],[142,-52],[50,-118],[-71,-149],[-209,4],[-163,-21]],[[28554,61038],[-156,95],[-159,215],[34,135],[116,41],[64,-20],[187,-53],[147,-142],[46,-161],[-195,-11],[-84,-99]],[[30080,62227],[34,101],[217,-3],[165,-152],[73,15],[50,-209],[152,11],[-9,-176],[124,-21],[136,-217],[-103,-240],[-132,128],[-127,-25],[-92,28],[-50,-107],[-106,-37],[-43,144],[-92,-85],[-111,-405],[-71,94],[-14,170]],[[30081,61241],[-185,100],[-131,-41],[-169,43],[-130,-110],[-149,184],[24,190],[256,-82],[210,-47],[100,131],[-127,256],[2,226],[-175,92],[62,163],[170,-26],[241,-93]],[[80409,61331],[-228,183],[-8,509],[137,267],[304,166],[159,-14],[62,-226],[-122,-260],[-64,-341],[-240,-284]],[[6753,61756],[-69,84],[8,165],[-46,216],[14,65],[48,97],[-19,116],[16,55],[21,-11],[107,-100],[49,-51],[45,-79],[71,-207],[-7,-33],[-108,-126],[-89,-92],[-41,-99]],[[6551,62734],[-47,125],[-32,48],[-3,37],[27,50],[99,-56],[73,-90],[-23,-71],[-94,-43]],[[6447,63028],[-149,17],[21,72],[137,-26],[-9,-63]],[[6192,63143],[-19,8],[-97,21],[-35,133],[-11,24],[74,82],[23,-38],[80,-196],[-15,-34]],[[5704,63509],[-93,107],[14,43],[43,58],[64,-12],[5,-138],[-33,-58]],[[28401,62311],[186,329],[-113,154],[-179,39],[-96,171],[-66,336],[-157,-23],[-259,159],[-83,124],[-362,91],[-97,115],[104,148],[-273,30],[-199,-307],[-115,-8],[-40,-144],[-138,-65],[-118,56],[146,183],[60,213],[126,131],[142,116],[210,56],[67,65],[240,-42],[219,-7],[261,-201],[110,-216],[260,66],[98,-138],[235,-366],[173,-267],[92,8],[165,-120],[-20,-167],[205,-24],[210,-242],[-33,-138],[-185,-75],[-187,-29],[-191,46],[-398,-57]],[[28394,64588],[-70,340],[-104,171],[60,375],[84,-23],[97,-491],[1,-343],[-68,-29]],[[83540,63560],[-146,499],[-32,438],[163,581],[223,447],[127,-176],[-49,-357],[-167,-947],[-119,-485]],[[28080,66189],[-19,219],[130,47],[184,-18],[8,-153],[-303,-95]],[[28563,65870],[-51,75],[4,309],[-124,234],[-1,67],[220,-265],[-48,-420]],[[86948,69902],[-181,168],[2,281],[154,352],[158,-68],[114,248],[204,-127],[35,-203],[-156,-357],[-114,189],[-143,-137],[-73,-346]],[[59437,71293],[8,-48],[-285,-240],[-136,77],[-64,237],[132,22]],[[59092,71341],[19,3],[40,143],[200,-8],[253,176],[-188,-251],[21,-111]],[[56867,71211],[3,98],[-339,115],[52,251],[152,-199],[216,34],[207,-42],[-7,-103],[151,71],[-35,-175],[-400,-50]],[[54194,72216],[-213,222],[-141,64],[-387,300],[38,304],[325,-54],[284,64],[211,51],[-100,-465],[41,-183],[-58,-303]],[[52446,73567],[-105,156],[-11,713],[-64,338],[153,-30],[139,183],[166,-419],[-39,-782],[-126,38],[-113,-197]],[[86301,68913],[-135,229],[69,533],[-176,172],[-113,405],[263,182],[145,371],[280,306],[203,403],[553,177],[297,-121],[291,1050],[185,-282],[408,591],[158,229],[174,723],[-47,664],[117,374],[295,108],[152,-819],[-9,-479],[-256,-595],[4,-610],[-104,-472],[48,-296],[-145,-416],[-355,-278],[-488,-36],[-396,-675],[-186,227],[-12,442],[-483,-130],[-329,-279],[-325,-11],[282,-435],[-186,-1004],[-179,-248]],[[52563,75028],[-126,120],[-64,398],[56,219],[179,226],[47,-507],[-92,-456]],[[88876,75140],[-39,587],[138,455],[296,33],[81,817],[83,460],[326,-615],[213,-198],[195,-126],[197,250],[62,-663],[-412,-162],[-244,-587],[-436,404],[-152,-646],[-308,-9]],[[32535,77739],[-353,250],[-69,198],[105,183],[97,-288],[202,-79],[257,16],[-137,-242],[-102,-38]],[[32696,79581],[-360,186],[-258,279],[96,49],[365,-148],[284,-247],[8,-108],[-135,-11]],[[15552,79158],[-456,269],[-84,209],[-248,207],[-50,168],[-286,107],[-107,321],[24,137],[291,-129],[171,-89],[261,-63],[94,-204],[138,-280],[277,-244],[115,-327],[-140,-82]],[[35133,78123],[-183,111],[60,484],[-77,75],[-322,-513],[-166,21],[196,277],[-267,144],[-298,-35],[-539,18],[-43,175],[173,208],[-121,160],[234,356],[287,941],[172,336],[241,204],[129,-26],[-54,-160],[-148,-372],[-184,-517],[181,199],[187,-126],[-98,-206],[247,-162],[128,144],[277,-182],[-86,-433],[194,101],[36,-313],[86,-367],[-117,-520],[-125,-22]],[[13561,81409],[-111,1],[-167,270],[-103,272],[-140,184],[-51,260],[16,188],[131,-76],[267,47],[-84,-671],[242,-475]],[[89469,77738],[-51,496],[31,575],[-32,638],[64,446],[13,790],[-163,581],[24,808],[257,271],[-110,274],[123,83],[73,-391],[96,-569],[-7,-581],[114,-597],[280,-1046],[-411,195],[-171,-854],[271,-605],[-8,-413],[-211,356],[-182,-457]],[[47896,83153],[233,24],[298,-365],[-149,-406]],[[48278,82406],[46,-422],[-210,-528],[-493,-349],[-393,89],[225,617],[-145,601],[378,463],[210,276]],[[53358,82957],[-291,333],[-39,246],[408,195],[88,-296],[-166,-478]],[[7221,84100],[-142,152],[-43,277],[252,210],[148,90],[185,-40],[117,-183],[-240,-281],[-277,-225]],[[48543,80097],[-148,118],[407,621],[249,127],[-436,99],[-79,235],[291,183],[-152,319],[52,387],[414,-54],[40,343],[-190,372],[-337,104],[-66,160],[101,264],[-92,163],[-149,-279],[-17,569],[-140,301],[101,611],[216,480],[222,-47],[335,49],[-297,-639],[283,81],[304,-3],[-72,-481],[-250,-530],[287,-38],[270,-759],[190,-95],[171,-673],[79,-233],[337,-113],[-34,-378],[-142,-173],[111,-305],[-250,-310],[-371,6],[-473,-163],[-130,116],[-183,-276],[-257,67],[-195,-226]],[[3835,85884],[-182,110],[-168,161],[274,101],[220,-54],[27,-226],[-171,-92]],[[27873,86994],[-123,50],[-73,176],[13,41],[107,177],[114,-13],[70,-121],[-108,-310]],[[26925,87305],[-196,13],[-61,160],[207,273],[381,-6],[-6,-114],[-325,-326]],[[2908,87788],[-211,128],[-106,107],[-245,-34],[-66,52],[17,223],[171,-113],[173,61],[225,-156],[276,-79],[-23,-64],[-211,-125]],[[26243,87832],[-95,346],[-377,-57],[242,292],[35,465],[95,542],[201,-49],[51,-259],[143,91],[161,-155],[304,-203],[318,-184],[25,-281],[204,46],[199,-196],[-247,-186],[-432,142],[-156,266],[-275,-314],[-396,-306]],[[44817,88095],[-365,87],[-775,187],[273,261],[-605,289],[492,114],[-12,174],[-583,137],[188,385],[421,87],[433,-400],[422,321],[349,-167],[453,315],[461,-42],[-64,-382],[314,-403],[-361,-451],[-801,-405],[-240,-107]],[[28614,90223],[-69,289],[118,331],[255,82],[217,-163],[3,-253],[-32,-82],[-180,-174],[-312,-30]],[[1957,88542],[-260,17],[-212,206],[-369,172],[-62,257],[-283,96],[-315,-76],[-151,207],[60,219],[-333,-140],[126,-278],[-158,-251],[0,2354],[681,-451],[728,-588],[-24,-367],[187,-147],[-64,429],[754,-88],[544,-553],[-276,-257],[-455,-61],[-7,-578],[-111,-122]],[[23258,91203],[-374,179],[-226,-65],[-380,266],[245,183],[194,256],[295,-168],[166,-106],[84,-112],[169,-226],[-173,-207]],[[99694,92399],[-49,187],[354,247],[0,-404],[-305,-30]],[[0,92429],[0,404],[36,24],[235,-1],[402,-169],[-24,-81],[-286,-141],[-363,-36]],[[26228,91219],[16,648],[394,-45]],[[26638,91822],[411,-87],[373,-293],[17,-293],[-207,-315],[196,-316],[-36,-288],[-544,-413],[-386,-91],[-287,178],[-83,-297],[-268,-498]],[[25824,89109],[-81,-258],[-322,-400]],[[25421,88451],[-397,-39],[-220,-250],[-18,-384],[-323,-74],[-340,-479],[-301,-665],[-108,-466]],[[23714,86094],[-15,-686],[408,-99]],[[24107,85309],[125,-553],[130,-448],[388,117],[517,-256],[277,-225],[199,-279]],[[25743,83665],[348,-162],[294,-249]],[[26385,83254],[459,-34],[302,-58],[-45,-511],[86,-594],[201,-661],[414,-561],[214,192],[150,607],[-145,934],[-196,311],[445,276],[314,415],[154,411]],[[28738,83981],[-22,395],[-189,502]],[[28527,84878],[-338,445],[328,619],[-121,535],[-93,922],[194,137],[476,-161],[286,-57],[230,155],[258,-200],[342,-343],[85,-229],[495,-45],[-8,-496],[92,-747],[254,-92],[201,-348],[402,328],[266,652],[184,274],[216,-527],[362,-754],[307,-709],[-112,-371],[370,-333],[250,-338],[442,-152],[179,-189],[110,-500],[216,-78],[112,-223],[20,-664],[-202,-222],[-199,-207],[-458,-210],[-349,-486],[-470,-96],[-594,125],[-417,4],[-287,-41],[-233,-424],[-354,-262],[-401,-782],[-320,-545],[236,97],[446,776],[583,493]],[[31513,79609],[416,59],[245,-290]],[[32174,79378],[-262,-397],[88,-637],[91,-446],[361,-295],[459,86],[278,664],[19,-429],[180,-214],[-344,-387],[-615,-351],[-276,-239],[-310,-426],[-211,44],[-11,500],[483,488],[-445,-19],[-309,-72]],[[31350,77248],[48,-194],[-296,-286],[-286,-204],[-293,-175]],[[30523,76389],[-159,-386],[-35,-98]],[[30329,75905],[-3,-313],[92,-313],[115,-15],[-29,216],[83,-131],[-22,-169],[-188,-96]],[[30377,75084],[-133,12],[-205,-104]],[[30039,74992],[-121,-29],[-162,-29],[-231,-171],[408,111],[82,-112],[-389,-177],[-177,-1],[8,72],[-84,-164],[82,-27],[-60,-424],[-203,-455],[-20,152]],[[29172,73738],[-61,31],[-91,147]],[[29020,73916],[57,-318]],[[29077,73598],[66,-106],[8,-222]],[[29151,73270],[-89,-230],[-157,-472],[-25,24],[86,402]],[[28966,72994],[-142,226],[-33,490]],[[28791,73710],[-53,-255],[59,-375]],[[28797,73080],[-175,88],[183,-186]],[[28805,72982],[12,-562],[79,-41],[29,-204],[39,-591],[-176,-439],[-288,-175],[-182,-346],[-139,-38],[-141,-217],[-39,-199],[-305,-383],[-157,-281],[-131,-351],[-43,-419],[50,-411],[92,-505],[124,-418],[1,-256],[132,-685],[-9,-398],[-12,-230],[-69,-361]],[[27672,65472],[-83,-74],[-137,71]],[[27452,65469],[-44,259]],[[27408,65728],[-106,136],[-147,508]],[[27155,66372],[-129,452],[-42,231],[57,393],[-77,325],[-217,494]],[[26747,68267],[-108,91],[-281,-269]],[[26358,68089],[-49,30]],[[26309,68119],[-135,276],[-174,146]],[[26000,68541],[-314,-75],[-247,66],[-212,-41]],[[25227,68491],[-118,-83],[54,-166]],[[25163,68242],[-5,-240],[59,-117],[-53,-77],[-103,87],[-104,-112],[-202,18]],[[24755,67801],[-207,313],[-242,-74]],[[24306,68040],[-202,137],[-173,-42],[-234,-138],[-253,-438],[-276,-255],[-152,-282],[-63,-266],[-3,-407],[14,-284],[52,-201]],[[23016,65864],[1,-1],[-1,-1],[-107,-516]],[[22909,65346],[-49,-426],[-20,-791],[-27,-289],[48,-322],[86,-288],[56,-458],[184,-440],[65,-337],[109,-291],[295,-157],[114,-247],[244,165],[212,60],[208,106],[175,101],[176,241],[67,345],[22,496],[48,173],[188,155],[294,137],[246,-21],[169,50],[66,-125],[-9,-285],[-149,-351],[-66,-360],[51,-103],[-42,-255],[-69,-461],[-71,152],[-58,-10]],[[25472,61510],[1,-87],[53,-3],[-5,-160],[-45,-256],[24,-91],[-29,-212],[18,-56],[-32,-299],[-55,-156],[-50,-19],[-55,-205]],[[25297,59966],[90,-107],[24,88],[82,-75]],[[25493,59872],[29,-23],[61,104],[79,8],[26,-48],[43,29],[129,-53]],[[25860,59889],[128,16],[90,65]],[[26078,59970],[32,66],[89,-31],[66,-40],[73,14],[55,51],[127,-82],[44,-13],[85,-110],[80,-132],[101,-91],[73,-162]],[[26903,59440],[-24,-57],[-14,-132],[29,-216],[-64,-202],[-30,-237],[-9,-261],[15,-152],[7,-266],[-43,-58],[-26,-253],[19,-156],[-56,-151],[12,-159],[43,-97]],[[26762,57043],[70,-321],[108,-238],[130,-252]],[[27070,56232],[100,-212]],[[27170,56020],[-6,-125],[111,-27]],[[27275,55868],[26,48],[77,-145],[136,42],[119,150],[168,119],[95,176],[153,-34],[-10,-58],[155,-21],[124,-102],[90,-177],[105,-164]],[[28513,55702],[143,-18],[209,412],[114,63],[3,195],[51,500],[159,274],[175,11],[22,123],[218,-49],[218,298],[109,132],[134,285],[98,-36],[73,-156],[-54,-199]],[[30185,57537],[-8,-139],[-163,-69],[91,-268],[-3,-309]],[[30102,56752],[-123,-343],[105,-469]],[[30084,55940],[120,38],[62,427],[-86,208],[-14,447],[346,241],[-38,278],[97,186],[100,-415],[195,-9],[180,-330],[11,-195],[249,-6],[297,61],[159,-264]],[[31762,56607],[213,-73],[155,184]],[[32130,56718],[4,149],[344,35],[333,9],[-236,-175],[95,-279],[222,-44],[210,-291],[45,-473],[144,13],[109,-139]],[[33400,55523],[183,-217],[171,-385],[8,-304],[105,-14],[149,-289],[109,-205]],[[34125,54109],[333,-119],[30,107],[225,43],[298,-159]],[[35011,53981],[95,-65],[204,-140],[294,-499],[46,-242]],[[35650,53035],[95,28],[69,-327],[155,-1033],[149,-97],[7,-408],[-208,-487],[86,-178],[491,-92],[10,-593],[211,388],[349,-212],[462,-361],[135,-346],[-45,-327],[323,182],[540,-313],[415,23],[411,-489],[355,-662],[214,-170],[237,-24],[101,-186],[94,-752],[46,-358],[-110,-977],[-142,-385],[-391,-822],[-177,-668],[-206,-513],[-69,-11],[-78,-435],[20,-1107],[-77,-910],[-30,-390],[-88,-233],[-49,-790],[-282,-771],[-47,-610],[-225,-256],[-65,-355],[-302,2],[-437,-227],[-195,-263],[-311,-173],[-327,-470],[-235,-586],[-41,-441],[46,-326],[-51,-597],[-63,-289],[-195,-325],[-308,-1040],[-244,-468],[-189,-277],[-127,-562],[-183,-337]],[[35174,30629],[-121,-372],[-313,-328],[-205,118],[-151,-63],[-256,253],[-189,-19],[-169,327]],[[33770,30545],[-19,-308],[353,-506],[-38,-408],[173,-257],[-14,-289],[-267,-757],[-412,-317],[-557,-123],[-305,59],[59,-352],[-57,-442],[51,-298],[-167,-208],[-284,-82],[-267,216],[-108,-155],[39,-587],[188,-178],[152,186],[82,-307],[-255,-183],[-223,-367],[-41,-595],[-66,-316],[-262,-2],[-218,-302],[-80,-443]],[[31227,23224],[274,-433],[265,-119]],[[31766,22672],[-96,-531],[-328,-333],[-180,-692],[-254,-234],[-113,-276],[89,-614],[185,-342],[-117,30]],[[30952,19680],[-247,4],[-134,-145],[-250,-213],[-45,-552],[-118,-14],[-313,192],[-318,412],[-346,338],[-87,374],[79,346],[-140,393],[-36,1007],[119,568],[293,457],[-422,172],[265,522],[94,982],[309,-208],[145,1224],[-186,157],[-87,-738],[-175,83],[87,845],[95,1095],[127,404]],[[29661,27385],[-79,576],[-23,666]],[[29559,28627],[117,19],[170,954],[192,945],[118,881],[-64,885],[83,487],[-34,730],[163,721],[50,1143],[89,1227],[87,1321],[-20,967],[-58,832]],[[30452,39739],[-279,340],[-24,242],[-551,593],[-498,646],[-214,365],[-115,488],[46,170],[-236,775],[-274,1090],[-262,1177],[-114,269],[-87,435],[-216,386],[-198,239],[90,264],[-134,563],[86,414],[221,373]],[[27693,48568],[148,442],[-60,258],[-106,-275],[-166,259],[56,167],[-47,536],[97,89],[52,368],[105,381],[-20,241],[153,126],[190,236]],[[28095,51396],[-37,183],[103,44],[-12,296],[65,214],[138,40],[117,371],[106,310],[-102,141],[52,343],[-62,540],[59,155],[-44,500],[-112,315]],[[28366,54848],[-93,170],[-59,319],[68,158],[-70,40]],[[28212,55535],[-52,195],[-138,165]],[[28022,55895],[-122,-38],[-56,-205],[-112,-149],[-61,-20],[-27,-123],[132,-321],[-75,-76],[-40,-87],[-130,-30],[-48,353],[-36,-101],[-92,35],[-56,238],[-114,39],[-72,69],[-119,-1],[-8,-128],[-32,89]],[[26954,55439],[-151,131],[-56,124],[32,103],[-11,130],[-77,142],[-109,116],[-95,76],[-19,173],[-73,105],[18,-172],[-55,-141],[-64,164],[-89,58],[-38,120],[2,179],[36,187],[-78,83],[64,114]],[[26191,57131],[-96,186],[-130,238],[-61,200],[-117,185],[-140,267]],[[25647,58207],[31,92],[46,-89]],[[25724,58210],[21,41]],[[25745,58251],[-48,185]],[[25697,58436],[-84,52],[-31,-140]],[[25582,58348],[-161,9],[-100,57],[-115,117],[-154,37],[-79,127]],[[24973,58695],[-142,103],[-174,11],[-127,117],[-149,244]],[[24381,59170],[-314,636]],[[24067,59806],[-144,192],[-226,154]],[[23697,60152],[-156,-43],[-223,-223],[-140,-58],[-196,156],[-208,112],[-260,271],[-208,83],[-314,275],[-233,282],[-70,158],[-155,35],[-284,187],[-116,270],[-299,335],[-139,373],[-66,288],[93,57],[-29,169],[64,153],[1,204],[-93,266],[-25,235],[-94,298],[-244,587],[-280,462],[-135,368],[-238,241],[-51,145],[42,365]],[[19641,66203],[-142,137],[-164,288]],[[19335,66628],[-69,412],[-149,48],[-162,311],[-130,288],[-12,184],[-149,446],[-99,452],[5,227]],[[18570,68996],[-201,235],[-93,-26]],[[18276,69205],[-159,163],[-44,-240],[46,-284],[27,-444],[95,-243],[206,-407],[46,-139],[42,-42],[37,-203],[49,8],[56,-381],[85,-150],[59,-210],[174,-300],[92,-550],[83,-259],[77,-277],[15,-311],[134,-20],[112,-268],[100,-264],[-6,-106],[-117,-217],[-49,3],[-74,359]],[[19362,64423],[-182,337],[-200,286]],[[18980,65046],[-142,150],[9,432],[-42,320],[-132,183],[-191,264],[-37,-76],[-70,154],[-171,143],[-164,343],[20,44],[115,-33],[103,221],[10,266],[-214,422],[-163,163],[-102,369],[-103,388],[-129,472],[-113,531]],[[17464,69802],[-46,302],[-180,340],[-130,71],[-30,169],[-156,30],[-100,159],[-258,59]],[[16564,70932],[-70,95],[-34,324]],[[16460,71351],[-270,594],[-231,821],[10,137],[-123,195],[-215,495],[-38,482],[-148,323],[61,489],[-10,507],[-89,453],[109,557],[67,1072],[-50,792],[-88,506],[-80,274],[33,115],[402,-200],[148,-558]],[[15948,78405],[68,156],[-44,485],[-94,484]],[[15878,79530],[-38,1],[-537,581],[-199,255]],[[15104,80367],[-503,245],[-155,523],[40,362]],[[14486,81497],[-356,252],[-48,476],[-336,429],[-6,304]],[[13740,82958],[-153,223],[-245,188],[-78,515],[-358,478],[-150,558],[-267,38],[-441,15],[-326,170],[-574,613],[-266,112],[-486,211]],[[10396,86079],[-385,-50],[-546,271]],[[9465,86300],[-330,252],[-309,-125],[58,-411],[-154,-38],[-321,-123],[-245,-199]],[[8164,85656],[-307,-126],[-40,348]],[[7817,85878],[125,580],[295,182],[-76,148],[-354,-329],[-190,-394],[-400,-420],[203,-287],[-262,-424]],[[7158,84934],[-299,-247],[-278,-181]],[[6581,84506],[-69,-261],[-434,-305],[-87,-278],[-325,-252],[-191,45],[-259,-165],[-282,-201],[-231,-197],[-477,-169],[-43,99],[304,276],[271,182],[296,324],[345,66],[137,243],[385,353],[62,119],[205,208],[48,448],[141,349],[-320,-179],[-90,102],[-150,-215],[-181,300],[-75,-212],[-104,294],[-278,-236],[-170,0],[-24,352]],[[4985,85596],[50,217],[-179,210]],[[4856,86023],[-361,-113],[-235,277],[-190,142],[-1,334],[-214,252],[108,340],[226,330],[99,303],[225,43],[191,-94],[224,285],[201,-51],[212,183],[-52,270],[-155,106],[205,228],[-170,-7],[-295,-128],[-85,-131],[-219,131],[-392,-67],[-407,142],[-117,238],[-351,343],[390,247],[620,289],[228,0]],[[4541,89915],[-38,-295],[586,22]],[[5089,89642],[-225,366]],[[4864,90008],[-342,226],[-197,295]],[[4325,90529],[-267,252],[-381,187],[155,309],[493,19],[350,270],[66,287],[284,281],[271,68],[526,262],[256,-40],[427,315],[421,-124],[201,-266],[123,114],[469,-35],[-16,-136],[425,-101],[283,59],[585,-186],[534,-56],[214,-77],[370,96],[421,-177],[302,-83]],[[10837,91767],[518,-142]],[[11355,91625],[438,-284],[289,-55]],[[12082,91286],[244,247],[336,184],[413,-72],[416,259],[455,148],[191,-245],[207,138],[62,278],[192,-63],[470,-530],[369,401]],[[15437,92031],[38,-448],[341,96]],[[15816,91679],[105,173],[337,-34],[424,-248],[650,-217],[383,-100],[272,38]],[[17987,91291],[375,-300],[-391,-293]],[[17971,90698],[502,-127],[750,70],[236,103],[296,-354],[302,299],[-283,251],[179,202],[338,27],[223,59],[224,-141],[279,-321],[310,47],[491,-266],[431,94],[405,-14],[-32,367],[247,103],[431,-200],[-2,-559],[177,471],[223,-16],[126,594],[-298,364],[-324,239],[22,653],[329,429],[366,-95],[281,-261],[378,-666],[-247,-290],[517,-120],[-1,-604],[371,463],[332,-380],[-83,-438],[269,-399],[290,427],[202,510]],[[19722,91216],[-824,-103],[-374,-41]],[[18524,91072],[-151,279],[-379,161],[-246,-66],[-343,468],[185,62],[429,101],[392,-26],[362,103],[-537,138],[-594,-47],[-394,12],[-146,217],[644,237],[-428,-9],[-485,156],[233,443],[193,235],[744,359],[284,-114],[-139,-277],[618,179],[386,-298],[314,302],[254,-194],[227,-580],[140,244],[-197,606],[244,86],[276,-94],[311,-239],[175,-575],[86,-417],[466,-293],[502,-279],[-31,-260],[-456,-48],[178,-227],[-94,-217],[-503,93],[-478,160],[-322,-36],[-522,-201]],[[20728,93568],[-434,413],[95,83],[372,24],[211,-130],[-244,-390]],[[27920,93557],[-80,36],[-306,313],[12,213],[133,39],[636,-63],[479,-325],[25,-163],[-296,17],[-299,13],[-304,-80]],[[31620,87170],[-753,236],[-596,343],[-337,287],[97,167],[-414,304],[-405,286],[5,-171],[-803,-94],[-235,203],[183,435],[522,10],[571,76],[-92,211],[96,294],[360,576],[-77,261],[-107,203],[-425,286],[-563,201],[178,150],[-294,367],[-245,34],[-219,201],[-149,-175],[-503,-76],[-1011,132],[-588,174],[-450,89],[-231,207],[290,270],[-394,2],[-88,599],[213,528],[286,241],[717,158],[-204,-382],[219,-369],[256,477],[704,242],[477,-611],[-42,-387],[550,172],[263,235],[616,-299],[383,-282],[36,-258],[515,134],[290,-376],[670,-234],[242,-238],[263,-553],[-510,-275],[654,-386],[441,-130],[400,-543],[437,-39],[-87,-414],[-487,-687],[-342,253],[-437,568],[-359,-74],[-35,-338],[292,-344],[377,-272],[114,-157],[181,-584],[-96,-425],[-350,160],[-697,473],[393,-509],[289,-357],[45,-206]],[[22678,92689],[-268,50],[-192,225],[-690,456],[5,189],[567,-73],[-306,386],[329,286],[331,-124],[496,75],[72,-172],[-259,-283],[420,-254],[-50,-532],[-455,-229]],[[89468,93831],[-569,66],[-49,31],[263,234],[348,54],[394,-226],[34,-155],[-421,-4]],[[23814,93133],[-317,22],[-173,519],[4,294],[145,251],[276,161],[579,-20],[530,-144],[-415,-526],[-331,-115],[-298,-442]],[[15808,92470],[-147,259],[-641,312]],[[15020,93041],[93,193],[218,489]],[[15331,93723],[241,388],[-272,362],[939,93],[397,-123],[709,-33],[270,-171],[298,-249],[-349,-149],[-681,-415],[-344,-414]],[[16539,93012],[0,-248],[-731,-294]],[[91548,94707],[-444,53],[-516,233],[66,192],[518,-89],[697,-155],[-321,-234]],[[23845,94650],[-403,44],[-337,155],[148,266],[399,159],[243,-208],[101,-187],[-151,-229]],[[88598,94662],[-550,384],[149,406],[366,111],[734,-26],[1004,-313],[-219,-439],[-1023,16],[-461,-139]],[[22275,94831],[-298,94],[5,345],[-455,-46],[-18,457],[299,-18],[419,201],[390,-34],[22,77],[212,-273],[9,-303],[-127,-440],[-458,-60]],[[18404,94533],[-35,193],[577,261],[-1255,-70],[-389,106],[379,577],[262,165],[782,-199],[493,-350],[485,-45],[-397,565],[255,215],[286,-68],[94,-282],[109,-210],[247,99],[291,-26],[49,-289],[-169,-281],[-940,-91],[-701,-256],[-423,-14]],[[65817,92311],[-907,77],[-74,262],[-503,158],[-40,320],[284,126],[-10,323],[551,503],[-255,73],[665,518],[-75,268],[621,312],[917,380],[925,110],[475,220],[541,76],[193,-233],[-187,-184],[-984,-293],[-848,-282],[-863,-562],[-414,-577],[-435,-568],[56,-491],[531,-484],[-164,-52]],[[25514,94532],[-449,73],[-738,190],[-96,325],[-34,293],[-279,258],[-574,72],[-322,183],[104,242],[573,-37],[308,-190],[547,1],[240,-194],[-64,-222],[319,-134],[177,-140],[374,-26],[406,-50],[441,128],[566,51],[451,-42],[298,-223],[62,-244],[-174,-157],[-414,-127],[-355,72],[-797,-91],[-570,-11]],[[16250,95423],[-377,128],[472,442],[570,383],[426,-9],[381,87],[-38,-454],[-214,-205],[-259,-29],[-517,-252],[-444,-91]],[[81143,94175],[250,112],[142,-379]],[[81535,93908],[122,153],[444,93],[892,-97],[67,-276],[1162,-88],[15,451]],[[84237,94144],[590,-103],[443,3]],[[85270,94044],[449,-312],[128,-378],[-165,-247],[349,-465],[437,-240],[268,620],[446,-266],[473,159],[538,-182],[204,166],[455,-83],[-201,549],[367,256],[2509,-384],[236,-351],[727,-451],[1122,112],[553,-98],[231,-244],[-33,-432],[342,-168],[372,121],[492,15],[525,-116],[526,66],[484,-526],[344,189],[-224,378]],[[97224,91732],[123,263],[886,-166]],[[98233,91829],[578,36],[799,-282],[389,-258],[0,-2354],[-2,-3],[-357,-260],[-360,44],[250,-315],[166,-487],[128,-159],[32,-244],[-71,-157],[-518,129],[-777,-445],[-247,-69],[-425,-415],[-403,-362],[-102,-269],[-397,409],[-724,-464]],[[96192,85904],[-126,220],[-268,-254]],[[95798,85870],[-371,81],[-90,-388],[-333,-572],[10,-239],[316,-132],[-37,-860],[-258,-22],[-119,-494],[116,-255]],[[95032,82989],[-486,-301],[-96,-675]],[[94450,82013],[-415,-144],[-83,-600],[-400,-551],[-103,407],[-119,862],[-155,1313],[134,819],[234,353]],[[93543,84472],[15,276],[431,132]],[[93989,84880],[496,744],[479,608],[499,471],[223,833],[-337,-50],[-167,-487]],[[95182,86999],[-705,-648],[-227,726]],[[94250,87077],[-717,-201],[-696,-990],[230,-362],[-620,-154],[-430,-61],[20,427],[-431,90],[-344,-291],[-850,102]],[[90412,85637],[-913,-175],[-900,-1153]],[[88599,84309],[-1065,-1394],[438,-74],[136,-370],[270,-132]],[[88378,82339],[178,296],[305,-39]],[[88861,82596],[401,-650]],[[89262,81946],[9,-502],[-217,-591]],[[89054,80853],[-23,-705],[-126,-945],[-418,-855],[-94,-409],[-377,-688],[-374,-682],[-179,-349],[-370,-346],[-175,-8],[-175,287],[-373,-432],[-43,-197]],[[86327,75524],[-106,36]],[[86221,75560],[-120,-201],[-83,-201]],[[86018,75158],[10,-424],[-143,-130],[-50,-105],[-104,-174],[-185,-97],[-121,-159],[-9,-256],[-32,-65],[111,-96],[157,-259]],[[85652,73393],[240,-697],[68,-383],[3,-681],[-105,-325],[-252,-113],[-222,-245],[-250,-51],[-31,322]],[[85103,71220],[52,443],[-123,615]],[[85032,72278],[206,99],[-190,506]],[[85048,72883],[-135,113],[-34,-112]],[[84879,72884],[-81,-49],[-10,112],[-72,54],[-75,94]],[[84641,73095],[77,260],[65,69]],[[84783,73424],[-25,108],[71,319]],[[84829,73851],[-18,97],[-163,64]],[[84648,74012],[-131,158]],[[84517,74170],[-388,-171],[-204,-277],[-300,-161],[148,274],[-58,230],[220,397],[-147,310],[-242,-209],[-314,-411],[-171,-381],[-272,-29],[-142,-275],[147,-400],[227,-97],[9,-265]],[[83030,72705],[220,-172],[311,421]],[[83561,72954],[247,-230],[179,-15]],[[83987,72709],[46,-310],[-394,-165]],[[83639,72234],[-130,-319],[-270,-296],[-142,-414]],[[83097,71205],[299,-324],[109,-582]],[[83505,70299],[169,-541],[189,-454],[-5,-439],[-174,-161],[66,-315],[164,-184],[-43,-481],[-71,-468],[-155,-53],[-203,-640],[-225,-775],[-258,-705],[-382,-545],[-386,-498],[-313,-68],[-170,-262],[-96,192],[-157,-294],[-388,-296],[-294,-90],[-95,-624],[-154,-35],[-73,429],[66,228]],[[80517,63220],[-373,190],[-131,-97]],[[80013,63313],[-371,-505],[-231,-558],[-61,-410],[212,-623],[260,-772],[252,-365],[169,-475],[127,-1093],[-37,-1039],[-232,-389],[-318,-381],[-227,-492],[-346,-550],[-101,378],[78,401],[-206,335]],[[78981,56775],[-233,87],[-112,307],[-141,611]],[[78495,57780],[-249,271],[-238,-11],[41,464],[-245,-3],[-22,-650],[-150,-863],[-90,-522],[19,-428],[181,-18],[113,-539],[50,-512],[155,-338],[168,-69],[144,-306]],[[78372,54256],[64,-56],[164,-356],[116,-396],[16,-398],[-29,-269],[27,-203],[20,-349],[98,-163],[109,-523],[-5,-199],[-197,-40],[-263,438],[-329,469],[-32,301],[-161,395],[-38,489],[-100,322],[30,431],[-61,250]],[[77801,54399],[-110,227],[-47,292],[-148,334],[-135,280],[-45,-347],[-53,328],[30,369],[82,566]],[[77375,56448],[-27,439],[86,452],[-94,350],[23,644],[-113,306],[-90,707],[-50,746],[-121,490],[-183,-297],[-315,-421],[-156,53],[-172,138],[96,732],[-58,554],[-218,681],[34,213],[-163,76],[-197,481]],[[75657,62792],[-79,309],[-16,301],[-53,284]],[[75509,63686],[-116,344],[-256,23],[25,-243],[-87,-329],[-118,120],[-41,-108],[-78,65],[-108,53]],[[74730,63611],[-39,-216],[-189,7],[-343,-122],[16,-445],[-148,-349],[-400,-398],[-311,-695],[-209,-373]],[[73107,61020],[-276,-386],[-1,-272]],[[72830,60362],[-138,-146]],[[72692,60216],[-250,-212],[-130,-31]],[[72312,59973],[-84,-450],[58,-769],[15,-490],[-118,-561],[-1,-1004],[-144,-29],[-126,-450],[84,-195]],[[71996,56025],[-253,-167],[-93,-402]],[[71650,55456],[-112,-170],[-263,552],[-128,827],[-107,596],[-97,279],[-148,568],[-69,739],[-48,369],[-253,811],[-115,1145],[-83,756],[1,716],[-54,553],[-404,-353],[-196,70],[-362,716],[133,214],[-82,232],[-326,501]],[[68937,64577],[-203,150]],[[68734,64727],[-83,425],[-215,449]],[[68436,65601],[-512,-111],[-451,-11],[-391,-83]],[[67082,65396],[-523,179]],[[66559,65575],[-302,136],[-314,76]],[[65943,65787],[-118,725],[-133,105],[-214,-106],[-280,-286],[-339,196],[-281,454],[-267,168],[-186,561],[-205,788],[-149,-96],[-177,196]],[[63594,68492],[-103,-231],[-165,29]],[[63326,68290],[58,-261],[-25,-135],[89,-445]],[[63448,67449],[109,-510],[137,-135],[47,-207]],[[63741,66597],[190,-248],[16,-244]],[[63947,66105],[-27,-197],[35,-199],[80,-165],[37,-194],[41,-145]],[[64113,65205],[-18,430],[75,310],[76,64]],[[64246,66009],[84,-186],[5,-345]],[[64335,65478],[-61,-348]],[[64274,65130],[53,-226]],[[64327,64904],[49,29],[11,-162],[217,93],[230,-15],[168,-18],[190,400],[207,379],[176,364]],[[65575,65974],[80,201],[35,-51],[-26,-244],[-37,-108]],[[65627,65772],[38,-466]],[[65665,65306],[125,-404],[155,-214]],[[65945,64688],[204,-78],[164,-107]],[[66313,64503],[125,-339],[75,-196],[100,-75],[-1,-132],[-101,-352],[-44,-166],[-117,-189],[-104,-404],[-126,31],[-58,-141],[-44,-300],[34,-395],[-26,-72],[-128,2],[-174,-221],[-27,-288],[-63,-125],[-173,5],[-109,-149]],[[65352,60997],[1,-239],[-134,-164]],[[65219,60594],[-153,56],[-186,-199]],[[64880,60451],[-128,-33],[-201,-159]],[[64551,60259],[-54,-263],[-6,-201],[-277,-249],[-444,-276],[-249,-417]],[[63521,58853],[-122,-32],[-83,34]],[[63316,58855],[-163,-245]],[[63153,58610],[-177,-113],[-233,-31]],[[62743,58466],[-70,-34],[-61,-156],[-73,-43]],[[62539,58233],[-42,-150],[-138,13]],[[62359,58096],[-89,-80],[-192,30],[-72,345],[8,323],[-46,174],[-54,437],[-80,243],[56,29],[-29,270],[34,114],[-12,257]],[[61883,60238],[-36,253],[-84,177]],[[61763,60668],[-22,236],[-143,212],[-148,495],[-79,482],[-192,406],[-124,97],[-184,563],[-32,411],[12,350],[-159,655],[-130,231],[-150,122],[-92,339],[15,133]],[[60335,65400],[-77,307],[-81,131]],[[60177,65838],[-108,440],[-170,476],[-141,406],[-139,-3],[44,325],[12,206],[34,236]],[[59709,67924],[-9,86]],[[59700,68010],[-78,-238],[-60,-446],[-75,-308],[-65,-103],[-93,191],[-125,263],[-198,847],[-29,-53],[115,-624],[171,-594],[210,-920],[102,-321],[90,-334],[249,-654],[-55,-103],[9,-384],[323,-530],[49,-121]],[[60240,63578],[90,-580],[-61,-107],[40,-608],[102,-706],[106,-145],[152,-219]],[[60669,61213],[161,-683],[77,-543]],[[60907,59987],[152,-288],[379,-558],[154,-336],[151,-341],[87,-203],[136,-178]],[[61966,58083],[66,-183],[-9,-245],[-158,-142],[119,-161]],[[61984,57352],[91,-109]],[[62075,57243],[54,-244],[125,-248]],[[62254,56751],[138,-2],[262,151],[302,70],[245,184],[138,39],[99,108],[158,20]],[[63596,57321],[89,12],[128,88],[147,59],[132,202],[105,2],[6,-163],[-25,-344],[1,-310],[-59,-214],[-78,-639],[-134,-659],[-172,-755],[-238,-866],[-237,-661],[-327,-806],[-278,-479],[-415,-586],[-259,-450],[-304,-715],[-64,-312],[-63,-140]],[[61551,49585],[-195,-236],[-68,-246],[-104,-44],[-40,-416],[-89,-238],[-54,-393],[-112,-195]],[[60889,47817],[-128,-728],[16,-335],[178,-216],[8,-153],[-76,-357],[16,-180],[-18,-282],[97,-370],[115,-583],[101,-129]],[[61198,44484],[45,-265],[-11,-588],[34,-519],[11,-923],[49,-290],[-83,-422],[-108,-410],[-177,-366],[-254,-225],[-313,-287],[-313,-634],[-107,-108],[-194,-420],[-115,-136],[-23,-421],[132,-448],[54,-346],[4,-177],[49,29],[-8,-579]],[[59870,36949],[-45,-274],[65,-102]],[[59890,36573],[-41,-246],[-116,-210]],[[59733,36117],[-229,-199],[-334,-320],[-122,-219],[24,-248],[71,-40],[-24,-311]],[[59119,34780],[-70,-430],[-32,-491],[-72,-267],[-190,-298],[-54,-86],[-118,-300],[-77,-303],[-158,-424],[-314,-609],[-196,-355]],[[57838,31217],[-209,-269],[-291,-229]],[[57338,30719],[-141,-31],[-36,-164],[-169,88],[-138,-113],[-301,114],[-168,-72],[-115,31],[-286,-233],[-238,-94],[-171,-223],[-127,-14],[-117,210],[-94,11],[-120,264],[-13,-82],[-37,159],[2,346],[-90,396],[89,108],[-7,453],[-182,553],[-139,501],[-1,1],[-199,768]],[[54540,33696],[-207,446],[-108,432],[-62,575],[-68,428],[-93,910],[-7,707],[-35,322],[-108,243],[-144,489],[-146,708],[-60,371],[-226,577],[-17,453]],[[53259,40357],[-26,372],[38,519],[96,541],[15,254],[90,532],[66,243],[159,386],[90,263],[29,438],[-15,335],[-83,211],[-74,358],[-68,355],[15,122],[85,235],[-84,570],[-57,396],[-139,374],[26,115]],[[53422,46976],[-39,183]],[[53383,47159],[-74,444]],[[53309,47603],[-228,626]],[[53081,48229],[-285,596],[-184,488],[-169,610],[9,196],[61,189],[67,430],[56,438]],[[52636,51176],[-52,90],[96,663]],[[52680,51929],[40,467],[-108,390]],[[52612,52786],[-127,100],[-56,265]],[[52429,53151],[-71,85],[3,163]],[[52361,53399],[-289,-213]],[[52072,53186],[-105,32],[-107,-133]],[[51860,53085],[-222,13],[-149,370],[-91,427]],[[51398,53895],[-197,390],[-209,-8]],[[50992,54277],[-245,1]],[[50747,54278],[-229,-69]],[[50518,54209],[-224,-126]],[[50294,54083],[-436,-346],[-154,-203],[-250,-171],[-248,168]],[[49206,53531],[-126,-7],[-194,116],[-178,-7],[-329,-103],[-193,-170],[-275,-217],[-54,15]],[[47857,53158],[-73,-5],[-286,282]],[[47498,53435],[-252,450],[-237,323]],[[47009,54208],[-187,381]],[[46822,54589],[-75,44],[-200,238],[-144,316],[-49,216],[-34,437]],[[46320,55840],[-122,349],[-108,232],[-71,76],[-69,118],[-32,261],[-41,130],[-80,97]],[[45797,57103],[-149,247],[-117,39],[-63,166],[1,90],[-84,125],[-18,127]],[[45367,57897],[-46,453]],[[45321,58350],[36,262]],[[45357,58612],[-115,460],[-138,210],[122,112],[134,415],[66,304]],[[45426,60113],[-24,318],[78,291],[34,557],[-30,583],[-34,294],[28,295],[-72,281],[-146,255]],[[45260,62987],[12,249]],[[45272,63236],[13,274],[106,161],[91,308],[-18,200],[96,417],[155,376],[93,95],[74,344],[6,315],[100,365],[185,216],[177,603],[144,235]],[[46494,67145],[259,66],[219,403],[139,158]],[[47111,67772],[232,493],[-70,735],[106,508],[37,312],[179,399],[278,270],[206,244],[186,612],[87,362],[205,-2],[167,-251],[264,41],[288,-131],[121,-6]],[[49397,71358],[267,323],[300,102],[175,244],[268,180],[471,105],[459,48],[140,-87],[262,232],[297,5],[113,-137],[190,35]],[[52339,72408],[302,239],[195,-71],[-9,-299],[236,217],[20,-113]],[[53083,72381],[-139,-289],[-2,-274]],[[52942,71818],[96,-147],[-36,-511],[-183,-297],[53,-322],[143,-10],[70,-281],[106,-92]],[[53191,70158],[326,-204],[117,51],[232,-98],[368,-264],[130,-526],[250,-114],[391,-248],[296,-293],[136,153],[133,272],[-65,452],[87,288],[200,277],[192,80],[375,-121],[95,-264],[104,-2],[88,-101]],[[56646,69496],[276,-69],[68,-196]],[[56990,69231],[369,10],[268,-156],[275,-175],[129,-92],[214,188],[114,169],[245,49],[198,-75],[75,-293],[65,193],[222,-140],[217,-33],[137,149]],[[59518,69025],[80,194],[-19,34],[74,276],[56,446],[40,149],[8,6]],[[59757,70130],[99,482],[138,416],[5,21]],[[59999,71049],[-26,452],[68,243]],[[60041,71744],[-102,268],[105,222],[-169,-51],[-233,136],[-191,-340],[-421,-66],[-225,317],[-300,20],[-64,-245]],[[58441,72005],[-192,-71],[-268,315]],[[57981,72249],[-303,-10],[-165,587]],[[57513,72826],[-203,328],[135,459],[-176,283],[308,565],[428,23],[117,449],[529,-78],[334,383],[324,167],[459,13]],[[59768,75418],[485,-416],[399,-229]],[[60652,74773],[323,91],[239,-53],[328,309]],[[61542,75120],[42,252],[-70,403],[-160,218],[-154,68],[-102,181]],[[61098,76242],[-354,499],[-317,223],[-240,347],[202,95],[231,494],[-156,234],[410,241],[-8,129],[-249,-95]],[[60617,78409],[-222,-48],[-185,-191],[-260,-31],[-239,-220],[16,-368],[136,-142],[284,35],[-55,-210],[-304,-103],[-377,-342],[-154,121],[61,277],[-304,173],[50,113],[265,197],[-80,135],[-432,149],[-19,221],[-257,-73],[-103,-325],[-215,-437]],[[58223,77340],[6,-152],[-135,-128],[-84,56],[-78,-713]],[[57932,76403],[-144,-245],[-101,-422],[89,-337]],[[57776,75399],[33,-228],[243,-190],[-51,-145],[-330,-33],[-118,-182],[-232,-319]],[[57321,74302],[-87,275],[3,122]],[[57237,74699],[-169,17],[-145,56],[-336,-154],[192,-332],[-141,-96]],[[56638,74190],[-154,0],[-147,304]],[[56337,74494],[-52,-130],[62,-353],[139,-277]],[[56486,73734],[-105,-130],[155,-272]],[[56536,73332],[137,-171],[4,-334],[-257,157],[82,-302],[-176,-62],[105,-521]],[[56431,72099],[-184,-7],[-228,257],[-104,472]],[[55915,72821],[-49,393],[-108,272],[-143,337],[-18,168]],[[55597,73991],[-48,41],[-5,130],[-154,199],[-24,281],[23,403],[38,184]],[[55427,75229],[-46,93],[-59,46]],[[55322,75368],[-78,192],[-120,118]],[[55124,75678],[-261,218],[-161,213],[-254,176]],[[54448,76285],[-233,435],[56,44]],[[54271,76764],[-127,248],[-5,200],[-179,93],[-85,-255],[-82,198],[6,205],[10,9]],[[53809,77462],[62,54]],[[53871,77516],[-221,86],[-226,-210],[15,-293],[-34,-168],[91,-301],[261,-298],[140,-488],[309,-476],[217,3],[68,-130],[-78,-118]],[[54413,75123],[249,-213],[204,-179]],[[54866,74731],[238,-308],[29,-111],[-52,-211],[-154,276],[-242,97],[-116,-382],[200,-219],[-33,-309],[-116,-35],[-148,-506],[-116,-46],[1,181],[57,317],[60,126],[-108,342],[-85,298],[-115,74],[-82,255],[-179,107],[-120,238],[-206,38],[-217,267],[-254,384]],[[53108,75604],[-189,341],[-86,584]],[[52833,76529],[-138,68],[-226,195],[-128,-80],[-161,-274],[-115,-43]],[[52065,76395],[-252,-334],[-548,160],[-404,-192],[-32,-355]],[[50829,75674],[15,-344],[-263,-393],[-356,-125],[-25,-199],[-171,-327],[-107,-481],[108,-338],[-160,-263],[-60,-384],[-210,-118]],[[49600,72702],[-197,-455],[-352,-8]],[[49051,72239],[-265,11],[-174,-209],[-106,-223],[-136,49],[-103,199],[-79,340],[-259,92]],[[47929,72498],[-112,-153],[-146,83],[-143,-65],[42,462],[-26,363],[-124,55],[-67,224],[22,386],[111,215],[20,239],[58,355],[-6,250],[-56,212],[-12,200]],[[47490,75324],[14,420],[-114,257],[393,426]],[[47783,76427],[340,-107],[373,4]],[[48496,76324],[296,-101],[230,31],[449,-19]],[[49471,76235],[144,354],[53,1177],[-287,620],[-205,299]],[[49176,78685],[-424,228],[-28,430]],[[48724,79343],[360,129],[466,-152],[-88,669],[263,-254],[646,461],[84,484],[243,119]],[[50698,80799],[222,117]],[[50920,80916],[143,162]],[[51063,81078],[244,870],[380,247]],[[51687,82195],[231,-17]],[[51918,82178],[54,125],[232,32],[52,-130],[188,291],[-63,222],[-13,335]],[[52368,83053],[-113,328],[-8,604],[46,159]],[[52293,84144],[80,178],[244,36]],[[52617,84358],[98,163],[223,167],[-9,-304],[-82,-192],[33,-166],[151,-89],[-68,-223],[-83,64],[-200,-425],[76,-288]],[[52756,83065],[4,-228],[281,-138],[-3,-210],[283,111],[156,162],[313,-233],[132,-189]],[[53922,82340],[189,174],[434,273],[350,200],[277,-100],[21,-144],[268,-7]],[[55461,82736],[63,260],[383,191]],[[55907,83187],[-59,497]],[[55848,83684],[10,445],[136,371],[262,202],[221,-442],[223,12],[53,453]],[[56753,84725],[32,349],[-102,-75],[-176,210],[-24,340],[351,164],[350,86],[301,-97],[287,17]],[[57772,85719],[316,327],[-291,280]],[[57797,86326],[-504,-47],[-489,-216],[-452,-125]],[[56352,85938],[-161,322],[-269,195],[62,581]],[[55984,87036],[-135,534],[133,344]],[[55982,87914],[252,371],[635,640],[185,124],[-28,250],[-387,279]],[[56639,89578],[-478,-167],[-269,-413],[43,-361],[-441,-475],[-537,-509],[-202,-832],[198,-416],[265,-328],[-255,-666],[-289,-138],[-106,-992],[-157,-554],[-337,57],[-158,-468],[-321,-27],[-89,558],[-232,671],[-211,835]],[[53063,85353],[-187,363],[-548,-684]],[[52328,85032],[-370,-138],[-385,301]],[[51573,85195],[-99,635]],[[51474,85830],[-88,1364],[256,380]],[[51642,87574],[733,496],[549,609],[508,824],[668,1141],[465,444],[763,741],[610,259],[457,-31],[423,489],[506,-26],[499,118],[869,-433],[-358,-158],[305,-371]],[[58639,91676],[286,206],[456,-358],[761,-140],[1050,-668],[213,-281],[18,-393],[-308,-311],[-454,-157],[-1240,449],[-204,-75],[453,-433],[36,-878],[358,-180],[217,-153],[36,286]],[[60317,88590],[-174,263],[183,215]],[[60326,89068],[672,-368]],[[60998,88700],[234,144],[-187,433]],[[61045,89277],[647,578],[256,-34],[260,-206],[161,406],[-231,352],[136,353],[-204,367],[777,-190],[158,-331],[-351,-73]],[[62654,90499],[2,-328],[218,-203]],[[62874,89968],[429,128],[68,377]],[[63371,90473],[581,282],[969,507]],[[64921,91262],[209,-29],[-273,-359],[344,-61],[199,202],[521,16],[412,245],[317,-356],[315,391],[-291,343],[145,195],[820,-179],[385,-185],[1006,-675],[186,309],[-282,313],[-8,125],[-335,58],[92,280],[-149,461],[-8,189],[512,535]],[[69038,93080],[182,537],[207,116]],[[69427,93733],[735,-156],[58,-328]],[[70220,93249],[-263,-479],[173,-189],[89,-413],[-63,-809],[307,-362],[-120,-395],[-544,-839],[318,-87],[110,213],[306,151],[74,293],[240,281],[-162,336],[130,390],[-304,49],[-67,328]],[[70444,91717],[222,594],[-361,481]],[[70305,92792],[497,398],[-64,421],[139,13],[145,-328],[-109,-570],[297,-108],[-127,426],[465,233],[577,31],[513,-337],[-247,492],[-28,630]],[[72363,94093],[484,119],[668,-26]],[[73515,94186],[602,77],[-226,309],[321,388],[319,16],[540,293],[734,79],[93,162],[729,55],[227,-133],[624,314],[510,-10],[77,255],[265,252],[656,242],[476,-191],[-378,-146],[629,-90],[75,-292],[254,143],[812,-7],[626,-289],[223,-221],[-69,-307],[-307,-175],[-730,-328],[-209,-175],[345,-83],[410,-149]],[[63720,73858],[-47,-207],[-102,-138]],[[63571,73513],[7,-293]],[[63578,73220],[88,-436],[263,-123],[193,-296],[395,-102],[434,156],[27,139]],[[64978,72558],[-52,417],[40,618],[-216,200],[71,405],[-184,34],[61,498],[262,-145],[244,189],[-202,355],[-80,338],[-224,-151],[-28,-433],[-87,383]],[[64583,75266],[-15,144],[68,246],[-53,206],[-322,202],[-125,530],[-154,150],[-9,192],[270,-56],[11,432],[236,96],[243,-88],[50,576],[-50,365],[-278,-28],[-236,144],[-321,-260],[-259,-124]],[[63639,77993],[-127,-350],[-269,-97],[-276,-610],[252,-561],[-27,-398],[303,-696]],[[63495,75281],[146,-311],[141,-419],[130,-28],[85,-159],[-228,-47],[-49,-459]],[[23807,96363],[-521,38],[-74,165],[559,-9],[195,-109],[-33,-68],[-126,-17]],[[18874,96315],[-411,191],[224,188],[406,60],[392,-92],[-93,-177],[-518,-170]],[[56247,96336],[-490,137],[191,152],[-167,189],[575,119],[110,-222],[401,-134],[-620,-241]],[[19199,96904],[-461,1],[5,84],[285,177],[149,-27],[361,-120],[-339,-115]],[[22969,96575],[-226,138],[-119,221],[-22,245],[360,-24],[162,-39],[332,-205],[-76,-214],[-411,-122]],[[22313,96609],[-453,66],[-457,192],[-619,21],[268,176],[-335,142],[-21,227],[546,-81],[751,-215],[212,-281],[108,-247]],[[77621,96617],[507,776],[229,66],[208,-38],[704,-336],[-82,-240],[-1566,-228]],[[54420,95937],[-598,361],[252,210],[-416,170],[-541,499],[-216,463],[757,212],[152,-207],[396,8],[105,202],[408,20],[350,-206],[915,-440],[-699,-233],[-155,-435],[-243,-111],[-132,-490],[-335,-23]],[[56395,97491],[-819,98],[-50,163],[-398,11],[-304,271],[858,165],[403,-142],[281,177],[702,-148],[545,-207],[-412,-318],[-806,-70]],[[63218,97851],[-301,140],[158,185],[-618,18],[542,107],[422,8],[57,-160],[159,142],[262,97],[412,-129],[-107,-90],[-373,-78],[-250,-45],[-39,-97],[-324,-98]],[[77154,97111],[-773,170],[-462,226],[-213,423],[-379,117],[722,404],[600,133],[540,-297],[640,-572],[-69,-531],[-606,-73]],[[24776,96791],[-575,76],[-299,240],[4,215],[220,157],[-508,-4],[-306,196],[-176,268],[193,262],[192,180],[285,42],[-122,135],[646,30],[355,-315],[468,-127],[455,-112],[220,-390],[334,-190],[-381,-176],[-513,-445],[-492,-42]],[[27622,95587],[-726,163],[-816,-91],[-414,71],[-525,31],[-35,284],[514,133],[-137,427],[170,41],[742,-255],[-379,379],[-450,113],[225,229],[492,141],[79,206],[-392,231],[-118,304],[759,-26],[220,-64],[433,216],[-625,68],[-972,-38],[-491,201],[-232,239],[-324,173],[-61,202],[413,112],[324,19],[545,96],[409,220],[344,-30],[300,-166],[211,319],[367,95],[498,65],[849,24],[148,-63],[802,100],[601,-38],[602,-37],[742,-47],[597,-75],[508,-161],[-12,-157],[-678,-257],[-672,-119],[-251,-133],[605,3],[-656,-358],[-452,-167],[-476,-483],[-573,-98],[-177,-120],[-841,-64],[383,-74],[-192,-105],[230,-292],[-264,-202],[-429,-167],[-132,-232],[-388,-176],[39,-134],[475,23],[6,-144],[-742,-355]],[[37559,86051],[-410,482],[-556,3],[-269,324],[-186,577],[-481,735],[-141,385],[-38,530],[-384,546],[100,435],[-186,208],[275,691],[418,220],[110,247],[58,461],[-318,-209],[-151,-88],[-249,-84],[-341,193],[-19,401],[109,314],[258,9],[567,-157],[-478,375],[-249,202],[-276,-83],[-232,147],[310,550],[-169,220],[-220,409],[-335,626],[-353,230],[3,247],[-745,346],[-590,43],[-743,-24],[-677,-44],[-323,188],[-482,372],[729,186],[559,31],[-1188,154],[-627,241],[39,229],[1051,285],[1018,284],[107,214],[-750,213],[243,235],[961,413],[404,63],[-115,265],[658,156],[854,93],[853,5],[303,-184],[737,325],[663,-221],[390,-46],[577,-192],[-660,318],[38,253],[932,353],[975,-27],[354,218],[982,57],[2219,-74],[1737,-469],[-513,-227],[-1062,-26],[-1496,-58],[140,-105],[984,65],[836,-204],[540,181],[231,-212],[-305,-344],[707,220],[1348,229],[833,-114],[156,-253],[-1132,-420],[-157,-136],[-888,-102],[643,-28],[-324,-431],[-224,-383],[9,-658],[333,-386],[-434,-24],[-457,-187],[513,-313],[65,-502],[-297,-55],[360,-508],[-617,-42],[322,-241],[-91,-208],[-391,-91],[-388,-2],[348,-400],[4,-263],[-549,244],[-143,-158],[375,-148],[364,-361],[105,-476],[-495,-114],[-214,228],[-344,340],[95,-401],[-322,-311],[732,-25],[383,-32],[-745,-515],[-755,-466],[-813,-204],[-306,-2],[-288,-228],[-386,-624],[-597,-414],[-192,-24],[-370,-145],[-399,-138],[-238,-365],[-4,-415],[-141,-388],[-453,-472],[112,-462],[-125,-488],[-142,-577],[-391,-36]],[[67002,71642],[284,-224],[209,79],[58,268],[219,89],[157,180],[55,472],[234,114],[44,211],[131,-158],[84,-19]],[[68477,72654],[154,-4],[210,-124]],[[68841,72526],[85,-72],[201,189],[93,-114],[90,271],[166,-12],[43,86],[29,239],[120,205],[150,-134],[-30,-181],[84,-28],[-26,-496],[110,-194],[97,125],[123,58],[173,265],[192,-44],[286,-1]],[[70827,72688],[50,-169]],[[70877,72519],[-162,-67],[-141,-109],[-319,-68],[-298,-124],[-163,-258],[66,-250],[32,-294],[-139,-248],[12,-227],[-76,-213],[-265,18],[110,-390],[-177,-150],[-118,-356],[15,-355],[-108,-166],[-103,55],[-212,-77],[-31,-166],[-207,1],[-154,-334],[-10,-503],[-361,-246],[-194,52],[-56,-129],[-166,75],[-278,-88],[-465,301]],[[66909,68203],[252,536],[-23,380],[-210,100],[-22,375],[-91,472],[119,323],[-121,87],[76,430],[113,736]],[[56642,44124],[29,-184],[-32,-286],[49,-277],[-41,-222],[24,-203],[-579,7],[-13,-1880],[188,-483],[181,-369]],[[56448,40227],[-510,-241],[-673,83],[-192,284],[-1126,-26],[-42,-41],[-166,267],[-180,17],[-166,-100],[-134,-113]],[[53422,46976],[115,79],[80,-11],[98,71],[820,-8],[68,-440],[80,-354],[64,-191],[106,-309],[184,47],[91,83],[154,-83],[42,148],[69,344],[172,23],[15,103],[142,2],[-24,-213],[337,5],[5,-372],[56,-228],[-41,-356],[21,-363],[93,-219],[-15,-703],[68,54],[121,-15],[172,89],[127,-35]],[[53309,47603],[112,255],[84,100],[104,-203]],[[53609,47755],[-101,-124],[-45,-152],[-9,-258],[-71,-62]],[[55719,75309],[-35,-201],[39,-254],[115,-144]],[[55838,74710],[-5,-155],[-91,-85],[-16,-192],[-129,-287]],[[55427,75229],[-47,93]],[[55380,75322],[-18,188],[120,291],[18,-111],[75,52]],[[55575,75742],[59,-159],[66,-60],[19,-214]],[[65575,65974],[52,-202]],[[65665,65306],[-142,-3],[-23,-384],[50,-82],[-126,-117],[-1,-241],[-81,-245],[-7,-238]],[[65335,63996],[-56,-125],[-835,298],[-106,599],[-11,136]],[[31400,18145],[-168,16],[-297,1],[0,1319]],[[32587,37434],[511,-964],[227,-89],[339,-437],[286,-231],[40,-261],[-273,-898],[280,-160],[312,-91],[220,95],[252,453],[45,521]],[[34826,35372],[138,114],[139,-341],[-6,-472],[-234,-326],[-186,-241],[-314,-573],[-370,-806]],[[33993,32727],[-70,-473],[-74,-607],[3,-588],[-61,-132],[-21,-382]],[[31227,23224],[273,-433],[266,-119]],[[30952,19680],[-257,93],[-672,79],[-115,344],[6,443],[-185,-38],[-98,214],[-24,626],[213,260],[88,375],[-33,299],[148,504],[101,782],[-30,347],[122,112],[-30,223],[-129,118],[92,248],[-126,224],[-65,682],[112,120],[-47,720],[65,605],[75,527],[166,215],[-84,576],[-1,543],[210,386],[-7,494],[159,576],[1,544],[-72,108],[-128,1020],[171,607],[-27,572],[100,537],[182,555],[196,367],[-83,232],[58,190],[-9,985],[302,291],[96,614],[-34,148]],[[31359,37147],[231,534],[364,-144],[163,-427],[109,475],[316,-24],[45,-127]],[[62492,74950],[57,-155],[106,-103],[-56,-148],[148,-202],[-78,-189],[118,-160],[124,-97],[7,-410]],[[62918,73486],[-101,-17]],[[62817,73469],[-113,342],[1,91],[-123,-2],[-82,159],[-58,-16]],[[62442,74043],[-109,172],[-207,147],[27,288],[-47,208]],[[62106,74858],[386,92]],[[1088,892],[38,-7],[32,-4]],[[1158,881],[402,-246],[352,246],[63,34],[816,104],[265,-138],[130,-71],[419,-196],[789,-151],[625,-185],[1072,-139],[800,162],[1181,-116],[669,-185],[734,174],[773,162],[60,278],[-1094,23],[-898,139],[-234,231],[-745,128],[49,266],[103,243],[104,220],[-55,243],[-462,162],[-212,209],[-430,185],[675,-35],[642,93],[402,-197],[495,173],[457,220],[223,197],[-98,243],[-359,162],[-408,174],[-571,35],[-500,81],[-539,58],[-180,220],[-359,185],[-217,208],[-87,672],[136,-58],[250,-185],[457,58],[441,81],[228,-255],[441,58],[370,127],[348,162],[315,197],[419,58],[-11,220],[-97,220],[81,208],[359,104],[163,-196],[425,115],[321,151],[397,12],[375,57],[376,139],[299,128],[337,127],[218,-35],[190,-46],[414,81],[370,-104],[381,11],[364,81],[375,-57],[414,-58],[386,23],[403,-12],[413,-11],[381,23],[283,174],[337,92],[349,-127],[331,104],[300,208],[179,-185],[98,-208],[180,-197],[288,174],[332,-220],[375,-70],[321,-162],[392,35],[354,104],[418,-23],[376,-81],[381,-104],[147,254],[-180,197],[-136,209],[-359,46],[-158,220],[-60,220],[-98,440],[213,-81],[364,-35],[359,35],[327,-93],[283,-174],[119,-208],[376,-35],[359,81],[381,116],[342,70],[283,-139],[370,46],[239,451],[224,-266],[321,-104],[348,58],[228,-232],[365,-23],[337,-69],[332,-128],[218,220],[108,209],[278,-232],[381,58],[283,-127],[190,-197],[370,58],[288,127],[283,151],[337,81],[392,69],[354,81],[272,127],[163,186],[65,254],[-32,244],[-87,231],[-98,232],[-87,231],[-71,209],[-16,231],[27,232],[130,220],[109,243],[44,231],[-55,255],[-32,232],[136,266],[152,173],[180,220],[190,186],[223,173],[109,255],[152,162],[174,151],[267,34],[174,186],[196,115],[228,70],[202,150],[157,186],[218,69],[163,-151],[-103,-196],[-283,-174],[-120,-127],[-206,92],[-229,-58],[-190,-139],[-202,-150],[-136,-174],[-38,-231],[17,-220],[130,-197],[-190,-139],[-261,-46],[-153,-197],[-163,-185],[-174,-255],[-44,-220],[98,-243],[147,-185],[229,-139],[212,-185],[114,-232],[60,-220],[82,-232],[130,-196],[82,-220],[38,-544],[81,-220],[22,-232],[87,-231],[-38,-313],[-152,-243],[-163,-197],[-370,-81],[-125,-208],[-169,-197],[-419,-220],[-370,-93],[-348,-127],[-376,-128],[-223,-243],[-446,-23],[-489,23],[-441,-46],[-468,0],[87,-232],[424,-104],[311,-162],[174,-208],[-310,-185],[-479,58],[-397,-151],[-17,-243],[-11,-232],[327,-196],[60,-220],[353,-220],[588,-93],[500,-162],[398,-185],[506,-186],[690,-92],[681,-162],[473,-174],[517,-197],[272,-278],[136,-220],[337,209],[457,173],[484,186],[577,150],[495,162],[691,12],[680,-81],[560,-139],[180,255],[386,173],[702,12],[550,127],[522,128],[577,81],[614,104],[430,150],[-196,209],[-119,208],[0,220],[-539,-23],[-571,-93],[-544,0],[-77,220],[39,440],[125,128],[397,138],[468,139],[337,174],[337,174],[251,231],[380,104],[376,81],[190,47],[430,23],[408,81],[343,116],[337,139],[305,139],[386,185],[245,197],[261,173],[82,232],[-294,139],[98,243],[185,185],[288,116],[305,139],[283,185],[217,232],[136,277],[202,163],[331,-35],[136,-197],[332,-23],[11,220],[142,231],[299,-58],[71,-220],[331,-34],[360,104],[348,69],[315,-34],[120,-243],[305,196],[283,105],[315,81],[310,81],[283,139],[310,92],[240,128],[168,208],[207,-151],[288,81],[202,-277],[157,-209],[316,116],[125,232],[283,162],[365,-35],[108,-220],[229,220],[299,69],[326,23],[294,-11],[310,-70],[300,-34],[130,-197],[180,-174],[304,104],[327,24],[315,0],[310,11],[278,81],[294,70],[245,162],[261,104],[283,58],[212,162],[152,324],[158,197],[288,-93],[109,-208],[239,-139],[289,46],[196,-208],[206,-151],[283,139],[98,255],[250,104],[289,197],[272,81],[326,116],[218,127],[228,139],[218,127],[261,-69],[250,208],[180,162],[261,-11],[229,139],[54,208],[234,162],[228,116],[278,93],[256,46],[244,-35],[262,-58],[223,-162],[27,-254],[245,-197],[168,-162],[332,-70],[185,-162],[229,-162],[266,-35],[223,116],[240,243],[261,-127],[272,-70],[261,-69],[272,-46],[277,0],[229,-614],[-11,-150],[-33,-267],[-266,-150],[-218,-220],[38,-232],[310,12],[-38,-232],[-141,-220],[-131,-243],[212,-185],[321,-58],[321,104],[153,232],[92,220],[153,185],[174,174],[70,208],[147,289],[174,58],[316,24],[277,69],[283,93],[136,231],[82,220],[190,220],[272,151],[234,115],[153,197],[157,104],[202,93],[277,-58],[250,58],[272,69],[305,-34],[201,162],[142,393],[103,-162],[131,-278],[234,-115],[266,-47],[267,70],[283,-46],[261,-12],[174,58],[234,-35],[212,-127],[250,81],[300,0],[255,81],[289,-81],[185,197],[141,196],[191,163],[348,439],[179,-81],[212,-162],[185,-208],[354,-359],[272,-12],[256,0],[299,70],[299,81],[229,162],[190,174],[310,23],[207,127],[218,-116],[141,-185],[196,-185],[305,23],[190,-150],[332,-151],[348,-58],[288,47],[218,185],[185,185],[250,46],[251,-81],[288,-58],[261,93],[250,0],[245,-58],[256,-58],[250,104],[299,93],[283,23],[316,0],[255,58],[251,46],[76,290],[11,243],[174,-162],[49,-266],[92,-244],[115,-196],[234,-105],[315,35],[365,12],[250,35],[364,0],[262,11],[364,-23],[310,-46],[196,-186],[-54,-220],[179,-173],[299,-139],[310,-151],[360,-104],[375,-92],[283,-93],[315,-12],[180,197],[245,-162],[212,-185],[245,-139],[337,-58],[321,-69],[136,-232],[316,-139],[212,-208],[310,-93],[321,12],[299,-35],[332,12],[332,-47],[310,-81],[288,-139],[289,-116],[195,-173],[-32,-232],[-147,-208],[-125,-266],[-98,-209],[-131,-243],[-364,-93],[-163,-208],[-360,-127],[-125,-232],[-190,-220],[-201,-185],[-115,-243],[-70,-220],[-28,-266],[6,-220],[158,-232],[60,-220],[130,-208],[517,-81],[109,-255],[-501,-93],[-424,-127],[-528,-23],[-234,-336],[-49,-278],[-119,-220],[-147,-220],[370,-196],[141,-244],[239,-219],[338,-197],[386,-186],[419,-185],[636,-185],[142,-289],[800,-128],[53,-45],[208,-175],[767,151],[636,-186],[-99504,-147],[245,344],[501,-185],[32,21],[294,188]],[[54716,79012],[-21,-241],[-156,-2],[53,-128],[-92,-380]],[[54500,78261],[-53,-100],[-243,-14],[-140,-134],[-229,45]],[[53835,78058],[-398,153],[-62,205],[-274,-102],[-32,-113],[-169,84]],[[52900,78285],[-142,16],[-125,108],[42,145],[-10,104]],[[52665,78658],[83,33],[141,-164],[39,156],[245,-25],[199,106],[133,-18],[87,-121],[26,100],[-40,385],[100,75],[98,272]],[[53776,79457],[206,-190],[157,242],[98,44],[215,-180],[131,30],[128,-111]],[[54711,79292],[-23,-75],[28,-205]],[[62817,73469],[-190,78],[-141,273],[-44,223]],[[63720,73858],[-48,-207],[-101,-138]],[[63578,73220],[-69,-29],[-173,309],[95,292],[-82,174],[-104,-44],[-327,-436]],[[62492,74950],[68,96],[207,-169],[149,-36],[38,70],[-136,319],[72,82]],[[62890,75312],[78,-20],[191,-359],[122,-40],[48,150],[166,238]],[[58149,47921],[-17,713],[-70,268]],[[58062,48902],[169,-46],[85,336],[147,-38]],[[58463,49154],[16,-233],[60,-134],[3,-192],[-69,-124],[-108,-308],[-101,-214],[-115,-28]],[[50920,80916],[204,-47],[257,123],[176,-258],[153,-138]],[[51710,80596],[-32,-400]],[[51678,80196],[-72,-22],[-30,-331]],[[51576,79843],[-243,269],[-143,-46],[-194,279],[-129,237],[-129,10],[-40,207]],[[50518,54209],[-69,407],[13,1357],[-56,122],[-11,290],[-96,207],[-85,174],[35,311]],[[50249,57077],[96,67],[56,258],[136,56],[61,176]],[[50598,57634],[93,173],[100,2],[212,-340]],[[51003,57469],[-11,-197],[62,-350],[-54,-238],[29,-159],[-135,-366],[-86,-181],[-52,-372],[7,-376],[-16,-952]],[[49214,56277],[-190,152],[-130,-22],[-97,-149],[-125,125],[-49,195],[-125,129]],[[48498,56707],[-18,343],[76,250],[-7,200],[221,490],[41,405],[76,144],[134,-79],[116,120],[38,152],[216,265],[53,184],[259,246],[153,84],[70,-114],[178,3]],[[50104,59400],[-22,-286],[37,-269],[156,-386],[9,-286],[320,-134],[-6,-405]],[[50249,57077],[-243,13]],[[50006,57090],[-128,47],[-90,-96],[-123,43],[-482,-27],[-7,-336],[38,-444]],[[75742,63602],[-6,-424],[-97,90],[18,-476]],[[75657,62792],[-79,308],[-16,301],[-53,285]],[[74730,63611],[-43,486],[-96,444],[47,356],[-171,159],[62,215],[173,220],[-200,313],[98,401],[220,-255],[133,-30],[24,-410],[265,-81],[257,8],[160,-101],[-128,-500],[-124,-34],[-86,-336],[152,-306],[46,377],[76,2],[147,-937]],[[56293,76715],[80,-243],[108,43],[213,-92],[408,-31],[138,150],[327,138],[202,-215],[163,-62]],[[57776,75399],[-239,79],[-283,-186]],[[57254,75292],[-3,-294],[-252,-56],[-196,206],[-222,-162],[-206,17]],[[56375,75003],[-20,391],[-139,189]],[[56216,75583],[46,84],[-30,70],[47,188],[105,185],[-135,255],[-24,216],[68,134]],[[55279,77084],[100,2],[-69,-260],[134,-227],[-41,-278],[-65,-27]],[[55338,76294],[-52,-53],[-90,-138],[-41,-325]],[[55155,75778],[-246,224],[-105,247],[-106,130],[-127,221],[-61,183],[-136,277],[59,245],[99,-136],[60,123],[130,13],[239,-98],[192,8],[126,-131]],[[56523,82432],[268,-4],[302,223],[64,333],[228,190],[-26,264]],[[57359,83438],[169,100],[298,228]],[[57826,83766],[293,-149],[39,-146],[146,70],[272,-141],[27,-277],[-60,-159],[174,-387],[113,-108],[-16,-107],[187,-104],[80,-157],[-108,-129],[-224,20],[-54,-55],[66,-196],[68,-379]],[[58829,81362],[-239,-35],[-85,-129],[-18,-298],[-111,57],[-250,-28],[-73,138],[-104,-103],[-105,86],[-218,12],[-310,141],[-281,47],[-215,-14],[-152,-160],[-133,-23]],[[56535,81053],[-6,263],[-85,274],[166,121],[2,235],[-77,225],[-12,261]],[[25238,61101],[-2,87],[33,27],[51,-70],[99,357],[53,8]],[[25297,59966],[-83,0],[22,667],[2,468]],[[31359,37147],[-200,-81],[-109,814],[-150,663],[88,572],[-146,250],[-37,426],[-136,402]],[[30669,40193],[175,638],[-119,496],[63,199],[-49,219],[108,295],[6,503],[13,415],[60,200],[-240,951]],[[30686,44109],[206,-50],[143,13],[62,179],[243,239],[147,222],[363,100],[-29,-443],[34,-227],[-23,-396],[302,-529],[311,-98],[109,-220],[188,-117],[115,-172],[175,6],[161,-175],[12,-342],[55,-172],[3,-255],[-81,-10],[107,-688],[533,-24],[-41,-342],[30,-233],[151,-166],[66,-367],[-49,-465],[-77,-259],[27,-337],[-87,-122]],[[33842,38659],[-4,182],[-259,302],[-258,9],[-484,-172],[-133,-520],[-7,-318],[-110,-708]],[[34826,35372],[54,341],[38,350],[0,325],[-100,107],[-104,-96],[-103,26],[-33,228],[-26,541],[-52,177],[-187,160],[-114,-116],[-293,113],[18,802],[-82,329]],[[30686,44109],[-157,-102],[-126,68],[18,898],[-228,-348],[-245,15],[-105,315],[-184,34],[59,254],[-155,359],[-115,532],[73,108],[0,250],[168,171],[-28,319],[71,206],[20,275],[318,402],[227,114],[37,89],[251,-28]],[[30585,48040],[125,1620],[6,256],[-43,339],[-123,215],[1,430],[156,97],[56,-61],[9,226],[-162,61],[-4,370],[541,-13],[92,203],[77,-187],[55,-349],[52,73]],[[31423,51320],[153,-312],[216,38],[54,181],[206,138],[115,97],[32,250],[198,168],[-15,124],[-235,51],[-39,372],[12,396],[-125,153],[52,55],[206,-76],[221,-148],[80,140],[200,92],[310,221],[102,225],[-37,167]],[[33129,53652],[145,26],[64,-136],[-36,-259],[96,-90],[63,-274],[-77,-209],[-44,-502],[71,-299],[20,-274],[171,-277],[137,-29],[30,116],[88,25],[126,104],[90,157],[154,-50],[67,21]],[[34294,51702],[151,-48],[25,120],[-46,118],[28,171],[112,-53],[131,61],[159,-125]],[[34854,51946],[121,-122],[86,160],[62,-25],[38,-166],[133,42],[107,224],[85,436],[164,540]],[[35174,30629],[-77,334],[122,280],[-160,402],[-218,327],[-286,379],[-103,-18],[-279,457],[-180,-63]],[[82069,53798],[-13,-291],[-16,-377],[-133,19],[-58,-202],[-126,307]],[[75471,66988],[113,-189],[-20,-363],[-227,-17],[-234,39],[-175,-92],[-252,224],[-6,119]],[[74670,66709],[184,439],[150,150],[198,-137],[147,-14],[122,-159]],[[58175,37528],[-393,-435],[-249,-442],[-93,-393],[-83,-222],[-152,-47],[-48,-283],[-28,-184],[-178,-138],[-226,29],[-133,166],[-117,71],[-135,-137],[-68,-283],[-132,-177],[-139,-264],[-199,-60],[-62,207],[26,360],[-165,562],[-75,88]],[[55526,35946],[0,1725],[274,20],[8,2105],[207,19],[428,207],[106,-243],[177,231],[85,2],[156,133]],[[56967,40145],[50,-44]],[[57017,40101],[107,-473],[56,-105],[87,-342],[315,-649],[119,-64],[0,-208],[82,-375],[215,-90],[177,-267]],[[54244,54965],[229,44],[52,152],[46,-11],[69,-134],[350,226],[118,230],[145,207],[-28,208],[78,54],[269,-36],[261,273],[201,645],[141,239],[176,101]],[[56351,57163],[31,-253],[160,-369],[1,-241],[-45,-246],[18,-184],[96,-170]],[[56612,55700],[212,-258]],[[56824,55442],[152,-239],[2,-192],[187,-308],[116,-255],[70,-355],[208,-234],[44,-187]],[[57603,53672],[-91,-63],[-178,14],[-209,62],[-104,-51],[-41,-143],[-90,-18],[-110,125],[-309,-295],[-127,60],[-38,-46],[-83,-357],[-207,115],[-203,59],[-177,218],[-229,200],[-149,-190],[-108,-300],[-25,-412]],[[55125,52650],[-178,33],[-188,99],[-166,-313],[-146,-550]],[[54447,51919],[-29,172],[-12,269],[-127,190],[-103,305],[-23,212],[-132,309],[23,176],[-28,249],[21,458],[67,107],[140,599]],[[26228,91219],[16,649],[394,-46]],[[25824,89109],[-81,-259],[-322,-399]],[[23714,86094],[-16,-686],[409,-99]],[[25743,83665],[348,-163],[294,-248]],[[28738,83981],[-23,395],[-188,502]],[[31513,79609],[415,58],[246,-289]],[[31350,77248],[-181,334],[0,805],[-123,171],[-187,-100],[-92,155],[-212,-446],[-84,-460],[-99,-269],[-118,-91],[-89,-30],[-28,-146],[-512,0],[-422,-4],[-125,-109],[-294,-425],[-34,-46],[-89,-231],[-255,1],[-273,-3],[-125,-93],[44,-116],[25,-181],[-5,-60],[-363,-293],[-286,-93],[-323,-316],[-70,0],[-94,93],[-31,85],[6,61],[61,207],[131,325],[81,349],[-56,514],[-59,536],[-290,277],[35,105],[-41,73],[-76,0],[-56,93],[-14,140],[-54,-61],[-75,18],[17,59],[-65,58],[-27,155],[-216,189],[-224,197],[-272,229],[-261,214],[-248,-167],[-91,-6],[-342,154],[-225,-77],[-269,183],[-284,94],[-194,36],[-86,100],[-49,325],[-94,-3],[-1,-227],[-575,0],[-951,0],[-944,0],[-833,0],[-834,0],[-819,0],[-847,0],[-273,0],[-825,0],[-788,0]],[[15104,80367],[-503,244],[-155,523],[40,363]],[[13740,82958],[154,285],[-7,373],[-473,376],[-284,674],[-173,424],[-255,266],[-187,242],[-147,306],[-279,-192],[-270,-330],[-247,388],[-194,259],[-271,164],[-273,17],[1,3364],[2,2193]],[[11355,91625],[438,-285],[289,-54]],[[15437,92031],[38,-449],[341,97]],[[17987,91291],[374,-300],[-390,-293]],[[19722,91216],[-704,-88],[-494,-56]],[[15020,93041],[119,250],[192,432]],[[16539,93012],[0,-257],[-731,-285]],[[52900,78285],[-22,-242],[-122,-100],[-206,75],[-60,-239],[-132,-19],[-48,94],[-156,-200],[-134,-28],[-120,126]],[[51900,77752],[-95,259],[-133,-92],[5,267],[203,332],[-9,150],[126,-54],[77,101]],[[52074,78715],[236,-4],[57,128],[298,-181]],[[31400,18145],[-92,-239],[-238,-183]],[[31070,17723],[-137,19],[-164,48]],[[30769,17790],[-202,177],[-291,86],[-350,330],[-283,317],[-383,662],[229,-124],[390,-395],[369,-212],[143,271],[90,405],[256,244],[198,-70]],[[29661,27385],[-80,576],[-22,666]],[[30452,39739],[143,151],[74,303]],[[86288,75628],[-179,348],[-111,-331],[-429,-254],[44,-312],[-241,22],[-131,185],[-191,-419],[-306,-318],[-227,-379]],[[83030,72705],[220,-173],[311,422]],[[83987,72709],[45,-310],[-393,-165]],[[83097,71205],[299,-325],[109,-581]],[[80517,63220],[-373,189],[-131,-96]],[[80013,63313],[-280,154],[-132,240],[44,340],[-254,108],[-134,222],[-236,-315],[-271,-68],[-221,3],[-149,-145]],[[78380,63852],[-144,-86],[42,-676],[-148,16],[-25,139]],[[78105,63245],[-9,244],[-203,-172],[-121,109],[-206,222],[81,490],[-176,115],[-66,544],[-293,-98],[33,701],[263,493],[11,487],[-8,452],[-121,141],[-93,348],[-162,-44]],[[77035,67277],[-300,89],[94,248],[-130,367],[-198,-249],[-233,145],[-321,-376],[-252,-439],[-224,-74]],[[74670,66709],[-23,465],[-170,-124]],[[74477,67050],[-324,57],[-314,136],[-225,259],[-216,117],[-93,284],[-157,84],[-280,385],[-223,182],[-115,-141]],[[72530,68413],[-386,413],[-273,374],[-78,651],[200,-79],[9,301],[-111,303],[28,482],[-298,692]],[[71621,71550],[-457,239],[-82,454],[-205,276]],[[70827,72688],[-42,337],[10,230],[-169,134],[-91,-59],[-70,546]],[[70465,73876],[79,136],[-39,138],[266,279],[192,116],[294,-80],[105,378],[356,70],[99,234],[438,320],[39,134]],[[72294,75601],[-22,337],[190,154],[-250,1026],[550,236],[143,131],[200,1058],[551,-194],[155,267],[13,592],[230,56],[212,393]],[[74266,79657],[109,49]],[[74375,79706],[73,-413],[233,-313],[396,-222],[192,-476],[-107,-690],[100,-256],[330,-101],[374,-83],[336,-368],[171,-66],[127,-544],[163,-351],[306,14],[574,-133],[369,82],[274,-88],[411,-359],[336,1],[123,-184],[324,318],[448,205],[417,22],[324,208],[200,316],[194,199],[-45,195],[-89,227],[146,381],[156,-53],[286,-120],[277,313],[423,229],[204,391],[195,168],[404,78],[219,-66],[30,210],[-251,413],[-223,189],[-214,-219],[-274,92],[-157,-74],[-72,241],[197,590],[135,446]],[[82410,80055],[333,-223],[392,373],[-3,260],[251,627],[155,189],[-4,326],[-152,141],[229,294],[345,106],[369,16],[415,-176],[244,-217],[172,-596],[104,-254],[97,-363],[103,-579],[483,-189],[329,-420],[112,-555],[423,-1],[240,233],[459,175],[-146,-532],[-107,-216],[-96,-647],[-186,-575],[-338,104],[-238,-208],[73,-506],[-40,-698],[-142,-16],[2,-300]],[[47857,53158],[22,487],[26,74],[-8,233],[-118,247],[-88,40],[-81,162],[60,262],[-28,286],[13,172]],[[47655,55121],[44,0],[17,258],[-22,114],[27,82],[103,71],[-69,473],[-64,245],[23,200],[55,46]],[[47769,56610],[36,54],[77,-89],[215,-5],[51,172],[48,-11],[80,67],[43,-253],[65,74],[114,88]],[[49214,56277],[74,-841],[-117,-496],[-73,-667],[121,-509],[-13,-233]],[[53632,51919],[-35,32],[-164,-76],[-169,79],[-132,-38]],[[53132,51916],[-452,13]],[[52680,51929],[40,466],[-108,391]],[[52429,53151],[-72,85],[4,163]],[[52361,53399],[71,418],[132,570],[81,6],[165,345],[105,10],[156,-243],[191,199],[26,246],[63,238],[43,299],[148,243],[56,414],[59,132],[39,307],[74,377],[234,457],[14,196],[31,107],[-110,235]],[[53939,57955],[9,188],[78,34]],[[54026,58177],[111,-378],[18,-392],[-10,-393],[151,-537],[-155,6],[-78,-42],[-127,60],[-60,-279],[164,-345],[121,-100],[39,-245],[87,-407],[-43,-160]],[[54447,51919],[-20,-319],[-220,140],[-225,156],[-350,23]],[[58564,52653],[-16,-691],[111,-80],[-89,-210],[-107,-157],[-106,-308],[-59,-274],[-15,-475],[-65,-225],[-2,-446]],[[58216,49787],[-80,-165],[-10,-351],[-38,-46],[-26,-323]],[[58149,47921],[50,-544],[-27,-307]],[[58172,47070],[55,-343],[161,-330]],[[58388,46397],[150,-745]],[[58538,45652],[-109,60],[-373,-99],[-75,-71],[-79,-377],[62,-261],[-49,-699],[-34,-593],[75,-105],[194,-230],[76,107],[23,-637],[-212,5],[-114,325],[-103,252],[-213,82],[-62,310],[-170,-187],[-222,83],[-93,268],[-176,55],[-131,-15],[-15,184],[-96,15]],[[53609,47755],[73,-60],[95,226],[152,-6],[17,-167],[104,-105],[164,370],[161,289],[71,189],[-10,486],[121,574],[127,304],[183,285],[32,189],[7,216],[45,205],[-14,335],[34,524],[55,368],[83,316],[16,357]],[[57603,53672],[169,-488],[124,-71],[75,99],[128,-39],[155,125],[66,-252],[244,-393]],[[53081,48229],[212,326],[-105,391],[95,148],[187,73],[23,261],[148,-283],[245,-25],[85,279],[36,393],[-31,461],[-131,350],[120,684],[-69,117],[-207,-48],[-78,305],[21,258]],[[29063,50490],[-119,140],[-137,195],[-79,-94],[-235,82],[-68,255],[-52,-10],[-278,338]],[[28366,54848],[36,287],[89,-43],[52,176],[-64,348],[34,86]],[[30185,57537],[-178,-99],[-71,-295],[-107,-169],[-81,-220],[-34,-422],[-77,-345],[144,-40],[35,-271],[62,-130],[21,-238],[-33,-219],[10,-123],[69,-49],[66,-207],[357,57],[161,-75],[196,-508],[112,63],[200,-32],[158,68],[99,-102],[-50,-318],[-62,-199],[-22,-423],[56,-393],[79,-175],[9,-133],[-140,-294],[100,-130],[74,-207],[85,-589]],[[30585,48040],[-139,314],[-83,14],[179,602],[-213,276],[-166,-51],[-101,103],[-153,-157],[-207,74],[-163,620],[-129,152],[-89,279],[-184,280],[-74,-56]],[[26191,57131],[42,76],[183,-156],[63,77],[89,-50],[46,-121],[82,-40],[66,126]],[[27070,56232],[-107,-53],[1,-238],[58,-88],[-41,-70],[10,-107],[-23,-120],[-14,-117]],[[59437,71293],[-30,21],[-53,-45],[-42,12],[-14,-22],[-5,59],[-20,37],[-54,6],[-75,-51],[-52,31]],[[53776,79457],[-157,254],[-141,142],[-30,249],[-49,176],[202,129],[103,147],[200,114],[70,113],[73,-68],[124,62]],[[54171,80775],[132,-191],[207,-51],[-17,-163],[151,-122],[41,153],[191,-66],[26,-185],[207,-36],[127,-291]],[[55236,79823],[-82,-1],[-43,-106],[-64,-26],[-18,-134],[-54,-28],[-7,-55],[-95,-61],[-123,10],[-39,-130]],[[53922,82340],[64,-300],[-77,-158],[101,-210],[69,-316],[-22,-204],[114,-377]],[[52074,78715],[35,421],[140,404],[-400,109],[-131,155]],[[51718,79804],[16,259],[-56,133]],[[51710,80596],[-47,619],[167,0],[70,222],[69,541],[-51,200]],[[52368,83053],[210,-78],[178,90]],[[61984,57352],[-102,-317]],[[61882,57035],[-62,106],[-67,-42],[-155,10],[-4,180],[-22,163],[94,277],[98,261]],[[61764,57990],[119,-51],[83,144]],[[52293,84144],[80,177],[244,37]],[[30081,61241],[5,161],[-71,177],[68,99],[21,228],[-24,321]],[[53333,64447],[-952,-1126],[-804,-1161],[-392,-263]],[[51185,61897],[-308,-58],[-3,376],[-129,96],[-173,169],[-66,277],[-937,1289],[-937,1289]],[[48632,65335],[-1045,1431]],[[47587,66766],[6,114],[-1,40]],[[47592,66920],[-2,700],[449,436],[277,90],[227,159],[107,295],[324,234],[12,438],[161,51],[126,219],[363,99],[51,230],[-73,125],[-96,624],[-17,359],[-104,379]],[[52339,72408],[-57,-303],[44,-563],[-65,-487],[-171,-330],[24,-445],[227,-352],[3,-143],[171,-238],[118,-1061]],[[52633,68486],[90,-522],[15,-274],[-49,-482],[21,-270],[-36,-323],[24,-371],[-110,-247],[164,-431],[11,-253],[99,-330],[130,109],[219,-275],[122,-370]],[[29063,50490],[38,-449],[-86,-384],[-303,-619],[-334,-233],[-170,-514],[-53,-398],[-157,-243],[-116,298],[-113,64],[-114,-47],[-8,216],[79,141],[-33,246]],[[60240,63578],[-1102,0],[-1077,0],[-1117,0]],[[56944,63578],[0,2175],[0,2101],[-83,476],[71,365],[-43,253],[101,283]],[[59518,69025],[182,-1015]],[[61764,57990],[-95,191],[-114,346],[-124,190],[-71,204],[-242,237],[-191,7],[-67,124],[-163,-139],[-168,268],[-87,-441],[-323,124]],[[60119,59101],[-30,236],[120,868],[27,393],[88,181],[204,97],[141,337]],[[60669,61213],[161,-684],[77,-542]],[[47783,76427],[340,-106],[373,3]],[[49471,76235],[111,-230],[511,-268],[101,127],[313,-267],[322,77]],[[49600,72702],[-197,-454],[-352,-9]],[[47929,72498],[-23,195],[103,222],[38,161],[-96,175],[77,388],[-111,355],[120,48],[11,280],[45,86],[3,461],[129,160],[-78,296],[-162,21],[-47,-75],[-164,0],[-70,289],[-113,-86],[-101,-150]],[[57772,85719],[42,-103],[-198,-341],[83,-551],[-120,-187]],[[57579,84537],[-229,1],[-239,219],[-121,73],[-237,-105]],[[61882,57035],[-61,-209],[103,-325],[102,-285],[106,-210],[909,-702],[233,4]],[[63274,55308],[-785,-1773],[-362,-26],[-247,-417],[-178,-11],[-76,-186]],[[61626,52895],[-190,0],[-112,200],[-254,-247],[-82,-247],[-185,47],[-62,68],[-65,-16],[-87,6],[-352,502],[-193,0],[-95,194],[0,332],[-145,99]],[[59804,53833],[-164,643],[-127,137],[-48,236],[-141,288],[-171,42],[95,337],[147,14],[42,181]],[[59437,55711],[-4,531]],[[59433,56242],[82,618],[132,166],[28,241],[119,451],[168,293],[112,582],[45,508]],[[57942,91385],[-41,-414],[425,-394],[-256,-445],[323,-673],[-187,-506],[250,-440],[-113,-385],[411,-405],[-105,-301],[-258,-341],[-594,-755]],[[56352,85938],[-161,323],[-269,193],[62,582]],[[55984,87036],[-135,533],[133,345]],[[56639,89578],[-93,230],[-8,910],[-433,402],[-371,289]],[[55734,91409],[167,156],[309,-312],[362,29],[298,-143],[265,262],[137,433],[431,200],[356,-235],[-117,-414]],[[34854,51946],[70,252],[24,269],[48,253],[-107,349]],[[34889,53069],[-22,404],[144,508]],[[51576,79843],[62,-52],[80,13]],[[51900,77752],[-11,-167],[82,-222],[-97,-180],[72,-457],[151,-75],[-32,-256]],[[49176,78685],[-424,227],[-28,431]],[[52636,51176],[94,35],[404,-6],[-2,711]],[[48278,82406],[-210,122],[-172,-9],[57,317],[-57,317]],[[49420,83612],[22,-62],[248,-697]],[[49690,82853],[190,-95],[171,-673],[79,-233],[337,-113],[-34,-378],[-142,-173],[111,-305],[-250,-310],[-371,6],[-473,-163],[-130,116],[-183,-276],[-257,67],[-195,-226],[-148,118],[407,621],[249,127]],[[49051,80963],[-2,1],[-434,98]],[[48615,81062],[-79,235],[291,183],[-152,319],[52,387]],[[48727,82186],[413,-54],[1,0]],[[49141,82132],[40,343]],[[49181,82475],[-186,364],[-4,8]],[[48991,82847],[-337,104],[-66,160],[101,264],[-92,163],[-149,-279],[-17,569],[-140,301],[101,611],[216,480],[222,-47],[335,49],[-297,-639],[283,81],[304,-3],[-72,-481],[-250,-530],[287,-38]],[[61098,76242],[34,70],[235,-101],[409,-96],[378,-283],[48,-110],[169,93],[259,-124],[85,-242],[175,-137]],[[62106,74858],[-268,290],[-296,-28]],[[50006,57090],[-20,-184],[116,-305],[-1,-429],[27,-466],[69,-215],[-61,-532],[22,-294],[74,-375],[62,-207]],[[47655,55121],[-78,15],[-57,-238],[-78,3],[-55,126],[19,237],[-116,362],[-73,-67],[-59,-13]],[[47158,55546],[-77,-34],[3,217],[-44,155],[9,171],[-60,249],[-78,211],[-222,1],[-65,-112],[-76,-13],[-48,-128],[-32,-163],[-148,-260]],[[45797,57103],[123,288],[84,-11],[73,99],[61,1],[44,78],[-24,196],[31,62],[5,200]],[[46194,58016],[134,-6],[200,-144],[61,13],[21,66],[151,-47],[40,33]],[[46801,57931],[16,-216],[44,1],[73,78],[46,-19],[77,-150],[119,-48],[76,128],[90,79],[67,83],[55,-15],[62,-130],[33,-163],[114,-248],[-57,-152],[-11,-192],[59,58],[35,-69],[-15,-176],[85,-170]],[[45357,58612],[302,17],[63,140],[88,9],[110,-145],[86,-3],[92,99],[56,-170],[-120,-133],[-121,11],[-119,124],[-103,-136],[-50,-5],[-67,-83],[-253,13]],[[45367,57897],[147,96],[92,-19],[75,67],[513,-25]],[[56638,74190],[-154,-1],[-147,305]],[[56486,73734],[-105,-129],[155,-273]],[[56431,72099],[-184,-8],[-228,257],[-104,473]],[[55838,74710],[182,53],[106,129],[150,-12],[46,103],[53,20]],[[57254,75292],[135,-157],[-86,-369],[-66,-67]],[[24381,59170],[7,172],[32,138],[-39,111],[133,481],[357,2],[7,201],[-45,36],[-31,128],[-103,136],[-103,198],[125,1],[1,333],[259,1],[257,-7]],[[25493,59872],[-127,-225],[-131,-166],[-20,-113],[22,-116],[-58,-150]],[[25179,59102],[-65,-37],[15,-69],[-52,-66],[-95,-149],[-9,-86]],[[34125,54109],[-44,-532],[-169,-154],[15,-139],[-51,-305],[123,-429],[89,-1],[37,-333],[169,-514]],[[33129,53652],[-188,448],[75,163],[-5,273],[171,95],[69,110],[-95,220],[24,215],[220,347]],[[25697,58436],[-84,51]],[[25613,58487],[19,237],[-38,64],[-57,42],[-122,-70],[-10,79],[-84,95],[-60,118],[-82,50]],[[25860,59889],[128,15],[90,66]],[[26903,59440],[-95,12],[-38,-81],[-97,-77],[-70,0],[-61,-76],[-56,27],[-47,90],[-29,-17],[-36,-141],[-27,5],[-4,-121],[-97,-163],[-51,-70],[-29,-74],[-82,120],[-60,-158],[-58,4],[-65,-14],[6,-290],[-41,-5],[-35,-135],[-86,-25]],[[55230,77704],[67,-229],[89,-169],[-107,-222]],[[55155,75778],[-31,-100]],[[54448,76285],[-233,434],[56,45]],[[53809,77462],[194,-20],[51,100],[94,-97],[109,-11],[-1,165],[97,60],[27,239],[221,157]],[[54601,78055],[88,-73],[208,-253],[229,-114],[104,89]],[[54716,79012],[141,-151],[103,-65],[233,73],[22,118],[111,18],[135,92],[30,-38],[130,74],[66,139],[91,36],[297,-180],[59,61]],[[56134,79189],[155,-161],[19,-159]],[[56308,78869],[-170,-123],[-131,-401],[-168,-401],[-223,-111]],[[55616,77833],[-173,26],[-213,-155]],[[54601,78055],[-54,200],[-47,6]],[[84713,45326],[28,-117],[5,-179]],[[89166,49043],[5,-1925],[4,-1925]],[[80461,51765],[47,-395],[190,-334],[179,121],[177,-43],[162,299],[133,52],[263,-166],[226,126],[143,822],[107,205],[96,672],[319,0],[241,-100]],[[72530,68413],[-176,-268],[-108,-553],[269,-224],[262,-289],[362,-332],[381,-76],[160,-301],[215,-56],[334,-138],[231,10],[32,234],[-36,375],[21,255]],[[77035,67277],[20,-224],[-97,-108],[23,-364],[-199,107],[-359,-408],[8,-338],[-153,-496],[-14,-288],[-124,-487],[-217,135],[-11,-612],[-63,-201],[30,-251],[-137,-140]],[[73107,61020],[-276,-387],[-1,-271]],[[72692,60216],[-251,-212],[-129,-31]],[[71996,56025],[-253,-168],[-93,-401]],[[68937,64577],[185,395],[612,-2],[-56,507],[-156,300],[-31,455],[-182,265],[306,619],[323,-45],[290,620],[174,599],[270,593],[-4,421],[236,342],[-224,292],[-96,400],[-99,517],[137,255],[421,-144],[310,88],[268,496]],[[64978,72558],[244,114],[197,338],[186,-17],[122,110],[197,-55],[308,-299],[221,-65],[318,-523],[207,-21],[24,-498]],[[66909,68203],[137,-310],[112,-357],[266,-260],[7,-520],[133,-96],[23,-272],[-400,-305],[-105,-687]],[[66559,65575],[-303,136],[-313,76]],[[63594,68492],[-104,-231]],[[63490,68261],[-153,311],[-3,314],[-89,0],[46,428],[-143,449],[-340,324],[-193,562],[65,461],[139,204],[-21,345],[-182,177],[-180,705]],[[62436,72541],[-152,473],[55,183],[-87,678],[190,168]],[[63326,68290],[-187,49],[-204,-567]],[[62935,67772],[-516,47],[-784,1188],[-413,414],[-335,160]],[[60887,69581],[-112,720]],[[60775,70301],[615,614],[105,715],[-26,431],[152,146],[142,369]],[[61763,72576],[119,92],[324,-77],[97,-150],[133,100]],[[63490,68261],[-164,29]],[[59873,69719],[-100,82],[-58,-394],[69,-66],[-71,-81],[-12,-156],[131,80]],[[59832,69184],[7,-230],[-139,-944]],[[59757,70130],[93,-1],[25,104],[75,8]],[[59950,70241],[4,-242],[-38,-90],[6,-4]],[[59922,69905],[-49,-186]],[[53835,78058],[-31,-291],[67,-251]],[[54413,75123],[249,-214],[204,-178]],[[53108,75604],[-189,340],[-86,585]],[[59922,69905],[309,-234],[544,630]],[[60887,69581],[-53,-89],[-556,-296],[277,-591],[-92,-101],[-46,-197],[-212,-82],[-66,-213],[-120,-182],[-310,94]],[[59832,69184],[41,173],[0,362]],[[69711,75551],[-159,-109],[-367,-412],[-121,-422],[-104,-4],[-76,280],[-353,19],[-57,484],[-135,4],[21,593],[-333,431],[-476,-46],[-326,-86],[-265,533],[-227,223],[-431,423],[-52,51],[-715,-349],[11,-2178]],[[65546,74986],[-142,-29],[-195,463],[-188,166],[-315,-123],[-123,-197]],[[63639,77993],[-142,96],[29,304],[-177,395],[-207,-17],[-235,401],[160,448],[-81,120],[222,649],[285,-342],[35,431],[573,643],[434,15],[612,-409],[329,-239],[295,249],[440,12],[356,-306],[80,175],[391,-25],[69,280],[-450,406],[267,288],[-52,161],[266,153],[-200,405],[127,202],[1039,205],[136,146],[695,218],[250,245],[499,-127],[88,-612],[290,144],[356,-202],[-23,-322],[267,33],[696,558],[-102,-185],[355,-457],[620,-1500],[148,309],[383,-340],[399,151],[154,-106],[133,-341],[194,-115],[119,-251],[358,79],[147,-361]],[[72294,75601],[-171,87],[-140,212],[-412,62],[-461,16],[-100,-65],[-396,248],[-158,-122],[-43,-349],[-457,204],[-183,-84],[-62,-259]],[[60889,47817],[-399,590],[-19,343],[-1007,1203],[-47,65]],[[59417,50018],[-3,627],[80,239],[137,391],[101,431],[-123,678],[-32,296],[-132,411]],[[59445,53091],[171,352],[188,390]],[[61626,52895],[-243,-670],[3,-2152],[165,-488]],[[70465,73876],[-526,-89],[-343,192],[-301,-46],[26,340],[303,-98],[101,182]],[[69725,74357],[212,-58],[355,425],[-329,311],[-198,-147],[-205,223],[234,382],[-83,58]],[[78495,57780],[-66,713],[178,492],[359,112],[261,-84]],[[79227,59013],[229,-232],[126,407],[246,-217]],[[79828,58971],[64,-394],[-34,-708],[-467,-455],[122,-358],[-292,-43],[-240,-238]],[[85103,71220],[51,443],[-122,615]],[[85048,72883],[17,54],[124,-21],[108,266],[197,29],[118,39],[40,143]],[[55575,75742],[52,132]],[[55627,75874],[66,43],[38,196],[50,33],[40,-84],[52,-36],[36,-94],[46,-28],[54,-110],[39,4],[-31,-144],[-33,-71],[9,-44]],[[55993,75539],[-62,-23],[-164,-91],[-13,-121],[-35,5]],[[63448,67449],[-196,-16],[-69,282],[-248,57]],[[79227,59013],[90,266],[12,500],[-224,515],[-18,583],[-211,480],[-210,40],[-56,-205],[-163,-17],[-83,104],[-293,-353],[-6,530],[68,623],[-188,27],[-16,355],[-120,182]],[[77809,62643],[59,218],[237,384]],[[78380,63852],[162,-466],[125,-537],[342,-5],[108,-515],[-178,-155],[-80,-212],[333,-353],[231,-699],[175,-520],[210,-411],[70,-418],[-50,-590]],[[59999,71049],[125,-31],[45,-231],[-151,-223],[-68,-323]],[[47498,53435],[-252,449],[-237,324]],[[46822,54589],[66,189],[15,172],[126,320],[129,276]],[[54125,64088],[-197,-220],[-156,324],[-439,255]],[[52633,68486],[136,137],[24,250],[-30,244],[191,228],[86,189],[135,170],[16,454]],[[56646,69496],[276,-70],[68,-195]],[[56944,63578],[0,-1180],[-320,-2],[-3,-248]],[[56621,62148],[-1108,1131],[-1108,1132],[-280,-323]],[[57708,32474],[-209,454],[148,374],[151,232],[130,120],[121,-182],[96,-178],[-85,-288],[-47,-192],[-155,-93],[-51,-188],[-99,-59]],[[56314,82678],[-23,150],[30,162],[-123,94],[-291,103]],[[55848,83684],[318,181],[466,-38],[273,59],[39,-123],[148,-38],[267,-287]],[[56523,82432],[-67,182],[-142,64]],[[57579,84537],[134,-136],[24,-287],[89,-348]],[[47592,66920],[-42,0],[7,-317],[-172,-19],[-90,-134],[-126,0],[-100,76],[-234,-63],[-91,-460],[-86,-44],[-131,-745],[-386,-637],[-92,-816],[-114,-265],[-33,-213],[-625,-48],[-5,1]],[[45272,63236],[13,274],[106,161],[91,308],[-18,200],[96,417],[155,376],[93,95],[74,344],[6,315],[100,365],[185,216],[177,603]],[[46350,66910],[5,8],[139,227]],[[46494,67145],[259,65],[218,404],[140,158]],[[57394,79070],[66,87],[185,58],[204,-184],[115,-22],[125,-159],[-20,-200],[101,-97],[40,-247],[97,-150],[-19,-88],[52,-60],[-74,-44],[-164,18],[-27,81],[-58,-47],[20,-106],[-76,-188],[-49,-203],[-70,-64]],[[57842,77455],[-50,270],[30,252],[-9,259],[-160,352],[-89,249],[-86,175],[-84,58]],[[23016,65864],[-107,-518],[-49,-426],[-20,-791],[-27,-289],[48,-322],[86,-288],[56,-458],[184,-440],[65,-337],[109,-291],[295,-157],[114,-247],[244,165],[212,60],[208,106],[175,101],[176,241],[67,345],[22,496],[48,173],[188,155],[294,137],[246,-21],[169,50],[66,-125],[-9,-285],[-149,-351],[-66,-360],[51,-103],[-42,-255],[-69,-461],[-71,152],[-58,-10]],[[24067,59806],[-144,191],[-226,155]],[[19641,66203],[-142,138],[-164,287]],[[18570,68996],[-201,234],[-93,-25]],[[19362,64423],[-181,337],[-201,286]],[[17464,69802],[316,46],[353,64],[-26,-116],[419,-287],[634,-416],[552,4],[221,0],[0,244],[481,0],[102,-210],[142,-186],[165,-260],[92,-309],[69,-325],[144,-178],[230,-177],[175,467],[227,11],[196,-236],[139,-404],[96,-346],[164,-337],[61,-414],[78,-277],[217,-184],[197,-130],[108,18]],[[55993,75539],[95,35],[128,9]],[[46619,59216],[93,107],[47,348],[88,14],[194,-165],[157,117],[107,-39],[42,131],[1114,9],[62,414],[-48,73],[-134,2550],[-134,2550],[425,10]],[[51185,61897],[1,-1361],[-152,-394],[-24,-364],[-247,-94],[-379,-51],[-102,-210],[-178,-23]],[[46801,57931],[13,184],[-24,229],[-104,166],[-54,338],[-13,368]],[[77809,62643],[-159,-137],[-162,-256],[-196,-26],[-127,-639],[-117,-107],[134,-519],[177,-431],[113,-390],[-101,-514],[-96,-109],[66,-296],[185,-470],[32,-330],[-4,-274],[108,-539],[-152,-551],[-135,-607]],[[55338,76294],[74,-101],[40,-82],[91,-63],[106,-123],[-22,-51]],[[55380,75322],[-58,46]],[[74375,79706],[292,102],[530,509],[423,278],[242,-182],[289,-8],[186,-276],[277,-22],[402,-148],[270,411],[-113,348],[288,612],[311,-244],[252,-69],[327,-152],[53,-443],[394,-248],[263,109],[351,78],[279,-78],[272,-284],[168,-302],[258,6],[350,-96],[255,146],[366,98],[407,416],[166,-63],[146,-198],[331,49]],[[59599,43773],[209,48],[334,-166],[73,74],[193,16],[99,177],[167,-10],[303,230],[221,342]],[[59870,36949],[-45,-275],[65,-101]],[[59890,36573],[-41,-245],[-116,-211]],[[59119,34780],[-211,5]],[[58908,34785],[-24,261],[-41,265]],[[58843,35311],[-23,212],[49,659],[-72,419],[-133,832]],[[58664,37433],[292,671],[74,426],[42,53],[31,348],[-45,175],[12,442],[54,409],[0,748],[-145,190],[-132,43],[-60,146],[-128,125],[-232,-12],[-18,220]],[[58409,41417],[-26,421],[843,487]],[[59226,42325],[159,-284],[77,54],[110,-149],[16,-237],[-59,-274],[21,-417],[181,-365],[85,410],[120,124],[-24,760],[-116,427],[-100,191],[-97,-9],[-77,768],[77,449]],[[46619,59216],[-184,405],[-168,435],[-184,157],[-133,173],[-155,-6],[-135,-129],[-138,51],[-96,-189]],[[45260,62987],[60,197],[1088,-4],[-53,853],[68,304],[261,53],[-9,1512],[911,-31],[1,895]],[[59226,42325],[-147,153],[85,549],[87,205],[-53,490],[56,479],[47,160],[-71,501],[-131,264]],[[59099,45126],[273,-110],[55,-164],[95,-275],[77,-804]],[[77801,54399],[48,105],[227,-258],[22,-304],[183,71],[91,243]],[[56448,40227],[228,134],[180,-34],[109,-133],[2,-49]],[[55526,35946],[0,-2182],[-248,-302],[-149,-43],[-175,112],[-125,43],[-47,252],[-109,162],[-133,-292]],[[54125,64088],[68,-919],[104,-153],[4,-188],[116,-203],[-60,-254],[-107,-1199],[-15,-769],[-354,-557],[-120,-778],[115,-219],[0,-380],[178,-13],[-28,-279]],[[53939,57955],[-52,-13],[-188,647],[-65,24],[-217,-331],[-215,173],[-150,34],[-80,-83],[-163,18],[-164,-252],[-141,-14],[-337,305],[-131,-145],[-142,10],[-104,223],[-279,221],[-298,-70],[-72,-128],[-39,-340],[-80,-238],[-19,-527]],[[52072,53186],[-105,31],[-107,-132]],[[51398,53895],[-197,389],[-209,-7]],[[25647,58207],[31,91],[46,-88]],[[51063,81078],[244,869],[380,248]],[[58639,91676],[-473,-237],[-224,-54]],[[55734,91409],[-172,-24],[-41,-389],[-523,95],[-74,-329],[-267,2],[-183,-421],[-278,-655],[-431,-831],[101,-202],[-97,-234],[-275,10],[-180,-554],[17,-784],[177,-300],[-92,-694],[-231,-405],[-122,-341]],[[52328,85032],[-371,-138],[-384,301]],[[51474,85830],[-88,1363],[256,381]],[[65352,60997],[1,-238],[-134,-165]],[[64880,60451],[-128,-34]],[[64752,60417],[-91,413],[-217,975]],[[64444,61805],[833,591],[185,1182],[-127,418]],[[65945,64688],[203,-78],[165,-107]],[[68734,64727],[-83,424],[-215,450]],[[28212,55535],[-52,196],[-138,164]],[[27170,56020],[-6,-126],[111,-26]],[[55461,82736],[342,-67],[511,9]],[[56535,81053],[139,-515],[-29,-166],[-138,-69],[-252,-491],[71,-266],[-60,35]],[[56266,79581],[-264,227],[-200,-84],[-131,61],[-165,-127],[-140,210],[-114,-81],[-16,36]],[[86221,75560],[-120,-200],[-83,-202]],[[85048,72883],[-135,112],[-34,-111]],[[84641,73095],[76,260],[66,69]],[[84829,73851],[-18,96],[-163,65]],[[86288,75628],[39,-104]],[[64246,66009],[84,-185],[5,-346]],[[64274,65130],[-77,-42],[-84,117]],[[56308,78869],[120,127],[172,-65],[178,-3],[129,-144],[95,91],[205,56],[69,139],[118,0]],[[57842,77455],[124,-109],[131,95],[126,-101]],[[56293,76715],[-51,103],[65,99],[-69,74],[-87,-133],[-162,172],[-22,244],[-169,139],[-31,188],[-151,232]],[[81143,94175],[251,112],[141,-379]],[[84237,94144],[590,-104],[443,4]],[[97224,91732],[123,262],[886,-165]],[[96192,85904],[-126,219],[-268,-253]],[[95032,82989],[-486,-302],[-96,-674]],[[93543,84472],[14,276],[432,132]],[[95182,86999],[-705,-649],[-227,727]],[[90412,85637],[-914,-175],[-899,-1153]],[[88378,82339],[178,295],[305,-38]],[[89262,81946],[9,-503],[-217,-590]],[[60617,78409],[9,262],[143,165],[269,43],[44,197],[-62,326],[113,310],[-3,173],[-410,192],[-162,-6],[-172,277],[-213,-94],[-352,208],[6,116],[-99,256],[-222,29],[-23,183],[70,120],[-178,334],[-288,-57],[-84,30],[-70,-134],[-104,23]],[[58639,91676],[286,206],[456,-358],[761,-140],[1050,-668],[213,-281],[18,-393],[-308,-311],[-454,-157],[-1240,449],[-204,-75],[453,-433]],[[59670,89515],[18,-274],[18,-604]],[[59706,88637],[358,-180],[217,-153],[36,286]],[[60317,88590],[-168,254],[177,224]],[[60998,88700],[233,144],[-186,433]],[[62654,90499],[1,-328],[219,-203]],[[63371,90473],[580,282],[970,507]],[[69038,93080],[183,537],[206,116]],[[69427,93733],[736,-156],[57,-328]],[[70444,91717],[222,593],[-361,482]],[[72363,94093],[483,119],[669,-26]],[[58449,49909],[110,-333],[-16,-348],[-80,-74]],[[58216,49787],[67,-60],[166,182]],[[61883,60238],[-37,252],[-83,178]],[[60335,65400],[-77,306],[-81,132]],[[63741,66597],[190,-249],[16,-243]],[[64444,61805],[-801,-226],[-259,-266],[-199,-620],[-130,-99],[-70,197],[-106,-30],[-269,60],[-50,59],[-321,-14],[-75,-53],[-114,153],[-74,-290],[28,-249],[-121,-189]],[[56351,57163],[3,143],[-102,174],[-3,343],[-58,228],[-98,-34],[28,217],[72,246],[-32,245],[92,181],[-58,138],[73,365],[127,435],[240,-41],[-14,2345]],[[59433,56242],[1,-71]],[[59434,56171],[-39,12],[5,294],[-33,203],[-143,233],[-34,426],[34,436],[-129,41],[-19,-132],[-167,-30],[67,-173],[23,-355],[-152,-324],[-138,-426],[-144,-61],[-233,345],[-105,-122],[-29,-172],[-143,-112],[-9,-122],[-277,0],[-38,122],[-200,20],[-100,-101],[-77,51],[-143,344],[-48,163],[-200,-81],[-76,-274],[-72,-528],[-95,-111],[-85,-65]],[[56635,55672],[-23,28]],[[59445,53091],[-171,-272],[-195,1],[-224,-138],[-176,132],[-115,-161]],[[56824,55442],[-189,230]],[[59434,56171],[3,-460]],[[25613,58487],[-31,-139]],[[62075,57243],[54,-245],[125,-247]],[[63596,57321],[-2,-9],[-1,-244],[0,-596],[0,-308],[-125,-363],[-194,-493]],[[34889,53069],[109,-351],[-49,-254],[-24,-270],[-71,-248]],[[56266,79581],[-77,-154],[-55,-238]],[[58908,34785],[-56,-263],[-163,-63],[-166,320],[-2,204],[76,222],[26,172],[80,42],[140,-108]],[[60041,71744],[74,129],[75,130],[15,329],[91,-115],[306,165],[147,-112],[229,2],[320,222],[149,-10],[316,92]],[[68841,72526],[156,598],[-60,440],[-204,140],[72,261],[232,-28],[132,326],[89,380],[371,137],[-58,-274],[40,-164],[114,15]],[[65546,74986],[313,8],[-45,297],[237,204],[234,343],[374,-312],[30,-471],[106,-121],[301,27],[93,-108],[137,-609],[317,-408],[181,-278],[291,-289],[369,-253],[-7,-362]],[[53083,72381],[-139,-290],[-2,-273]],[[58441,72005],[-192,-70],[-268,314]],[[57981,72249],[-303,-11],[-165,588]],[[59768,75418],[485,-417],[399,-228]],[[57321,74302],[-87,276],[3,121]],[[59099,45126],[-157,177],[-177,100],[-111,99],[-116,150]],[[58388,46397],[-161,331],[-55,342]],[[58449,49909],[98,71],[304,-7],[566,45]],[[30523,76389],[-147,-351],[-47,-133]],[[30377,75084],[-133,11],[-205,-103]],[[29172,73738],[-61,30],[-91,148]],[[29077,73598],[69,-105],[5,-223]],[[28966,72994],[-142,225],[-33,491]],[[28797,73080],[-183,93],[191,-191]],[[27672,65472],[-83,-75],[-137,72]],[[27408,65728],[-105,136],[-148,508]],[[26747,68267],[-108,90],[-281,-268]],[[26309,68119],[-135,275],[-174,147]],[[25227,68491],[-114,-92],[50,-157]],[[24755,67801],[-207,312],[-242,-73]],[[16564,70932],[-71,95],[-33,324]],[[16460,71351],[-270,594],[-231,821],[10,137],[-123,195],[-215,495],[-38,482],[-148,323],[61,489],[-10,507],[-89,453],[109,557]],[[15516,76404],[34,536],[33,536]],[[15583,77476],[-50,792],[-88,506],[-80,274],[33,115],[402,-200],[148,-558]],[[15948,78405],[69,156],[-45,484],[-94,485]],[[10396,86079],[-385,-51],[-546,272]],[[8164,85656],[-308,-126],[-39,348]],[[7158,84934],[-299,-248],[-278,-180]],[[4985,85596],[50,216],[-179,211]],[[4541,89915],[-38,-296],[586,23]],[[4864,90008],[-342,225],[-197,296]],[[30102,56752],[-123,-344],[105,-468]],[[31762,56607],[213,-74],[155,185]],[[63521,58853],[-122,-33],[-83,35]],[[63153,58610],[-177,-114],[-233,-30]],[[62539,58233],[-43,-150],[-137,13]],[[64752,60417],[-201,-158]],[[57838,31217],[-210,-269],[-290,-229]],[[58175,37528],[113,-7],[134,-100],[94,71],[148,-59]],[[58409,41417],[-210,-81],[-159,-235],[-33,-205],[-100,-46],[-241,-486],[-154,-383],[-94,-13],[-90,68],[-311,65]]]} \ No newline at end of file diff --git a/examples/video/The-Audience-Is-Programming.mp4 b/examples/video/The-Audience-Is-Programming.mp4 deleted file mode 100644 index 3bdd3889d4f4..000000000000 Binary files a/examples/video/The-Audience-Is-Programming.mp4 and /dev/null differ diff --git a/examples/viewer-integr-messaging.js b/examples/viewer-integr-messaging.js index d4a70488cb02..f0fa1eb3f4aa 100644 --- a/examples/viewer-integr-messaging.js +++ b/examples/viewer-integr-messaging.js @@ -15,6 +15,16 @@ */ +/** + * @enum {string} + */ +var MessageType = { + REQUEST: 'q', + RESPONSE: 's', +}; + +var APP = '__AMPHTML__'; + /** * This is a very simple messaging protocol between viewer and viewer client. * @param {!Window} target @@ -25,20 +35,16 @@ * @constructor */ function ViewerMessaging(target, targetOrigin, requestProcessor, opt_targetId) { - this.sentinel_ = '__AMP__'; - this.requestSentinel_ = this.sentinel_ + 'REQUEST'; - this.responseSentinel_ = this.sentinel_ + 'RESPONSE'; - this.requestIdCounter_ = 0; this.waitingForResponse_ = {}; - /** @const @private {!Widnow} */ + /** @private {!Widnow} */ this.target_ = target; - /** @const @private {string|undefined} */ + /** @private {string|undefined} */ this.targetId_ = opt_targetId; - /** @const @private {string} */ + /** @private {string} */ this.targetOrigin_ = targetOrigin; - /** @const @private {function(string, *, boolean):(!Promise<*>|undefined)} */ + /** @private {function(string, *, boolean):(!Promise<*>|undefined)} */ this.requestProcessor_ = requestProcessor; if (!this.targetOrigin_) { @@ -62,12 +68,25 @@ ViewerMessaging.prototype.sendRequest = function(eventType, payload, var promise = new Promise(function(resolve, reject) { this.waitingForResponse_[requestId] = {resolve, reject}; }.bind(this)); - this.sendMessage_(this.requestSentinel_, requestId, eventType, payload, - true); + var message = { + app: APP, + requestid: requestId, + rsvp: true, + name: eventType, + data: payload, + type: MessageType.REQUEST, + }; + this.sendMessage_(message); return promise; } - this.sendMessage_(this.requestSentinel_, requestId, eventType, payload, - false); + var message = { + app: APP, + requestid: requestId, + name: eventType, + data: payload, + type: MessageType.REQUEST, + }; + this.sendMessage_(message); return undefined; }; @@ -81,9 +100,10 @@ ViewerMessaging.prototype.onMessage_ = function(event) { return; } var message = event.data; - if (message.sentinel == this.requestSentinel_) { + if (message.type == MessageType.REQUEST) { this.onRequest_(message); - } else if (message.sentinel == this.responseSentinel_) { + } + if (message.type == MessageType.RESPONSE) { this.onResponse_(message); } }; @@ -94,13 +114,13 @@ ViewerMessaging.prototype.onMessage_ = function(event) { * @private */ ViewerMessaging.prototype.onRequest_ = function(message) { - var requestId = message.requestId; - var promise = this.requestProcessor_(message.type, message.payload, + var requestId = message.requestid; + var promise = this.requestProcessor_(message.name, message.data, message.rsvp); if (message.rsvp) { if (!promise) { this.sendResponseError_(requestId, 'no response'); - throw new Error('expected response but none given: ' + message.type); + throw new Error('expected response but none given: ' + message.name); } promise.then(function(payload) { this.sendResponse_(requestId, payload); @@ -116,36 +136,24 @@ ViewerMessaging.prototype.onRequest_ = function(message) { * @private */ ViewerMessaging.prototype.onResponse_ = function(message) { - var requestId = message.requestId; + var requestId = message.requestid; var pending = this.waitingForResponse_[requestId]; if (pending) { delete this.waitingForResponse_[requestId]; - if (message.type == 'ERROR') { - pending.reject(message.payload); + if (message.error) { + pending.reject(message.error); } else { - pending.resolve(message.payload); + pending.resolve(message.data); } } }; /** - * @param {string} sentinel - * @param {string} requestId - * @param {string} eventType - * @param {*} payload - * @param {boolean} awaitResponse + * @param {*} message * @private */ -ViewerMessaging.prototype.sendMessage_ = function(sentinel, requestId, - eventType, payload, awaitResponse) { - var message = { - sentinel, - requestId, - type: eventType, - payload, - rsvp: awaitResponse - }; +ViewerMessaging.prototype.sendMessage_ = function(message) { this.target_./*OK*/postMessage(message, this.targetOrigin_); }; @@ -156,7 +164,13 @@ ViewerMessaging.prototype.sendMessage_ = function(sentinel, requestId, * @private */ ViewerMessaging.prototype.sendResponse_ = function(requestId, payload) { - this.sendMessage_(this.responseSentinel_, requestId, null, payload, false); + var message = { + app: APP, + requestid: requestId, + data: payload, + type: MessageType.RESPONSE, + }; + this.sendMessage_(message); }; @@ -166,7 +180,13 @@ ViewerMessaging.prototype.sendResponse_ = function(requestId, payload) { * @private */ ViewerMessaging.prototype.sendResponseError_ = function(requestId, reason) { - this.sendMessage_(this.responseSentinel_, requestId, 'ERROR', reason, false); + var message = { + app: APP, + requestid: requestId, + error: reason, + type: MessageType.RESPONSE, + }; + this.sendMessage_(message); }; diff --git a/examples/viewer-integr.js b/examples/viewer-integr.js index edb900478ba1..191eef1f9654 100644 --- a/examples/viewer-integr.js +++ b/examples/viewer-integr.js @@ -43,7 +43,7 @@ function whenMessagingLoaded(callback) { if (window.parent && window.parent != window) { var handshakePromise = new Promise(function(resolve) { - var unconfirmedViewerOrigin = viewer.getParam('viewerorigin'); + var unconfirmedViewerOrigin = viewer.getParam('origin'); if (!unconfirmedViewerOrigin) { throw new Error('Expected viewer origin must be specified!'); } @@ -58,7 +58,7 @@ function whenMessagingLoaded(callback) { }; window.addEventListener('message', listener, false); - window.parent./*OK*/postMessage('amp-handshake-request', + window.parent./*OK*/postMessage('channelOpen', unconfirmedViewerOrigin); }); diff --git a/examples/viewer.html b/examples/viewer.html index 81cda95688d0..55be75d8d512 100644 --- a/examples/viewer.html +++ b/examples/viewer.html @@ -85,10 +85,16 @@ bottom: 0; overflow-x: hidden; visibility: hidden; + -webkit-transform: translateX(-100%); + -ms-transform: translateX(-100%); + transform: translateX(-100%); } viewer[data-show-container="1"] #container1 { visibility: visible; + -webkit-transform: none; + -ms-transform: none; + transform: none; } viewer[data-show-container="1"] #show-container1-anchor { @@ -97,6 +103,9 @@ viewer[data-show-container="2"] #container2 { visibility: visible; + -webkit-transform: none; + -ms-transform: none; + transform: none; } viewer[data-show-container="2"] #show-container2-anchor { @@ -105,12 +114,37 @@ viewer[data-show-container="3"] #container3 { visibility: visible; + -webkit-transform: none; + -ms-transform: none; + transform: none; } viewer[data-show-container="3"] #show-container3-anchor { text-decoration: underline; } + viewer[data-show-container="4"] #container4 { + visibility: visible; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } + + viewer[data-show-container="4"] #show-container4-anchor { + text-decoration: underline; + } + + viewer[data-show-container="5"] #container5 { + visibility: visible; + -webkit-transform: none; + -ms-transform: none; + transform: none; + } + + viewer[data-show-container="5"] #show-container5-anchor { + text-decoration: underline; + } + viewer.natural container { overflow-y: hidden; } @@ -130,10 +164,8 @@ + + + + + + +

    Vega Visualization

    + +

    Responsive size with remote data

    + + + +

    Responsive size with inline data

    + + + + + +

    fixed-height size world map with remote data pointing to remote data using topojson

    + + + + + +

    Responsive size interactive graph with remote data

    + + + +

    Responsive size inside lightbox

    + + + + + + + + diff --git a/examples/vrview.amp.html b/examples/vrview.amp.html new file mode 100644 index 000000000000..ea1f7c65f0ca --- /dev/null +++ b/examples/vrview.amp.html @@ -0,0 +1,41 @@ + + + + + AMP #0 + + + + + + + + + + + + diff --git a/examples/youtube.amp.html b/examples/youtube.amp.html index ac4db1dc39b0..dadd3ec3b21a 100644 --- a/examples/youtube.amp.html +++ b/examples/youtube.amp.html @@ -17,15 +17,27 @@ - + width="480" height="270"> + + + + + + + `: ```html ``` -Current list of extended components: +Current list of extended components by category: + +- [Access](#access) +- [Ads](#ads) +- [Analytics](#analytics) +- [Audio/Video](#audiovideo) +- [Dynamic lists](#dynamic-lists) +- [Forms](#forms) +- [Frames](#frames) +- [Presentation](#presentation) +- [Scripts](#scripts) +- [Social](#social) + +### Access | Component | Description | | --------- | ----------- | | [`amp-access`](amp-access/amp-access.md) | Provides AMP paywall and subscription support. | -| [`amp-accordion`](amp-accordion/amp-accordion.md) | Provides a way for viewers to have a glance at the outline of the content and jump to a section of their choice at will. | + +### Ads + +| Component | Description | +| --------- | ----------- | +| [`amp-ad`](amp-ad/amp-ad.md) | Container to display an ad. | +| [`amp-embed`](amp-ad/amp-embed.md) | An alias to the `amp-ad` tag. | + +### Analytics + +| Component | Description | +| --------- | ----------- | | [`amp-analytics`](amp-analytics/amp-analytics.md) | Captures analytics data from an AMP document. | -| [`amp-anim`](amp-anim/amp-anim.md) | Manages an animated image, typically a GIF. | +| [`amp-experiment`](amp-experiment/amp-experiment.md) | Conducts user experience experiments on an AMP document. | + +### Audio/Video + +| Component | Description | +| --------- | ----------- | | [`amp-audio`](amp-audio/amp-audio.md) | Replaces the HTML5 `audio` tag. | +| [`amp-o2-player`](amp-o2-player/amp-o2-player.md) | Displays a AOL O2Player. | | [`amp-brid-player`](amp-brid-player/amp-brid-player.md) | Displays a Brid.tv player. | | [`amp-brightcove`](amp-brightcove/amp-brightcove.md) | Displays a Brightcove Video Cloud or Perform player. | -| [`amp-carousel`](amp-carousel/amp-carousel.md) | Displays multiple similar pieces of content along a horizontal axis. | | [`amp-dailymotion`](amp-dailymotion/amp-dailymotion.md) | Displays a [Dailymotion](https://www.dailymotion.com) video. | -| [`amp-dynamic-css-classes`](amp-dynamic-css-classes/amp-dynamic-css-classes.md) | Adds several dynamic CSS class names onto the HTML element. | -| [`amp-facebook`](amp-facebook/amp-facebook.md) | Displays a Facebook post or video. | -| [`amp-fit-text`](amp-fit-text/amp-fit-text.md) | Expands or shrinks font size to fit the content within the space given. | -| [`amp-font`](amp-font/amp-font.md) | Triggers and monitors the loading of custom fonts. | -| [`amp-fx-flying-carpet`](amp-fx-flying-carpet/amp-fx-flying-carpet.md) | Wraps its children in a unique full-screen scrolling container allowing you to display a full-screen ad without taking up the entire viewport. | -| [`amp-iframe`](amp-iframe/amp-iframe.md) | Displays an iframe. | -| [`amp-image-lightbox`](amp-image-lightbox/amp-image-lightbox.md) | Allows for an “image lightbox” or similar experience. | -| [`amp-instagram`](amp-instagram/amp-instagram.md) | Displays an Instagram embed. | -| [`amp-install-serviceworker`](amp-install-serviceworker/amp-install-serviceworker.md) | Installs a ServiceWorker. | +| [`amp-gfycat`](amp-gfycat/amp-gfycat.md) | Displays a [Gfycat](https://gfycat.com) video GIF. | | [`amp-jwplayer`](amp-jwplayer/amp-jwplayer.md) | Displays a cloud-hosted [JW Player](https://www.jwplayer.com/). | | [`amp-kaltura-player`](amp-kaltura-player/amp-kaltura-player.md) | Displays the Kaltura Player as used in [Kaltura's Video Platform](https://corp.kaltura.com/). | -| [`amp-lightbox`](amp-lightbox/amp-lightbox.md) | Allows for a “lightbox” or similar experience. | -| [`amp-list`](amp-list/amp-list.md) | Dynamically downloads data and creates list items using a template. | -| [`amp-mustache`](amp-mustache/amp-mustache.md) | Allows rendering of [`Mustache.js`](https://github.com/janl/mustache.js/) templates. | -| [`amp-pinterest`](amp-pinterest/amp-pinterest.md) | Displays a Pinterest widget or Pin It button. | +| [`amp-ooyala-player`](amp-ooyala-player/amp-ooyala-player.md) | Displays an [`Ooyala`](http://ooyala.com) player. | | [`amp-reach-player`](amp-reach-player/amp-reach-player.md) | Displays a [Beachfront Reach](https://beachfrontreach.com/) video player. | -| [`amp-sidebar`](amp-sidebar/amp-sidebar.md) | Provides a way to display meta content intended for temporary access such as navigation, links, buttons, menus. | -| [`amp-social-share`](amp-social-share/amp-social-share.md) | Displays a social share button. | | [`amp-soundcloud`](amp-soundcloud/amp-soundcloud.md) | Displays a [Soundcloud](https://soundcloud.com/) clip. | | [`amp-springboard-player`](amp-springboard-player/amp-springboard-player.md) | Displays a [Springboard Platform](http://publishers.springboardplatform.com/users/login) video player | -| [`amp-twitter`](amp-twitter/amp-twitter.md) | Displays a Twitter tweet. | -| [`amp-user-notification`](amp-user-notification/amp-user-notification.md) | Displays a dismissable notification to the user. | | [`amp-vimeo`](amp-vimeo/amp-vimeo.md) | Displays a Vimeo video. | | [`amp-vine`](amp-vine/amp-vine.md) | Displays a Vine simple embed. | | [`amp-youtube`](amp-youtube/amp-youtube.md) | Displays a YouTube video. | +### Dynamic lists -## AMP HTML Extended Templates +| Component | Description | +| --------- | ----------- | +| [`amp-list`](amp-list/amp-list.md) | Dynamically downloads data and creates list items using a template. | +| [`amp-live-list`](amp-live-list/amp-live-list.md) | Provides a way to display and update content live. | -NOT LAUNCHED YET +### Forms -Extended templates must be explicitly included into the document as custom templates. +| Component | Description | +| --------- | ----------- | +| [`amp-form`](amp-form/amp-form.md) | Provides form support. | -For example, to include an amp-mustache template in your page -include the following script in the ``: +### Frames -```html - -``` +| Component | Description | +| --------- | ----------- | +| [`amp-iframe`](amp-iframe/amp-iframe.md) | Displays an iframe. | + +### Presentation -Current list of extended templates: +| Component | Description | +| --------- | ----------- | +| [`amp-accordion`](amp-accordion/amp-accordion.md) | Provides a way for viewers to have a glance at the outline of the content and jump to a section of their choice at will. | +| [`amp-anim`](amp-anim/amp-anim.md) | Manages an animated image, typically a GIF. | +| [`amp-carousel`](amp-carousel/amp-carousel.md) | Displays multiple similar pieces of content along a horizontal axis. | +| [`amp-dynamic-css-classes`](amp-dynamic-css-classes/amp-dynamic-css-classes.md) | Adds several dynamic CSS class names onto the HTML element. | +| [`amp-fit-text`](amp-fit-text/amp-fit-text.md) | Expands or shrinks font size to fit the content within the space given. | +| [`amp-font`](amp-font/amp-font.md) | Triggers and monitors the loading of custom fonts. | +| [`amp-fx-flying-carpet`](amp-fx-flying-carpet/amp-fx-flying-carpet.md) | Wraps its children in a unique full-screen scrolling container allowing you to display a full-screen ad without taking up the entire viewport. | +| [`amp-image-lightbox`](amp-image-lightbox/amp-image-lightbox.md) | Allows for an “image lightbox” or similar experience. | +| [`amp-lightbox`](amp-lightbox/amp-lightbox.md) | Allows for a “lightbox” or similar experience. | +| [`amp-mustache`](amp-mustache/amp-mustache.md) | Allows rendering of [`Mustache.js`](https://github.com/janl/mustache.js/) templates. | +| [`amp-sidebar`](amp-sidebar/amp-sidebar.md) | Provides a way to display meta content intended for temporary access such as navigation, links, buttons, menus. | +| [`amp-sticky-ad`](amp-sticky-ad/amp-sticky-ad.md) | Provides a way to display and stick ad content at the bottom of the page.| +| [`amp-user-notification`](amp-user-notification/amp-user-notification.md) | Displays a dismissable notification to the user. | + +### Scripts + +| Component | Description | +| --------- | ----------- | +| [`amp-install-serviceworker`](amp-install-serviceworker/amp-install-serviceworker.md) | Installs a ServiceWorker. | + +### Social + +| Component | Description | +| --------- | ----------- | +| [`amp-facebook`](amp-facebook/amp-facebook.md) | Displays a Facebook post or video. | +| [`amp-gfycat`](amp-gfycat/amp-gfycat.md) | Displays a [Gfycat](https://gfycat.com) video GIF. | +| [`amp-instagram`](amp-instagram/amp-instagram.md) | Displays an Instagram embed. | +| [`amp-pinterest`](amp-pinterest/amp-pinterest.md) | Displays a Pinterest widget or Pin It button. | +| [`amp-reddit`](amp-reddit/amp-reddit.md) | Displays a Reddit post or comment. | +| [`amp-social-share`](amp-social-share/amp-social-share.md) | Displays a social share button. | +| [`amp-twitter`](amp-twitter/amp-twitter.md) | Displays a Twitter tweet. | +| [`amp-vine`](amp-vine/amp-vine.md) | Displays a Vine simple embed. | + + +## AMP HTML Extended Templates -| Component | Description | -| --------------------------------------------- | ------------------------------------------------------------------------------------------- -| [`amp-mustache`](amp-mustache/amp-mustache.md) | Mustache template. | +See the [AMP template spec](../spec/amp-html-templates.md) for details about supported templates. diff --git a/extensions/amp-a4a/0.1/a4a-variable-source.js b/extensions/amp-a4a/0.1/a4a-variable-source.js new file mode 100644 index 000000000000..939781bde4c9 --- /dev/null +++ b/extensions/amp-a4a/0.1/a4a-variable-source.js @@ -0,0 +1,118 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {urlReplacementsForDoc} from '../../../src/url-replacements'; +import { + VariableSource, + getNavigationData, + getTimingDataSync, + getTimingDataAsync, +} from '../../../src/service/variable-source'; +import {user} from '../../../src/log'; + + +const WHITELISTED_VARIABLES = [ + 'RANDOM', + 'COUNTER', + 'CANONICAL_URL', + 'CANONICAL_HOST', + 'CANONICAL_HOSTNAME', + 'CANONICAL_PATH', + 'DOCUMENT_REFERRER', + 'TITLE', + 'AMPDOC_URL', + 'AMPDOC_HOST', + 'AMPDOC_HOSTNAME', + 'SOURCE_URL', + 'SOURCE_HOST', + 'SOURCE_HOSTNAME', + 'SOURCE_PATH', + 'PAGE_VIEW_ID', + 'CLIENT_ID', + 'VARIANT', + 'VARIANTS', + 'SHARE_TRACKING_INCOMING', + 'SHARE_TRACKING_OUTGOING', + 'TIMESTAMP', + 'TIMEZONE', + 'SCROLL_TOP', + 'SCROLL_LEFT', + 'SCROLL_HEIGHT', + 'SCROLL_WIDTH', + 'VIEWPORT_HEIGHT', + 'VIEWPORT_WIDTH', + 'SCREEN_WIDTH', + 'SCREEN_HEIGHT', + 'AVAILABLE_SCREEN_HEIGHT', + 'AVAILABLE_SCREEN_WIDTH', + 'SCREEN_COLOR_DEPTH', + 'DOCUMENT_CHARSET', + 'BROWSER_LANGUAGE', + 'VIEWER', + 'TOTAL_ENGAGED_TIME', + 'AMP_VERSION', +]; + + +/** Provides A4A specific variable substitution. */ +export class A4AVariableSource extends VariableSource { + /** + * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc + * @param {!Window} embedWin + */ + constructor(ampdoc, embedWin) { + super(); + /** @private {VariableSource} global variable source for fallback. */ + this.globalVariableSource_ = urlReplacementsForDoc(ampdoc) + .getVariableSource(); + + /** @private {!Window} */ + this.win_ = embedWin; + } + + /** @override */ + initialize() { + this.set('AD_NAV_TIMING', (startAttribute, endAttribute) => { + user().assert(startAttribute, 'The first argument to AD_NAV_TIMING, the' + + ' start attribute name, is required'); + return getTimingDataSync( + this.win_, + /**@type {string}*/(startAttribute), + /**@type {string}*/(endAttribute)); + }).setAsync('AD_NAV_TIMING', (startAttribute, endAttribute) => { + user().assert(startAttribute, 'The first argument to AD_NAV_TIMING, the' + + ' start attribute name, is required'); + return getTimingDataAsync( + this.win_, + /**@type {string}*/(startAttribute), + /**@type {string}*/(endAttribute)); + }); + + this.set('AD_NAV_TYPE', () => { + return getNavigationData(this.win_, 'type'); + }); + + this.set('AD_NAV_REDIRECT_COUNT', () => { + return getNavigationData(this.win_, 'redirectCount'); + }); + + for (let v = 0; v < WHITELISTED_VARIABLES.length; v++) { + const varName = WHITELISTED_VARIABLES[v]; + const resolvers = this.globalVariableSource_.get(varName); + this.set(varName, resolvers.sync).setAsync(varName, resolvers.async); + } + } +} diff --git a/extensions/amp-a4a/0.1/amp-a4a.js b/extensions/amp-a4a/0.1/amp-a4a.js new file mode 100644 index 000000000000..6fc8f60cb376 --- /dev/null +++ b/extensions/amp-a4a/0.1/amp-a4a.js @@ -0,0 +1,1410 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + is3pThrottled, + getAmpAdRenderOutsideViewport, + incrementLoadingAds, +} from '../../amp-ad/0.1/concurrent-load'; +import {adConfig} from '../../../ads/_config'; +import {signingServerURLs} from '../../../ads/_a4a-config'; +import { + closestByTag, + removeChildren, + createElementWithAttributes, +} from '../../../src/dom'; +import {cancellation} from '../../../src/error'; +import { + installFriendlyIframeEmbed, + setFriendlyIframeEmbedVisible, +} from '../../../src/friendly-iframe-embed'; +import {isLayoutSizeDefined} from '../../../src/layout'; +import {isAdPositionAllowed} from '../../../src/ad-helper'; +import {dev, user} from '../../../src/log'; +import {getMode} from '../../../src/mode'; +import {isArray, isObject, isEnumValue} from '../../../src/types'; +import {urlReplacementsForDoc} from '../../../src/url-replacements'; +import {some} from '../../../src/utils/promise'; +import {utf8Decode} from '../../../src/utils/bytes'; +import {viewerForDoc} from '../../../src/viewer'; +import {xhrFor} from '../../../src/xhr'; +import {endsWith} from '../../../src/string'; +import {platformFor} from '../../../src/platform'; +import {cryptoFor} from '../../../src/crypto'; +import {isExperimentOn} from '../../../src/experiments'; +import {setStyle} from '../../../src/style'; +import {handleClick} from '../../../ads/alp/handler'; +import {AdDisplayState} from '../../../extensions/amp-ad/0.1/amp-ad-ui'; +import { + getDefaultBootstrapBaseUrl, + generateSentinel, +} from '../../../src/3p-frame'; +import {installUrlReplacementsForEmbed,} + from '../../../src/service/url-replacements-impl'; +import {extensionsFor} from '../../../src/extensions'; +import {A4AVariableSource} from './a4a-variable-source'; +import {rethrowAsync} from '../../../src/log'; +// TODO(tdrl): Temporary. Remove when we migrate to using amp-analytics. +import {getTimingDataAsync} from '../../../src/service/variable-source'; +import {getContextMetadata} from '../../../src/iframe-attributes'; + +/** @private @const {string} */ +const ORIGINAL_HREF_ATTRIBUTE = 'data-a4a-orig-href'; + +/** @type {string} */ +const METADATA_STRING = ''); + if (metadataEnd < 0) { + // Couldn't find a metadata blob. + dev().warn(TAG, this.element.getAttribute('type'), + 'Could not locate closing script tag for amp meta data in: %s', + creative); + return null; + } + try { + const metaDataObj = JSON.parse( + creative.slice(metadataStart + metadataString.length, metadataEnd)); + const ampRuntimeUtf16CharOffsets = + metaDataObj['ampRuntimeUtf16CharOffsets']; + if (!isArray(ampRuntimeUtf16CharOffsets) || + ampRuntimeUtf16CharOffsets.length != 2 || + typeof ampRuntimeUtf16CharOffsets[0] !== 'number' || + typeof ampRuntimeUtf16CharOffsets[1] !== 'number') { + throw new Error('Invalid runtime offsets'); + } + const metaData = {}; + if (metaDataObj['customElementExtensions']) { + metaData.customElementExtensions = + metaDataObj['customElementExtensions']; + if (!isArray(metaData.customElementExtensions)) { + throw new Error( + 'Invalid extensions', metaData.customElementExtensions); + } + } else { + metaData.customElementExtensions = []; + } + if (metaDataObj['customStylesheets']) { + // Expect array of objects with at least one key being 'href' whose + // value is URL. + metaData.customStylesheets = metaDataObj['customStylesheets']; + const errorMsg = 'Invalid custom stylesheets'; + if (!isArray(metaData.customStylesheets)) { + throw new Error(errorMsg); + } + metaData.customStylesheets.forEach(stylesheet => { + if (!isObject(stylesheet) || !stylesheet['href'] || + typeof stylesheet['href'] !== 'string' || + !/^https:\/\//i.test(stylesheet['href'])) { + throw new Error(errorMsg); + } + }); + } + // TODO(keithwrightbos): OK to assume ampRuntimeUtf16CharOffsets is before + // metadata as its in the head? + metaData.minifiedCreative = + creative.slice(0, ampRuntimeUtf16CharOffsets[0]) + + creative.slice(ampRuntimeUtf16CharOffsets[1], metadataStart) + + creative.slice(metadataEnd + ''.length); + return metaData; + } catch (err) { + dev().warn( + TAG, this.element.getAttribute('type'), 'Invalid amp metadata: %s', + creative.slice(metadataStart + METADATA_STRING.length, metadataEnd)); + return null; + } + } + + /** + * Registers a click handler for "A2A" (AMP-to-AMP navigation where the AMP + * viewer navigates to an AMP destination on our behalf. + * @param {!Window} iframeWin + */ + registerAlpHandler_(iframeWin) { + if (!isExperimentOn(this.win, 'alp-for-a4a')) { + return; + } + iframeWin.document.documentElement.addEventListener('click', event => { + handleClick(event, url => { + viewerForDoc(this.getAmpDoc()).navigateTo(url, 'a4a'); + }); + }); + } + + /** + * Registers a handler that performs URL replacement on the href + * of an ad click. + * @param {!Window} iframeWin + */ + registerExpandUrlParams_(iframeWin) { + iframeWin.document.documentElement.addEventListener('click', + this.maybeExpandUrlParams_.bind(this), /* capture */ true); + } + + /** + * Handle click on links and replace variables in the click URL. + * The function changes the actual href value and stores the + * template in the ORIGINAL_HREF_ATTRIBUTE attribute + * @param {!Event} e + */ + maybeExpandUrlParams_(e) { + const target = closestByTag(dev().assertElement(e.target), 'A'); + if (!target || !target.href) { + // Not a click on a link. + return; + } + const hrefToExpand = + target.getAttribute(ORIGINAL_HREF_ATTRIBUTE) || target.getAttribute('href'); + if (!hrefToExpand) { + return; + } + const vars = { + 'CLICK_X': () => { + return e.pageX; + }, + 'CLICK_Y': () => { + return e.pageY; + }, + }; + const newHref = urlReplacementsForDoc(this.getAmpDoc()).expandSync( + hrefToExpand, vars, undefined, /* opt_whitelist */ { + // For now we only allow to replace the click location vars + // and nothing else. + // NOTE: Addition to this whitelist requires additional review. + 'CLICK_X': true, + 'CLICK_Y': true, + }); + if (newHref != hrefToExpand) { + // Store original value so that later clicks can be processed with + // freshest values. + if (!target.getAttribute(ORIGINAL_HREF_ATTRIBUTE)) { + target.setAttribute(ORIGINAL_HREF_ATTRIBUTE, hrefToExpand); + } + target.setAttribute('href', newHref); + } + } + + /** + * Receive collapse notifications and record lifecycle events for them. + * + * @param unusedElement {!AmpElement} + * @override + */ + collapsedCallback(unusedElement) { + this.protectedEmitLifecycleEvent_('adSlotCollapsed'); + } + + /** + * To be overriden by network specific implementation. + * This function will be called for each lifecycle event as specified in the + * LIFECYCLE_STAGES enum declaration. It may additionally pass extra + * variables of the form { name: val }. It is up to the subclass what to + * do with those variables. + * + * @param {string} unusedEventName + * @param {!Object=} opt_extraVariables + */ + emitLifecycleEvent(unusedEventName, opt_extraVariables) {} +} diff --git a/extensions/amp-a4a/0.1/test/test-a4a-integration.js b/extensions/amp-a4a/0.1/test/test-a4a-integration.js new file mode 100644 index 000000000000..a11bccd44a84 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/test-a4a-integration.js @@ -0,0 +1,292 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + MockA4AImpl, + SIGNATURE_HEADER, + TEST_URL, +} from './utils'; +import {Xhr} from '../../../../src/service/xhr-impl'; +import {createIframePromise} from '../../../../testing/iframe'; +import { + data as validCSSAmp, +} from './testdata/valid_css_at_rules_amp.reserialized'; +import {installCryptoService} from '../../../../src/service/crypto-impl'; +import {installDocService} from '../../../../src/service/ampdoc-impl'; +import {FetchResponseHeaders} from '../../../../src/service/xhr-impl'; +import {adConfig} from '../../../../ads/_config'; +import {a4aRegistry} from '../../../../ads/_a4a-config'; +import {signingServerURLs} from '../../../../ads/_a4a-config'; +import { + resetScheduledElementForTesting, + upgradeOrRegisterElement, +} from '../../../../src/custom-element'; +import {utf8Encode} from '../../../../src/utils/bytes'; +import '../../../amp-ad/0.1/amp-ad-xorigin-iframe-handler'; +import {loadPromise} from '../../../../src/event-helper'; +import * as sinon from 'sinon'; + +// Integration tests for A4A. These stub out accesses to the outside world +// (e.g., XHR requests and interfaces to ad network-specific code), but +// otherwise test the complete A4A flow, without making assumptions about +// the structure of that flow. + +/** + * Checks various consistency properties on the friendly iframe created by + * A4A privileged path rendering. Note that this returns a Promise, so its + * value must be returned from any test invoking it. + * + * @param {!Element} element amp-ad element to examine. + * @param {string} srcdoc A string that must occur somewhere in the friendly + * iframe `srcdoc` attribute. + * @return {!Promise} Promise that executes assertions on friendly + * iframe contents. + */ +function expectRenderedInFriendlyIframe(element, srcdoc) { + expect(element, 'ad element').to.be.ok; + const child = element.querySelector('iframe[srcdoc]'); + expect(child, 'iframe child').to.be.ok; + expect(child.getAttribute('srcdoc')).to.contain.string(srcdoc); + return loadPromise(child).then(() => { + const childDocument = child.contentDocument.documentElement; + expect(childDocument, 'iframe doc').to.be.ok; + expect(element, 'ad tag').to.be.visible; + expect(child, 'iframe child').to.be.visible; + expect(childDocument, 'ad creative content doc').to.be.visible; + }); +} + +function expectRenderedInXDomainIframe(element, src) { + // Note: Unlike expectRenderedInXDomainIframe, this doesn't return a Promise + // because it doesn't (cannot) inspect the contents of the iframe. + expect(element, 'ad element').to.be.ok; + expect(element.querySelector('iframe[srcdoc]'), + 'does not have a friendly iframe child').to.not.be.ok; + const child = element.querySelector('iframe[src]'); + expect(child, 'iframe child').to.be.ok; + expect(child.getAttribute('src')).to.contain.string(src); + expect(element, 'ad tag').to.be.visible; + expect(child, 'iframe child').to.be.visible; +} + +describe('integration test: a4a', () => { + let sandbox; + let xhrMock; + let fixture; + let mockResponse; + let a4aElement; + let headers; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + xhrMock = sandbox.stub(Xhr.prototype, 'fetch'); + // Expect key set fetches for signing services. + const fetchJsonMock = sandbox.stub(Xhr.prototype, 'fetchJson'); + for (const serviceName in signingServerURLs) { + fetchJsonMock.withArgs(signingServerURLs[serviceName], + { + mode: 'cors', + method: 'GET', + ampCors: false, + credentials: 'omit', + }).returns( + Promise.resolve({keys: [JSON.parse(validCSSAmp.publicKey)]})); + } + // Expect ad request. + headers = {}; + headers[SIGNATURE_HEADER] = validCSSAmp.signature; + mockResponse = { + arrayBuffer: () => utf8Encode(validCSSAmp.reserialized), + bodyUsed: false, + headers: new FetchResponseHeaders({ + getResponseHeader(name) { + return headers[name]; + }, + }), + }; + xhrMock.withArgs(TEST_URL, { + mode: 'cors', + method: 'GET', + credentials: 'include', + }).onFirstCall().returns(Promise.resolve(mockResponse)); + adConfig['mock'] = {}; + a4aRegistry['mock'] = () => {return true;}; + return createIframePromise().then(f => { + fixture = f; + installDocService(fixture.win, /* isSingleDoc */ true); + installCryptoService(fixture.win); + upgradeOrRegisterElement(fixture.win, 'amp-a4a', MockA4AImpl); + const doc = fixture.doc; + a4aElement = doc.createElement('amp-a4a'); + a4aElement.setAttribute('width', 200); + a4aElement.setAttribute('height', 50); + a4aElement.setAttribute('type', 'mock'); + }); + }); + + afterEach(() => { + sandbox.restore(); + resetScheduledElementForTesting(window, 'amp-a4a'); + delete adConfig['mock']; + delete a4aRegistry['mock']; + }); + + it('should render a single AMP ad in a friendly iframe', () => { + return fixture.addElement(a4aElement).then(unusedElement => { + return expectRenderedInFriendlyIframe(a4aElement, 'Hello, world.'); + }); + }); + + it('should fall back to 3p when no signature is present', () => { + delete headers[SIGNATURE_HEADER]; + return fixture.addElement(a4aElement).then(unusedElement => { + expectRenderedInXDomainIframe(a4aElement, TEST_URL); + }); + }); + + it('should fall back to 3p when the XHR fails', () => { + xhrMock.resetBehavior(); + xhrMock.throws(new Error('Testing network error')); + // TODO(tdrl) Currently layoutCallback rejects, even though something *is* + // rendered. This should be fixed in a refactor, and we should change this + // .catch to a .then. + return fixture.addElement(a4aElement).catch(error => { + expect(error.message).to.contain.string('Testing network error'); + expect(error.message).to.contain.string('AMP-A4A-'); + expectRenderedInXDomainIframe(a4aElement, TEST_URL); + }); + }); + + it('should fall back to 3p when extractCreative throws', () => { + sandbox.stub(MockA4AImpl.prototype, 'extractCreativeAndSignature').throws( + new Error('Testing extractCreativeAndSignature error')); + // TODO(tdrl) Currently layoutCallback rejects, even though something *is* + // rendered. This should be fixed in a refactor, and we should change this + // .catch to a .then. + return fixture.addElement(a4aElement).catch(error => { + expect(error.message).to.contain.string( + 'Testing extractCreativeAndSignature error'); + expect(error.message).to.contain.string('amp-a4a:'); + expectRenderedInXDomainIframe(a4aElement, TEST_URL); + }); + }); + + it('should fall back to 3p when extractCreative returns empty sig', () => { + const extractCreativeAndSignatureStub = + sandbox.stub(MockA4AImpl.prototype, 'extractCreativeAndSignature'); + extractCreativeAndSignatureStub.onFirstCall().returns({ + creative: utf8Encode(validCSSAmp.reserialized), + signature: null, + size: null, + }); + return fixture.addElement(a4aElement).then(unusedElement => { + expect(extractCreativeAndSignatureStub).to.be.calledOnce; + expectRenderedInXDomainIframe(a4aElement, TEST_URL); + }); + }); + + it('should fall back to 3p when extractCreative returns empty creative', + () => { + sandbox.stub(MockA4AImpl.prototype, 'extractCreativeAndSignature') + .onFirstCall().returns({ + creative: null, + signature: validCSSAmp.signature, + size: null, + }) + .onSecondCall().throws(new Error( + 'Testing extractCreativeAndSignature should not occur error')); + // TODO(tdrl) Currently layoutCallback rejects, even though something + // *is* rendered. This should be fixed in a refactor, and we should + // change this .catch to a .then. + return fixture.addElement(a4aElement).catch(error => { + expect(error.message).to.contain.string('Key failed to validate'); + expect(error.message).to.contain.string('amp-a4a:'); + expectRenderedInXDomainIframe(a4aElement, TEST_URL); + }); + }); + + it('should collapse slot when creative response has code 204', () => { + headers = {}; + headers[SIGNATURE_HEADER] = validCSSAmp.signature; + mockResponse = { + arrayBuffer: () => utf8Encode(validCSSAmp.reserialized), + bodyUsed: false, + headers: new FetchResponseHeaders({ + getResponseHeader(name) { + return headers[name]; + }, + }), + status: 204, + }; + xhrMock.withArgs(TEST_URL, { + mode: 'cors', + method: 'GET', + credentials: 'include', + }).onFirstCall().returns(Promise.resolve(mockResponse)); + const forceCollapseStub = + sandbox.stub(MockA4AImpl.prototype, 'forceCollapse'); + return fixture.addElement(a4aElement).then(unusedElement => { + expect(forceCollapseStub).to.be.calledOnce; + }); + }); + + it('should collapse slot when creative response is null', () => { + xhrMock.withArgs(TEST_URL, { + mode: 'cors', + method: 'GET', + credentials: 'include', + }).onFirstCall().returns(Promise.resolve(null)); + const forceCollapseStub = + sandbox.stub(MockA4AImpl.prototype, 'forceCollapse'); + return fixture.addElement(a4aElement).then(unusedElement => { + expect(forceCollapseStub).to.be.calledOnce; + }); + }); + + it('should collapse slot when creative response.arrayBuffer is null', () => { + headers = {}; + headers[SIGNATURE_HEADER] = validCSSAmp.signature; + mockResponse = { + arrayBuffer: () => null, + bodyUsed: false, + headers: new FetchResponseHeaders({ + getResponseHeader(name) { + return headers[name]; + }, + }), + status: 204, + }; + xhrMock.withArgs(TEST_URL, { + mode: 'cors', + method: 'GET', + credentials: 'include', + }).onFirstCall().returns(Promise.resolve(mockResponse)); + const forceCollapseStub = + sandbox.stub(MockA4AImpl.prototype, 'forceCollapse'); + return fixture.addElement(a4aElement).then(unusedElement => { + expect(forceCollapseStub).to.be.calledOnce; + }); + }); + + + // TODO(@ampproject/a4a): Need a test that double-checks that thrown errors + // are propagated out and printed to console and/or sent upstream to error + // logging systems. This is a bit tricky, because it's handled by the AMP + // runtime and can't be done within the context of a + // fixture.addElement().then() or .catch(). This should be integrated into + // all tests, so that we know precisely when errors are being reported and + // to whom. + it('should propagate errors out and report them to upstream error log'); +}); diff --git a/extensions/amp-a4a/0.1/test/test-a4a-var-source.js b/extensions/amp-a4a/0.1/test/test-a4a-var-source.js new file mode 100644 index 000000000000..4ae169de46be --- /dev/null +++ b/extensions/amp-a4a/0.1/test/test-a4a-var-source.js @@ -0,0 +1,93 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {createIframePromise} from '../../../../testing/iframe'; +import {A4AVariableSource} from '../a4a-variable-source'; +import {installDocumentInfoServiceForDoc,} from + '../../../../src/service/document-info-impl'; + + +describe('A4AVariableSource', () => { + + let varSource; + + beforeEach(() => { + return createIframePromise().then(iframe => { + iframe.doc.title = 'Pixel Test'; + const link = iframe.doc.createElement('link'); + link.setAttribute('href', 'https://pinterest.com:8080/pin1'); + link.setAttribute('rel', 'canonical'); + iframe.doc.head.appendChild(link); + installDocumentInfoServiceForDoc(iframe.ampdoc); + varSource = new A4AVariableSource(iframe.ampdoc, iframe.win); + }); + }); + + function expandAsync(varName, opt_params) { + return varSource.get(varName).async.apply(null, opt_params); + } + + function expandSync(varName, opt_params) { + return varSource.get(varName).sync.apply(null, opt_params); + } + + it('should replace RANDOM', () => { + expect(expandSync('RANDOM')).to.match(/(\d+(\.\d+)?)$/); + }); + + it('should replace CANONICAL_URL', () => { + expect(expandSync('CANONICAL_URL')) + .to.equal('https://pinterest.com:8080/pin1'); + }); + + it('should replace AD_NAV_TIMING', () => { + expect(expandSync('AD_NAV_TIMING', ['navigationStart'])).to.match(/\d+/); + return expandAsync('AD_NAV_TIMING', ['navigationStart']).then(val => + expect(val).to.match(/\d+/) + ); + }); + + it('should replace AD_NAV_TYPE', () => { + expect(expandSync('AD_NAV_TYPE')).to.match(/\d/); + }); + + it('should replace AD_NAV_REDIRECT_COUNT', () => { + expect(expandSync('AD_NAV_REDIRECT_COUNT')).to.match(/\d/); + }); + + function undefinedVariable(varName) { + it('should not replace ' + varName, () => { + expect(varSource.get(varName)).to.be.undefined; + }); + } + + // Performance timing info. + undefinedVariable('NAV_TIMING'); + undefinedVariable('NAV_TYPE'); + undefinedVariable('NAV_REDIRECT_COUNT'); + undefinedVariable('PAGE_LOAD_TIME'); + undefinedVariable('DOMAIN_LOOKUP_TIME'); + undefinedVariable('TCP_CONNECT_TIME'); + undefinedVariable('SERVER_RESPONSE_TIME'); + undefinedVariable('PAGE_DOWNLOAD_TIME'); + undefinedVariable('REDIRECT_TIME'); + undefinedVariable('DOM_INTERACTIVE_TIME'); + undefinedVariable('CONTENT_LOAD_TIME'); + + // Access data. + undefinedVariable('ACCESS_READER_ID'); + undefinedVariable('AUTHDATA'); +}); diff --git a/extensions/amp-a4a/0.1/test/test-amp-a4a.js b/extensions/amp-a4a/0.1/test/test-amp-a4a.js new file mode 100644 index 000000000000..ace4d7d7c336 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/test-amp-a4a.js @@ -0,0 +1,1545 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + MockA4AImpl, + TEST_URL, + SIGNATURE_HEADER, +} from './utils'; +import { + AmpA4A, + RENDERING_TYPE_HEADER, + SAFEFRAME_IMPL_PATH, + protectFunctionWrapper, +} from '../amp-a4a'; +import {Xhr} from '../../../../src/service/xhr-impl'; +import {Extensions} from '../../../../src/service/extensions-impl'; +import {Viewer} from '../../../../src/service/viewer-impl'; +import {ampdocServiceFor} from '../../../../src/ampdoc'; +import {cryptoFor} from '../../../../src/crypto'; +import {cancellation} from '../../../../src/error'; +import {createIframePromise} from '../../../../testing/iframe'; +import { + data as validCSSAmp, +} from './testdata/valid_css_at_rules_amp.reserialized'; +import {data as testFragments} from './testdata/test_fragments'; +import {installDocService} from '../../../../src/service/ampdoc-impl'; +import {FetchResponseHeaders} from '../../../../src/service/xhr-impl'; +import {base64UrlDecodeToBytes} from '../../../../src/utils/base64'; +import {utf8Encode} from '../../../../src/utils/bytes'; +import {resetScheduledElementForTesting} from '../../../../src/custom-element'; +import {urlReplacementsForDoc} from '../../../../src/url-replacements'; +import {incrementLoadingAds} from '../../../amp-ad/0.1/concurrent-load'; +import {platformFor} from '../../../../src/platform'; +import '../../../../extensions/amp-ad/0.1/amp-ad-xorigin-iframe-handler'; +import {dev} from '../../../../src/log'; +import {createElementWithAttributes} from '../../../../src/dom'; +import {AmpContext} from '../../../../3p/ampcontext.js'; +import * as sinon from 'sinon'; + +/** + * Create a promise for an iframe that has a super-minimal mock AMP environment + * in it. + * + * @return {!Promise<{ + * win: !Window, + * doc: !Document, + * iframe: !Element, + * addElement: function(!Element):!Promise + * }> + */ +function createAdTestingIframePromise() { + return createIframePromise().then(fixture => { + installDocService(fixture.win, /* isSingleDoc */ true); + const doc = fixture.doc; + // TODO(a4a-cam@): This is necessary in the short term, until A4A is + // smarter about host document styling. The issue is that it needs to + // inherit the AMP runtime style element in order for shadow DOM-enclosed + // elements to behave properly. So we have to set up a minimal one here. + const ampStyle = doc.createElement('style'); + ampStyle.setAttribute('amp-runtime', 'scratch-fortesting'); + doc.head.appendChild(ampStyle); + return fixture; + }); +} + + +describe('amp-a4a', () => { + let sandbox; + let xhrMock; + let xhrMockJson; + let getSigningServiceNamesMock; + let viewerWhenVisibleMock; + let mockResponse; + let onAmpCreativeRenderSpy; + let headers; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + xhrMock = sandbox.stub(Xhr.prototype, 'fetch'); + xhrMockJson = sandbox.stub(Xhr.prototype, 'fetchJson'); + getSigningServiceNamesMock = sandbox.stub(AmpA4A.prototype, + 'getSigningServiceNames'); + onAmpCreativeRenderSpy = + sandbox.spy(AmpA4A.prototype, 'onAmpCreativeRender'); + getSigningServiceNamesMock.returns(['google']); + xhrMockJson.withArgs( + 'https://cdn.ampproject.org/amp-ad-verifying-keyset.json', + { + mode: 'cors', + method: 'GET', + ampCors: false, + credentials: 'omit', + }).returns( + Promise.resolve({keys: [JSON.parse(validCSSAmp.publicKey)]})); + viewerWhenVisibleMock = sandbox.stub(Viewer.prototype, 'whenFirstVisible'); + viewerWhenVisibleMock.returns(Promise.resolve()); + mockResponse = { + arrayBuffer: function() { + return utf8Encode(validCSSAmp.reserialized); + }, + bodyUsed: false, + headers: new FetchResponseHeaders({ + getResponseHeader(name) { + return headers[name]; + }, + }), + catch: callback => callback(), + }; + headers = {}; + headers[SIGNATURE_HEADER] = validCSSAmp.signature; + }); + + afterEach(() => { + sandbox.restore(); + resetScheduledElementForTesting(window, 'amp-a4a'); + }); + + function createA4aElement(doc) { + const element = createElementWithAttributes(doc, 'amp-a4a', { + 'width': '200', + 'height': '50', + 'type': 'adsense', + }); + element.getAmpDoc = () => { + const ampdocService = ampdocServiceFor(doc.defaultView); + return ampdocService.getAmpDoc(element); + }; + element.isBuilt = () => {return true;}; + doc.body.appendChild(element); + return element; + } + + function buildCreativeString(opt_additionalInfo) { + const baseTestDoc = testFragments.minimalDocOneStyle; + const offsets = opt_additionalInfo || {}; + offsets.ampRuntimeUtf16CharOffsets = [ + baseTestDoc.indexOf(' + +

    some text

    `, + + minimalDocOneStyle: ` + + + +

    some text

    `, +}; diff --git a/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.original.html b/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.original.html new file mode 100644 index 000000000000..c2413df04688 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.original.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + +Hello, world. + diff --git a/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.reserialized.js b/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.reserialized.js new file mode 100644 index 000000000000..20548671674c --- /dev/null +++ b/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.reserialized.js @@ -0,0 +1,75 @@ +// Reserialized version of /usr/local/google/home/kjwright/github/amphtml/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.original.html. +// Automatically generated by script. Do not edit directly. + +export const data = { + reserialized: ` + + +Hello, world. + + +`, + + original: ` + + + + + + + + + + + +Hello, world. +`, + + signature: `ACWMjZEOeJKFek0b9LAGVFDsXPuGI8c1lFSUf+yVgVyswH0gBCoY9jaeNZnyWrKbSn2fNGp7F/tu5XGqkazkPatPRsUIRAII0aIbouiiRK/KUd+fX+gMiOssJH5yLBEukAMY5Te4pNSSOO27s0tzf/fhx9okOW2/23NkhazV9cGKAX6jxlMKSKgTQuZ3phynC3zyy1Tzn2r3jj1G9wvZXWqnexesOcbcX9BQYGvk5wGTjLUElA1QXqvhdzSQgyyX+ajvWBUNwVFHDQaQsV+2WGx0DhHaXmtgEBVnFSjzahELBPwH2SxrHzIfEZC4TP86VquNuAwetXulhwn5VjkbTOQoAeuw`, + + publicKey: `{ + "kty": "RSA", + "e": "AQAB", + "n": "oDK9vY5WkwS25IJWhFTmyy_xTeBHA5b72On2FqhjZPLSwadlC0gZG0lvzPjxE1bakbAM3rR2mRJmtrKDAcZSZxIfxpVhG5e7yFAZURnKSKGHvLLwSeohnR6zHgZ0Rm6fnvBhYBpHGaFboPXgK1IjgVZ_aEq5CRj24JLvqovMtpJJXwJ1fndMprEfDAzw5rEzfZxvGP3QObEQENHAlyPe54Z0vfCYhiXLWhQuOyaKkVIf3xn7t6Pu7PbreCN9f-Ca8noVVKNUZCdlUqiQjXZZfu5pi8ZCto_HEN26hE3nqoEFyBWQwMvgJMhpkS2NjIX2sQuM5KangAkjJRe-Ej6aaQ", + "alg": "RS256", + "ext": true + }` +}; diff --git a/extensions/amp-a4a/0.1/test/utils.js b/extensions/amp-a4a/0.1/test/utils.js new file mode 100644 index 000000000000..1d835982a0d9 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/utils.js @@ -0,0 +1,58 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {AmpA4A} from '../amp-a4a'; +import {base64UrlDecodeToBytes} from '../../../../src/utils/base64'; + +/** @type {string} @private */ +export const SIGNATURE_HEADER = 'X-TestSignatureHeader'; + +/** @type {string} @private */ +export const SIZE_HEADER = 'X-CreativeSize'; + +/** @type {string} @private */ +export const TEST_URL = 'http://iframe.localhost:' + location.port + + '/test/fixtures/served/iframe.html?args'; + +export class MockA4AImpl extends AmpA4A { + getAdUrl() { + return Promise.resolve(TEST_URL); + } + + updatePriority() { + // Do nothing. + } + + extractCreativeAndSignature(responseArrayBuffer, responseHeaders) { + return Promise.resolve({ + creative: responseArrayBuffer, + signature: responseHeaders.has(SIGNATURE_HEADER) ? + base64UrlDecodeToBytes(responseHeaders.get(SIGNATURE_HEADER)) : null, + size: responseHeaders.has(SIZE_HEADER) ? + responseHeaders.get(SIZE_HEADER).split('x') : null, + }); + } + + /** @override */ + handleResize() { + return; + } + + getFallback() { + return null; + } +} + diff --git a/extensions/amp-a4a/OWNERS.yaml b/extensions/amp-a4a/OWNERS.yaml new file mode 100644 index 000000000000..ce5da8549c89 --- /dev/null +++ b/extensions/amp-a4a/OWNERS.yaml @@ -0,0 +1,2 @@ +- ampproject/a4a + diff --git a/extensions/amp-a4a/amp-a4a-format.md b/extensions/amp-a4a/amp-a4a-format.md new file mode 100644 index 000000000000..7423ce6b1341 --- /dev/null +++ b/extensions/amp-a4a/amp-a4a-format.md @@ -0,0 +1,464 @@ + + + +# AMP A4A AD CREATIVE FORMAT + +![Draft standard](https://upload.wikimedia.org/wikipedia/commons/f/ff/DRAFT_ICON.png "By Reneman (Own work) [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons") + +** >> WORK IN PROGRESS. SUBJECT TO CHANGE. << ** + +_This set of standards is still in development and is likely to be revised. +Feedback from the community is welcome. Please comment here or on the [Intent +to Implement](https://github.com/ampproject/amphtml/issues/4264)_. + +A4A (AMP for Ads) is a mechanism for rendering fast, +performant ads in AMP pages. To ensure that A4A ad documents ("A4A +creatives") can be rendered quickly and smoothly in the browser and do +not degrade user experience, A4A creatives must obey a set of validation +rules. Similar in spirit to the +[AMP format rules](../../spec/amp-html-format.md), A4A creatives have +access to a limited set of allowed tags, capabilities, and extensions. + +## A4A Format Rules + +1. Unless otherwise specified below, the creative must obey all rules + given by the [AMP format rules](../../spec/amp-html-format.md), + included here by reference. For example, the A4A + [Boilerplate](amp-a4a-format.md#2) deviates from the AMP + standard boilerplate. + + _*In addition*_: + +1. The creative must use `` or `` as its enclosing + tags. + + _Rationale_: Allows validators to identify a creative document as either a + general AMP doc or a restricted A4A doc and to dispatch appropriately. + +1. The creative must include `` + as the runtime script instead of `https://cdn.ampproject.org/v0.js`. + + _Rationale_: Allows tailored runtime behaviors for A4A served in cross-origin iframes. + +1. Unlike in general AMP, the creative must not include a `` tag. + + _Rationale_: Ad creatives don't have a "non-AMP canonical version" + and won't be independently search-indexed, so self-referencing + would be useless. + +1. Media: Videos must not enable autoplay. This includes +both the `` + tag as well as autoplay on ``, ``, and 3P video + tags such as ``. + + _Rationale_: Autoplay forces video content to be downloaded immediately, + which slows the page load. + +1. Media: Audio must not enable autoplay. This includes both the `` + tag as well as all audio-including video tags, as described in the previous + point. + + _Rationale_: Same as for video. + +1. Analytics: `` viewability tracking may only target the full-ad + selector, via `"visibilitySpec": { "selector": "amp-ad" }`, as defined in + [Issue #4018](https://github.com/ampproject/amphtml/issues/4018) and + [PR #4368](https://github.com/ampproject/amphtml/pull/4368). In + particular, it may not target any selectors for elements within the ad + creative. + + _Rationale_: In some cases, A4A may choose to render an ad creative in an + iframe. In those cases, host page analytics can only target the entire + iframe anyway, and won’t have access to any finer-grained selectors. + + _Example_: + + ```html + + + + ``` + + _This configuration sends a request to URL + `https://example.com/nestedAmpAnalytics` when 50% of the enclosing ad has been + continuously visible on the screen for 1 second._ + +### Boilerplate + +A4A creatives require a different, and considerably simpler, boilerplate style line than +[general AMP documents do](https://github.com/ampproject/amphtml/blob/master/spec/amp-boilerplate.md): + +```html + +``` + +_Rationale:_ The `amp-boilerplate` style hides body content until the AMP +runtime is ready and can unhide it. If Javascript is disabled or the AMP +runtime fails to load, the default boilerplate ensures that the content is +eventually displayed regardless. In A4A, however, if Javascript is entirely +disabled, A4A won't run and no ad will ever be shown, so there is no need for +the `
    Description The amp-pixel element is meant to be used as a typical tracking pixel - to count page views.
    Availability Stable
    Supported Layoutsfixed, nodisplay
    Examples everything.amp.html
    + + + + + + + + + + + + +
    amp-accordion
    amp-analytics
    amp-anim
    amp-audio
    amp-carousel
    amp-fit-text
    amp-font
    amp-form
    amp-img
    amp-pixel
    amp-social-share
    amp-video
    + +### HTML Tags + +The following are _allowed_ tags in an A4A creative. Tags not explicitly +allowed are prohibited. This list is a subset of the general [AMP tag +addendum whitelist](../../spec/amp-tag-addendum.md). Like that list, it is +ordered consistent with HTML5 spec in section 4 [The Elements of HTML](http://www.w3.org/TR/html5/single-page.html#html-elements). + +Most of the omissions are either for performance or because the tags are not +HTML5 standard. For example, `

    This is a fallback + + + + +
    + This is a placeholder +
    +
    + This is a fallback +
    +
    diff --git a/test/manual/amp-videoplayers-autoplay.html b/test/manual/amp-videoplayers-autoplay.html new file mode 100644 index 000000000000..3c5f00374260 --- /dev/null +++ b/test/manual/amp-videoplayers-autoplay.html @@ -0,0 +1,165 @@ + + + + + Video Autoplay + + + + + + + + + + + +
    + Accelerated Times +
    +
    +
    + + +
    +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit!

    +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. + Aliquam iaculis tincidunt quam sed maximus. Suspendisse faucibus + ornare sodales. Nullam id dolor vitae arcu consequat ornare a + et lectus. Sed tempus eget enim eget lobortis. + Mauris sem est, accumsan sed tincidunt ut, sagittis vel arcu. + Nullam in libero nisi. +

    + + +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus + luctus nunc ut elit cursus, et imperdiet diam vehicula. + Duis et nisi sed urna blandit bibendum et sit amet erat. + Suspendisse potenti. Curabitur consequat volutpat arcu nec + elementum. Etiam a turpis ac libero varius condimentum. + Maecenas sollicitudin felis aliquam tortor vulputate, + ac posuere velit semper. +

    +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. + Aliquam iaculis tincidunt quam sed maximus. Suspendisse faucibus + ornare sodales. Nullam id dolor vitae arcu consequat ornare a + et lectus. Sed tempus eget enim eget lobortis. + Mauris sem est, accumsan sed tincidunt ut, sagittis vel arcu. + Nullam in libero nisi. +

    +

    In carousel

    +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. + Aliquam iaculis tincidunt quam sed maximus. Suspendisse faucibus +

    + + + + + + + + + + + +

    + Fusce pretium tempor justo, vitae consequat dolor maximus eget. + Aliquam iaculis tincidunt quam sed maximus. Suspendisse faucibus + ornare sodales. Nullam id dolor vitae arcu consequat ornare a + et lectus. Sed tempus eget enim eget lobortis. + Mauris sem est, accumsan sed tincidunt ut, sagittis vel arcu. + Nullam in libero nisi. +

    +
    +
    + + +

    + Auto-playing Design Background Video, Not Interactive! +

    +
    +
    + + diff --git a/test/manual/cache-sw.html b/test/manual/cache-sw.html new file mode 100644 index 000000000000..8eb0b54a6550 --- /dev/null +++ b/test/manual/cache-sw.html @@ -0,0 +1,33 @@ + + + + + Cache SW Manual Testing + + + + + + + + + + +

    Special Instructions

    +

    To test the Cache Service Worker locally, must go to: +

    +

    +

    This is because the SW only activates for pages that are under its scope (`/dist`, in this case).

    +
    +

    Every 5 minutes, the RTV will be updated. You'll need to "skipWaiting" the SW, and your next request will stale-while-revalidate serve. +


    +

    Shit's gonna get funky if you have an active breakpoint in the SW and request anything in `/dist`.

    + + diff --git a/test/manual/error.amp.html b/test/manual/error.amp.html new file mode 100644 index 000000000000..afa9ec4e5ee4 --- /dev/null +++ b/test/manual/error.amp.html @@ -0,0 +1,16 @@ + + + + + Error messages + + + + + + +

    Show AMP in error state. Load with #development=1

    + + + + diff --git a/test/manual/fakead3p.amp.html b/test/manual/fakead3p.amp.html new file mode 100644 index 000000000000..8baba2abf080 --- /dev/null +++ b/test/manual/fakead3p.amp.html @@ -0,0 +1,105 @@ + + + + + Ad examples + + + + + + + + + + +

    fake3pAd send no-content

    +
    + +
    + +

    fake3pAd send render-start w/ width & height

    +
    + +
    + +

    fake3pAd send render-start w/o width or height

    +
    + +
    + +

    fake3pAd send render-start w/ only width

    +
    + +
    + +

    fake3pAd send render-start w/ only height

    +
    + +
    + +

    Below viewport fake3pAd send render-start w/ width & height

    +
    + +
    + +

    Below viewport fake3pAd send no-content

    +
    + +
    + + + diff --git a/test/manual/gallery.amp.html b/test/manual/gallery.amp.html new file mode 100644 index 000000000000..14864445bf4e --- /dev/null +++ b/test/manual/gallery.amp.html @@ -0,0 +1,68 @@ + + + + + AMP #0 + + + + + + + + + + + +

    amp #0

    + + +

    Main carousel

    + + + + + + + + + + + +

    Selectable carousel

    + + + + + + +
    + Video +
    + + +
    + + + + + diff --git a/test/manual/pre-render-load.html b/test/manual/pre-render-load.html new file mode 100644 index 000000000000..5a0c821ca624 --- /dev/null +++ b/test/manual/pre-render-load.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/manual/resize-fixture-iframe.html b/test/manual/resize-fixture-iframe.html new file mode 100644 index 000000000000..d7fbb48f519e --- /dev/null +++ b/test/manual/resize-fixture-iframe.html @@ -0,0 +1,19 @@ + + + + + diff --git a/test/manual/unavailable-font.amp.html b/test/manual/unavailable-font.amp.html new file mode 100644 index 000000000000..b924cdfbc1a7 --- /dev/null +++ b/test/manual/unavailable-font.amp.html @@ -0,0 +1,47 @@ + + + + + Font example + + + + + + + + + + + + + +

    + Font doesn't exist test text, should turn red after 1sec +

    + + + diff --git a/test/size.csv b/test/size.csv new file mode 100644 index 000000000000..0cc8111e375a --- /dev/null +++ b/test/size.csv @@ -0,0 +1,93 @@ +"datetime","alp.js","amp4ads-host-v0.js / amp-inabox-host.js","amp4ads-v0.js / amp-inabox.js","f.js","integration-check-types.js","shadow-v0.js / amp-shadow.js","sw-kill.js","sw.js","v0.js","v0/amp-a4a-0.1.js","v0/amp-access-0.1.js","v0/amp-access-laterpay-0.1.js","v0/amp-accordion-0.1.js","v0/amp-ad-0.1.js","v0/amp-ad-network-adsense-impl-0.1.js","v0/amp-ad-network-doubleclick-impl-0.1.js","v0/amp-ad-network-fake-impl-0.1.js","v0/amp-analytics-0.1.js","v0/amp-anim-0.1.js","v0/amp-animation-0.1.js","v0/amp-apester-media-0.1.js","v0/amp-app-banner-0.1.js","v0/amp-audio-0.1.js","v0/amp-auto-ads-0.1.js","v0/amp-brid-player-0.1.js","v0/amp-brightcove-0.1.js","v0/amp-carousel-0.1.js","v0/amp-dailymotion-0.1.js","v0/amp-dynamic-css-classes-0.1.js","v0/amp-experiment-0.1.js","v0/amp-facebook-0.1.js","v0/amp-fit-text-0.1.js","v0/amp-font-0.1.js","v0/amp-form-0.1.js","v0/amp-fresh-0.1.js","v0/amp-fx-flying-carpet-0.1.js","v0/amp-gfycat-0.1.js","v0/amp-google-vrview-image-0.1.js","v0/amp-hulu-0.1.js","v0/amp-iframe-0.1.js","v0/amp-image-lightbox-0.1.js","v0/amp-instagram-0.1.js","v0/amp-install-serviceworker-0.1.js","v0/amp-jwplayer-0.1.js","v0/amp-kaltura-player-0.1.js","v0/amp-lightbox-0.1.js","v0/amp-lightbox-viewer-0.1.js","v0/amp-list-0.1.js","v0/amp-live-list-0.1.js","v0/amp-mustache-0.1.js","v0/amp-o2-player-0.1.js","v0/amp-pinterest-0.1.js","v0/amp-reach-player-0.1.js","v0/amp-reddit-0.1.js","v0/amp-selector-0.1.js","v0/amp-share-tracking-0.1.js","v0/amp-sidebar-0.1.js","v0/amp-slides-0.1.js","v0/amp-social-share-0.1.js","v0/amp-soundcloud-0.1.js","v0/amp-springboard-player-0.1.js","v0/amp-sticky-ad-0.1.js","v0/amp-sticky-ad-1.0.js","v0/amp-twitter-0.1.js","v0/amp-user-notification-0.1.js","v0/amp-viewer-integration-0.1.js","v0/amp-vimeo-0.1.js","v0/amp-vine-0.1.js","v0/amp-viz-vega-0.1.js","v0/amp-youtube-0.1.js","v0/cache-service-worker-0.1.js" +"2015-10-03 23:45:35 -0700","","","","50360.000","","","","","136790.000","","","","","","","","","","9770.000","","","","7000.000","","","","44270.000","","","","","6780.000","","","","","","","","8310.000","66830.000","5860.000","","","","44150.000","","","","","","","","","","","","18330.000","","","","","","10380.000","","","","","","5840.000","" +"2015-10-13 17:09:16 -0700","","","","50430.000","","","","","143220.000","","","","","","","","","","10080.000","","","","7130.000","","","","44920.000","","","","","6800.000","","","","","","","","8350.000","67900.000","5870.000","","","","44820.000","","","","","","","","","","","","18450.000","","","","","","10460.000","","","","","","6090.000","" +"2015-10-18 11:30:34 -0700","","","","21280.000","","","","","133710.000","","","","","","","","","","10280.000","","","","7390.000","","","","48960.000","","","","","6990.000","","","","","","","","8620.000","71470.000","6090.000","","","","48010.000","","","","","","18070.000","","","","","","18720.000","","","","","","10730.000","","","","","","6320.000","" +"2015-10-20 20:20:27 -0700","","","","21290.000","","","","","134660.000","","","","","","","","","","10290.000","","","","7490.000","","","","48980.000","","","","","7000.000","","","","","","","","8630.000","71480.000","6150.000","","","","48020.000","","","","","","19360.000","","","","","","18730.000","","","","","","10740.000","","","","","","6370.000","" +"2015-10-22 07:35:35 -0700","","","","21290.000","","","","","135200.000","","","","","","","","","","10290.000","","","","7490.000","","","","48980.000","","","","","7000.000","","","","","","","","8630.000","71480.000","6150.000","","","","48020.000","","","","","","19360.000","","","","","","18730.000","","","","","","10740.000","","","","","","6370.000","" +"2015-11-03 11:09:39 -0800","","","","10730.000","","","","","139560.000","","","","","","","","","","10180.000","","","","7460.000","","","","60060.000","","","","","6950.000","","","","","","","","14390.000","73150.000","6070.000","","","","49800.000","","","","","","19340.000","","","","","","30510.000","","","","","","14900.000","","","","","","6290.000","" +"2015-11-11 15:48:16 -0800","","","","10730.000","","","","","139560.000","","","","","","","","","","10180.000","","","","7460.000","","","","60060.000","","","","","6950.000","","","","","","","","14390.000","73150.000","6070.000","","","","49800.000","","","","35050.000","","19340.000","","","","","","30510.000","","","","","","14900.000","","","","","","6290.000","" +"2015-12-08 16:25:23 -0800","","","","11590.000","","","","","152610.000","","","","","","","","","30040.000","10420.000","","","","7710.000","","","7000.000","62040.000","","","","","7040.000","27860.000","","","","","","","14390.000","68150.000","6320.000","6960.000","","","44210.000","","18300.000","","42860.000","","19600.000","","","","","","33270.000","","","","","","16290.000","","","","6420.000","","6590.000","" +"2015-12-22 21:44:42 -0800","","","","11800.000","","","","","149940.000","","","","","","","","","65510.000","9990.000","","","","7370.000","","","6730.000","59620.000","","","","","6800.000","27150.000","","","","","","","14450.000","65760.000","6050.000","6710.000","","","42490.000","","48500.000","","42880.000","","23990.000","","","","","","32370.000","","","","","","15960.000","","","","6150.000","","6320.000","" +"2015-12-22 21:55:24 -0800","","","","11800.000","","","","","150650.000","","","","","","","","","41920.000","9990.000","","","","7370.000","","","6730.000","50690.000","","","","","6800.000","17700.000","","","","","","","14450.000","56820.000","6050.000","6710.000","","","33560.000","","24380.000","","42880.000","","23990.000","","","","","","22920.000","","","","","","15960.000","","","","6150.000","","6320.000","" +"2015-12-22 22:32:57 -0800","","","","11800.000","","","","","150870.000","","","","","","","","","38960.000","9990.000","","","","7370.000","","","6730.000","48300.000","","","","","6800.000","10540.000","","","","","","","14450.000","54430.000","6050.000","6710.000","","","27090.000","","19980.000","","42880.000","","23990.000","","","","","","19830.000","","","","","","15960.000","","","","6150.000","","6320.000","" +"2016-01-11 20:08:21 -0800","","","","11280.000","","","","","153900.000","","48740.000","","","","","","","38550.000","10240.000","","","","7910.000","","","6980.000","48720.000","","9890.000","","","6910.000","10640.000","","","","","","","15020.000","55020.000","6300.000","5280.000","","","27140.000","","21160.000","","34630.000","","24560.000","","","","","","20320.000","","","","","","16610.000","25410.000","","","6410.000","","6570.000","" +"2016-01-11 20:06:51 -0800","","","","7830.000","","","","","133850.000","","42700.000","","","","","","","34670.000","10130.000","","","","7980.000","","","7220.000","37030.000","","7220.000","","","7190.000","10380.000","","","","","","","13630.000","46970.000","6550.000","4710.000","","","18740.000","","18620.000","","34020.000","","23730.000","","","","","","18340.000","","","","","","14650.000","21720.000","","","6650.000","","6810.000","" +"2016-01-29 09:53:57 -0800","","","","13030.000","","","","","148770.000","","90330.000","","","","","","","86230.000","25790.000","","","","24960.000","","","22850.000","53700.000","","24570.000","","31920.000","7240.000","25630.000","","","","","","","33500.000","63670.000","22180.000","6100.000","","","34050.000","","65620.000","","34060.000","","24810.000","","","","","","34970.000","","","","","","31930.000","69660.000","","","22280.000","","22440.000","" +"2016-01-29 11:24:54 -0800","","","","","","","","","148770.000","","74640.000","","","","","","","70510.000","10210.000","","","","9380.000","","","7300.000","38000.000","","8970.000","","16270.000","7240.000","10070.000","","","","","","","17850.000","47950.000","6630.000","6100.000","","","18430.000","","49930.000","","34060.000","","24810.000","","","","","","19310.000","","","","","","16280.000","53960.000","","","6730.000","","6890.000","" +"2016-02-17 16:48:19 -0800","","","","18010.000","","","","","157840.000","","81740.000","","12660.000","","","","","84320.000","10700.000","","","","9710.000","","","10960.000","38360.000","7430.000","9180.000","","17090.000","7420.000","10060.000","","","","","","","19470.000","48650.000","6860.000","6320.000","","","18580.000","","54810.000","","40380.000","","25940.000","","","","","","19320.000","","7180.000","","","","17100.000","57200.000","","7100.000","6960.000","","9010.000","" +"2016-03-11 02:00:16 -0800","","","","26210.000","","","","","166910.000","","82750.000","","13750.000","","","","","92250.000","11090.000","","","","10230.000","","","11200.000","40380.000","7560.000","9880.000","","17900.000","7610.000","10610.000","","","","","","","20830.000","49880.000","10520.000","6900.000","","","18820.000","","57390.000","","41200.000","","26520.000","","","","","","19850.000","16680.000","7310.000","","","","18020.000","58040.000","","7230.000","7090.000","","10110.000","" +"2016-03-17 11:04:50 -0700","","","","29950.000","","","","","169890.000","","84860.000","","15630.000","","","","","94830.000","11170.000","","","","10450.000","","9650.000","11420.000","42720.000","7560.000","11770.000","","18130.000","7610.000","17480.000","","","","","","","22720.000","51860.000","10520.000","9990.000","","13040.000","18820.000","","59440.000","","44930.000","","26790.000","","","","","","21740.000","","7310.000","9800.000","","","18250.000","60250.000","","7230.000","7090.000","","10110.000","" +"2016-03-17 11:30:24 -0700","","","","29950.000","","","","","169890.000","","84860.000","","15630.000","","","","","94830.000","11170.000","","","","10450.000","","9650.000","11420.000","42720.000","7560.000","11770.000","","18130.000","7610.000","17480.000","","","","","","","22720.000","51860.000","10520.000","9990.000","","13040.000","18820.000","","59440.000","","44930.000","","26790.000","","","","","","21740.000","16900.000","7310.000","9800.000","","","18250.000","60250.000","","7230.000","7090.000","","10110.000","" +"2016-03-24 11:03:17 -0700","","","","31930.000","","","","","174260.000","","86690.000","","15890.000","","","","","97110.000","18090.000","","","","13820.000","","16500.000","16740.000","42750.000","14410.000","12010.000","","20980.000","14470.000","17930.000","","","","","","","23320.000","52370.000","18050.000","9990.000","","16420.000","25740.000","","61270.000","","44970.000","","30200.000","13970.000","","","","","21770.000","21610.000","14160.000","16650.000","","","21100.000","62050.000","","14070.000","13940.000","","16970.000","" +"2016-03-31 11:03:00 -0700","","","","33100.000","","","","","176050.000","","87070.000","","13160.000","","","","","98310.000","18160.000","","","","13890.000","","16590.000","16810.000","42800.000","14480.000","9300.000","","21050.000","14520.000","17980.000","","","","","","","23440.000","52440.000","18120.000","10040.000","16010.000","16490.000","25790.000","","61650.000","","45020.000","","30270.000","14040.000","","","","","21810.000","25390.000","14230.000","16740.000","","","21170.000","62430.000","","14140.000","14010.000","","17040.000","" +"2016-04-07 11:22:43 -0700","","","","33410.000","","","","","177710.000","","87370.000","","9220.000","","","","","98720.000","14190.000","","","","13520.000","","12650.000","16520.000","38870.000","10550.000","10730.000","","20710.000","10630.000","14010.000","","","","","","","23090.000","48650.000","14190.000","12680.000","12070.000","16120.000","21830.000","","61990.000","","44620.000","","31160.000","10110.000","","","","14030.000","17870.000","25030.000","10300.000","12800.000","","","20830.000","62800.000","","10210.000","10080.000","","13100.000","" +"2016-04-14 18:55:22 -0700","","","","36050.000","","","","","177690.000","","88430.000","","9220.000","","","","","101440.000","14190.000","","","","13520.000","","12650.000","16520.000","38870.000","10550.000","10730.000","","24300.000","10630.000","14010.000","","","","","","","23250.000","48650.000","14190.000","12680.000","12070.000","16120.000","21830.000","","62590.000","","44620.000","","31160.000","10110.000","","","","17050.000","17870.000","24900.000","10300.000","12800.000","","","24420.000","63400.000","","10210.000","10080.000","","13100.000","" +"2016-04-18 18:29:17 -0700","","","","36890.000","","","","","178040.000","","88660.000","","9380.000","","","","","101860.000","14340.000","","","","13690.000","","12800.000","16690.000","39030.000","10710.000","10900.000","","24460.000","10790.000","14170.000","","","","","","","23420.000","48810.000","14350.000","12850.000","12230.000","16290.000","21990.000","","62820.000","","44790.000","","31390.000","10270.000","","","","17250.000","18030.000","25100.000","10460.000","12950.000","","","24590.000","63630.000","","10370.000","10230.000","","13260.000","" +"2016-04-21 11:19:11 -0700","","","","36890.000","","","","","178050.000","","88660.000","","9380.000","","","","","103810.000","14340.000","","","","13690.000","","12800.000","16690.000","39050.000","10710.000","10900.000","","24490.000","10790.000","14170.000","","","","","","","23420.000","48810.000","14350.000","12850.000","12230.000","16290.000","21990.000","","62820.000","","44790.000","","31390.000","10270.000","","","","17250.000","18030.000","25100.000","10460.000","12950.000","","","24610.000","63630.000","","10370.000","10230.000","","13260.000","" +"2016-04-22 11:56:04 -0700","","","","36850.000","","","","","173350.000","","86940.000","","9350.000","","","","","104410.000","14260.000","","","","13660.000","","12770.000","16660.000","38880.000","10680.000","10840.000","","24340.000","10760.000","14110.000","","","","","","","23330.000","48580.000","14320.000","12780.000","12200.000","16260.000","21810.000","","62360.000","","44740.000","","30890.000","10240.000","","","","17160.000","17940.000","25010.000","10430.000","12930.000","","","24460.000","63140.000","","10340.000","10210.000","","13230.000","" +"2016-04-22 17:45:17 -0700","","","","30410.000","","","","","157730.000","","49260.000","","5960.000","","","","","65790.000","9490.000","","","","7220.000","","8330.000","7980.000","33170.000","6960.000","5280.000","","14450.000","6720.000","9660.000","","","","","","","14600.000","40040.000","8120.000","8630.000","7790.000","8660.000","15350.000","","24920.000","","40500.000","","24950.000","6520.000","","","","11600.000","12750.000","16110.000","6670.000","8510.000","","","14570.000","26000.000","","6620.000","6490.000","","8800.000","" +"2016-04-24 21:37:52 -0700","","","","30410.000","","","","","157780.000","","41210.000","","5960.000","","","","","58330.000","9490.000","","","","7220.000","","8330.000","7980.000","33170.000","6960.000","5280.000","","14450.000","6720.000","9660.000","","","","","","","14600.000","40040.000","8120.000","8630.000","7790.000","8660.000","15350.000","","13520.000","","40500.000","","24950.000","6520.000","","","","11600.000","12750.000","16110.000","6670.000","8510.000","","","14570.000","17860.000","","6620.000","6490.000","","8800.000","" +"2016-04-25 08:36:01 -0700","","","","30410.000","","","","","161320.000","","37760.000","","5960.000","","","","","54630.000","9490.000","","","","7220.000","","8330.000","7980.000","33170.000","6960.000","5280.000","","14450.000","6720.000","9660.000","","","","","","","14600.000","40040.000","8120.000","8630.000","7790.000","8660.000","15350.000","","9040.000","","40500.000","","20460.000","6520.000","","","","11600.000","12750.000","16110.000","6670.000","8510.000","","","14570.000","13430.000","","6620.000","6490.000","","8800.000","" +"2016-04-25 14:27:17 -0700","","","","30860.000","","","","","161400.000","","37720.000","","5960.000","","","","","54030.000","9490.000","","","","7220.000","","8330.000","7980.000","33170.000","6960.000","5280.000","","14450.000","6720.000","9660.000","","","","","","","14600.000","40040.000","8120.000","8630.000","7790.000","8660.000","15350.000","","9040.000","","40500.000","","20460.000","6520.000","","","","11600.000","12750.000","16110.000","6670.000","8510.000","","","14570.000","11310.000","","6620.000","6490.000","","8800.000","" +"2016-04-25 19:06:05 -0700","5500.000","","","30870.000","","","","","161790.000","","37730.000","","5970.000","","","","","54050.000","9510.000","","","","7240.000","","8350.000","8000.000","33180.000","6980.000","5300.000","","14470.000","6730.000","9680.000","","","","","","","14620.000","40050.000","8130.000","8640.000","7810.000","8680.000","15370.000","","9050.000","","40510.000","","20480.000","6540.000","","","","11620.000","12760.000","16130.000","6690.000","8520.000","","","14590.000","11330.000","","6640.000","6500.000","","8810.000","" +"2016-05-08 04:01:19 -0700","2190.000","","","33680.000","","","","","166150.000","","38540.000","","6070.000","","","","","57030.000","9530.000","","","","7240.000","","8360.000","8070.000","33200.000","6990.000","5300.000","","15320.000","6750.000","9680.000","","","","","","","15460.000","40050.000","7770.000","8640.000","7820.000","8780.000","15370.000","","9070.000","","40510.000","","20490.000","6560.000","","","","11320.000","12780.000","13240.000","6690.000","8540.000","","","15440.000","11330.000","","6660.000","6520.000","","8870.000","" +"2016-05-08 04:11:47 -0700","5500.000","","","33680.000","","","","","166190.000","","38540.000","","6070.000","","","","","57030.000","9530.000","","","","7240.000","","8360.000","8070.000","33200.000","6990.000","5300.000","","15320.000","6750.000","9680.000","","","","","","","15460.000","40050.000","7770.000","8640.000","7820.000","8780.000","15370.000","","9070.000","","40510.000","","20490.000","6560.000","","","","11320.000","12780.000","13240.000","6690.000","8540.000","","","15440.000","11330.000","","6660.000","6520.000","","8870.000","" +"2016-05-12 11:16:59 -0700","5530.000","","","34140.000","","","","","166760.000","","38540.000","","6070.000","","","","","58250.000","9530.000","","","","7240.000","","8360.000","8100.000","33200.000","6990.000","5300.000","","15320.000","6750.000","9680.000","","","","","","","15460.000","40050.000","7770.000","8640.000","7820.000","8800.000","15450.000","","9070.000","13140.000","40510.000","","20490.000","6560.000","","","","11320.000","12780.000","13260.000","6690.000","8540.000","5800.000","","15440.000","11360.000","","6660.000","6520.000","","9540.000","" +"2016-05-14 20:18:50 -0700","5530.000","","","34350.000","","","","","167370.000","","41490.000","","6070.000","","","","","58250.000","9530.000","","","","7240.000","","8360.000","8100.000","33200.000","6990.000","5300.000","","15320.000","6750.000","9680.000","","","","","","","15460.000","40050.000","7770.000","8640.000","7820.000","8800.000","15450.000","","9070.000","14130.000","40510.000","","20490.000","6560.000","","","","11570.000","12780.000","13260.000","6690.000","8540.000","5800.000","","15440.000","11360.000","","6660.000","6520.000","","9540.000","" +"2016-05-15 12:47:06 -0700","5530.000","","","34350.000","","","","","167410.000","","41490.000","","6070.000","","","","","58250.000","9530.000","","","","7240.000","","8360.000","8100.000","33200.000","6990.000","5300.000","","15320.000","6750.000","9680.000","","","","","","","15460.000","40050.000","7770.000","8640.000","7820.000","8800.000","15450.000","","9070.000","14130.000","40510.000","","20490.000","6560.000","","","","11570.000","12780.000","13260.000","6690.000","8540.000","5800.000","","15440.000","11360.000","","6660.000","6520.000","","9540.000","" +"2016-05-16 16:43:03 -0700","5530.000","","","34280.000","","","","","153470.000","","41490.000","","6090.000","27000.000","","","","58250.000","9530.000","","","","7240.000","","8360.000","8100.000","33200.000","6990.000","5300.000","","15320.000","6750.000","9680.000","","","","","","","15460.000","40050.000","7770.000","8640.000","7820.000","8800.000","15450.000","","9070.000","14180.000","40510.000","","20490.000","6560.000","","","","11570.000","12780.000","13260.000","6690.000","8540.000","5800.000","","15440.000","11360.000","","6660.000","6520.000","","9540.000","" +"2016-06-02 14:39:05 -0700","5650.000","","","34480.000","","","","","155470.000","","41780.000","","6090.000","28000.000","","","","60670.000","9530.000","","","","7240.000","","8360.000","8100.000","34810.000","6990.000","5300.000","","15720.000","6750.000","9740.000","","","7450.000","","","","15790.000","39920.000","7770.000","8640.000","7820.000","8800.000","15370.000","","9070.000","16630.000","40510.000","","20640.000","6560.000","","","","10970.000","12780.000","11740.000","6690.000","8540.000","7490.000","","15850.000","11360.000","","6660.000","6520.000","","9540.000","" +"2016-06-06 07:15:50 -0700","5460.000","","","33970.000","","","","","144680.000","","39180.000","","5880.000","26870.000","","","","58080.000","8910.000","","","","6970.000","","7980.000","7740.000","31860.000","6630.000","5080.000","","15240.000","6420.000","8860.000","","","7160.000","","","","14990.000","36910.000","7430.000","8320.000","7440.000","8420.000","14060.000","","8520.000","14700.000","40860.000","","20410.000","6290.000","","","","10290.000","11880.000","11480.000","6420.000","8150.000","7040.000","","15370.000","10700.000","","6390.000","6250.000","","8970.000","" +"2016-06-08 05:59:14 -0700","5410.000","","","33810.000","","","","","145520.000","","38990.000","","5840.000","26680.000","","","","58230.000","8820.000","","","","6930.000","","7920.000","7680.000","31750.000","6570.000","5060.000","","15180.000","6370.000","8890.000","","","7120.000","","","","14900.000","36780.000","7380.000","8300.000","7390.000","8370.000","14010.000","","8520.000","14620.000","40810.000","","20370.000","6230.000","","","","10270.000","11820.000","11440.000","6360.000","8100.000","7000.000","","15300.000","10680.000","","6330.000","6190.000","","8910.000","" +"2016-06-10 16:25:13 -0400","5300.000","","","33840.000","","","","","129320.000","","36180.000","","5690.000","25050.000","","","","54860.000","8380.000","","","","6720.000","","7620.000","7430.000","27320.000","6350.000","4950.000","","14810.000","6030.000","7950.000","","","6910.000","","","","13570.000","31160.000","7090.000","8040.000","7090.000","8140.000","11770.000","","8020.000","13420.000","39520.000","","20260.000","6010.000","","","","9560.000","11010.000","11270.000","6140.000","7780.000","6670.000","","14930.000","9870.000","","6110.000","5970.000","","8500.000","" +"2016-06-15 19:46:24 -0700","5580.000","","","34300.000","","","","","136750.000","","37970.000","","6630.000","26530.000","","","","55950.000","9040.000","","","","7220.000","","8210.000","8070.000","31590.000","6800.000","5290.000","","15930.000","6470.000","8400.000","7970.000","","8070.000","","","","14470.000","33530.000","7860.000","8580.000","7680.000","8730.000","13080.000","","8490.000","14100.000","40140.000","","20820.000","6460.000","","","","10840.000","11810.000","11550.000","6580.000","8370.000","7400.000","","16020.000","10410.000","","6550.000","6420.000","","9120.000","" +"2016-06-16 11:47:48 -0700","5580.000","","","34300.000","","","","","136750.000","","37970.000","","6630.000","26540.000","","","","55950.000","9040.000","","","","7220.000","","8210.000","8070.000","31590.000","6800.000","5290.000","","15930.000","6470.000","8400.000","7970.000","","8070.000","","","","14470.000","33530.000","7860.000","8580.000","7680.000","8730.000","13080.000","","8490.000","14100.000","40140.000","","20820.000","6460.000","","","","10840.000","11810.000","11550.000","6580.000","8370.000","7400.000","","16060.000","10410.000","","6550.000","6420.000","","9120.000","" +"2016-06-23 13:46:07 -0700","5640.000","","","34820.000","","16590.000","","","142900.000","","35330.000","","6340.000","27610.000","","","","56620.000","8660.000","","","","6930.000","","7920.000","7780.000","33130.000","6510.000","5000.000","","15590.000","6090.000","8110.000","10790.000","","7720.000","","","","14220.000","33210.000","7570.000","8270.000","7390.000","8440.000","12730.000","","6020.000","13600.000","39860.000","","20530.000","6170.000","","","","10490.000","11430.000","11260.000","6300.000","8080.000","7110.000","","15710.000","10130.000","","6260.000","6130.000","","8830.000","" +"2016-06-27 00:32:56 -0700","5940.000","","","35740.000","","128960.000","","","143870.000","","35990.000","","6630.000","28160.000","","","","57150.000","8950.000","","","","7250.000","","8220.000","8080.000","33630.000","6800.000","5380.000","5830.000","15980.000","6390.000","8470.000","11180.000","","8020.000","","","","14540.000","33690.000","7870.000","8650.000","7690.000","8740.000","13100.000","","6410.000","14160.000","40170.000","","20910.000","6460.000","","","6070.000","10860.000","11870.000","11620.000","6590.000","8380.000","7410.000","","16100.000","10510.000","","6560.000","6430.000","","9120.000","" +"2016-06-27 11:48:54 -0700","5920.000","","","35640.000","","128390.000","","","143250.000","","35760.000","","6610.000","27900.000","","","","57050.000","8930.000","","","","7160.000","","8200.000","8060.000","33610.000","6780.000","5290.000","5810.000","15710.000","6370.000","8450.000","11080.000","","8000.000","","","","14440.000","33670.000","7840.000","8580.000","7670.000","8720.000","13080.000","","6320.000","13940.000","40080.000","","20820.000","6440.000","","","6050.000","10840.000","11850.000","11600.000","6570.000","8360.000","7390.000","","15840.000","10420.000","","6540.000","6410.000","","9100.000","" +"2016-07-01 13:22:31 -0700","6010.000","","","35700.000","","129250.000","","","144270.000","25520.000","35780.000","","6640.000","30420.000","","","","58380.000","8960.000","","","","7180.000","","8230.000","8090.000","34250.000","6810.000","5320.000","7550.000","15890.000","6390.000","8480.000","13270.000","","8020.000","","","","14470.000","33700.000","7870.000","8590.000","7700.000","8740.000","13110.000","","6340.000","14000.000","40110.000","","20850.000","6470.000","","","6080.000","10860.000","11880.000","11630.000","6600.000","8380.000","9070.000","","16010.000","10560.000","","6570.000","6430.000","","9230.000","" +"2016-07-01 16:21:12 -0700","5760.000","","","35460.000","","120800.000","","","135380.000","24390.000","34780.000","","6400.000","29340.000","","","","57400.000","8360.000","","","","6770.000","","7670.000","7480.000","31420.000","6400.000","5080.000","7140.000","15270.000","6010.000","8070.000","12640.000","","7470.000","","","","13680.000","31440.000","7250.000","8180.000","7140.000","8190.000","11900.000","","6100.000","13350.000","39870.000","","20440.000","6060.000","","","5670.000","10070.000","11130.000","11390.000","6190.000","7830.000","8500.000","","15390.000","10080.000","","6160.000","6020.000","","8650.000","" +"2016-07-17 22:01:17 -0700","5770.000","","","38590.000","","131230.000","","","137020.000","7190.000","34690.000","","6430.000","31320.000","32800.000","31530.000","","58270.000","8540.000","","","","7010.000","","7910.000","7710.000","32300.000","6590.000","5030.000","8850.000","15380.000","6000.000","8050.000","12710.000","","8150.000","","8490.000","","14560.000","31520.000","7480.000","8220.000","7380.000","8460.000","11960.000","","6030.000","13440.000","39920.000","6760.000","20380.000","6250.000","","","5720.000","10160.000","11070.000","11320.000","6380.000","8070.000","7990.000","","15510.000","10280.000","","6350.000","6210.000","","8920.000","" +"2016-07-19 13:42:56 -0700","5470.000","","","38290.000","","130940.000","","","136740.000","6890.000","34390.000","","6130.000","31010.000","32500.000","31230.000","","57980.000","8250.000","","","","6710.000","","7610.000","7420.000","32000.000","6300.000","4730.000","8550.000","15080.000","5710.000","7760.000","12410.000","","7860.000","","8200.000","","14260.000","31210.000","7180.000","7920.000","7080.000","8170.000","11660.000","","5730.000","13140.000","39620.000","6470.000","20080.000","5960.000","","","5420.000","9860.000","10770.000","11020.000","6080.000","7780.000","7910.000","","15200.000","9980.000","","6050.000","5910.000","","8630.000","" +"2016-07-19 21:16:39 -0700","5470.000","","","38290.000","","131360.000","","","137150.000","6890.000","34460.000","","6130.000","31060.000","32500.000","31230.000","","58090.000","8250.000","","","","6710.000","","7610.000","7420.000","32020.000","6300.000","4730.000","8650.000","15150.000","5710.000","7760.000","12430.000","","7880.000","","8220.000","","14260.000","31210.000","7180.000","7920.000","7080.000","8170.000","11660.000","","5730.000","13160.000","39620.000","6470.000","20080.000","5960.000","","","5440.000","9860.000","10770.000","11020.000","6080.000","7780.000","7910.000","","15230.000","9980.000","","6050.000","5910.000","","8620.000","" +"2016-07-20 20:45:32 -0700","5470.000","","","38290.000","","131190.000","","","137000.000","6890.000","34550.000","","6130.000","31060.000","32460.000","31190.000","","58120.000","8250.000","","","","6710.000","","7610.000","7420.000","31950.000","6300.000","4780.000","8630.000","15130.000","5710.000","7660.000","12460.000","","7870.000","","8210.000","","14250.000","31130.000","7170.000","7910.000","7080.000","8170.000","11570.000","","5550.000","13080.000","39610.000","6470.000","19920.000","5960.000","","","5440.000","9820.000","10730.000","10820.000","6080.000","7780.000","7910.000","","15250.000","9940.000","","6050.000","5910.000","","8620.000","" +"2016-07-25 15:10:35 -0700","5360.000","","","39200.000","","134050.000","","","137610.000","6890.000","34580.000","","6120.000","31580.000","33340.000","32060.000","","58190.000","8250.000","","","","6710.000","","7610.000","7420.000","32380.000","6300.000","4780.000","8740.000","15240.000","5710.000","7660.000","12510.000","","7870.000","","8210.000","","14600.000","31150.000","7170.000","7950.000","7080.000","8170.000","11590.000","","5550.000","13060.000","39630.000","6470.000","19920.000","5960.000","","","5440.000","9810.000","10730.000","10820.000","6180.000","7780.000","7910.000","","15360.000","9950.000","","6050.000","5910.000","","8620.000","" +"2016-07-27 08:49:53 -0700","5490.000","","","39340.000","","134680.000","","","138240.000","7020.000","34710.000","","6250.000","31620.000","33350.000","32060.000","","58310.000","8380.000","","","","6840.000","","7750.000","7550.000","32590.000","6430.000","4910.000","8870.000","15400.000","5840.000","7790.000","12640.000","","8020.000","","8340.000","","14750.000","31290.000","7300.000","8080.000","7210.000","8300.000","11800.000","","5680.000","13190.000","39760.000","6600.000","20050.000","6090.000","","","5570.000","9940.000","10860.000","10960.000","6310.000","7910.000","8040.000","","15520.000","10080.000","","6180.000","6040.000","7260.000","8870.000","" +"2016-08-03 00:11:33 -0700","5490.000","","","39150.000","","135000.000","","","138530.000","7020.000","36170.000","","6250.000","31670.000","32650.000","31360.000","","58350.000","8380.000","","","","6840.000","","7750.000","7550.000","35630.000","6430.000","4910.000","8900.000","15390.000","5840.000","7790.000","13760.000","","8020.000","","8340.000","","14780.000","31290.000","7300.000","8080.000","7210.000","8300.000","11800.000","","5700.000","13330.000","39760.000","6600.000","20050.000","6090.000","","","6120.000","9940.000","10860.000","10960.000","6310.000","7910.000","8060.000","","15510.000","10080.000","","6180.000","6040.000","7260.000","8870.000","" +"2016-08-10 00:08:41 -0700","5410.000","","","39110.000","","136190.000","","","139760.000","2000.000","39530.000","","6300.000","32280.000","31690.000","30430.000","","58840.000","8300.000","","","","6750.000","","7660.000","7370.000","35450.000","6330.000","2100.000","7960.000","15370.000","3060.000","7720.000","11600.000","","6970.000","","8350.000","","14840.000","31400.000","7210.000","8010.000","7120.000","8130.000","11720.000","","5660.000","13250.000","39730.000","6500.000","20110.000","5990.000","","","5220.000","9810.000","9670.000","10930.000","6220.000","7820.000","7980.000","","15490.000","10140.000","","6080.000","5950.000","6110.000","8720.000","" +"2016-08-12 10:20:39 -0700","5320.000","","","40210.000","","136190.000","","","139750.000","1670.000","39320.000","","6210.000","32680.000","31570.000","30280.000","24320.000","58910.000","8280.000","","","","6760.000","","7670.000","7280.000","35520.000","6330.000","2100.000","7950.000","15380.000","3110.000","7740.000","11630.000","","6990.000","","8370.000","","14850.000","31410.000","7220.000","8020.000","7130.000","8050.000","11740.000","","5670.000","13170.000","39740.000","6500.000","20120.000","5990.000","","","5230.000","9820.000","9680.000","10840.000","6220.000","7830.000","7960.000","","15510.000","10150.000","","6080.000","5950.000","466480.000","8640.000","" +"2016-08-17 00:10:13 -0700","5310.000","","","40390.000","","136200.000","","","139750.000","1670.000","39400.000","","6210.000","32810.000","31550.000","30270.000","24310.000","58870.000","8310.000","","","15900.000","6750.000","","7700.000","7170.000","35510.000","6330.000","2100.000","7990.000","15480.000","3150.000","7730.000","11620.000","","6990.000","","8400.000","","14880.000","31400.000","7210.000","8010.000","7160.000","8020.000","11730.000","","5660.000","13170.000","39730.000","6500.000","20110.000","5990.000","","","5330.000","9880.000","9670.000","12870.000","6210.000","7830.000","7960.000","","15600.000","10050.000","","6080.000","5940.000","466470.000","8650.000","" +"2016-09-12 13:29:41 -0700","5620.000","","","45930.000","","144410.000","428.000","1950.000","145930.000","47910.000","41860.000","","6410.000","75080.000","76890.000","75260.000","69710.000","62780.000","8230.000","","9340.000","17060.000","6930.000","","7560.000","7380.000","36570.000","6310.000","2250.000","9590.000","15500.000","3070.000","7920.000","15490.000","4330.000","7080.000","6070.000","8380.000","","15210.000","32160.000","7240.000","8500.000","7030.000","8210.000","12510.000","13620.000","5910.000","14210.000","40330.000","6700.000","20360.000","5970.000","","","5830.000","10040.000","9810.000","14250.000","6290.000","7760.000","8050.000","","15620.000","10410.000","","6070.000","5930.000","514290.000","8740.000","3580.000" +"2016-09-12 18:43:11 -0700","5520.000","","","45830.000","","143880.000","428.000","1950.000","145450.000","3480.000","41750.000","","6300.000","34890.000","36630.000","35010.000","28690.000","62680.000","8130.000","","9240.000","16960.000","6830.000","","7460.000","7280.000","36470.000","6210.000","2250.000","9490.000","15380.000","3070.000","7820.000","15390.000","4230.000","6980.000","5960.000","8280.000","","15110.000","32020.000","7110.000","8390.000","6920.000","8110.000","12410.000","13520.000","5800.000","14100.000","40220.000","6600.000","20250.000","5870.000","","","5720.000","9930.000","9700.000","14150.000","6190.000","7660.000","7950.000","","15500.000","10310.000","","5960.000","5830.000","514190.000","8610.000","3580.000" +"2016-09-12 19:54:40 -0700","5620.000","","","45930.000","","143970.000","278.000","382.000","145550.000","2070.000","40400.000","","6300.000","33550.000","35290.000","33670.000","27350.000","61040.000","8230.000","","9340.000","16300.000","6830.000","","7560.000","7280.000","35920.000","6310.000","2140.000","8170.000","15370.000","3070.000","7920.000","13930.000","4330.000","7080.000","6070.000","8280.000","","15100.000","31510.000","7240.000","8390.000","7030.000","8110.000","11860.000","13620.000","5800.000","12770.000","40220.000","6700.000","20250.000","5970.000","","","5830.000","10040.000","9810.000","13510.000","6290.000","7760.000","8050.000","","15490.000","10310.000","","6070.000","5930.000","514190.000","8640.000","" +"2016-09-13 10:47:22 -0700","5620.000","","","45930.000","","144000.000","278.000","382.000","145580.000","2070.000","39460.000","","6300.000","32670.000","34410.000","32770.000","26320.000","60150.000","7240.000","","8290.000","15400.000","5840.000","","6570.000","6290.000","34900.000","5320.000","2140.000","8170.000","14490.000","3070.000","6870.000","13930.000","4330.000","7080.000","5080.000","7280.000","","14110.000","30450.000","6250.000","7480.000","6040.000","7120.000","7820.000","12700.000","5800.000","11900.000","40220.000","5710.000","20250.000","4980.000","","","5830.000","6200.000","9810.000","13510.000","5300.000","6770.000","7060.000","","14610.000","10310.000","","5070.000","4940.000","514190.000","7650.000","" +"2016-09-13 11:20:32 -0700","5620.000","","","45930.000","","144020.000","278.000","382.000","145600.000","2070.000","39460.000","","6300.000","32680.000","34410.000","32770.000","26320.000","60150.000","6570.000","","7680.000","15400.000","5180.000","","5950.000","5630.000","34900.000","4660.000","2140.000","8170.000","13880.000","3070.000","6870.000","13930.000","4330.000","7080.000","4400.000","6620.000","","13510.000","29860.000","5590.000","7480.000","5390.000","6480.000","7820.000","12700.000","5800.000","11990.000","40220.000","5040.000","20250.000","1350.000","","","5830.000","6200.000","9810.000","13510.000","4640.000","6140.000","7060.000","","14000.000","10310.000","","4410.000","4280.000","514190.000","7060.000","" +"2016-09-13 12:42:05 -0700","5770.000","","","46090.000","","144250.000","278.000","405.000","145830.000","2120.000","37270.000","","3450.000","30590.000","31520.000","29900.000","23450.000","57260.000","3740.000","","4830.000","12710.000","2340.000","","3150.000","2790.000","31980.000","1810.000","2180.000","5300.000","11790.000","3110.000","4030.000","11260.000","1480.000","4220.000","1550.000","3800.000","","10630.000","26960.000","2750.000","5300.000","2580.000","3680.000","7860.000","9870.000","2960.000","9140.000","37360.000","2240.000","17430.000","1390.000","","","2990.000","6240.000","7000.000","10660.000","1790.000","3330.000","4220.000","","11910.000","7630.000","","1560.000","1430.000","511350.000","4260.000","" +"2016-09-13 14:53:34 -0700","5770.000","","","46060.000","","146540.000","278.000","400.000","148120.000","2110.000","37240.000","","3450.000","30450.000","25160.000","23550.000","16500.000","54840.000","3740.000","","4830.000","11840.000","2310.000","","3150.000","2790.000","31980.000","1810.000","2180.000","5300.000","11680.000","3110.000","4030.000","11230.000","1480.000","4220.000","1550.000","3780.000","","10610.000","26960.000","2750.000","5270.000","2580.000","3670.000","7860.000","9870.000","2930.000","9140.000","37360.000","2240.000","17400.000","1390.000","","","2990.000","6240.000","7000.000","10660.000","1790.000","3330.000","4220.000","","11800.000","7610.000","","1560.000","1430.000","511320.000","4260.000","" +"2016-09-14 10:19:40 -0700","5770.000","","","46060.000","","149510.000","428.000","577.000","151080.000","2110.000","37240.000","","3450.000","30450.000","25160.000","23550.000","16500.000","54830.000","3740.000","","4830.000","11840.000","2310.000","","3150.000","2790.000","31980.000","1810.000","2180.000","5300.000","11680.000","3110.000","4030.000","11230.000","1480.000","4220.000","1550.000","3780.000","","10690.000","26960.000","2750.000","5250.000","2580.000","3670.000","7860.000","9870.000","2930.000","9140.000","37360.000","2240.000","17400.000","1390.000","","","3000.000","6240.000","7000.000","10660.000","1790.000","3330.000","4220.000","","11800.000","7610.000","","1560.000","1430.000","511320.000","4260.000","1550.000" +"2016-09-14 21:27:11 -0700","5770.000","","","46040.000","","149460.000","278.000","400.000","151030.000","2110.000","37240.000","","3450.000","29320.000","25160.000","23550.000","16500.000","54840.000","3740.000","","4830.000","10680.000","2310.000","","3150.000","2790.000","31990.000","1810.000","2180.000","5300.000","10550.000","3110.000","4030.000","11240.000","1480.000","4220.000","1550.000","3780.000","","10690.000","26960.000","2750.000","5250.000","2580.000","3670.000","7860.000","12430.000","2930.000","9140.000","37360.000","2240.000","17400.000","1390.000","","","3000.000","4960.000","7000.000","9330.000","1790.000","3330.000","4220.000","","10670.000","7610.000","","1560.000","1430.000","511320.000","4260.000","" +"2016-09-18 12:49:25 -0700","5870.000","","","46270.000","","155490.000","428.000","681.000","156780.000","","37280.000","","3450.000","29440.000","25260.000","23630.000","16570.000","54920.000","3740.000","","4840.000","10910.000","2310.000","","3150.000","2790.000","32460.000","1810.000","2220.000","5340.000","10550.000","3110.000","4050.000","11280.000","3490.000","4220.000","1550.000","3780.000","","10700.000","27250.000","2750.000","5290.000","2580.000","3670.000","7890.000","12470.000","2950.000","9180.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","4980.000","7270.000","9340.000","1790.000","3330.000","4670.000","","10670.000","7640.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-20 12:48:36 -0700","5870.000","","","46650.000","","156000.000","428.000","681.000","157310.000","","37330.000","","3450.000","29830.000","28650.000","26740.000","19720.000","54950.000","3740.000","","4840.000","10910.000","2310.000","","3150.000","2790.000","32470.000","1810.000","3920.000","5340.000","10550.000","3110.000","4050.000","14250.000","3490.000","4220.000","1550.000","3780.000","","10700.000","27260.000","2750.000","5290.000","2580.000","3670.000","7900.000","12470.000","2950.000","9180.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","4980.000","7270.000","9340.000","1790.000","3330.000","4670.000","","10670.000","7640.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-20 14:20:50 -0700","5870.000","","","46120.000","","156000.000","428.000","681.000","157310.000","","37330.000","","3450.000","29820.000","28630.000","26730.000","19710.000","54950.000","3740.000","","4840.000","10910.000","2310.000","","3150.000","2790.000","32470.000","1810.000","3920.000","5340.000","10550.000","3110.000","4050.000","14250.000","3490.000","4220.000","1550.000","3780.000","","10700.000","27260.000","2750.000","5290.000","2580.000","3670.000","7900.000","12470.000","2950.000","9180.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","4980.000","7270.000","9340.000","1790.000","3330.000","4670.000","","10670.000","7640.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-21 00:19:10 -0700","5870.000","","","46120.000","","156580.000","428.000","681.000","158970.000","","37350.000","","3450.000","29820.000","28630.000","26730.000","19710.000","54970.000","3740.000","","4840.000","10910.000","2310.000","","3150.000","2790.000","32550.000","1810.000","3920.000","5340.000","10550.000","3110.000","4050.000","14250.000","3490.000","4220.000","1550.000","3780.000","","10700.000","27260.000","2750.000","5290.000","2580.000","3670.000","7900.000","12470.000","2960.000","9180.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","4980.000","7270.000","9350.000","1790.000","3330.000","4670.000","","10670.000","7650.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-21 23:27:38 -0700","5870.000","","","46660.000","","163260.000","428.000","681.000","165680.000","","37390.000","","3450.000","30070.000","28890.000","26990.000","19960.000","54970.000","3740.000","","4840.000","10910.000","2310.000","","3150.000","2790.000","32550.000","1810.000","3920.000","5340.000","10550.000","3110.000","4050.000","14280.000","3490.000","4220.000","1550.000","3780.000","","10790.000","27570.000","2750.000","5290.000","2580.000","3670.000","8210.000","12480.000","2960.000","9190.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","5350.000","7270.000","9350.000","1790.000","3330.000","4680.000","","10670.000","7660.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-22 10:23:47 -0700","5870.000","","","46660.000","","163440.000","428.000","681.000","165850.000","","37390.000","","3450.000","30070.000","28890.000","26990.000","19960.000","54970.000","3740.000","","4840.000","10910.000","2310.000","","3150.000","2790.000","32550.000","1810.000","3920.000","5340.000","10550.000","3110.000","4050.000","14280.000","3490.000","4220.000","1550.000","3780.000","","10790.000","27590.000","2750.000","5290.000","2580.000","3670.000","8210.000","12480.000","2960.000","9190.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","5350.000","7270.000","9350.000","1790.000","3330.000","4680.000","","10670.000","7660.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-22 18:44:49 -0700","6070.000","","","46900.000","","163620.000","428.000","13030.000","166030.000","","37420.000","","3450.000","30100.000","28910.000","27010.000","19990.000","54970.000","3740.000","","4840.000","10830.000","2310.000","","3150.000","2790.000","32550.000","1810.000","3920.000","5340.000","10550.000","3110.000","4050.000","14310.000","3490.000","4220.000","1550.000","3780.000","","10790.000","27590.000","2750.000","5290.000","2580.000","3670.000","8210.000","12510.000","2960.000","9220.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","5350.000","7270.000","9350.000","1790.000","3330.000","4610.000","","10670.000","7690.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-23 09:09:29 -0700","6070.000","","","46900.000","","163620.000","428.000","681.000","166030.000","","37420.000","","3450.000","30100.000","28910.000","27010.000","19990.000","54970.000","3740.000","","4840.000","10830.000","2310.000","","3150.000","2790.000","32550.000","1810.000","3920.000","5340.000","10550.000","3110.000","4050.000","14310.000","3490.000","4220.000","1550.000","3780.000","","10790.000","27590.000","2750.000","5290.000","2580.000","3670.000","8210.000","12510.000","2960.000","9220.000","37360.000","2240.000","17420.000","1390.000","","","3030.000","5350.000","7270.000","9350.000","1790.000","3330.000","4610.000","","10670.000","7690.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-27 15:13:18 -0700","6180.000","","","47280.000","","166280.000","428.000","681.000","165940.000","","37490.000","","3450.000","30410.000","30840.000","28940.000","21900.000","56580.000","3740.000","","4840.000","10880.000","2310.000","","3150.000","2790.000","32560.000","1810.000","3920.000","5340.000","10670.000","3110.000","4050.000","14360.000","3490.000","4220.000","1550.000","3780.000","","10810.000","27590.000","2750.000","5400.000","2580.000","3670.000","8210.000","12510.000","2960.000","9210.000","37370.000","2240.000","17420.000","1390.000","","","3030.000","5350.000","7270.000","9370.000","1790.000","3330.000","4620.000","","10790.000","7690.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-28 09:17:06 -0700","6180.000","","","47280.000","","166280.000","428.000","681.000","165940.000","","37490.000","","3450.000","30410.000","30840.000","28940.000","21900.000","56580.000","3740.000","","4840.000","10880.000","2310.000","","3150.000","2790.000","32560.000","1810.000","3920.000","5340.000","10670.000","3110.000","4050.000","14360.000","3490.000","4220.000","1550.000","3780.000","","10810.000","27590.000","2750.000","5400.000","2580.000","3670.000","8210.000","12510.000","2960.000","9210.000","37370.000","2240.000","17420.000","1390.000","","","3030.000","5350.000","7270.000","9370.000","1790.000","3330.000","4620.000","","10790.000","7690.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-09-29 14:56:26 -0700","6180.000","","","47280.000","","168040.000","428.000","681.000","167710.000","","37500.000","","3450.000","30410.000","30840.000","28940.000","21900.000","56610.000","3740.000","","4840.000","10920.000","2310.000","","3150.000","2790.000","32560.000","1810.000","3920.000","5340.000","10670.000","3110.000","4050.000","14360.000","3490.000","4220.000","1550.000","3780.000","","10810.000","27630.000","2750.000","5400.000","2580.000","3670.000","8210.000","13570.000","3310.000","9210.000","37370.000","2240.000","17420.000","1390.000","","","3030.000","5350.000","7270.000","9740.000","1790.000","3330.000","4620.000","","10790.000","7950.000","","1560.000","1430.000","511380.000","4290.000","1550.000" +"2016-10-06 23:04:11 -0700","6180.000","","","47300.000","19030.000","169500.000","428.000","681.000","169180.000","","37580.000","","3450.000","31050.000","32330.000","30450.000","24170.000","56830.000","3740.000","","4840.000","11120.000","2310.000","","2900.000","2790.000","30040.000","1810.000","2080.000","5520.000","9230.000","3110.000","4050.000","14420.000","3490.000","4220.000","1550.000","3780.000","","10840.000","27640.000","2750.000","5500.000","2250.000","3120.000","8210.000","13570.000","3310.000","9470.000","37380.000","2270.000","17420.000","1390.000","","","3230.000","5200.000","7270.000","9740.000","1790.000","2930.000","11200.000","","9350.000","7990.000","","1560.000","1430.000","511380.000","4290.000","1890.000" +"2016-10-07 15:30:41 -0700","6210.000","","","47430.000","","169600.000","428.000","681.000","169280.000","","37590.000","","3460.000","31140.000","34450.000","32580.000","26360.000","56890.000","3750.000","","4850.000","11130.000","2320.000","","2910.000","2790.000","30050.000","1820.000","2080.000","5530.000","9240.000","3110.000","4060.000","14430.000","3500.000","4220.000","1560.000","3790.000","","10850.000","27650.000","2750.000","5510.000","2260.000","3130.000","8210.000","13570.000","3310.000","9470.000","37380.000","2280.000","17430.000","1390.000","","","3240.000","5200.000","7280.000","9750.000","1800.000","2940.000","11300.000","","9360.000","7990.000","","1570.000","1430.000","511390.000","4300.000","1890.000" +"2016-10-12 09:13:45 -0700","6210.000","","","47580.000","","170600.000","428.000","681.000","170270.000","","37590.000","","3460.000","31520.000","35150.000","33270.000","27060.000","56910.000","3750.000","","4850.000","11130.000","2320.000","","2910.000","2790.000","30110.000","1820.000","2080.000","5530.000","9240.000","3110.000","4060.000","14430.000","3500.000","4220.000","1560.000","3790.000","","11110.000","27650.000","2750.000","5510.000","2260.000","3130.000","8210.000","13570.000","3310.000","9470.000","37430.000","2280.000","17430.000","1390.000","","","3290.000","5200.000","7280.000","9750.000","1800.000","2940.000","11410.000","","9360.000","7990.000","","1570.000","1430.000","511390.000","4300.000","1890.000" +"2016-10-19 00:22:41 -0700","6190.000","","","49050.000","","177590.000","428.000","1380.000","176510.000","","37590.000","","3470.000","31930.000","35480.000","33600.000","27390.000","56910.000","4060.000","","4850.000","11130.000","2370.000","","2910.000","2790.000","30190.000","1820.000","2080.000","5530.000","9240.000","3110.000","4060.000","14410.000","3500.000","4220.000","1560.000","3790.000","","11110.000","28110.000","2750.000","5560.000","2260.000","3130.000","8220.000","13570.000","3310.000","9470.000","37780.000","2280.000","17430.000","1390.000","","","3290.000","5400.000","7280.000","9740.000","1800.000","2940.000","4780.000","","9360.000","7990.000","","1570.000","1430.000","511390.000","4300.000","1890.000" +"2016-10-24 11:10:17 -0700","6190.000","","","49230.000","","183270.000","428.000","1380.000","182160.000","","37590.000","","3500.000","32510.000","36640.000","34770.000","28550.000","57290.000","4060.000","","4850.000","11150.000","2370.000","","2910.000","2790.000","30220.000","1820.000","2080.000","5530.000","9240.000","3110.000","4060.000","14510.000","3500.000","4270.000","1560.000","3790.000","","11110.000","28140.000","2750.000","5560.000","2260.000","3130.000","8310.000","13570.000","3300.000","9510.000","38020.000","2280.000","17430.000","1390.000","","","3290.000","5540.000","7280.000","9740.000","1800.000","2940.000","4780.000","","9360.000","8030.000","","1570.000","1430.000","511380.000","4300.000","1940.000" +"2016-10-25 10:41:32 -0700","6190.000","","","49650.000","","183200.000","428.000","1380.000","182090.000","","38490.000","2470.000","3500.000","33150.000","37480.000","35600.000","29380.000","57390.000","4060.000","","4850.000","11150.000","2370.000","","2910.000","2790.000","30220.000","1820.000","2080.000","5530.000","9240.000","3110.000","4060.000","14510.000","3500.000","4270.000","1560.000","3790.000","","11110.000","28140.000","2750.000","5560.000","2260.000","3130.000","8310.000","13570.000","3300.000","9510.000","38020.000","2280.000","17430.000","1390.000","","","3290.000","5540.000","7280.000","9740.000","1800.000","2940.000","4780.000","","9360.000","8030.000","","1570.000","1430.000","511380.000","4300.000","1940.000" +"2016-10-25 16:34:44 -0700","6140.000","","","50250.000","","183160.000","386.000","1340.000","182050.000","","38450.000","2420.000","3460.000","33240.000","37530.000","35660.000","29440.000","57420.000","4020.000","","4810.000","11110.000","2330.000","","2870.000","2750.000","30180.000","1780.000","2040.000","5490.000","9200.000","3060.000","4020.000","14460.000","3460.000","4230.000","1510.000","3750.000","","11070.000","28100.000","2710.000","5520.000","2220.000","3080.000","8260.000","13530.000","3260.000","9460.000","37980.000","2240.000","17390.000","1340.000","","","3240.000","5500.000","7240.000","9700.000","1760.000","2900.000","4740.000","","9320.000","7990.000","","1530.000","1390.000","511340.000","4260.000","1900.000" +"2016-10-27 11:29:28 -0700","6040.000","","","49120.000","","181520.000","386.000","1340.000","180410.000","","38450.000","2420.000","3460.000","33240.000","37530.000","35660.000","29440.000","57420.000","4020.000","","4810.000","11110.000","2330.000","","2870.000","2750.000","30180.000","1780.000","2040.000","5490.000","9200.000","3060.000","4020.000","14460.000","3460.000","4230.000","1510.000","3750.000","","11070.000","28100.000","2710.000","5520.000","2220.000","3080.000","8260.000","13530.000","3260.000","9460.000","37980.000","2240.000","17390.000","1340.000","","","3430.000","5500.000","7240.000","9700.000","1760.000","2900.000","4740.000","","9320.000","7990.000","","1530.000","1390.000","511340.000","4260.000","1900.000" +"2016-10-28 14:52:55 -0700","6030.000","","","49490.000","","181720.000","386.000","1330.000","180600.000","","38440.000","2410.000","3450.000","32420.000","36800.000","34890.000","29710.000","57410.000","4010.000","","4800.000","10140.000","2320.000","","2860.000","2740.000","30170.000","1770.000","2030.000","5480.000","8080.000","3060.000","4010.000","14450.000","3450.000","4220.000","1500.000","3730.000","","11090.000","28090.000","2700.000","4570.000","2210.000","3070.000","8250.000","13520.000","3250.000","9460.000","37970.000","2230.000","17380.000","1330.000","","","3420.000","5490.000","7230.000","9690.000","1750.000","2890.000","6080.000","","8200.000","7980.000","","1520.000","1380.000","511330.000","4250.000","1900.000" +"2016-11-02 00:34:43 -0700","6090.000","","","50500.000","","182780.000","386.000","1360.000","182980.000","","38690.000","2410.000","3450.000","32780.000","38410.000","36820.000","28640.000","57660.000","4010.000","1320.000","4800.000","10850.000","2320.000","","2860.000","2740.000","30370.000","1770.000","2030.000","4540.000","8130.000","3060.000","4010.000","14490.000","3450.000","4220.000","1500.000","3730.000","","11100.000","28700.000","2700.000","4600.000","2210.000","3070.000","8200.000","13520.000","3470.000","9480.000","37970.000","2230.000","17380.000","1330.000","6780.000","","3420.000","5490.000","7230.000","9870.000","1750.000","2890.000","6080.000","","8250.000","8290.000","","1520.000","1380.000","511330.000","5550.000","1770.000" +"2016-11-14 11:15:25 -0800","6090.000","4040.000","183440.000","53530.000","","183360.000","386.000","1390.000","183480.000","","39030.000","2410.000","3270.000","34320.000","40850.000","39300.000","32120.000","59130.000","4390.000","1310.000","4790.000","10880.000","2350.000","","2850.000","2740.000","19760.000","1760.000","2030.000","4540.000","8910.000","3060.000","4010.000","15660.000","3460.000","4340.000","1500.000","3760.000","1520.000","11670.000","28750.000","2700.000","7080.000","2200.000","3070.000","8170.000","13900.000","3500.000","9470.000","37970.000","2230.000","17410.000","1330.000","7530.000","","3400.000","5650.000","7160.000","10790.000","1740.000","2890.000","6190.000","6100.000","9030.000","8620.000","143.000","1510.000","1380.000","511350.000","5570.000","1770.000" +"2016-11-21 13:53:57 -0800","6090.000","4040.000","185030.000","54010.000","","184880.000","386.000","1390.000","185110.000","","39030.000","2410.000","3270.000","36400.000","46220.000","44690.000","37480.000","59700.000","4390.000","50660.000","4790.000","10090.000","2350.000","2500.000","2850.000","2740.000","19760.000","1760.000","2030.000","4540.000","8910.000","3060.000","4010.000","15750.000","3460.000","4340.000","1500.000","3760.000","1520.000","11670.000","28750.000","2700.000","7080.000","2200.000","3070.000","8170.000","13900.000","3500.000","9470.000","37970.000","2230.000","17410.000","1330.000","7530.000","","3400.000","5940.000","7160.000","10790.000","1740.000","2890.000","6190.000","6110.000","9030.000","8620.000","143.000","1510.000","1380.000","511350.000","5570.000","1770.000" +"2016-11-21 14:25:19 -0800","6090.000","4040.000","185400.000","54010.000","","185250.000","386.000","1390.000","185480.000","","39030.000","2410.000","3270.000","36400.000","46220.000","44690.000","37480.000","59700.000","4390.000","50660.000","4790.000","10090.000","2350.000","2500.000","2850.000","2740.000","19830.000","1760.000","2030.000","4540.000","8910.000","3060.000","4010.000","15750.000","3460.000","4340.000","1500.000","3760.000","1520.000","11670.000","28750.000","2700.000","7080.000","2200.000","3070.000","8170.000","13900.000","3500.000","9660.000","37970.000","2230.000","17410.000","1330.000","7530.000","","3400.000","5940.000","7160.000","10790.000","1740.000","2890.000","6190.000","6110.000","9030.000","8620.000","143.000","1510.000","1380.000","511350.000","5570.000","1770.000" +"2016-11-28 12:55:38 -0800","6940.000","4040.000","185530.000","54130.000","","185290.000","386.000","1400.000","185600.000","","39000.000","2410.000","3270.000","37450.000","46480.000","44950.000","37740.000","59180.000","4390.000","50660.000","4790.000","9990.000","2350.000","2500.000","2850.000","2740.000","19950.000","1760.000","2030.000","4540.000","8910.000","3060.000","4010.000","15820.000","3460.000","4340.000","1500.000","3760.000","1520.000","12630.000","28760.000","2700.000","7000.000","2200.000","3070.000","8170.000","13900.000","3500.000","9660.000","37940.000","2230.000","17410.000","1330.000","7530.000","3110.000","3400.000","5920.000","7160.000","10790.000","1740.000","2890.000","5090.000","6110.000","9030.000","8620.000","143.000","1510.000","1380.000","511350.000","5500.000","1770.000" diff --git a/test/size.txt b/test/size.txt index a4a26bf48dc0..f37ea54faa10 100644 --- a/test/size.txt +++ b/test/size.txt @@ -1,44 +1,76 @@ - max | min | gzip | brotli | file - --- | --- | --- | --- | --- - 55.04 kB | 5.58 kB | 2.31 kB | 1.99 kB | alp.js / alp.max.js -653.46 kB | 136.75 kB | 40.88 kB | 35.58 kB | v0.js / amp.js -249.42 kB | 37.97 kB | 12.84 kB | 11.4 kB | v0/amp-access-0.1.js - 45.05 kB | 6.63 kB | 2.62 kB | 2.22 kB | v0/amp-accordion-0.1.js -186.09 kB | 26.54 kB | 9.7 kB | 8.65 kB | v0/amp-ad-0.1.js -259.11 kB | 55.95 kB | 20.39 kB | 17.7 kB | v0/amp-analytics-0.1.js - 90.28 kB | 9.04 kB | 3.55 kB | 3.1 kB | v0/amp-anim-0.1.js - 88.6 kB | 7.22 kB | 2.95 kB | 2.55 kB | v0/amp-audio-0.1.js - 81.97 kB | 8.21 kB | 3.15 kB | 2.72 kB | v0/amp-brid-player-0.1.js -105.55 kB | 8.07 kB | 3.08 kB | 2.66 kB | v0/amp-brightcove-0.1.js -209.65 kB | 31.59 kB | 9.77 kB | 8.67 kB | v0/amp-carousel-0.1.js - 74.56 kB | 6.8 kB | 2.77 kB | 2.37 kB | v0/amp-dailymotion-0.1.js - 93.06 kB | 5.29 kB | 2.29 kB | 1.99 kB | v0/amp-dynamic-css-classes-0.1.js -146.44 kB | 15.93 kB | 6.32 kB | 5.62 kB | v0/amp-facebook-0.1.js - 35.17 kB | 6.47 kB | 2.62 kB | 2.26 kB | v0/amp-fit-text-0.1.js -101.36 kB | 8.4 kB | 3.33 kB | 2.89 kB | v0/amp-font-0.1.js - 96.41 kB | 7.97 kB | 3.26 kB | 2.81 kB | v0/amp-form-0.1.js - 85.31 kB | 8.07 kB | 3.1 kB | 2.65 kB | v0/amp-fx-flying-carpet-0.1.js -142.71 kB | 14.47 kB | 5.57 kB | 4.93 kB | v0/amp-iframe-0.1.js - 229.2 kB | 33.53 kB | 10.47 kB | 9.28 kB | v0/amp-image-lightbox-0.1.js - 99.38 kB | 7.86 kB | 3.08 kB | 2.66 kB | v0/amp-instagram-0.1.js - 85.49 kB | 8.58 kB | 3.5 kB | 3.04 kB | v0/amp-install-serviceworker-0.1.js - 81.4 kB | 7.68 kB | 3.04 kB | 2.61 kB | v0/amp-jwplayer-0.1.js -110.87 kB | 8.73 kB | 3.41 kB | 2.94 kB | v0/amp-kaltura-player-0.1.js -139.83 kB | 13.08 kB | 4.58 kB | 4.04 kB | v0/amp-lightbox-0.1.js -111.73 kB | 8.49 kB | 3.39 kB | 2.96 kB | v0/amp-list-0.1.js -152.12 kB | 14.1 kB | 5.17 kB | 4.56 kB | v0/amp-live-list-0.1.js -145.88 kB | 40.14 kB | 14.38 kB | 12.91 kB | v0/amp-mustache-0.1.js -125.91 kB | 20.82 kB | 6.19 kB | 5.42 kB | v0/amp-pinterest-0.1.js - 73.67 kB | 6.46 kB | 2.61 kB | 2.23 kB | v0/amp-reach-player-0.1.js - 91.39 kB | 10.84 kB | 3.86 kB | 3.34 kB | v0/amp-sidebar-0.1.js -108.67 kB | 11.81 kB | 4.42 kB | 3.89 kB | v0/amp-slides-0.1.js -105.84 kB | 11.55 kB | 4.39 kB | 3.75 kB | v0/amp-social-share-0.1.js - 74.25 kB | 6.58 kB | 2.66 kB | 2.26 kB | v0/amp-soundcloud-0.1.js - 82.22 kB | 8.37 kB | 3.14 kB | 2.69 kB | v0/amp-springboard-player-0.1.js - 86.22 kB | 7.4 kB | 2.95 kB | 2.54 kB | v0/amp-sticky-ad-0.1.js -146.91 kB | 16.06 kB | 6.35 kB | 5.65 kB | v0/amp-twitter-0.1.js -112.08 kB | 10.41 kB | 4.01 kB | 3.46 kB | v0/amp-user-notification-0.1.js - 74.17 kB | 6.55 kB | 2.65 kB | 2.26 kB | v0/amp-vimeo-0.1.js - 73.71 kB | 6.42 kB | 2.61 kB | 2.23 kB | v0/amp-vine-0.1.js -114.22 kB | 9.12 kB | 3.57 kB | 3.1 kB | v0/amp-youtube-0.1.js -155.99 kB | 34.3 kB | 11.9 kB | 10.67 kB | current-min/f.js / current/integration.js \ No newline at end of file + max | min | gzip | file + --- | --- | --- | --- + 77.32 kB | 7.25 kB | 2.94 kB | alp.js / alp.max.js +224.19 kB | 7.85 kB | 3.15 kB | amp4ads-host-v0.js / amp-inabox-host.js + 1.03 MB | 191.45 kB | 56.93 kB | amp4ads-v0.js / amp-inabox.js +956.91 kB | 188.54 kB | 56.07 kB | shadow-v0.js / amp-shadow.js + 5.8 kB | 386 B | 250 B | sw-kill.js / sw-kill.max.js + 17.32 kB | 2.08 kB | 1.07 kB | sw.js / sw.max.js +994.58 kB | 188.89 kB | 56.13 kB | v0.js / amp.js + 353.2 kB | 39.77 kB | 13.25 kB | v0/amp-access-0.1.js +164.73 kB | 2.8 kB | 1.31 kB | v0/amp-access-laterpay-0.1.js + 79.31 kB | 4.43 kB | 1.85 kB | v0/amp-accordion-0.1.js +555.19 kB | 41.2 kB | 14.11 kB | v0/amp-ad-0.1.js +497.25 kB | 52.95 kB | 17.8 kB | v0/amp-ad-network-adsense-impl-0.1.js +493.34 kB | 52.31 kB | 17.57 kB | v0/amp-ad-network-doubleclick-impl-0.1.js +473.54 kB | 45.11 kB | 15.19 kB | v0/amp-ad-network-fake-impl-0.1.js +382.09 kB | 64.77 kB | 23.02 kB | v0/amp-analytics-0.1.js + 84.33 kB | 4.34 kB | 2.01 kB | v0/amp-anim-0.1.js +273.52 kB | 55.38 kB | 18.94 kB | v0/amp-animation-0.1.js +181.01 kB | 5.71 kB | 2.34 kB | v0/amp-apester-media-0.1.js +194.15 kB | 9.78 kB | 3.88 kB | v0/amp-app-banner-0.1.js + 73.16 kB | 2.34 kB | 1.16 kB | v0/amp-audio-0.1.js +175.65 kB | 5.21 kB | 2.33 kB | v0/amp-auto-ads-0.1.js +263.91 kB | 32.24 kB | 10.53 kB | v0/amp-bind-0.1.js + 75.37 kB | 2.85 kB | 1.04 kB | v0/amp-brid-player-0.1.js + 95.38 kB | 2.73 kB | 1.21 kB | v0/amp-brightcove-0.1.js +255.14 kB | 20.07 kB | 6.45 kB | v0/amp-carousel-0.1.js + 73.83 kB | 1.76 kB | 883 B | v0/amp-dailymotion-0.1.js +131.79 kB | 2.02 kB | 1.02 kB | v0/amp-dynamic-css-classes-0.1.js +190.09 kB | 4.54 kB | 2.02 kB | v0/amp-experiment-0.1.js +221.25 kB | 10.07 kB | 4.53 kB | v0/amp-facebook-0.1.js + 76.72 kB | 3.1 kB | 1.35 kB | v0/amp-fit-text-0.1.js +148.73 kB | 4.03 kB | 1.78 kB | v0/amp-font-0.1.js +247.75 kB | 19.3 kB | 6.95 kB | v0/amp-form-0.1.js +178.32 kB | 3.89 kB | 1.8 kB | v0/amp-fresh-0.1.js +164.54 kB | 3.43 kB | 1.48 kB | v0/amp-fx-flying-carpet-0.1.js + 73.2 kB | 1.5 kB | 753 B | v0/amp-gfycat-0.1.js + 74.58 kB | 4.15 kB | 1.87 kB | v0/amp-google-vrview-image-0.1.js + 93.26 kB | 1.51 kB | 769 B | v0/amp-hulu-0.1.js +235.01 kB | 13.3 kB | 5.56 kB | v0/amp-iframe-0.1.js +315.08 kB | 28.79 kB | 9.35 kB | v0/amp-image-lightbox-0.1.js + 95.86 kB | 2.69 kB | 1.26 kB | v0/amp-instagram-0.1.js + 171.2 kB | 7.46 kB | 3.18 kB | v0/amp-install-serviceworker-0.1.js + 94.7 kB | 2.28 kB | 970 B | v0/amp-jwplayer-0.1.js + 94.73 kB | 3.06 kB | 1.33 kB | v0/amp-kaltura-player-0.1.js +221.48 kB | 9.34 kB | 3.55 kB | v0/amp-lightbox-0.1.js +216.86 kB | 14.41 kB | 4.81 kB | v0/amp-lightbox-viewer-0.1.js +160.71 kB | 3.85 kB | 1.76 kB | v0/amp-list-0.1.js +217.21 kB | 9.69 kB | 3.77 kB | v0/amp-live-list-0.1.js +163.24 kB | 37.91 kB | 13.65 kB | v0/amp-mustache-0.1.js + 74.37 kB | 2.22 kB | 947 B | v0/amp-o2-player-0.1.js +216.57 kB | 8.75 kB | 3.38 kB | v0/amp-ooyala-player-0.1.js +201.65 kB | 17.53 kB | 4.98 kB | v0/amp-pinterest-0.1.js +212.19 kB | 10.7 kB | 5.67 kB | v0/amp-playbuzz-0.1.js + 72.93 kB | 1.32 kB | 663 B | v0/amp-reach-player-0.1.js +205.15 kB | 8.69 kB | 3.83 kB | v0/amp-reddit-0.1.js + 82.36 kB | 3.67 kB | 1.56 kB | v0/amp-selector-0.1.js +166.42 kB | 3.78 kB | 1.78 kB | v0/amp-share-tracking-0.1.js +185.88 kB | 6.66 kB | 2.56 kB | v0/amp-sidebar-0.1.js +185.23 kB | 7.15 kB | 2.94 kB | v0/amp-slides-0.1.js +187.75 kB | 14.17 kB | 5.46 kB | v0/amp-social-share-0.1.js + 73.95 kB | 1.74 kB | 833 B | v0/amp-soundcloud-0.1.js + 75.4 kB | 2.88 kB | 1.01 kB | v0/amp-springboard-player-0.1.js +186.39 kB | 4.96 kB | 2.06 kB | v0/amp-sticky-ad-0.1.js +187.43 kB | 5.67 kB | 2.34 kB | v0/amp-sticky-ad-1.0.js +221.61 kB | 10.19 kB | 4.57 kB | v0/amp-twitter-0.1.js +181.36 kB | 8.68 kB | 3.43 kB | v0/amp-user-notification-0.1.js +212.24 kB | 9.02 kB | 3.6 kB | v0/amp-video-0.1.js +124.93 kB | 2.86 kB | 1.25 kB | v0/amp-viewer-integration-0.1.js + 73.25 kB | 1.51 kB | 766 B | v0/amp-vimeo-0.1.js + 72.84 kB | 1.37 kB | 719 B | v0/amp-vine-0.1.js +690.35 kB | 511.78 kB | 167.63 kB | v0/amp-viz-vega-0.1.js +221.93 kB | 9.78 kB | 3.89 kB | v0/amp-youtube-0.1.js + 19.58 kB | 2.03 kB | 1.04 kB | v0/cache-service-worker-0.1.js +227.34 kB | 7.72 kB | 3.02 kB | current-min/ampcontext-v0.js / current/ampcontext-lib.js +293.65 kB | 61.35 kB | 20.59 kB | current-min/f.js / current/integration.js \ No newline at end of file diff --git a/testing/ad-iframe.js b/testing/ad-iframe.js deleted file mode 100644 index 18df6085e9de..000000000000 --- a/testing/ad-iframe.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright 2016 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -import {createIframePromise} from './iframe'; -import {markElementScheduledForTesting} from '../src/custom-element'; - - -/** - * Creates an iframe with an ad inside it for use in tests. - * - * Returns a promise for when the ad is usable for testing that produces - * an object with properties related to the created iframe and utility methods: - * - win: The created window. - * - doc: The created document. - * - iframe: The host iframe element. Useful for e.g. resizing. - * - awaitEvent: A function that returns a promise for when the given custom - * event fired at least the given number of times. - * - errors: Array of console.error fired during page load. - * - * @param {string} name Type of element to create. - * @param {function(!Window)} installer Function to install ad. - * @param {!Object} attributes Attributes to add to the element. - * @param {?string} canonical Rel, href link for element. - * @param {function(!Element)=} opt_handleElement Called just before adding - * element to iframe. - * @param {function(!Window)=} opt_beforeLayoutCallback Called just before any - * other JS executes in the window. - * @return {!Promise} - */ -export function createAdPromise(name, attributes, canonical, - opt_handleElement, opt_beforeLayoutCallback) { - return createIframePromise(undefined, opt_beforeLayoutCallback) - .then(iframe => { - iframe.iframe.style.height = '400px'; - iframe.iframe.style.width = '400px'; - markElementScheduledForTesting(iframe.win, 'amp-user-notification'); - if (canonical) { - const link = iframe.doc.createElement('link'); - link.setAttribute('rel', 'canonical'); - link.setAttribute('href', canonical); - iframe.doc.head.appendChild(link); - } - let a = iframe.doc.createElement(name); - for (const key in attributes) { - a.setAttribute(key, attributes[key]); - } - if (attributes.resizable !== undefined) { - const overflowEl = iframe.doc.createElement('div'); - overflowEl.setAttribute('overflow', ''); - a.appendChild(overflowEl); - } - // Make document long. - a.style.marginBottom = '1000px'; - if (opt_handleElement) { - a = opt_handleElement(a); - } - return iframe.addElement(a); - }); -} - diff --git a/testing/describes.js b/testing/describes.js new file mode 100644 index 000000000000..7c188d3dcca4 --- /dev/null +++ b/testing/describes.js @@ -0,0 +1,624 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import installCustomElements from + 'document-register-element/build/document-register-element.node'; +import {BaseElement} from '../src/base-element'; +import { + FakeCustomElements, + FakeLocation, + FakeWindow, + interceptEventListeners, +} from './fake-dom'; +import {installFriendlyIframeEmbed} from '../src/friendly-iframe-embed'; +import {doNotLoadExternalResourcesInTest} from './iframe'; +import { + adopt, + adoptShadowMode, + installAmpdocServices, + installRuntimeServices, + registerElementForTesting, +} from '../src/runtime'; +import {cssText} from '../build/css'; +import {createAmpElementProto} from '../src/custom-element'; +import {installDocService} from '../src/service/ampdoc-impl'; +import { + installBuiltinElements, + installExtensionsService, + registerExtension, +} from '../src/service/extensions-impl'; +import {resetScheduledElementForTesting} from '../src/custom-element'; +import {setStyles} from '../src/style'; +import * as sinon from 'sinon'; + +/** Should have something in the name, otherwise nothing is shown. */ +const SUB = ' '; + + +/** @type {number} */ +let iframeCount = 0; + + +/** + * @const {!Object} + */ +const extensionsBuffer = {}; + + +/** + * @param {string} name + * @param {function(!Object)} installer + * @const + */ +export function bufferExtension(name, installer) { + extensionsBuffer[name] = installer; +} + + +/** + * @typedef {{ + * fakeRegisterElement: (boolean|undefined), + * }} + */ +export let TestSpec; + + +/** + * - ampdoc: "single", "shadow", "multi", "none". + * + * @typedef {{ + * runtimeOn: (boolean|undefined), + * extensions: (!Array|undefined), + * canonicalUrl: (string|undefined), + * ampdoc: (string|undefined), + * params: (!Object|undefined), + * }} + */ +export let AmpTestSpec; + + +/** + * @typedef {{ + * win: !Window, + * extensions: !Extensions, + * ampdocService: !AmpDocService, + * ampdoc: (!AmpDoc|undefined), + * flushVsync: function(), + * }} + */ +export let AmpTestEnv; + + +/** + * A test with a sandbox. + * @param {string} name + * @param {!TestSpec} spec + * @param {function()} fn + */ +export const sandboxed = describeEnv(spec => []); + + +/** + * A test with a fake window. + * @param {string} name + * @param {{ + * win: !FakeWindowSpec, + * amp: (!AmpTestSpec|undefined), + * }} spec + * @param {function({ + * win: !FakeWindow, + * amp: (!AmpTestEnv|undefined), + * })} fn + */ +export const fakeWin = describeEnv(spec => [ + new FakeWinFixture(spec), + new AmpFixture(spec), + ]); + + +/** + * A test with a real (iframed) window. + * @param {string} name + * @param {{ + * fakeRegisterElement: (boolean|undefined), + * amp: (!AmpTestSpec|undefined), + * }} spec + * @param {function({ + * win: !Window, + * iframe: !HTMLIFrameElement, + * amp: (!AmpTestEnv|undefined), + * })} fn + */ +export const realWin = describeEnv(spec => [ + new RealWinFixture(spec), + new AmpFixture(spec), + ]); + + +/** + * A repeating test. + * @param {string} name + * @param {!Object} variants + * @param {function(string, *)} fn + */ +export const repeated = (function() { + /** + * @param {string} name + * @param {!Object} variants + * @param {function(string, *)} fn + * @param {function(string, function())} describeFunc + */ + const templateFunc = function(name, variants, fn, describeFunc) { + return describeFunc(name, function() { + for (const name in variants) { + describe(name ? ` ${name} ` : SUB, function() { + fn.call(this, name, variants[name]); + }); + } + }); + }; + + /** + * @param {string} name + * @param {!Object} variants + * @param {function(string, *)} fn + */ + const mainFunc = function(name, variants, fn) { + return templateFunc(name, variants, fn, describe); + }; + + /** + * @param {string} name + * @param {!Object} variants + * @param {function(string, *)} fn + */ + mainFunc.only = function(name, variants, fn) { + return templateFunc(name, variants, fn, describe./*OK*/only); + }; + + return mainFunc; +})(); + + +/** + * A test within a described environment. + * @param {function(!Object):!Array} factory + */ +function describeEnv(factory) { + /** + * @param {string} name + * @param {!Object} spec + * @param {function(!Object)} fn + * @param {function(string, function())} describeFunc + */ + const templateFunc = function(name, spec, fn, describeFunc) { + const fixtures = [new SandboxFixture(spec)]; + factory(spec).forEach(fixture => { + if (fixture && fixture.isOn()) { + fixtures.push(fixture); + } + }); + return describeFunc(name, function() { + + const env = Object.create(null); + + beforeEach(() => { + let totalPromise = undefined; + // Set up all fixtures. + fixtures.forEach((fixture, index) => { + if (totalPromise) { + totalPromise = totalPromise.then(() => fixture.setup(env)); + } else { + const res = fixture.setup(env); + if (res && typeof res.then == 'function') { + totalPromise = res; + } + } + }); + return totalPromise; + }); + + afterEach(() => { + // Tear down all fixtures. + fixtures.slice(0).reverse().forEach(fixture => { + fixture.teardown(env); + }); + + // Delete all other keys. + for (const key in env) { + delete env[key]; + } + }); + + describe(SUB, function() { + fn.call(this, env); + }); + }); + }; + + /** + * @param {string} name + * @param {!Object} spec + * @param {function(!Object)} fn + */ + const mainFunc = function(name, spec, fn) { + return templateFunc(name, spec, fn, describe); + }; + + /** + * @param {string} name + * @param {!Object} spec + * @param {function(!Object)} fn + */ + mainFunc.only = function(name, spec, fn) { + return templateFunc(name, spec, fn, describe./*OK*/only); + }; + + return mainFunc; +} + + +/** @interface */ +class Fixture { + + /** @return {boolean} */ + isOn() {} + + /** + * @param {!Object} env + * @return {!Promise|undefined} + */ + setup(env) {} + + /** + * @param {!Object} env + */ + teardown(env) {} +} + + +/** @implements {Fixture} */ +class SandboxFixture { + + /** @param {!TestSpec} spec */ + constructor(spec) { + /** @const */ + this.spec = spec; + + /** @private {boolean} */ + this.sandboxOwner_ = false; + } + + /** @override */ + isOn() { + return true; + } + + /** @override */ + setup(env) { + const spec = this.spec; + + // Sandbox. + let sandbox = global.sandbox; + if (!sandbox) { + sandbox = global.sandbox = sinon.sandbox.create(); + this.sandboxOwner_ = true; + } + env.sandbox = sandbox; + } + + /** @override */ + teardown(env) { + // Sandbox. + if (this.sandboxOwner_) { + env.sandbox.restore(); + delete global.sandbox; + this.sandboxOwner_ = false; + } + } +} + + +/** @implements {Fixture} */ +class FakeWinFixture { + + /** @param {!{win: !FakeWindowSpec}} spec */ + constructor(spec) { + /** @const */ + this.spec = spec; + } + + /** @override */ + isOn() { + return true; + } + + /** @override */ + setup(env) { + env.win = new FakeWindow(this.spec.win || {}); + } + + /** @override */ + teardown(env) { + } +} + + +/** @implements {Fixture} */ +class RealWinFixture { + + /** @param {!{ + * fakeRegisterElement: boolean, + * ampCss: boolean, + * allowExternalResources: boolean + * }} spec */ + constructor(spec) { + /** @const */ + this.spec = spec; + } + + /** @override */ + isOn() { + return true; + } + + /** @override */ + setup(env) { + const spec = this.spec; + return new Promise(function(resolve, reject) { + const iframe = document.createElement('iframe'); + env.iframe = iframe; + iframe.name = 'test_' + iframeCount++; + iframe.srcdoc = '' + + '' + + '
    '; + iframe.onload = function() { + const win = iframe.contentWindow; + env.win = win; + + // Flag as being a test window. + win.AMP_TEST_IFRAME = true; + // Set the testLocation on iframe to parent's location since location of + // the test iframe is about:srcdoc. + // Unfortunately location object is not configurable, so we have to + // define a new property. + win.testLocation = new FakeLocation(window.location.href, win); + + if (!spec.allowExternalResources) { + doNotLoadExternalResourcesInTest(win); + } + + // Install AMP CSS if requested. + if (spec.ampCss) { + installRuntimeStylesPromise(win); + } + + if (spec.fakeRegisterElement) { + const customElements = new FakeCustomElements(win); + Object.defineProperty(win, 'customElements', { + get: () => customElements, + }); + } else { + installCustomElements(win); + } + + // Intercept event listeners + interceptEventListeners(win); + interceptEventListeners(win.document); + interceptEventListeners(win.document.documentElement); + interceptEventListeners(win.document.body); + env.interceptEventListeners = interceptEventListeners; + + resolve(); + }; + iframe.onerror = reject; + document.body.appendChild(iframe); + }); + } + + /** @override */ + teardown(env) { + // TODO(dvoytenko): test that window is returned in a good condition. + if (env.iframe.parentNode) { + env.iframe.parentNode.removeChild(env.iframe); + } + } +} + + +/** @implements {Fixture} */ +class AmpFixture { + + /** + * @param {!{amp: (boolean|!AmpTestSpec)}} spec + */ + constructor(spec) { + /** @const */ + this.spec = spec; + } + + /** @override */ + isOn() { + return !!this.spec.amp; + } + + /** @override */ + setup(env) { + const spec = this.spec.amp; + const win = env.win; + let completePromise; + + // AMP requires canonical URL. + const link = win.document.createElement('link'); + link.setAttribute('rel', 'canonical'); + link.setAttribute('href', spec.canonicalUrl || window.location.href); + win.document.head.appendChild(link); + + if (!spec.runtimeOn) { + win.name = '__AMP__off=1'; + } + const ampdocType = spec.ampdoc || 'single'; + const singleDoc = ampdocType == 'single' || ampdocType == 'fie'; + const ampdocService = installDocService(win, singleDoc); + env.ampdocService = ampdocService; + env.extensions = installExtensionsService(win); + installBuiltinElements(win); + installRuntimeServices(win); + env.flushVsync = function() { + win.services.vsync.obj.runScheduledTasks_(); + }; + if (singleDoc) { + // Install AMP CSS for main runtime, if it hasn't been installed yet. + completePromise = installRuntimeStylesPromise(win); + const ampdoc = ampdocService.getAmpDoc(win.document); + env.ampdoc = ampdoc; + installAmpdocServices(ampdoc, spec.params); + adopt(win); + } else if (ampdocType == 'multi') { + adoptShadowMode(win); + // Notice that ampdoc's themselves install runtime styles in shadow roots. + // Thus, not changes needed here. + } + const extensionIds = []; + if (spec.extensions) { + spec.extensions.forEach(extensionIdWithVersion => { + const tuple = extensionIdWithVersion.split(':'); + const extensionId = tuple[0]; + extensionIds.push(extensionId); + // Default to 0.1 if no version was provided. + const version = tuple[1] || '0.1'; + const installer = extensionsBuffer[`${extensionId}:${version}`]; + if (installer) { + registerExtension(env.extensions, extensionId, installer, win.AMP); + } else { + registerElementForTesting(win, extensionId); + } + }); + } + + /** + * Creates a custom element without registration. + * @param {string=} opt_name + * @param {function(new:./base-element.BaseElement, !Element)} opt_implementationClass + * @return {!AmpElement} + */ + env.createAmpElement = createAmpElement.bind(null, win); + + // Friendly embed setup. + if (ampdocType == 'fie') { + const container = win.document.createElement('div'); + const embedIframe = win.document.createElement('iframe'); + container.appendChild(embedIframe); + embedIframe.setAttribute('frameborder', '0'); + embedIframe.setAttribute('allowfullscreen', ''); + embedIframe.setAttribute('scrolling', 'no'); + setStyles(embedIframe, { + width: '300px', + height: '150px', + }); + win.document.body.appendChild(container); + const html = '' + + '' + + '' + + '' + + ''; + const promise = installFriendlyIframeEmbed( + embedIframe, container, { + url: 'http://ads.localhost:8000/example', + html, + extensionIds, + }, embedWin => { + interceptEventListeners(embedWin); + interceptEventListeners(embedWin.document); + interceptEventListeners(embedWin.document.documentElement); + interceptEventListeners(embedWin.document.body); + }).then(embed => { + env.embed = embed; + env.parentWin = env.win; + env.win = embed.win; + }); + completePromise = completePromise ? + completePromise.then(() => promise) : promise; + } + + return completePromise; + } + + /** @override */ + teardown(env) { + const win = env.win; + if (env.embed) { + env.embed.destroy(); + } + if (win.customElements && win.customElements.elements) { + for (const k in win.customElements.elements) { + resetScheduledElementForTesting(win, k); + } + } + if (this.spec.amp.extensions) { + this.spec.amp.extensions.forEach(extensionId => { + if (extensionId.indexOf(':') != -1) { + extensionId = extensionId.substring(0, extensionId.indexOf(':')); + } + resetScheduledElementForTesting(win, extensionId); + }); + } + } +} + + +/** + * @param {!Window} win + */ +function installRuntimeStylesPromise(win) { + if (win.document.querySelector('style[amp-runtime]')) { + // Already installed. + return; + } + const style = document.createElement('style'); + style.setAttribute('amp-runtime', ''); + style./*OK*/textContent = cssText; + win.document.head.appendChild(style); +} + + +/** + * Creates a custom element without registration. + * @param {!Window} win + * @param {string=} opt_name + * @param {function(new:./base-element.BaseElement, !Element)} opt_implementationClass + * @return {!AmpElement} + */ +function createAmpElement(win, opt_name, opt_implementationClass) { + // Create prototype and constructor. + const name = opt_name || 'amp-element'; + const proto = createAmpElementProto(win, name); + const ctor = function() { + const el = win.document.createElement(name); + el.__proto__ = proto; + return el; + }; + ctor.prototype = proto; + proto.constructor = ctor; + + // Create the element instance. + const element = new ctor(); + element.implementationClassForTesting = + opt_implementationClass || BaseElement; + element.createdCallback(); + element.classList.add('i-amphtml-element'); + return element; +}; diff --git a/testing/fake-dom.js b/testing/fake-dom.js new file mode 100644 index 000000000000..07ec616f75f1 --- /dev/null +++ b/testing/fake-dom.js @@ -0,0 +1,643 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {parseUrl, resolveRelativeUrl} from '../src/url'; + + +/** + * @typedef {{ + * hidden: (boolean|undefined), + * historyOff: (boolean|undefined), + * localStorageOff: (boolean|undefined), + * location: (string|undefined), + * navigator: ({userAgent:(string|undefined)}|undefined), + * readyState: (boolean|undefined), + * top: (FakeWindowSpec|undefined), + * }} + */ +export let FakeWindowSpec; + + +/** @extends {!Window} */ +export class FakeWindow { + + /** + * @param {!FakeWindowSpec=} opt_spec + */ + constructor(opt_spec) { + + const spec = opt_spec || {}; + + /** @type {string} */ + this.readyState = spec.readyState || 'complete'; + + // Passthrough. + /** @const */ + this.Object = window.Object; + /** @const */ + this.HTMLElement = window.HTMLElement; + /** @const */ + this.HTMLFormElement = window.HTMLFormElement; + /** @const */ + this.Element = window.Element; + /** @const */ + this.Node = window.Node; + /** @const */ + this.EventTarget = window.EventTarget; + /** @const */ + this.DOMTokenList = window.DOMTokenList; + /** @const */ + this.Math = window.Math; + + // Top Window points to itself if spec.top was not passed. + /** @const */ + this.top = spec.top ? new FakeWindow(spec.top) : this; + + // Events. + EventListeners.intercept(this); + + // Document. + /** @const {!HTMLDocument} */ + this.document = self.document.implementation.createHTMLDocument(''); + Object.defineProperty(this.document, 'defaultView', { + get: () => this, + }); + + EventListeners.intercept(this.document); + EventListeners.intercept(this.document.documentElement); + EventListeners.intercept(this.document.body); + + // Document.hidden property. + /** @private {boolean} */ + this.documentHidden_ = spec.hidden !== undefined ? spec.hidden : false; + Object.defineProperty(this.document, 'hidden', { + get: () => this.documentHidden_, + set: value => { + this.documentHidden_ = value; + this.document.eventListeners.fire({type: 'visibilitychange'}); + }, + }); + + // Create element to enhance test elements. + const nativeDocumentCreate = this.document.createElement; + /** @this {HTMLDocument} */ + this.document.createElement = function() { + const result = nativeDocumentCreate.apply(this, arguments); + EventListeners.intercept(result); + return result; + }; + + /** @const {!FakeCustomElements} */ + this.customElements = new FakeCustomElements(this); + + // History. + /** @const {!FakeHistory|undefined} */ + this.history = spec.historyOff ? undefined : new FakeHistory(this); + + // Location. + /** @private @const {!FakeLocation} */ + this.location_ = new FakeLocation( + spec.location || window.location.href, + this, this.history); + Object.defineProperty(this, 'location', { + get: () => this.location_, + set: href => this.location_.assign(href), + }); + + // Navigator. + /** @const {!Navigator} */ + this.navigator = freeze({ + userAgent: spec.navigator && spec.navigator.userAgent || + window.navigator.userAgent, + }); + + // Storage. + /** @const {!FakeStorage|undefined} */ + this.localStorage = spec.localStorageOff ? + undefined : new FakeStorage(this); + + // Timers and animation frames. + /** + * @param {function()} handler + * @param {number=} timeout + * @param {...*} var_args + * @return {number} + * @const + */ + this.setTimeout = function () { + return window.setTimeout.apply(window, arguments); + }; + + /** + * @param {number} id + * @const + */ + this.clearTimeout = function () { + return window.clearTimeout.apply(window, arguments); + }; + + /** + * @param {function()} handler + * @param {number=} timeout + * @param {...*} var_args + * @return {number} + * @const + */ + this.setInterval = function () { + return window.setInterval.apply(window, arguments); + }; + + /** + * @param {number} id + * @const + */ + this.clearInterval = function () { + return window.clearInterval.apply(window, arguments); + }; + + let raf = window.requestAnimationFrame + || window.webkitRequestAnimationFrame; + if (raf) { + raf = raf.bind(window); + } else { + raf = function(fn) { + window.setTimeout(fn, 16); + }; + } + /** + * @param {function()} handler + * @const + */ + this.requestAnimationFrame = raf; + } + + /** + * @param {string} type + * @param {function(!Event)} handler + * @param {(boolean|!Object)=} captureOrOpts + */ + addEventListener() {} + + /** + * @param {string} type + * @param {function(!Event)} handler + * @param {(boolean|!Object)=} captureOrOpts + */ + removeEventListener() {} +} + + +/** + * @typedef {{ + * type: string, + * handler: function(!Event), + * capture: boolean, + * options: ?Object + * }} + */ +export let EventListener; + + +/** + * Helper for testing event listeners. + */ +class EventListeners { + + /** + * @param {!EventTarget} target + * @return {!EventListeners} + */ + static intercept(target) { + target.eventListeners = new EventListeners(); + const originalAdd = target.addEventListener; + const originalRemove = target.removeEventListener; + target.addEventListener = function(type, handler, captureOrOpts) { + target.eventListeners.add(type, handler, captureOrOpts); + if (originalAdd) { + originalAdd.apply(target, arguments); + } + }; + target.removeEventListener = function(type, handler, captureOrOpts) { + target.eventListeners.remove(type, handler, captureOrOpts); + if (originalRemove) { + originalRemove.apply(target, arguments); + } + }; + } + + constructor() { + /** @const {!Array} */ + this.listeners = []; + } + + /** + * @param {string} type + * @param {function(!Event)} handler + * @param {(boolean|!Object)=} captureOrOpts + * @private + */ + listener_(type, handler, captureOrOpts) { + return { + type, + handler, + capture: typeof captureOrOpts == 'boolean' ? captureOrOpts : + typeof captureOrOpts == 'object' ? captureOrOpts.capture || false : + false, + options: typeof captureOrOpts == 'object' ? captureOrOpts : null, + }; + } + + /** + * @param {string} type + * @param {function(!Event)} handler + * @param {(boolean|!Object)=} captureOrOpts + */ + add(type, handler, captureOrOpts) { + const listener = this.listener_(type, handler, captureOrOpts); + this.listeners.push(listener); + } + + /** + * @param {string} type + * @param {function(!Event)} handler + * @param {(boolean|!Object)=} captureOrOpts + */ + remove(type, handler, captureOrOpts) { + const toRemove = this.listener_(type, handler, captureOrOpts); + for (let i = this.listeners.length - 1; i >= 0; i--) { + const listener = this.listeners[i]; + if (listener.type == toRemove.type && + listener.handler == toRemove.handler && + listener.capture == toRemove.capture) { + this.listeners.splice(i, 1); + } + } + } + + /** + * @param {string} type + * @return {!Array} + */ + forType(type) { + return this.listeners.filter(listener => listener.type == type); + } + + /** + * @param {string} type + * @return {number} + */ + count(type) { + return this.forType(type).length; + } + + /** + * @param {!Event} event + */ + fire(event) { + this.forType(event.type).forEach(listener => { + listener.handler.call(null, event); + }); + } +} + + +/** + * @param {!EventTarget} target + */ +export function interceptEventListeners(target) { + EventListeners.intercept(target); +} + + +/** + * @extends {!Location} + */ +export class FakeLocation { + + /** + * @param {string} href + * @param {!FakeWindow} win + * @param {?History} history + */ + constructor(href, win, history) { + + /** @const {!Window} */ + this.win = win; + + /** @private @const {?History} */ + this.history_ = history; + + /** @const {!Array} */ + this.changes = []; + + /** @private {!Location} */ + this.url_ = parseUrl(href, true); + + // href + Object.defineProperty(this, 'href', { + get: () => this.url_.href, + set: href => this.assign(href), + }); + + const properties = ['protocol', 'host', 'hostname', 'port', 'pathname', + 'search', 'hash', 'origin']; + properties.forEach(property => { + Object.defineProperty(this, property, { + get: () => this.url_[property], + }); + }); + + if (this.history_) { + this.history_.replaceState(null, '', this.url_.href, + /* fireEvent */ false); + } + } + + /** + * @param {string} href + */ + set_(href) { + const oldHash = this.url_.hash; + this.url_ = parseUrl(resolveRelativeUrl(href, this.url_)); + if (this.url_.hash != oldHash) { + this.win.eventListeners.fire({type: 'hashchange'}); + } + } + + /** + * @param {!Object} args + */ + change_(args) { + const change = parseUrl(this.url_.href); + Object.assign({}, change, args); + this.changes.push(change); + } + + /** + * @param {string} href + */ + assign(href) { + this.set_(href); + if (this.history_) { + this.history_.pushState(null, '', this.url_.href, + /* fireEvent */ true); + } + this.change_({assign: true}); + } + + /** + * @param {string} href + */ + replace(href) { + this.set_(href); + if (this.history_) { + this.history_.replaceState(null, '', this.url_.href, + /* fireEvent */ true); + } + this.change_({replace: true}); + } + + /** + * @param {boolean} forceReload + */ + reload(forceReload) { + this.change_({reload: true, forceReload}); + } + + /** + * Resets the URL without firing any events or triggering a history + * entry. + * @param {string} href + */ + resetHref(href) { + this.url_ = parseUrl(resolveRelativeUrl(href, this.url_)); + } +} + + +/** + * @extends {!History} + */ +export class FakeHistory { + + /** @param {!FakeWindow} win */ + constructor(win) { + /** @const */ + this.win = win; + + /** @const {!Array} */ + this.stack = [{url: '', state: null}]; + + /** @const {number} */ + this.index = 0; + + Object.defineProperty(this, 'length', { + get: () => this.stack.length, + }); + + Object.defineProperty(this, 'state', { + get: () => this.stack[this.index].state, + }); + } + + /** */ + back() { + this.go(-1); + } + + /** */ + forward() { + this.go(1); + } + + /** + * @param {number} steps + */ + go(steps) { + const newIndex = this.index + steps; + if (newIndex == this.index) { + return; + } + if (newIndex < 0) { + throw new Error('can\'t go back'); + } + if (newIndex >= this.stack.length) { + throw new Error('can\'t go forward'); + } + this.index = newIndex; + // Make sure to restore the location href before firing popstate to match + // real browsers behaviors. + this.win.location.resetHref(this.stack[this.index].url); + this.win.eventListeners.fire({type: 'popstate'}); + } + + /** + * @param {?Object} state + * @param {?string} title + * @param {?string} url + * @param {boolean=} opt_fireEvent + */ + pushState(state, title, url, opt_fireEvent) { + this.index++; + if (this.index < this.stack.length) { + // Remove tail. + this.stack.splice(this.index, thius.stack.length - this.index); + } + this.stack[this.index] = { + state: state ? freeze(state) : null, + url, + }; + if (opt_fireEvent) { + this.win.eventListeners.fire({type: 'popstate'}); + } + } + + /** + * @param {?Object} state + * @param {?string} title + * @param {?string} url + * @param {boolean=} opt_fireEvent + */ + replaceState(state, title, url, opt_fireEvent) { + const cell = this.stack[this.index]; + cell.state = state ? freeze(state) : null; + cell.url = url; + if (opt_fireEvent) { + this.win.eventListeners.fire({type: 'popstate'}); + } + } +} + + +/** + * @extends {Storage} + */ +export class FakeStorage { + + /** @param {!Window} win */ + constructor(win) { + /** @const */ + this.win = win; + + /** @const {!Object} */ + this.values = {}; + + // Length. + Object.defineProperty(this, 'length', { + get: () => Object.keys(this.values).length, + }); + } + + /** + * @param {number} n + * @return {string} + */ + key(n) { + return Object.keys(this.values)[n]; + } + + /** + * @param {string} name + * @return {?string} + */ + getItem(name) { + if (name in this.values) { + return this.values[name]; + } + return null; + } + + /** + * @param {string} name + * @param {*} value + * @return {?string} + */ + setItem(name, value) { + this.values[name] = String(value); + } + + /** + * @param {string} name + */ + removeItem(name) { + delete this.values[name]; + } + + /** + */ + clear() { + Object.keys(this.values).forEach(name => { + delete this.values[name]; + }); + } +} + + +/** + * @extends {CustomElementRegistry} + */ +export class FakeCustomElements { + + /** @param {!Window} win */ + constructor(win) { + /** @const */ + this.win = win; + + /** @type {number} */ + this.count = 0; + + /** @const {!Object} */ + this.elements = {}; + + /** + * Custom Elements V0 API. + * @param {string} name + * @param {{prototype: !Prototype}} spec + */ + this.win.document.registerElement = (name, spec) => { + if (this.elements[name]) { + throw new Error('custom element already defined: ' + name); + } + this.elements[name] = spec; + this.count++; + }; + } + + /** + * Custom Elements V1 API. + * @param {string} name + * @param {!Function} klass + */ + define(name, klass) { + if (this.elements[name]) { + throw new Error('custom element already defined: ' + name); + } + this.elements[name] = klass.prototype; + this.count++; + } +} + + +/** + * @param {!Object} obj + * @return {!Object} + */ +function freeze(obj) { + if (!Object.freeze) { + return obj; + } + return Object.freeze(obj); +} diff --git a/testing/iframe.js b/testing/iframe.js index dfa25e94a274..afb68ab8417d 100644 --- a/testing/iframe.js +++ b/testing/iframe.js @@ -14,10 +14,19 @@ * limitations under the License. */ - +import {FakeLocation} from './fake-dom'; import {Timer} from '../src/timer'; -import {installCoreServices} from '../src/amp-core-service'; -import {registerForUnitTest} from '../src/runtime'; +import installCustomElements from + 'document-register-element/build/document-register-element.node'; +import {installDocService} from '../src/service/ampdoc-impl'; +import {installExtensionsService} from '../src/service/extensions-impl'; +import { + installAmpdocServices, + installRuntimeServices, + registerForUnitTest, +} from '../src/runtime'; +import {installStyles} from '../src/style-installer'; +import {cssText} from '../build/css'; let iframeCount = 0; @@ -55,7 +64,7 @@ export function createFixtureIframe(fixture, initialIframeHeight, opt_beforeLoad 'amp:attached': 0, 'amp:error': 0, 'amp:stubbed': 0, - 'amp:load:start': 0 + 'amp:load:start': 0, }; const messages = []; let html = __html__[fixture]; @@ -71,7 +80,13 @@ export function createFixtureIframe(fixture, initialIframeHeight, opt_beforeLoad // on that window. window.beforeLoad = function(win) { // Flag as being a test window. + win.AMP_TEST_IFRAME = true; win.AMP_TEST = true; + // Set the testLocation on iframe to parent's location since location of + // the test iframe is about:srcdoc. + // Unfortunately location object is not configurable, so we have to define + // a new property. + win.testLocation = new FakeLocation(window.location.href, win); win.ampTestRuntimeConfig = window.ampTestRuntimeConfig; if (opt_beforeLoad) { opt_beforeLoad(win); @@ -126,7 +141,7 @@ export function createFixtureIframe(fixture, initialIframeHeight, opt_beforeLoad }; let timeout = setTimeout(function() { reject(new Error('Timeout waiting for elements to start loading.')); - }, 1000); + }, 2000); // Declare the test ready to run when the document was fully parsed. window.afterLoad = function() { resolve({ @@ -143,6 +158,9 @@ export function createFixtureIframe(fixture, initialIframeHeight, opt_beforeLoad html = html.replace('>', '>'); html += ''; let iframe = document.createElement('iframe'); + if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { + iframe.setAttribute('scrolling', 'no'); + } iframe.name = 'test_' + fixture + iframeCount++; iframe.onerror = function(event) { reject(event.error); @@ -178,6 +196,7 @@ export function createFixtureIframe(fixture, initialIframeHeight, opt_beforeLoad * @return {!Promise<{ * win: !Window, * doc: !Document, + * ampdoc: !../src/service/ampdoc-impl.AmpDoc, * iframe: !Element, * addElement: function(!Element):!Promise * }>} @@ -187,46 +206,54 @@ export function createIframePromise(opt_runtimeOff, opt_beforeLayoutCallback) { let iframe = document.createElement('iframe'); iframe.name = 'test_' + iframeCount++; iframe.srcdoc = '' + - '
    '; + '
    '; iframe.onload = function() { // Flag as being a test window. - iframe.contentWindow.AMP_TEST = true; + iframe.contentWindow.AMP_TEST_IFRAME = true; + iframe.contentWindow.testLocation = new FakeLocation(window.location.href, + iframe.contentWindow); if (opt_runtimeOff) { iframe.contentWindow.name = '__AMP__off=1'; } - installCoreServices(iframe.contentWindow); + const ampdocService = installDocService(iframe.contentWindow, true); + const ampdoc = ampdocService.getAmpDoc(iframe.contentWindow.document); + installExtensionsService(iframe.contentWindow); + installRuntimeServices(iframe.contentWindow); + installCustomElements(iframe.contentWindow); + installAmpdocServices(ampdoc); registerForUnitTest(iframe.contentWindow); // Act like no other elements were loaded by default. - iframe.contentWindow.ampExtendedElements = {}; - resolve({ - win: iframe.contentWindow, - doc: iframe.contentWindow.document, - iframe: iframe, - addElement: function(element) { - iframe.contentWindow.document.getElementById('parent') - .appendChild(element); - // Wait for mutation observer to fire. - return new Timer(window).promise(16).then(() => { - // Make sure it has dimensions since no styles are available. - element.style.display = 'block'; - element.build(true); - if (!element.getPlaceholder()) { - const placeholder = element.createPlaceholder(); - if (placeholder) { - element.appendChild(placeholder); + installStyles(iframe.contentWindow.document, cssText, () => { + resolve({ + win: iframe.contentWindow, + doc: iframe.contentWindow.document, + ampdoc: ampdoc, + iframe: iframe, + addElement: function(element) { + const iWin = iframe.contentWindow; + const p = onInsert(iWin).then(() => { + element.build(true); + if (!element.getPlaceholder()) { + const placeholder = element.createPlaceholder(); + if (placeholder) { + element.appendChild(placeholder); + } } - } - if (element.layoutCount_ == 0) { - if (opt_beforeLayoutCallback) { - opt_beforeLayoutCallback(element); + if (element.layoutCount_ == 0) { + if (opt_beforeLayoutCallback) { + opt_beforeLayoutCallback(element); + } + return element.layoutCallback().then(() => { + return element; + }); } - return element.layoutCallback().then(() => { - return element; - }); - } - return element; - }); - } + return element; + }); + iWin.document.getElementById('parent') + .appendChild(element); + return p; + }, + }); }); }; iframe.onerror = reject; @@ -241,8 +268,9 @@ export function createServedIframe(src) { iframe.src = src; iframe.onload = function() { const win = iframe.contentWindow; + win.AMP_TEST_IFRAME = true; win.AMP_TEST = true; - installCoreServices(win); + installRuntimeServices(win); registerForUnitTest(win); resolve({ win: win, @@ -255,6 +283,81 @@ export function createServedIframe(src) { }); } +const IFRAME_STUB_URL = + '//ads.localhost:9876/test/fixtures/served/iframe-stub.html#'; + +/** + * Creates an iframe fixture in the given window that can be used for + * window.postMessage related tests. + * + * It provides functions like: + * - instruct the iframe to post a message to the parent window + * - verify the iframe has received a message from the parent window + * + * See /test/fixtures/served/iframe-stub.html for implementation. + * + * @param win {!Window} + * @returns {!HTMLIFrameElement} + */ +export function createIframeWithMessageStub(win) { + const element = win.document.createElement('iframe'); + element.src = IFRAME_STUB_URL; + + /** + * Instructs the iframe to send a message to parent window. + */ + element.postMessageToParent = msg => { + element.src = IFRAME_STUB_URL + encodeURIComponent(JSON.stringify(msg)); + }; + + /** + * Returns a Promise that resolves when the iframe acknowledged the reception + * of the specified message. + */ + element.expectMessageFromParent = msg => { + return new Promise(resolve => { + const listener = event => { + let expectMsg = msg; + let actualMsg = event.data.receivedMessage; + if (typeof expectMsg !== 'string') { + expectMsg = JSON.stringify(expectMsg); + actualMsg = JSON.stringify(actualMsg); + } + if (event.source == element.contentWindow + && event.data.testStubEcho + && expectMsg == actualMsg) { + win.removeEventListener('message', listener); + resolve(msg); + } + }; + win.addEventListener('message', listener); + }); + }; + return element; +} + +/** + * Returns a Promise that resolves when a post message is observed from the + * given source window to target window. + * + * @param sourceWin {!Window} + * @param targetwin {!Window} + * @param msg {!Object} + * @returns {!Promise} + */ +export function expectPostMessage(sourceWin, targetwin, msg) { + return new Promise(resolve => { + const listener = event => { + if (event.source == sourceWin + && JSON.stringify(msg) == JSON.stringify(event.data)) { + targetwin.removeEventListener('message', listener); + resolve(event.data); + } + }; + targetwin.addEventListener('message', listener); + }); +} + /** * Returns a promise for when the condition becomes true. * @param {string} description @@ -268,14 +371,14 @@ export function createServedIframe(src) { */ export function poll(description, condition, opt_onError, opt_timeout) { return new Promise((resolve, reject) => { - let start = new Date().getTime(); + let start = Date.now(); function poll() { const ret = condition(); if (ret) { clearInterval(interval); resolve(ret); } else { - if (new Date().getTime() - start > (opt_timeout || 1600)) { + if (Date.now() - start > (opt_timeout || 1600)) { clearInterval(interval); if (opt_onError) { reject(opt_onError()); @@ -302,7 +405,8 @@ export function poll(description, condition, opt_onError, opt_timeout) { */ export function pollForLayout(win, count, opt_timeout) { let getCount = () => { - return win.document.querySelectorAll('.-amp-layout,.-amp-error').length; + return win.document.querySelectorAll( + '.i-amphtml-layout,.i-amphtml-error').length; }; return poll('Waiting for elements to layout: ' + count, () => { return getCount() >= count; @@ -323,7 +427,7 @@ export function expectBodyToBecomeVisible(win) { (win.document.body.style.visibility == 'visible' && win.document.body.style.opacity != '0') || win.document.body.style.opacity == '1'); - }); + }, undefined, 5000); } /** @@ -373,6 +477,26 @@ export function doNotLoadExternalResourcesInTest(win) { }; } +/** + * Returns a promise for when an element has been added to the given + * window. This is for use in tests to wait until after the + * attachment of an element to the DOM should have been registered. + * @param {!Window} win + * @return {!Promise} + */ +function onInsert(win) { + return new Promise(resolve => { + const observer = new win.MutationObserver(() => { + observer.disconnect(); + resolve(); + }); + observer.observe(win.document.documentElement, { + childList: true, + subtree: true, + }); + }) +} + /** * Takes a HTML document that is pointing to unminified JS and HTML * binaries and massages the URLs to pointed to compiled binaries diff --git a/testing/screenshots/make-screenshot.js b/testing/screenshots/make-screenshot.js index 13d0f10e431f..696a4fbf014e 100644 --- a/testing/screenshots/make-screenshot.js +++ b/testing/screenshots/make-screenshot.js @@ -24,7 +24,7 @@ * ``` * phantomjs --ssl-protocol=any --ignore-ssl-errors=true --load-images=true \ * make-screenshot.js "http://localhost:8000" \ - * "/examples.build/everything.amp.max.html" \ + * "/examples/everything.amp.max.html" \ * "everything.png" "iPhone6+" * ``` */ @@ -224,6 +224,7 @@ page.open(url, function() { resources.forEach(function(resource) { log('Resource started: ' + resource.debugid); prepareResource(resource); + // Note: forceAll is no longer available. resource.forceAll().then(function() { completeResource(resource); }, function(reason) { diff --git a/testing/test-helper.js b/testing/test-helper.js new file mode 100644 index 000000000000..1bbb019ba442 --- /dev/null +++ b/testing/test-helper.js @@ -0,0 +1,60 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {getService, getServiceForDoc} from '../src/service'; + +export function stubService(sandbox, win, serviceId, method) { + const stub = sandbox.stub(); + getService(win, serviceId, () => { + const service = {}; + service[method] = stub; + return service; + }); + return stub; +} + +export function stubServiceForDoc(sandbox, ampdoc, serviceId, method) { + const stub = sandbox.stub(); + getServiceForDoc(ampdoc, serviceId, () => { + const service = {}; + service[method] = stub; + return service; + }); + return stub; +} + +/** + * Asserts that the given element is only visible to screen readers. + * @param {!Element} node + */ +export function assertScreenReaderElement(element) { + expect(element).to.exist; + expect(element.classList.contains('-amp-screen-reader')).to.be.true; + const win = element.ownerDocument.defaultView; + const computedStyle = win.getComputedStyle(element); + expect(computedStyle.getPropertyValue('position')).to.equal('fixed'); + expect(computedStyle.getPropertyValue('top')).to.equal('0px'); + expect(computedStyle.getPropertyValue('left')).to.equal('0px'); + expect(computedStyle.getPropertyValue('width')).to.equal('2px'); + expect(computedStyle.getPropertyValue('height')).to.equal('2px'); + expect(computedStyle.getPropertyValue('opacity')).to.equal('0'); + expect(computedStyle.getPropertyValue('overflow')).to.equal('hidden'); + expect(computedStyle.getPropertyValue('border')).to.contain('none'); + expect(computedStyle.getPropertyValue('margin')).to.equal('0px'); + expect(computedStyle.getPropertyValue('padding')).to.equal('0px'); + expect(computedStyle.getPropertyValue('display')).to.equal('block'); + expect(computedStyle.getPropertyValue('visibility')).to.equal('visible'); +} diff --git a/third_party/babel/custom-babel-helpers.js b/third_party/babel/custom-babel-helpers.js index cbf7fc737b7b..d1e20e5c577e 100644 --- a/third_party/babel/custom-babel-helpers.js +++ b/third_party/babel/custom-babel-helpers.js @@ -55,6 +55,10 @@ } }; + babelHelpers.interopRequireDefault = function (obj) { + return obj && obj.__esModule ? obj : { default: obj }; + }; + babelHelpers.get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); diff --git a/third_party/closure-compiler/compiler-and-tests.jar b/third_party/closure-compiler/compiler-and-tests.jar index 0f6b81d13dd6..ecad31fa2c38 100644 Binary files a/third_party/closure-compiler/compiler-and-tests.jar and b/third_party/closure-compiler/compiler-and-tests.jar differ diff --git a/third_party/closure-compiler/compiler.jar b/third_party/closure-compiler/compiler.jar index fbe69c66cf63..1b705b50aee8 100644 Binary files a/third_party/closure-compiler/compiler.jar and b/third_party/closure-compiler/compiler.jar differ diff --git a/third_party/closure-compiler/externs/shadow_dom.js b/third_party/closure-compiler/externs/shadow_dom.js new file mode 100644 index 000000000000..53750d68118d --- /dev/null +++ b/third_party/closure-compiler/externs/shadow_dom.js @@ -0,0 +1,49 @@ +// TODO(dvoytenko): Remove once Closure adds this extern. +// See: https://github.com/google/closure-compiler/issues/2018 +// See: https://dom.spec.whatwg.org/#dom-node-getrootnode + +/* + * Copyright 2016 The Closure Compiler Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @see https://dom.spec.whatwg.org/#dictdef-getrootnodeoptions + * @typedef {{ + * composed: boolean + * }} + */ +var GetRootNodeOptions; + +/** + * @see https://dom.spec.whatwg.org/#dom-node-getrootnode + * @param {GetRootNodeOptions=} opt_options + * @return {?Node} + */ +Node.prototype.getRootNode = function(opt_options) {}; + +/** + * @see https://www.w3.org/TR/shadow-dom/#idl-def-ShadowRootInit + * @typedef {{ + * mode: string + * }} + */ +var ShadowRootInit; + +/** + * @see https://www.w3.org/TR/shadow-dom/#h-extensions-to-element-interface + * @param {ShadowRootInit=} opt_init + * @return {!ShadowRoot} + */ +Element.prototype.attachShadow = function(opt_init) {}; diff --git a/third_party/closure-compiler/externs/web_animations.js b/third_party/closure-compiler/externs/web_animations.js new file mode 100644 index 000000000000..79f8ac03a17f --- /dev/null +++ b/third_party/closure-compiler/externs/web_animations.js @@ -0,0 +1,67 @@ +// TODO(dvoytenko): Remove once Closure adds this extern. +// See: https://github.com/google/closure-compiler/issues/2134 +// See: https://www.w3.org/TR/web-animations/ + +/* + * Copyright 2016 The Closure Compiler Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * See https://developer.mozilla.org/en-US/docs/Web/API/Animation + * @interface + * @extend {EventTarget} + */ +class Animation { + + /** + * Starts or resumes playing of an animation, or begins the animation again + * if it previously finished. + */ + play() {} + + /** + * Clears all keyframeEffects caused by this animation and aborts its + * playback. + */ + cancel() {} + + /** + * Seeks either end of an animation, depending on whether the animation is + * playing or reversing. + */ + finish() {} + + /** + * Suspends playing of an animation. + */ + pause() {} + + /** + * Reverses playback direction, stopping at the start of the animation. + * If the animation is finished or unplayed, it will play from end to + * beginning. + */ + reverse() {} +} + + +/** + * See https://developer.mozilla.org/en-US/docs/Web/API/Element/animate + * @param {!Object} keyframes + * @param {(!Object|number)=} opt_timing + * @return {!Animation} + */ +Element.prototype.animate = function(keyframes, opt_timing) {}; diff --git a/third_party/closure-library/base64.js b/third_party/closure-library/base64.js deleted file mode 100644 index efd90c6ddc24..000000000000 --- a/third_party/closure-library/base64.js +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2007 The Closure Library Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS-IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview Base64 en/decoding. Not much to say here except that we - * work with decoded values in arrays of bytes. By "byte" I mean a number - * in [0, 255]. - * - * @author doughtie@google.com (Gavin Doughtie) - * @author fschneider@google.com (Fritz Schneider) - */ - -goog.provide('goog.crypt.base64'); - -goog.require('goog.asserts'); -goog.require('goog.crypt'); - -// Static lookup maps, lazily populated by init_() - - -/** - * Maps bytes to characters. - * @type {Object} - * @private - */ -goog.crypt.base64.byteToCharMap_ = null; - - -/** - * Maps characters to bytes. - * @type {Object} - * @private - */ -goog.crypt.base64.charToByteMap_ = null; - - -/** - * Maps bytes to websafe characters. - * @type {Object} - * @private - */ -goog.crypt.base64.byteToCharMapWebSafe_ = null; - - -/** - * Maps websafe characters to bytes. - * @type {Object} - * @private - */ -goog.crypt.base64.charToByteMapWebSafe_ = null; - - -/** - * Our default alphabet, shared between - * ENCODED_VALS and ENCODED_VALS_WEBSAFE - * @type {string} - */ -goog.crypt.base64.ENCODED_VALS_BASE = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + - 'abcdefghijklmnopqrstuvwxyz' + - '0123456789'; - - -/** - * Our default alphabet. Value 64 (=) is special; it means "nothing." - * @type {string} - */ -goog.crypt.base64.ENCODED_VALS = - goog.crypt.base64.ENCODED_VALS_BASE + '+/='; - - -/** - * Our websafe alphabet. - * @type {string} - */ -goog.crypt.base64.ENCODED_VALS_WEBSAFE = - goog.crypt.base64.ENCODED_VALS_BASE + '-_.'; - - -/** - * FORK in AMP HTML: We assume all supported browsers have native support - * for base64. - * - * @const {boolean} - */ -goog.crypt.base64.HAS_NATIVE_SUPPORT = true; - - -/** - * Base64-encode an array of bytes. - * - * @param {Array|Uint8Array} input An array of bytes (numbers with - * value in [0, 255]) to encode. - * @param {boolean=} opt_webSafe Boolean indicating we should use the - * alternative alphabet. - * @return {string} The base64 encoded string. - */ -goog.crypt.base64.encodeByteArray = function(input, opt_webSafe) { - // Assert avoids runtime dependency on goog.isArrayLike, which helps reduce - // size of jscompiler output, and which yields slight performance increase. - goog.asserts.assert(goog.isArrayLike(input), - 'encodeByteArray takes an array as a parameter'); - - goog.crypt.base64.init_(); - - var byteToCharMap = opt_webSafe ? - goog.crypt.base64.byteToCharMapWebSafe_ : - goog.crypt.base64.byteToCharMap_; - - var output = []; - - for (var i = 0; i < input.length; i += 3) { - var byte1 = input[i]; - var haveByte2 = i + 1 < input.length; - var byte2 = haveByte2 ? input[i + 1] : 0; - var haveByte3 = i + 2 < input.length; - var byte3 = haveByte3 ? input[i + 2] : 0; - - var outByte1 = byte1 >> 2; - var outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4); - var outByte3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6); - var outByte4 = byte3 & 0x3F; - - if (!haveByte3) { - outByte4 = 64; - - if (!haveByte2) { - outByte3 = 64; - } - } - - output.push(byteToCharMap[outByte1], - byteToCharMap[outByte2], - byteToCharMap[outByte3], - byteToCharMap[outByte4]); - } - - return output.join(''); -}; - - -/** - * Base64-encode a string. - * - * @param {string} input A string to encode. - * @param {boolean=} opt_webSafe If true, we should use the - * alternative alphabet. - * @return {string} The base64 encoded string. - */ -goog.crypt.base64.encodeString = function(input, opt_webSafe) { - // Shortcut for Mozilla browsers that implement - // a native base64 encoder in the form of "btoa/atob" - if (goog.crypt.base64.HAS_NATIVE_SUPPORT && !opt_webSafe) { - return goog.global.btoa(input); - } - return goog.crypt.base64.encodeByteArray( - goog.crypt.stringToByteArray(input), opt_webSafe); -}; - - -/** - * Base64-decode a string. - * - * @param {string} input to decode. - * @param {boolean=} opt_webSafe True if we should use the - * alternative alphabet. - * @return {string} string representing the decoded value. - */ -goog.crypt.base64.decodeString = function(input, opt_webSafe) { - // Shortcut for Mozilla browsers that implement - // a native base64 encoder in the form of "btoa/atob" - if (goog.crypt.base64.HAS_NATIVE_SUPPORT && !opt_webSafe) { - return goog.global.atob(input); - } - return goog.crypt.byteArrayToString( - goog.crypt.base64.decodeStringToByteArray(input, opt_webSafe)); -}; - - -/** - * Base64-decode a string. - * - * In base-64 decoding, groups of four characters are converted into three - * bytes. If the encoder did not apply padding, the input length may not - * be a multiple of 4. - * - * In this case, the last group will have fewer than 4 characters, and - * padding will be inferred. If the group has one or two characters, it decodes - * to one byte. If the group has three characters, it decodes to two bytes. - * - * @param {string} input Input to decode. - * @param {boolean=} opt_webSafe True if we should use the web-safe alphabet. - * @return {!Array} bytes representing the decoded value. - */ -goog.crypt.base64.decodeStringToByteArray = function(input, opt_webSafe) { - goog.crypt.base64.init_(); - - var charToByteMap = opt_webSafe ? - goog.crypt.base64.charToByteMapWebSafe_ : - goog.crypt.base64.charToByteMap_; - - var output = []; - - for (var i = 0; i < input.length; ) { - var byte1 = charToByteMap[input.charAt(i++)]; - - var haveByte2 = i < input.length; - var byte2 = haveByte2 ? charToByteMap[input.charAt(i)] : 0; - ++i; - - var haveByte3 = i < input.length; - var byte3 = haveByte3 ? charToByteMap[input.charAt(i)] : 64; - ++i; - - var haveByte4 = i < input.length; - var byte4 = haveByte4 ? charToByteMap[input.charAt(i)] : 64; - ++i; - - if (byte1 == null || byte2 == null || - byte3 == null || byte4 == null) { - throw Error(); - } - - var outByte1 = (byte1 << 2) | (byte2 >> 4); - output.push(outByte1); - - if (byte3 != 64) { - var outByte2 = ((byte2 << 4) & 0xF0) | (byte3 >> 2); - output.push(outByte2); - - if (byte4 != 64) { - var outByte3 = ((byte3 << 6) & 0xC0) | byte4; - output.push(outByte3); - } - } - } - - return output; -}; - - -/** - * Lazy static initialization function. Called before - * accessing any of the static map variables. - * @private - */ -goog.crypt.base64.init_ = function() { - if (!goog.crypt.base64.byteToCharMap_) { - goog.crypt.base64.byteToCharMap_ = {}; - goog.crypt.base64.charToByteMap_ = {}; - goog.crypt.base64.byteToCharMapWebSafe_ = {}; - goog.crypt.base64.charToByteMapWebSafe_ = {}; - - // We want quick mappings back and forth, so we precompute two maps. - for (var i = 0; i < goog.crypt.base64.ENCODED_VALS.length; i++) { - goog.crypt.base64.byteToCharMap_[i] = - goog.crypt.base64.ENCODED_VALS.charAt(i); - goog.crypt.base64.charToByteMap_[goog.crypt.base64.byteToCharMap_[i]] = i; - goog.crypt.base64.byteToCharMapWebSafe_[i] = - goog.crypt.base64.ENCODED_VALS_WEBSAFE.charAt(i); - goog.crypt.base64.charToByteMapWebSafe_[ - goog.crypt.base64.byteToCharMapWebSafe_[i]] = i; - - // Be forgiving when decoding and correctly decode both encodings. - if (i >= goog.crypt.base64.ENCODED_VALS_BASE.length) { - goog.crypt.base64.charToByteMap_[ - goog.crypt.base64.ENCODED_VALS_WEBSAFE.charAt(i)] = i; - goog.crypt.base64.charToByteMapWebSafe_[ - goog.crypt.base64.ENCODED_VALS.charAt(i)] = i; - } - } - } -}; diff --git a/third_party/closure-library/compile.sh b/third_party/closure-library/compile.sh old mode 100644 new mode 100755 index 93826bc13ccc..88152f4e69e7 --- a/third_party/closure-library/compile.sh +++ b/third_party/closure-library/compile.sh @@ -19,16 +19,16 @@ java -jar $1 \ --js "$2/goog/array/array.js" \ --js "$2/goog/asserts/asserts.js" \ --js "$2/goog/crypt/crypt.js" \ - --js "base64.js" \ --js "$2/goog/crypt/hash.js" \ --js "$2/goog/crypt/sha2_64bit.js" \ --js "$2/goog/crypt/sha384.js" \ --js "$2/goog/debug/error.js" \ --js "$2/goog/dom/nodetype.js" \ --js "$2/goog/math/long.js" \ + --js "$2/goog/reflect/reflect.js" \ --js "$2/goog/string/string.js" \ --js_output_file "sha384-generated.js" \ - --output_wrapper "/* Generated from closure library commit $GIT_COMMIT */%output%;export function sha384Base64(input) { return ampSha384(input) }; export function sha384(input) { return ampSha384Digest(input) };" \ + --output_wrapper "/* Generated from closure library commit $GIT_COMMIT */%output%;export function base64(input) { return ampBase64(input) }; export function sha384(input) { return ampSha384Digest(input) };" \ --manage_closure_dependencies \ --process_closure_primitives \ --use_types_for_optimization \ diff --git a/third_party/closure-library/sha384-generated.js b/third_party/closure-library/sha384-generated.js index 60de003177e8..6b60b143289c 100644 --- a/third_party/closure-library/sha384-generated.js +++ b/third_party/closure-library/sha384-generated.js @@ -1,19 +1,20 @@ -/* Generated from closure library commit 4fa3f37e090d73374825faec334b2deb2c902c47 */var m=this;function p(a,b){var d=a.split("."),c=window||m;d[0]in c||!c.execScript||c.execScript("var "+d[0]);for(var e;d.length&&(e=d.shift());)d.length||void 0===b?c[e]?c=c[e]:c=c[e]={}:c[e]=b} -function aa(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var d=Object.prototype.toString.call(a);if("[object Window]"==d)return"object";if("[object Array]"==d||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==d||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null"; -else if("function"==b&&"undefined"==typeof a.call)return"object";return b}function r(a,b){function d(){}d.prototype=b.prototype;a.v=b.prototype;a.prototype=new d;a.prototype.constructor=a;a.u=function(a,d,f){for(var g=Array(arguments.length-2),l=2;la){var b=u[a];if(b)return b}b=new t(a|0,0>a?-1:0);-128<=a&&128>a&&(u[a]=b);return b}function z(a){isNaN(a)||!isFinite(a)?a=A():a<=-B?a=C():a+1>=B?(w[D]||(w[D]=new t(-1,2147483647)),a=w[D]):a=0>a?E(z(-a)):new t(a%F|0,a/F|0);return a}var F=4294967296,B=F*F/2;function A(){w[G]||(w[G]=y(0));return w[G]}function H(){w[I]||(w[I]=y(1));return w[I]}function ca(){w[J]||(w[J]=y(-1));return w[J]} -function C(){w[K]||(w[K]=new t(0,-2147483648));return w[K]}t.prototype.toString=function(a){a=a||10;if(2>a||36this.a){if(M(this,C())){var b=z(a),d=N(this,b),b=O(P(d,b),this);return d.toString(a)+b.b.toString(a)}return"-"+E(this).toString(a)}for(var d=z(Math.pow(a,6)),b=this,c="";;){var e=N(b,d),f=(O(b,P(e,d)).b>>>0).toString(a),b=e;if(L(b))return f+c;for(;6>f.length;)f="0"+f;c=""+f+c}};function Q(a){return 0<=a.b?a.b:F+a.b} -function L(a){return 0==a.a&&0==a.b}function M(a,b){return a.a==b.a&&a.b==b.b}function da(a){w[R]||(w[R]=y(16777216));return 0>S(a,w[R])}function S(a,b){if(M(a,b))return 0;var d=0>a.a,c=0>b.a;return d&&!c?-1:!d&&c?1:0>O(a,b).a?-1:1}function E(a){return M(a,C())?C():T(new t(~a.b,~a.a),H())} -function T(a,b){var d=a.a>>>16,c=a.a&65535,e=a.b>>>16,f=b.a>>>16,g=b.a&65535,l=b.b>>>16,n,q;q=0+((a.b&65535)+(b.b&65535));n=0+(q>>>16);n+=e+l;e=0+(n>>>16);e+=c+g;c=0+(e>>>16);c=c+(d+f)&65535;return new t((n&65535)<<16|q&65535,c<<16|e&65535)}function O(a,b){return T(a,E(b))} -function P(a,b){if(L(a)||L(b))return A();if(M(a,C()))return 1==(b.b&1)?C():A();if(M(b,C()))return 1==(a.b&1)?C():A();if(0>a.a)return 0>b.a?P(E(a),E(b)):E(P(E(a),b));if(0>b.a)return E(P(a,E(b)));if(da(a)&&da(b))return z((a.a*F+Q(a))*(b.a*F+Q(b)));var d=a.a>>>16,c=a.a&65535,e=a.b>>>16,f=a.b&65535,g=b.a>>>16,l=b.a&65535,n=b.b>>>16,q=b.b&65535,v,k,h,x;x=0+f*q;h=0+(x>>>16);h+=e*q;k=0+(h>>>16);h=(h&65535)+f*n;k+=h>>>16;h&=65535;k+=c*q;v=0+(k>>>16);k=(k&65535)+e*n;v+=k>>>16;k&=65535;k+=f*l;v+=k>>>16;k&= -65535;v=v+(d*q+c*n+e*l+f*g)&65535;return new t(h<<16|x&65535,v<<16|k)} -function N(a,b){if(L(b))throw Error("division by zero");if(L(a))return A();if(M(a,C())){if(M(b,H())||M(b,ca()))return C();if(M(b,C()))return H();var d;d=1;if(0==d)d=a;else{var c=a.a;d=32>d?new t(a.b>>>d|c<<32-d,c>>d):new t(c>>d-32,0<=c?0:-1)}d=N(d,b);c=1;if(0!=c){var e=d.b;d=32>c?new t(e<>>32-c):new t(0,e<b.a?H():ca();c=O(a,P(b,d));return T(d,N(c,b))}if(M(b,C()))return A();if(0>a.a)return 0>b.a?N(E(a),E(b)):E(N(E(a),b));if(0>b.a)return E(N(a,E(b)));e=A();for(c= -a;0<=S(c,b);){d=Math.max(1,Math.floor((c.a*F+Q(c))/(b.a*F+Q(b))));for(var f=Math.ceil(Math.log(d)/Math.LN2),f=48>=f?1:Math.pow(2,f-48),g=z(d),l=P(g,b);0>l.a||0X;X++)ha[X]=0;var ia=function(a){return Array.prototype.concat.apply(Array.prototype,arguments)}([128],ha); -function Y(a,b,d){d=void 0!==d?d:b.length;if(a.i)throw Error("this hasher needs to be reset");var c=a.f;if("string"==typeof b)for(var e=0;ef||255c;c++){var e=8*c;d[c]=new t(b[e+4]<<24|b[e+5]<<16|b[e+6]<<8|b[e+7],b[e]<<24|b[e+1]<<16|b[e+2]<<8|b[e+3])}for(c=16;80>c;c++){var e=d[c-15],b=e.b,e=e.a,f=d[c-2],g=f.b,f=f.a;d[c]=a.m(d[c-16],d[c-7],new t(b>>>1^e<<31^b>>>8^e<<24^b>>>7^e<<25,e>>>1^b<<31^e>>>8^b<<24^e>>>7),new t(g>>>19^f<<13^f>>>29^g<<3^g>>>6^f<<26,f>>>19^g<<13^g>>>29^f<<3^f>>>6))}for(var b=a.c[0],e=a.c[1],g=a.c[2],f=a.c[3],l=a.c[4],n=a.c[5],q=a.c[6],v=a.c[7],c=0;80>c;c++)var k=b.b,h=b.a,k=T(new t(k>>> -28^h<<4^h>>>2^k<<30^h>>>7^k<<25,h>>>28^k<<4^k>>>2^h<<30^k>>>7^h<<25),new t(b.b&e.b|e.b&g.b|b.b&g.b,b.a&e.a|e.a&g.a|b.a&g.a)),h=l.b,x=l.a,fa=l.b,ga=l.a,h=a.m(v,new t(h>>>14^x<<18^h>>>18^x<<14^x>>>9^h<<23,x>>>14^h<<18^x>>>18^h<<14^h>>>9^x<<23),new t(fa&n.b|~fa&q.b,ga&n.a|~ga&q.a),ja[c],d[c]),v=q,q=n,n=l,l=T(f,h),f=g,g=e,e=b,b=T(h,k);a.c[0]=T(a.c[0],b);a.c[1]=T(a.c[1],e);a.c[2]=T(a.c[2],g);a.c[3]=T(a.c[3],f);a.c[4]=T(a.c[4],l);a.c[5]=T(a.c[5],n);a.c[6]=T(a.c[6],q);a.c[7]=T(a.c[7],v)} -W.prototype.m=function(a,b,d){for(var c=(a.b^2147483648)+(b.b^2147483648),e=a.a+b.a,f=arguments.length-1;2<=f;--f)c+=arguments[f].b^2147483648,e+=arguments[f].a;arguments.length&1&&(c+=2147483648);e+=arguments.length>>1;e+=Math.floor(c/4294967296);return new t(c,e)};function ea(a){for(var b=[],d=0;da?r(ba,a,function(a){return new v(a|0,0>a?-1:0)}):new v(a|0,0>a?-1:0)}function y(a){return isNaN(a)?z():a<=-A?B():a+1>=A?ca():0>a?C(y(-a)):new v(a%D|0,a/D|0)}var D=4294967296,A=D*D/2;function z(){return r(w,da,function(){return x(0)})}function F(){return r(w,ea,function(){return x(1)})}function G(){return r(w,fa,function(){return x(-1)})}function ca(){return r(w,ga,function(){return new v(-1,2147483647)})} +function B(){return r(w,ha,function(){return new v(0,-2147483648)})}function H(){return r(w,ia,function(){return x(16777216)})} +v.prototype.toString=function(a){a=a||10;if(2>a||36this.a){if(J(this,B())){var b=y(a),c=K(this,b),b=L(c.multiply(b),this);return c.toString(a)+b.b.toString(a)}return"-"+C(this).toString(a)}for(var c=y(Math.pow(a,6)),b=this,d="";;){var e=K(b,c),f=(L(b,e.multiply(c)).b>>>0).toString(a),b=e;if(I(b))return f+d;for(;6>f.length;)f="0"+f;d=""+f+d}};function M(a){return 0<=a.b?a.b:D+a.b}function I(a){return 0==a.a&&0==a.b} +function J(a,b){return a.a==b.a&&a.b==b.b}v.prototype.compare=function(a){if(J(this,a))return 0;var b=0>this.a,c=0>a.a;return b&&!c?-1:!b&&c?1:0>L(this,a).a?-1:1};function C(a){return J(a,B())?B():(new v(~a.b,~a.a)).add(F())}v.prototype.add=function(a){var b=this.a>>>16,c=this.a&65535,d=this.b>>>16,e=a.a>>>16,f=a.a&65535,g=a.b>>>16;a=0+((this.b&65535)+(a.b&65535));g=0+(a>>>16)+(d+g);d=0+(g>>>16);d+=c+f;b=0+(d>>>16)+(b+e)&65535;return new v((g&65535)<<16|a&65535,b<<16|d&65535)}; +function L(a,b){return a.add(C(b))} +v.prototype.multiply=function(a){if(I(this)||I(a))return z();if(J(this,B()))return 1==(a.b&1)?B():z();if(J(a,B()))return 1==(this.b&1)?B():z();if(0>this.a)return 0>a.a?C(this).multiply(C(a)):C(C(this).multiply(a));if(0>a.a)return C(this.multiply(C(a)));if(0>this.compare(H())&&0>a.compare(H()))return y((this.a*D+M(this))*(a.a*D+M(a)));var b=this.a>>>16,c=this.a&65535,d=this.b>>>16,e=this.b&65535,f=a.a>>>16,g=a.a&65535,q=a.b>>>16;a=a.b&65535;var t,k,u,p;p=0+e*a;u=0+(p>>>16)+d*a;k=0+(u>>>16);u=(u&65535)+ +e*q;k+=u>>>16;k+=c*a;t=0+(k>>>16);k=(k&65535)+d*q;t+=k>>>16;k=(k&65535)+e*g;t=t+(k>>>16)+(b*a+c*q+d*g+e*f)&65535;return new v((u&65535)<<16|p&65535,t<<16|k&65535)}; +function K(a,b){if(I(b))throw Error("division by zero");if(I(a))return z();if(J(a,B())){if(J(b,F())||J(b,G()))return B();if(J(b,B()))return F();var c;c=1;if(0==c)c=a;else{var d=a.a;c=32>c?new v(a.b>>>c|d<<32-c,d>>c):new v(d>>c-32,0<=d?0:-1)}c=K(c,b).shiftLeft(1);if(J(c,z()))return 0>b.a?F():G();a=L(a,b.multiply(c));return c.add(K(a,b))}if(J(b,B()))return z();if(0>a.a)return 0>b.a?K(C(a),C(b)):C(K(C(a),b));if(0>b.a)return C(K(a,C(b)));for(d=z();0<=a.compare(b);){c=Math.max(1,Math.floor((a.a*D+M(a))/ +(b.a*D+M(b))));for(var e=Math.ceil(Math.log(c)/Math.LN2),e=48>=e?1:Math.pow(2,e-48),f=y(c),g=f.multiply(b);0>g.a||0a?new v(b<>>32-a):new v(0,b<Q;Q++)P[Q]=0;var R=function(a){return Array.prototype.concat.apply(Array.prototype,arguments)}([128],P);N.prototype.reset=function(){this.j=this.f=0;var a;a=this.o;var b=a.length;if(0e||255this.f?this.update(R,112-this.f):this.update(R,this.g-this.f+112);for(var b=127;112<=b;b--)this.h[b]=a&255,a/=256;S(this);for(var a=0,c=Array(8*this.l),b=0;b>f&255;for(f=24;0<=f;f-=8)c[a++]=d>>f&255}this.i=!0;return c}; +function S(a){for(var b=a.h,c=a.s,d=0;16>d;d++){var e=8*d;c[d]=new v(b[e+4]<<24|b[e+5]<<16|b[e+6]<<8|b[e+7],b[e]<<24|b[e+1]<<16|b[e+2]<<8|b[e+3])}for(d=16;80>d;d++){var e=c[d-15],b=e.b,e=e.a,f=c[d-2],g=f.b,f=f.a;c[d]=a.m(c[d-16],c[d-7],new v(b>>>1^e<<31^b>>>8^e<<24^b>>>7^e<<25,e>>>1^b<<31^e>>>8^b<<24^e>>>7),new v(g>>>19^f<<13^f>>>29^g<<3^g>>>6^f<<26,f>>>19^g<<13^g>>>29^f<<3^f>>>6))}for(var b=a.c[0],e=a.c[1],g=a.c[2],f=a.c[3],q=a.c[4],t=a.c[5],k=a.c[6],u=a.c[7],d=0;80>d;d++)var p=b.b,l=b.a,p=(new v(p>>> +28^l<<4^l>>>2^p<<30^l>>>7^p<<25,l>>>28^p<<4^p>>>2^l<<30^p>>>7^l<<25)).add(new v(b.b&e.b|e.b&g.b|b.b&g.b,b.a&e.a|e.a&g.a|b.a&g.a)),l=q.b,E=q.a,V=q.b,W=q.a,l=a.m(u,new v(l>>>14^E<<18^l>>>18^E<<14^E>>>9^l<<23,E>>>14^l<<18^E>>>18^l<<14^l>>>9^E<<23),new v(V&t.b|~V&k.b,W&t.a|~W&k.a),ja[d],c[d]),u=k,k=t,t=q,q=f.add(l),f=g,g=e,e=b,b=l.add(p);a.c[0]=a.c[0].add(b);a.c[1]=a.c[1].add(e);a.c[2]=a.c[2].add(g);a.c[3]=a.c[3].add(f);a.c[4]=a.c[4].add(q);a.c[5]=a.c[5].add(t);a.c[6]=a.c[6].add(k);a.c[7]=a.c[7].add(u)} +N.prototype.m=function(a,b,c){for(var d=(a.b^2147483648)+(b.b^2147483648),e=a.a+b.a,f=arguments.length-1;2<=f;--f)d+=arguments[f].b^2147483648,e+=arguments[f].a;arguments.length&1&&(d+=2147483648);e+=arguments.length>>1;e+=Math.floor(d/4294967296);return new v(d,e)};function O(a){for(var b=[],c=0;cb.f?Y(b,ia,112-b.f):Y(b,ia,b.g-b.f+112);for(a=127;112<=a;a--)b.h[a]=d&255,d/=256;Z(b);var d=0,c=Array(8*b.l);for(a=0;a>g&255;for(g=24;0<=g;g-=8)c[d++]=e>>g&255}b.i=!0;return c} -p("ampSha384",function(a){a=ma(a);if(!U){U={};V={};for(var b=0;65>b;b++)U[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b),V[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.".charAt(b)}for(var b=V,d=[],c=0;c>2,e=(e&3)<<4|g>>4,g=(g&15)<<2|n>>6,n=n&63;l||(n=64,f||(g=64));d.push(b[q],b[e],b[g],b[n])}return d.join("")});p("ampSha384Digest",ma);;export function sha384Base64(input) { return ampSha384(input) }; export function sha384(input) { return ampSha384Digest(input) }; +289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591]);function T(){N.call(this,6,ka)}n(T,N);var ka=[3418070365,3238371032,1654270250,914150663,2438529370,812702999,355462360,4144912697,1731405415,4290775857,2394180231,1750603025,3675008525,1694076839,1203062813,3204075428];function U(a){var b=new T;b.update(a);return new Uint8Array(b.digest())}var X=["ampSha384Digest"],Y=window||h;X[0]in Y||!Y.execScript||Y.execScript("var "+X[0]);for(var Z;X.length&&(Z=X.shift());)X.length||void 0===U?Y=Y[Z]?Y[Z]:Y[Z]={}:Y[Z]=U;;export function base64(input) { return ampBase64(input) }; export function sha384(input) { return ampSha384Digest(input) }; diff --git a/third_party/closure-library/sha384.js b/third_party/closure-library/sha384.js index 7124e6412286..db4c2e6b90b4 100644 --- a/third_party/closure-library/sha384.js +++ b/third_party/closure-library/sha384.js @@ -15,25 +15,15 @@ */ goog.require('goog.crypt.Sha384'); -goog.require('goog.crypt.base64'); /** * @param {!Uint8Array|string} input The value to hash. - * @return {!Array.} Web safe base64 of the digest of the input string. + * @return {!Uint8Array} Web safe base64 of the digest of the input string. */ var digest = function(input) { var sha384 = new goog.crypt.Sha384(); sha384.update(input); - return sha384.digest(); + return new Uint8Array(sha384.digest()); } -/** - * @param {!Uint8Array|string} input The value to hash. - * @return {string} Web safe base64 of the digest of the input string. - */ -var base64Digest = function(input) { - return goog.crypt.base64.encodeByteArray(digest(input), /* websafe */ true); -} - -goog.exportSymbol('ampSha384', base64Digest, window); goog.exportSymbol('ampSha384Digest', digest, window); diff --git a/third_party/css-escape/LICENSE b/third_party/css-escape/LICENSE new file mode 100644 index 000000000000..a41e0a7ef970 --- /dev/null +++ b/third_party/css-escape/LICENSE @@ -0,0 +1,20 @@ +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/third_party/css-escape/README.amp b/third_party/css-escape/README.amp new file mode 100644 index 000000000000..63dda7394903 --- /dev/null +++ b/third_party/css-escape/README.amp @@ -0,0 +1,9 @@ +URL: https://github.com/mathiasbynens/CSS.escape +License: The MIT License +License File: https://github.com/mathiasbynens/CSS.escape/blob/master/LICENSE-MIT.txt + +Description: +For of CSS.escape polyfill. + +Local Modifications: +Stripped polyfill part and turn it into utility function. diff --git a/third_party/css-escape/css-escape.js b/third_party/css-escape/css-escape.js new file mode 100644 index 000000000000..618e64e2dd36 --- /dev/null +++ b/third_party/css-escape/css-escape.js @@ -0,0 +1,85 @@ +/*! https://mths.be/cssescape v1.5.1 by @mathias | MIT license */ + + +/** + * https://drafts.csswg.org/cssom/#serialize-an-identifier + * @param {string} value + * @return {string} + */ +export function cssEscape(value) { + if (arguments.length == 0) { + throw new TypeError('`CSS.escape` requires an argument.'); + } + var string = String(value); + var length = string.length; + var index = -1; + var codeUnit; + var result = ''; + var firstCodeUnit = string.charCodeAt(0); + while (++index < length) { + codeUnit = string.charCodeAt(index); + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER + // (U+FFFD). + if (codeUnit == 0x0000) { + result += '\uFFFD'; + continue; + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + ( + index == 1 && + codeUnit >= 0x0030 && codeUnit <= 0x0039 && + firstCodeUnit == 0x002D + ) + ) { + // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point + result += '\\' + codeUnit.toString(16) + ' '; + continue; + } + + if ( + // If the character is the first character and is a `-` (U+002D), and + // there is no second character, […] + index == 0 && + length == 1 && + codeUnit == 0x002D + ) { + result += '\\' + string.charAt(index); + continue; + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit == 0x002D || + codeUnit == 0x005F || + codeUnit >= 0x0030 && codeUnit <= 0x0039 || + codeUnit >= 0x0041 && codeUnit <= 0x005A || + codeUnit >= 0x0061 && codeUnit <= 0x007A + ) { + // the character itself + result += string.charAt(index); + continue; + } + + // Otherwise, the escaped character. + // https://drafts.csswg.org/cssom/#escape-a-character + result += '\\' + string.charAt(index); + + } + return result; +} diff --git a/third_party/d3-geo-projection/LICENSE b/third_party/d3-geo-projection/LICENSE new file mode 100644 index 000000000000..3d0802c3bd11 --- /dev/null +++ b/third_party/d3-geo-projection/LICENSE @@ -0,0 +1,27 @@ +Copyright 2013-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/d3-geo-projection/README.amp b/third_party/d3-geo-projection/README.amp new file mode 100644 index 000000000000..5cbeb90de1a7 --- /dev/null +++ b/third_party/d3-geo-projection/README.amp @@ -0,0 +1,10 @@ +URL: https://github.com/d3/d3-geo-projection/tree/v0.2.16 +License: BSD +License File: https://github.com/d3/d3-geo-projection/blob/v0.2.16/LICENSE + +Description: +Copy of minified (d3-geo-projection.min.js) from release v0.2.16 +https://github.com/d3/d3-geo-projection/tree/v0.2.16 + +Local Modifications: +None diff --git a/third_party/d3-geo-projection/d3-geo-projection.js b/third_party/d3-geo-projection/d3-geo-projection.js new file mode 100644 index 000000000000..32babee51a01 --- /dev/null +++ b/third_party/d3-geo-projection/d3-geo-projection.js @@ -0,0 +1,2 @@ +!function(){function t(t,a){return{type:"Feature",id:t.id,properties:t.properties,geometry:n(t.geometry,a)}}function n(t,a){if(!t)return null;if("GeometryCollection"===t.type)return{type:"GeometryCollection",geometries:object.geometries.map(function(t){return n(t,a)})};if(!ga.hasOwnProperty(t.type))return null;var r=ga[t.type];return d3.geo.stream(t,a(r)),r.result()}function a(){}function r(t){if((n=t.length)<4)return!1;for(var n,a=0,r=t[n-1][1]*t[0][0]-t[n-1][0]*t[0][1];++a=r}function e(t,n){for(var a=n[0],r=n[1],e=!1,o=0,i=t.length,h=i-1;i>o;h=o++){var u=t[o],M=u[0],s=u[1],c=t[h],f=c[0],v=c[1];s>r^v>r&&(f-M)*(r-s)/(v-s)+M>a&&(e=!e)}return e}function o(t){return t?t/Math.sin(t):1}function i(t){return t>0?1:0>t?-1:0}function h(t){return t>1?wa:-1>t?-wa:Math.asin(t)}function u(t){return t>1?0:-1>t?pa:Math.acos(t)}function M(t){return t>0?Math.sqrt(t):0}function s(t){function n(t,n){var a=Math.cos(t),e=Math.cos(n),o=Math.sin(n),i=e*a,h=-((1-i?Math.log(.5*(1+i))/(1-i):-.5)+r/(1+i));return[h*e*Math.sin(t),h*o]}var a=Math.tan(.5*t),r=2*Math.log(Math.cos(.5*t))/(a*a);return n.invert=function(n,a){var e,o=Math.sqrt(n*n+a*a),i=t*-.5,u=50;if(!o)return[0,0];do{var M=.5*i,s=Math.cos(M),c=Math.sin(M),f=Math.tan(M),v=Math.log(1/s);i-=e=(2/f*v-r*f-o)/(-v/(c*c)+1-r/(2*s*s))}while(Math.abs(e)>da&&--u>0);var l=Math.sin(i);return[Math.atan2(n*l,o*Math.cos(i)),h(a*l/o)]},n}function c(){var t=wa,n=Qa(s),a=n(t);return a.radius=function(a){return arguments.length?n(t=a*pa/180):180*(t/pa)},a}function f(t,n){var a=Math.cos(n),r=o(u(a*Math.cos(t/=2)));return[2*a*Math.sin(t)*r,Math.sin(n)*r]}function v(t){function n(t,n){var h=Math.cos(n),u=Math.cos(t/=2);return[(1+h)*Math.sin(t),(e*n>-Math.atan2(u,o)-.001?0:10*-e)+i+Math.sin(n)*r-(1+h)*a*u]}var a=Math.sin(t),r=Math.cos(t),e=t>0?1:-1,o=Math.tan(e*t),i=(1+a-r)/2;return n.invert=function(t,n){var h=0,u=0,M=50;do{var s=Math.cos(h),c=Math.sin(h),f=Math.cos(u),v=Math.sin(u),l=1+f,g=l*c-t,d=i+v*r-l*a*s-n,b=.5*l*s,p=-c*v,w=.5*a*l*c,q=r*f+a*s*v,m=p*w-q*b,y=.5*(d*p-g*q)/m,S=(g*w-d*b)/m;h-=y,u-=S}while((Math.abs(y)>da||Math.abs(S)>da)&&--M>0);return e*u>-Math.atan2(Math.cos(h),o)-.001?[2*h,u]:null},n}function l(){var t=pa/9,n=t>0?1:-1,a=Math.tan(n*t),r=Qa(v),e=r(t),o=e.stream;return e.parallel=function(e){return arguments.length?(a=Math.tan((n=(t=e*pa/180)>0?1:-1)*t),r(t)):180*(t/pa)},e.stream=function(r){var i=e.rotate(),h=o(r),u=(e.rotate([0,0]),o(r));return e.rotate(i),h.sphere=function(){u.polygonStart(),u.lineStart();for(var r=-180*n;180>n*r;r+=90*n)u.point(r,90*n);for(;n*(r-=t)>=-180;)u.point(r,n*-Math.atan2(Math.cos(r*ma/2),a)*ya);u.lineEnd(),u.polygonEnd()},h},e}function g(t){return t=Math.exp(2*t),(t-1)/(t+1)}function d(t){return.5*(Math.exp(t)-Math.exp(-t))}function b(t){return.5*(Math.exp(t)+Math.exp(-t))}function p(t){return Math.log(t+M(t*t+1))}function w(t){return Math.log(t+M(t*t-1))}function q(t,n){var a=Math.tan(n/2),r=M(1-a*a),e=1+r*Math.cos(t/=2),o=Math.sin(t)*r/e,i=a/e,h=o*o,u=i*i;return[4/3*o*(3+h-3*u),4/3*i*(3+3*h-u)]}function m(t,n){var a=Math.abs(n);return pa/4>a?[t,Math.log(Math.tan(pa/4+n/2))]:[t*Math.cos(a)*(2*Math.SQRT2-1/Math.sin(a)),i(n)*(2*Math.SQRT2*(a-pa/4)-Math.log(Math.tan(a/2)))]}function y(t){function n(t,n){var r=Ta(t,n);if(Math.abs(t)>wa){var e=Math.atan2(r[1],r[0]),o=Math.sqrt(r[0]*r[0]+r[1]*r[1]),i=a*Math.round((e-wa)/a)+wa,u=Math.atan2(Math.sin(e-=i),2-Math.cos(e));e=i+h(pa/o*Math.sin(u))-u,r[0]=o*Math.cos(e),r[1]=o*Math.sin(e)}return r}var a=2*pa/t;return n.invert=function(t,n){var r=Math.sqrt(t*t+n*n);if(r>wa){var e=Math.atan2(n,t),o=a*Math.round((e-wa)/a)+wa,i=e>o?-1:1,h=r*Math.cos(o-e),u=1/Math.tan(i*Math.acos((h-pa)/Math.sqrt(pa*(pa-2*h)+r*r)));e=o+2*Math.atan((u+i*Math.sqrt(u*u-3))/3),t=r*Math.cos(e),n=r*Math.sin(e)}return Ta.invert(t,n)},n}function S(){var t=5,n=Qa(y),a=n(t),r=a.stream,e=.01,o=-Math.cos(e*ma),i=Math.sin(e*ma);return a.lobes=function(a){return arguments.length?n(t=+a):t},a.stream=function(n){var u=a.rotate(),M=r(n),s=(a.rotate([0,0]),r(n));return a.rotate(u),M.sphere=function(){s.polygonStart(),s.lineStart();for(var n=0,a=360/t,r=2*pa/t,u=90-180/t,M=wa;t>n;++n,u-=a,M-=r)s.point(Math.atan2(i*Math.cos(M),o)*ya,h(i*Math.sin(M))*ya),-90>u?(s.point(-90,-180-u-e),s.point(-90,-180-u+e)):(s.point(90,u+e),s.point(90,u-e));s.lineEnd(),s.polygonEnd()},M},a}function Q(t){return function(n){var a,r=t*Math.sin(n),e=30;do n-=a=(n+Math.sin(n)-r)/(1+Math.cos(n));while(Math.abs(a)>da&&--e>0);return n/2}}function R(t,n,a){function r(a,r){return[t*a*Math.cos(r=e(r)),n*Math.sin(r)]}var e=Q(a);return r.invert=function(r,e){var o=h(e/n);return[r/(t*Math.cos(o)),h((2*o+Math.sin(2*o))/a)]},r}function T(t,n){var a=2.00276,r=xa(n);return[a*t/(1/Math.cos(n)+1.11072/Math.cos(r)),(n+Math.SQRT2*Math.sin(r))/a]}function x(t){var n=0,a=Qa(t),r=a(n);return r.parallel=function(t){return arguments.length?a(n=t*pa/180):180*(n/pa)},r}function E(t,n){return[t*Math.cos(n),n]}function k(t){function n(n,r){var e=a+t-r,o=e?n*Math.cos(r)/e:e;return[e*Math.sin(o),a-e*Math.cos(o)]}if(!t)return E;var a=1/Math.tan(t);return n.invert=function(n,r){var e=Math.sqrt(n*n+(r=a-r)*r),o=a+t-e;return[e/Math.cos(o)*Math.atan2(n,r),o]},n}function P(t){function n(t,n){var r=wa-n,e=r?t*a*Math.sin(r)/r:r;return[r*Math.sin(e)/a,wa-r*Math.cos(e)]}var a=Math.sin(t);return n.invert=function(t,n){var r=t*a,e=wa-n,o=Math.sqrt(r*r+e*e),i=Math.atan2(r,e);return[(o?o/Math.sin(o):1)*i/a,wa-o]},n}function _(t){function n(n,a){for(var r=Math.sin(a),e=Math.cos(a),o=new Array(3),M=0;3>M;++M){var s=t[M];if(o[M]=B(a-s[1],s[3],s[2],e,r,n-s[0]),!o[M][0])return s.point;o[M][1]=j(o[M][1]-s.v[1])}for(var c=u.slice(),M=0;3>M;++M){var f=2==M?0:M+1,v=F(t[M].v[0],o[M][0],o[f][0]);o[M][1]<0&&(v=-v),M?1==M?(v=i-v,c[0]-=o[M][0]*Math.cos(v),c[1]-=o[M][0]*Math.sin(v)):(v=h-v,c[0]+=o[M][0]*Math.cos(v),c[1]+=o[M][0]*Math.sin(v)):(c[0]+=o[M][0]*Math.cos(v),c[1]-=o[M][0]*Math.sin(v))}return c[0]/=3,c[1]/=3,c}t=t.map(function(t){return[t[0],t[1],Math.sin(t[1]),Math.cos(t[1])]});for(var a,r=t[2],e=0;3>e;++e,r=a)a=t[e],r.v=B(a[1]-r[1],r[3],r[2],a[3],a[2],a[0]-r[0]),r.point=[0,0];var o=F(t[0].v[0],t[2].v[0],t[1].v[0]),i=F(t[0].v[0],t[1].v[0],t[2].v[0]),h=pa-o;t[2].point[1]=0,t[0].point[0]=-(t[1].point[0]=.5*t[0].v[0]);var u=[t[2].point[0]=t[0].point[0]+t[2].v[0]*Math.cos(o),2*(t[0].point[1]=t[1].point[1]=t[2].v[0]*Math.sin(o))];return n}function z(){var t=[[0,0],[0,0],[0,0]],n=Qa(_),a=n(t),r=a.rotate;return delete a.rotate,a.points=function(e){if(!arguments.length)return t;t=e;var o=d3.geo.centroid({type:"MultiPoint",coordinates:t}),i=[-o[0],-o[1]];return r.call(a,i),n(t.map(d3.geo.rotation(i)).map(A))},a.points([[-150,55],[-35,55],[-92.5,10]])}function B(t,n,a,r,e,o){var i,M=Math.cos(o);if(Math.abs(t)>1||Math.abs(o)>1)i=u(a*e+n*r*M);else{var s=Math.sin(.5*t),c=Math.sin(.5*o);i=2*h(Math.sqrt(s*s+n*r*c*c))}return Math.abs(i)>da?[i,Math.atan2(r*Math.sin(o),n*e-a*r*M)]:[0,0]}function F(t,n,a){return u(.5*(t*t+n*n-a*a)/(t*n))}function j(t){return t-2*pa*Math.floor((t+pa)/(2*pa))}function A(t){return[t[0]*ma,t[1]*ma]}function G(t,n){var a=M(1-Math.sin(n));return[2/qa*t*a,qa*(1-a)]}function C(t){function n(t,n){return[t,(t?t/Math.sin(t):1)*(Math.sin(n)*Math.cos(t)-a*Math.cos(n))]}var a=Math.tan(t);return n.invert=a?function(t,n){t&&(n*=Math.sin(t)/t);var r=Math.cos(t);return[t,2*Math.atan2(Math.sqrt(r*r+a*a-n*n)-r,a-n)]}:function(t,n){return[t,h(t?n*Math.tan(t)/t:n)]},n}function D(t,n){var a=Math.sqrt(3);return[a*t*(2*Math.cos(2*n/3)-1)/qa,a*qa*Math.sin(n/3)]}function L(t){function n(t,n){return[t*a,Math.sin(n)/a]}var a=Math.cos(t);return n.invert=function(t,n){return[t/a,h(n*a)]},n}function O(t){function n(t,n){return[t*a,(1+a)*Math.tan(.5*n)]}var a=Math.cos(t);return n.invert=function(t,n){return[t/a,2*Math.atan(n/(1+a))]},n}function H(t,n){var a=Math.sqrt(8/(3*pa));return[a*t*(1-Math.abs(n)/pa),a*n]}function I(t,n){var a=Math.sqrt(4-3*Math.sin(Math.abs(n)));return[2/Math.sqrt(6*pa)*t*a,i(n)*Math.sqrt(2*pa/3)*(2-a)]}function J(t,n){var a=Math.sqrt(pa*(4+pa));return[2/a*t*(1+Math.sqrt(1-4*n*n/(pa*pa))),4/a*n]}function K(t,n){var a=(2+wa)*Math.sin(n);n/=2;for(var r=0,e=1/0;10>r&&Math.abs(e)>da;r++){var o=Math.cos(n);n-=e=(n+Math.sin(n)*(o+2)-a)/(2*o*(1+o))}return[2/Math.sqrt(pa*(4+pa))*t*(1+Math.cos(n)),2*Math.sqrt(pa/(4+pa))*Math.sin(n)]}function N(t,n){return[t*(1+Math.cos(n))/Math.sqrt(2+pa),2*n/Math.sqrt(2+pa)]}function U(t,n){for(var a=(1+wa)*Math.sin(n),r=0,e=1/0;10>r&&Math.abs(e)>da;r++)n-=e=(n+Math.sin(n)-a)/(1+Math.cos(n));return a=Math.sqrt(2+pa),[t*(1+Math.cos(n))/a,2*n/a]}function V(t,n){var a=Math.sin(t/=2),r=Math.cos(t),e=Math.sqrt(Math.cos(n)),o=Math.cos(n/=2),i=Math.sin(n)/(o+Math.SQRT2*r*e),h=Math.sqrt(2/(1+i*i)),u=Math.sqrt((Math.SQRT2*o+(r+a)*e)/(Math.SQRT2*o+(r-a)*e));return[Pa*(h*(u-1/u)-2*Math.log(u)),Pa*(h*i*(u+1/u)-2*Math.atan(i))]}function W(t,n){var a=Math.tan(n/2);return[t*_a*M(1-a*a),(1+_a)*a]}function X(t,n){var a=n/2,r=Math.cos(a);return[2*t/qa*Math.cos(n)*r*r,qa*Math.tan(a)]}function Y(t,n){function a(n,a){var o=za(n,a),i=o[0],h=o[1],u=i*i+h*h;if(u>e){var M=Math.sqrt(u),s=Math.atan2(h,i),c=r*Math.round(s/r),f=s-c,v=t*Math.cos(f),l=(t*Math.sin(f)-f*Math.sin(v))/(wa-v),g=Z(f,l),d=(pa-t)/tn(g,v,pa);i=M;var b,p=50;do i-=b=(t+tn(g,v,i)*d-M)/(g(i)*d);while(Math.abs(b)>da&&--p>0);h=f*Math.sin(i),wa>i&&(h-=l*(i-wa));var w=Math.sin(c),q=Math.cos(c);o[0]=i*q-h*w,o[1]=i*w+h*q}return o}var r=2*pa/n,e=t*t;return a.invert=function(n,a){var o=n*n+a*a;if(o>e){var i=Math.sqrt(o),h=Math.atan2(a,n),u=r*Math.round(h/r),M=h-u,n=i*Math.cos(M);a=i*Math.sin(M);for(var s=n-wa,c=Math.sin(n),f=a/c,v=wa>n?1/0:0,l=10;;){var g=t*Math.sin(f),d=t*Math.cos(f),b=Math.sin(d),p=wa-d,w=(g-f*b)/p,q=Z(f,w);if(Math.abs(v)a&&(r-=n),Math.sqrt(1+r*r)}}function $(){var t=6,n=30*ma,a=Math.cos(n),r=Math.sin(n),e=Qa(Y),o=e(n,t),i=o.stream,h=.01,u=-Math.cos(h*ma),M=Math.sin(h*ma);return o.radius=function(o){return arguments.length?(a=Math.cos(n=o*ma),r=Math.sin(n),e(n,t)):n*ya},o.lobes=function(a){return arguments.length?e(n,t=+a):t},o.stream=function(n){var e=o.rotate(),h=i(n),s=(o.rotate([0,0]),i(n));return o.rotate(e),h.sphere=function(){s.polygonStart(),s.lineStart();for(var n=0,e=2*pa/t,o=0;t>n;++n,o-=e)s.point(Math.atan2(M*Math.cos(o),u)*ya,Math.asin(M*Math.sin(o))*ya),s.point(Math.atan2(r*Math.cos(o-e/2),a)*ya,Math.asin(r*Math.sin(o-e/2))*ya);s.lineEnd(),s.polygonEnd()},h},o}function tn(t,n,a){for(var r=50,e=(a-n)/r,o=t(n)+t(a),i=1,h=n;r>i;++i)o+=2*t(h+=e);return.5*o*e}function nn(t,n,a,r,e,o,i,h){function u(u,M){if(!M)return[t*u/pa,0];var s=M*M,c=t+s*(n+s*(a+s*r)),f=M*(e-1+s*(o-h+s*i)),v=(c*c+f*f)/(2*f),l=u*Math.asin(c/v)/pa;return[v*Math.sin(l),M*(1+s*h)+v*(1-Math.cos(l))]}return arguments.length<8&&(h=0),u.invert=function(u,s){var c,f,v=pa*u/t,l=s,g=50;do{var d=l*l,b=t+d*(n+d*(a+d*r)),p=l*(e-1+d*(o-h+d*i)),w=b*b+p*p,q=2*p,m=w/q,y=m*m,S=Math.asin(b/m)/pa,Q=v*S;if(xB2=b*b,dxBdφ=(2*n+d*(4*a+6*d*r))*l,dyBdφ=e+d*(3*o+5*d*i),dpdφ=2*(b*dxBdφ+p*(dyBdφ-1)),dqdφ=2*(dyBdφ-1),dmdφ=(dpdφ*q-w*dqdφ)/(q*q),cosα=Math.cos(Q),sinα=Math.sin(Q),mcosα=m*cosα,msinα=m*sinα,dαdφ=v/pa*(1/M(1-xB2/y))*(dxBdφ*m-b*dmdφ)/y,fx=msinα-u,fy=l*(1+d*h)+m-mcosα-s,δxδφ=dmdφ*sinα+mcosα*dαdφ,δxδλ=mcosα*S,δyδφ=1+dmdφ-(dmdφ*cosα-msinα*dαdφ),δyδλ=msinα*S,denominator=δxδφ*δyδλ-δyδφ*δxδλ,!denominator)break;v-=c=(fy*δxδφ-fx*δyδφ)/denominator,l-=f=(fx*δyδλ-fy*δxδλ)/denominator}while((Math.abs(c)>da||Math.abs(f)>da)&&--g>0);return[v,l]},u}function an(t,n){var a=t*t,r=n*n;return[t*(1-.162388*r)*(.87-952426e-9*a*a),n*(1+r/12)]}function rn(t){function n(){var t=!1,n=Qa(a),r=n(t);return r.quincuncial=function(a){return arguments.length?n(t=!!a):t},r}function a(n){var a=n?function(n,a){var e=Math.abs(n)0?n-pa:n+pa,a),h=(o[0]-o[1])*Math.SQRT1_2,u=(o[0]+o[1])*Math.SQRT1_2;if(e)return[h,u];var M=r*Math.SQRT1_2,s=h>0^u>0?-1:1;return[s*h-i(u)*M,s*u-i(h)*M]}:function(n,a){var e=n>0?-.5:.5,o=t(n+e*pa,a);return o[0]-=e*r,o};return t.invert&&(a.invert=n?function(n,a){var e=(n+a)*Math.SQRT1_2,o=(a-n)*Math.SQRT1_2,i=Math.abs(e)<.5*r&&Math.abs(o)<.5*r;if(!i){var h=r*Math.SQRT1_2,u=e>0^o>0?-1:1,M=-u*(n+(o>0?1:-1)*h),s=-u*(a+(e>0?1:-1)*h);e=(-M-s)*Math.SQRT1_2,o=(M-s)*Math.SQRT1_2}var c=t.invert(e,o);return i||(c[0]+=e>0?pa:-pa),c}:function(n,a){var e=n>0?-.5:.5,o=t.invert(n+e*r,a),i=o[0]-e*pa;return-pa>i?i+=2*pa:i>pa&&(i-=2*pa),o[0]=i,o}),a}var r=t(wa,0)[0]-t(-wa,0)[0];return n.raw=a,n}function en(t,n){var a=i(t),r=i(n),e=Math.cos(n),o=Math.cos(t)*e,u=Math.sin(t)*e,M=Math.sin(r*n);t=Math.abs(Math.atan2(u,M)),n=h(o),Math.abs(t-wa)>da&&(t%=wa);var s=on(t>pa/4?wa-t:t,n);return t>pa/4&&(M=s[0],s[0]=-s[1],s[1]=-M),s[0]*=a,s[1]*=-r,s}function on(t,n){if(n===wa)return[0,0];var a=Math.sin(n),r=a*a,e=r*r,o=1+e,i=1+3*e,u=1-e,s=h(1/Math.sqrt(o)),c=u+r*o*s,f=(1-a)/c,v=Math.sqrt(f),l=f*o,g=Math.sqrt(l),d=v*u;if(0===t)return[0,-(d+r*g)];var b=Math.cos(n),p=1/b,w=2*a*b,q=(-3*r+s*i)*w,m=(-c*b-(1-a)*q)/(c*c),y=.5*m/v,S=u*y-2*r*v*w,Q=r*o*m+f*i*w,R=-p*w,T=-p*Q,x=-2*p*S,E=4*t/pa;if(t>.222*pa||pa/4>n&&t>.175*pa){var k=(d+r*M(l*(1+e)-d*d))/(1+e);if(t>pa/4)return[k,k];var P=k,_=.5*k,z=50;k=.5*(_+P);do{var B=Math.sqrt(l-k*k),F=k*(x+R*B)+T*h(k/g)-E;if(!F)break;0>F?_=k:P=k,k=.5*(_+P)}while(Math.abs(P-_)>da&&--z>0)}else{var j,k=da,z=25;do{var A=k*k,B=M(l-A),G=x+R*B,F=k*G+T*h(k/g)-E,C=G+(T-R*A)/B;k-=j=B?F/C:0}while(Math.abs(j)>da&&--z>0)}return[k,-d-r*M(l-k*k)]}function hn(t,n){for(var a=0,r=1,e=.5,o=50;;){var i=e*e,h=Math.sqrt(e),u=Math.asin(1/Math.sqrt(1+i)),M=1-i+e*(1+i)*u,s=(1-h)/M,c=Math.sqrt(s),f=s*(1+i),v=c*(1-i),l=f-t*t,g=Math.sqrt(l),d=n+v+e*g;if(Math.abs(r-a)0?a=e:r=e,e=.5*(a+r)}if(!o)return null;var b=Math.asin(h),p=Math.cos(b),w=1/p,q=2*h*p,m=(-3*e+u*(1+3*i))*q,y=(-M*p-(1-h)*m)/(M*M),S=.5*y/c,Q=(1-i)*S-2*e*c*q,R=-2*w*Q,T=-w*q,x=-w*(e*(1+i)*y+s*(1+3*i)*q);return[pa/4*(t*(R+T*g)+x*Math.asin(t/Math.sqrt(f))),b]}function un(t,n,a){if(!t){var r=Mn(n,1-a);return[[0,r[0]/r[1]],[1/r[1],0],[r[2]/r[1],0]]}var e=Mn(t,a);if(!n)return[[e[0],0],[e[1],0],[e[2],0]];var r=Mn(n,1-a),o=r[1]*r[1]+a*e[0]*e[0]*r[0]*r[0];return[[e[0]*r[2]/o,e[1]*e[2]*r[0]*r[1]/o],[e[1]*r[1]/o,-e[0]*e[2]*r[0]*r[2]/o],[e[2]*r[1]*r[2]/o,-a*e[0]*e[1]*r[0]/o]]}function Mn(t,n){var a,r,e,o,i;if(da>n)return o=Math.sin(t),r=Math.cos(t),a=.25*n*(t-o*r),[o-a*r,r+a*o,1-.5*n*o*o,t-a];if(n>=1-da)return a=.25*(1-n),r=b(t),o=g(t),e=1/r,i=r*d(t),[o+a*(i-t)/(r*r),e-a*o*e*(i-t),e+a*o*e*(i+t),2*Math.atan(Math.exp(t))-wa+a*(i-t)/r];var u=[1,0,0,0,0,0,0,0,0],s=[Math.sqrt(n),0,0,0,0,0,0,0,0],c=0;for(r=Math.sqrt(1-n),i=1;Math.abs(s[c]/u[c])>da&&8>c;)a=u[c++],s[c]=.5*(a-r),u[c]=.5*(a+r),r=M(a*r),i*=2;e=i*u[c]*t;do o=s[c]*Math.sin(r=e)/u[c],e=.5*(h(o)+e);while(--c);return[Math.sin(e),o=Math.cos(e),o/Math.cos(e-r),e]}function sn(t,n,a){var r=Math.abs(t),e=Math.abs(n),o=d(e);if(r){var h=1/Math.sin(r),u=1/(Math.tan(r)*Math.tan(r)),s=-(u+a*o*o*h*h-1+a),c=(a-1)*u,f=.5*(-s+Math.sqrt(s*s-4*c));return[cn(Math.atan(1/Math.sqrt(f)),a)*i(t),cn(Math.atan(M((f/u-1)/a)),1-a)*i(n)]}return[0,cn(Math.atan(o),1-a)*i(n)]}function cn(t,n){if(!n)return t;if(1===n)return Math.log(Math.tan(t/2+pa/4));for(var a=1,r=Math.sqrt(1-n),e=Math.sqrt(n),o=0;Math.abs(e)>da;o++){if(t%pa){var i=Math.atan(r*Math.tan(t)/a);0>i&&(i+=pa),t+=i+~~(t/pa)*pa}else t+=t;e=(a+r)/2,r=Math.sqrt(a*r),e=((a=e)-r)/2}return t/(Math.pow(2,o)*a)}function fn(t,n){var a=(Math.SQRT2-1)/(Math.SQRT2+1),r=Math.sqrt(1-a*a),e=cn(wa,r*r),o=-1,i=Math.log(Math.tan(pa/4+Math.abs(n)/2)),h=Math.exp(o*i)/Math.sqrt(a),u=vn(h*Math.cos(o*t),h*Math.sin(o*t)),M=sn(u[0],u[1],r*r);return[-M[1],(n>=0?1:-1)*(.5*e-M[0])]}function vn(t,n){var a=t*t,r=n+1,e=1-a-n*n;return[.5*((t>=0?wa:-wa)-Math.atan2(e,2*t)),-.25*Math.log(e*e+4*a)+.5*Math.log(r*r+a)]}function ln(t,n){var a=n[0]*n[0]+n[1]*n[1];return[(t[0]*n[0]+t[1]*n[1])/a,(t[1]*n[0]-t[0]*n[1])/a]}function gn(t){function n(t,n){var o=e(t,n);t=o[0],n=o[1];var i=Math.sin(n),h=Math.cos(n),M=Math.cos(t),s=u(a*i+r*h*M),c=Math.sin(s),f=Math.abs(c)>da?s/c:1;return[f*r*Math.sin(t),(Math.abs(t)>wa?f:-f)*(a*h-r*i*M)]}var a=Math.sin(t),r=Math.cos(t),e=dn(t);return e.invert=dn(-t),n.invert=function(t,n){var r=Math.sqrt(t*t+n*n),o=-Math.sin(r),i=Math.cos(r),h=r*i,u=-n*o,s=r*a,c=M(h*h+u*u-s*s),f=Math.atan2(h*s+u*c,u*s-h*c),v=(r>wa?-1:1)*Math.atan2(t*o,r*Math.cos(f)*i+n*Math.sin(f)*o);return e.invert(v,f)},n}function dn(t){var n=Math.sin(t),a=Math.cos(t);return function(t,r){var e=Math.cos(r),o=Math.cos(t)*e,i=Math.sin(t)*e,u=Math.sin(r);return[Math.atan2(i,o*a-u*n),h(u*a+o*n)]}}function bn(){var t=0,n=Qa(gn),a=n(t),r=a.rotate,e=a.stream,o=d3.geo.circle();return a.parallel=function(r){if(!arguments.length)return 180*(t/pa);var e=a.rotate();return n(t=r*pa/180).rotate(e)},a.rotate=function(n){return arguments.length?(r.call(a,[n[0],n[1]-180*(t/pa)]),o.origin([-n[0],-n[1]]),a):(n=r.call(a),n[1]+=180*(t/pa),n)},a.stream=function(t){return t=e(t),t.sphere=function(){t.polygonStart();var n,a=.01,r=o.angle(90-a)().coordinates[0],e=r.length-1,i=-1;for(t.lineStart();++i=0;)t.point((n=r[i])[0],n[1]);t.lineEnd(),t.polygonEnd()},t},a}function pn(t,n){function a(a,r){var e=Ga(a/n,r);return e[0]*=t,e}return arguments.length<2&&(n=t),1===n?Ga:1/0===n?qn:(a.invert=function(a,r){var e=Ga.invert(a/t,r);return e[0]*=n,e},a)}function wn(){var t=2,n=Qa(pn),a=n(t);return a.coefficient=function(a){return arguments.length?n(t=+a):t},a}function qn(t,n){return[t*Math.cos(n)/Math.cos(n/=2),2*Math.sin(n)]}function mn(t,n){for(var a,r=Math.sin(n)*(0>n?2.43763:2.67595),e=0;20>e&&(n-=a=(n+Math.sin(n)-r)/(1+Math.cos(n)),!(Math.abs(a)n?1.93052:1.75859)]}function yn(t){function n(n,s){var c,f=Math.abs(s);if(f>r){var v=Math.min(t-1,Math.max(0,Math.floor((n+pa)/M)));n+=pa*(t-1)/t-v*M,c=d3.geo.collignon.raw(n,f),c[0]=c[0]*e/o-e*(t-1)/(2*t)+v*e/t,c[1]=i+4*(c[1]-h)*u/e,0>s&&(c[1]=-c[1])}else c=a(n,s);return c[0]/=2,c}var a=d3.geo.cylindricalEqualArea.raw(0),r=Ca*pa/180,e=2*pa,o=d3.geo.collignon.raw(pa,r)[0]-d3.geo.collignon.raw(-pa,r)[0],i=a(0,r)[1],h=d3.geo.collignon.raw(0,r)[1],u=d3.geo.collignon.raw(0,wa)[1]-h,M=2*pa/t;return n.invert=function(n,r){n*=2;var s=Math.abs(r);if(s>i){var c=Math.min(t-1,Math.max(0,Math.floor((n+pa)/M)));n=(n+pa*(t-1)/t-c*M)*o/e;var f=d3.geo.collignon.raw.invert(n,.25*(s-i)*e/u+h);return f[0]-=pa*(t-1)/t-c*M,0>r&&(f[1]=-f[1]),f}return a.invert(n,r)},n}function Sn(){function t(){var t=180/n;return{type:"Polygon",coordinates:[d3.range(-180,180+t/2,t).map(function(t,n){return[t,1&n?90-1e-6:Ca]}).concat(d3.range(180,-180-t/2,-t).map(function(t,n){return[t,1&n?-90+1e-6:-Ca]}))]}}var n=2,a=Qa(yn),r=a(n),e=r.stream;return r.lobes=function(t){return arguments.length?a(n=+t):n},r.stream=function(n){var a=r.rotate(),o=e(n),i=(r.rotate([0,0]),e(n));return r.rotate(a),o.sphere=function(){d3.geo.stream(t(),i)},o},r}function Qn(t){function n(n,e){var h,u,f=1-Math.sin(e);if(f&&2>f){var v,l=wa-e,g=25;do{var d=Math.sin(l),b=Math.cos(l),p=o+Math.atan2(d,r-b),w=1+c-2*r*b;l-=v=(l-s*o-r*d+w*p-.5*f*a)/(2*r*d*p)}while(Math.abs(v)>ba&&--g>0);h=i*Math.sqrt(w),u=n*p/pa}else h=i*(t+f),u=n*o/pa;return[h*Math.sin(u),M-h*Math.cos(u)]}var a,r=1+t,e=Math.sin(1/r),o=h(e),i=2*Math.sqrt(pa/(a=pa+4*o*r)),M=.5*i*(r+Math.sqrt(t*(2+t))),s=t*t,c=r*r;return n.invert=function(t,n){var e=t*t+(n-=M)*n,f=(1+c-e/(i*i))/(2*r),v=u(f),l=Math.sin(v),g=o+Math.atan2(l,r-f);return[h(t/Math.sqrt(e))*pa/g,h(1-2*(v-s*o-r*l+(1+c-2*r*f)*g)/a)]},n}function Rn(){var t=1,n=Qa(Qn),a=n(t);return a.ratio=function(a){return arguments.length?n(t=+a):t},a}function Tn(t,n){return n>-Da?(t=Ea(t,n),t[1]+=La,t):E(t,n)}function xn(t,n){return Math.abs(n)>Da?(t=Ea(t,n),t[1]-=n>0?La:-La,t):E(t,n)}function En(t,n){return[3*t/(2*pa)*Math.sqrt(pa*pa/3-n*n),n]}function kn(t){function n(n,a){if(Math.abs(Math.abs(a)-wa)a?-2:2];var r=Math.sin(a),e=Math.pow((1+r)/(1-r),t/2),o=.5*(e+1/e)+Math.cos(n*=t);return[2*Math.sin(n)/o,(e-1/e)/o]}return n.invert=function(n,a){var r=Math.abs(a);if(Math.abs(r-2)2)return null;n/=2,a/=2;var e=n*n,o=a*a,u=2*a/(1+e+o);return u=Math.pow((1+u)/(1-u),1/t),[Math.atan2(2*n,1-e-o)/t,h((u-1)/(u+1))]},n}function Pn(){var t=.5,n=Qa(kn),a=n(t);return a.spacing=function(a){return arguments.length?n(t=+a):t},a}function _n(t,n){return[t*(1+Math.sqrt(Math.cos(n)))/2,n/(Math.cos(n/2)*Math.cos(t/6))]}function zn(t,n){var a=t*t,r=n*n;return[t*(.975534+r*(-.119161+a*-.0143059+r*-.0547009)),n*(1.00384+a*(.0802894+r*-.02855+199025e-9*a)+r*(.0998909+r*-.0491032))]}function Bn(t,n){return[Math.sin(t)/Math.cos(n),Math.tan(n)*Math.cos(t)]}function Fn(t){function n(n,e){var o=e-t,i=Math.abs(o)=0;)s=t[M],c=s[0]+h*(e=c)-u*f,f=s[1]+h*f+u*e;return c=h*(e=c)-u*f,f=h*f+u*e,[c,f]}var a=t.length-1;return n.invert=function(n,r){var e=20,o=n,i=r;do{for(var u,M=a,s=t[M],c=s[0],f=s[1],v=0,l=0;--M>=0;)s=t[M],v=c+o*(u=v)-i*l,l=f+o*l+i*u,c=s[0]+o*(u=c)-i*f,f=s[1]+o*f+i*u;v=c+o*(u=v)-i*l,l=f+o*l+i*u,c=o*(u=c)-i*f-n,f=o*f+i*u-r;var g,d,b=v*v+l*l;o-=g=(c*v+f*l)/b,i-=d=(f*v-c*l)/b}while(Math.abs(g)+Math.abs(d)>da*da&&--e>0);if(e){var p=Math.sqrt(o*o+i*i),w=2*Math.atan(.5*p),q=Math.sin(w);return[Math.atan2(o*q,p*Math.cos(w)),p?h(i*q/p):0]}},n}function Gn(){var t=Oa.miller,n=Qa(An),a=n(t);return a.coefficients=function(a){return arguments.length?n(t="string"==typeof a?Oa[a]:a):t},a}function Cn(t,n){var a=Math.sqrt(6),r=Math.sqrt(7),e=Math.asin(7*Math.sin(n)/(3*a));return[a*t*(2*Math.cos(2*e/3)-1)/r,9*Math.sin(e/3)/r]}function Dn(t,n){for(var a,r=(1+Math.SQRT1_2)*Math.sin(n),e=n,o=0;25>o&&(e-=a=(Math.sin(e/2)+Math.sin(e)-r)/(.5*Math.cos(e/2)+Math.cos(e)),!(Math.abs(a)i&&(o-=a=(o/2+Math.sin(o)-e)/(.5+Math.cos(o)),!(Math.abs(a)da&&--M>0);var v=n*(s=Math.tan(i)),l=Math.tan(Math.abs(r)0?wa:-wa)*(M+o*(c-h)/2+o*o*(c-2*M+h)/2)]}function Un(t){function n(n,a){var r=Math.cos(a),e=(t-1)/(t-r*Math.cos(n));return[e*r*Math.sin(n),e*Math.sin(a)]}return n.invert=function(n,a){var r=n*n+a*a,e=Math.sqrt(r),o=(t-Math.sqrt(1-r*(t+1)/(t-1)))/((t-1)/e+e/(t-1));return[Math.atan2(n*o,e*Math.sqrt(1-o*o)),e?h(a*o/e):0]},n}function Vn(t,n){function a(n,a){var i=r(n,a),h=i[1],u=h*o/(t-1)+e;return[i[0]*e/u,h/u]}var r=Un(t);if(!n)return r;var e=Math.cos(n),o=Math.sin(n);return a.invert=function(n,a){var i=(t-1)/(t-1-a*o);return r.invert(i*n,i*a*e)},a}function Wn(){var t=1.4,n=0,a=Qa(Vn),r=a(t,n);return r.distance=function(r){return arguments.length?a(t=+r,n):t},r.tilt=function(r){return arguments.length?a(t,n=r*pa/180):180*n/pa},r}function Xn(t,n){var a=Math.tan(n/2),r=Math.sin(pa/4*a);return[t*(.74482-.34588*r*r),1.70711*a]}function Yn(t){function n(n,o){var i=u(Math.cos(o)*Math.cos(n-a)),h=u(Math.cos(o)*Math.cos(n-r)),s=0>o?-1:1;return i*=i,h*=h,[(i-h)/(2*t),s*M(4*e*h-(e-i+h)*(e-i+h))/(2*t)]}if(!t)return d3.geo.azimuthalEquidistant.raw;var a=-t/2,r=-a,e=t*t,o=Math.tan(r),i=.5/Math.sin(r);return n.invert=function(t,n){var e,h,M=n*n,s=Math.cos(Math.sqrt(M+(e=t+a)*e)),c=Math.cos(Math.sqrt(M+(e=t+r)*e));return[Math.atan2(h=s-c,e=(s+c)*o),(0>n?-1:1)*u(Math.sqrt(e*e+h*h)*i)]},n}function Zn(){var t=[[0,0],[0,0]],n=Qa(Yn),a=n(0),r=a.rotate;return delete a.rotate,a.points=function(a){if(!arguments.length)return t;t=a;var e=d3.geo.interpolate(a[0],a[1]),o=e(.5),i=d3.geo.rotation([-o[0],-o[1]])(a[0]),u=.5*e.distance,M=-h(Math.sin(i[1]*ma)/Math.sin(u));return i[0]>0&&(M=pa-M),r.call(i,[-o[0],-o[1],-M*ya]),n(2*u)},a}function $n(t){function n(t,n){var r=d3.geo.gnomonic.raw(t,n);return r[0]*=a,r}var a=Math.cos(t);return n.invert=function(t,n){return d3.geo.gnomonic.raw.invert(t/a,n)},n}function ta(){var t=[[0,0],[0,0]],n=Qa($n),a=n(0),r=a.rotate;return delete a.rotate,a.points=function(a){if(!arguments.length)return t;t=a;var e=d3.geo.interpolate(a[0],a[1]),o=e(.5),i=d3.geo.rotation([-o[0],-o[1]])(a[0]),u=.5*e.distance,M=-h(Math.sin(i[1]*ma)/Math.sin(u));return i[0]>0&&(M=pa-M),r.call(i,[-o[0],-o[1],-M*ya]),n(u)},a}function na(t,n){if(Math.abs(n)1?{type:"MultiPolygon",coordinates:t}:{type:"Polygon",coordinates:t[0]}:null}},ga={Point:fa,MultiPoint:fa,LineString:va,MultiLineString:va,Polygon:la,MultiPolygon:la,Sphere:la},da=1e-6,ba=da*da,pa=Math.PI,wa=pa/2,qa=Math.sqrt(pa),ma=pa/180,ya=180/pa,Sa=d3.geo.projection,Qa=d3.geo.projectionMutator;d3.geo.interrupt=function(t){function n(n,a){for(var r=0>a?-1:1,e=h[+(0>a)],o=0,i=e.length-1;i>o&&n>e[o][2][0];++o);var u=t(n-e[o][1][0],a);return u[0]+=t(e[o][1][0],r*a>r*e[o][0][1]?e[o][0][1]:a)[0],u}function a(){i=h.map(function(n){return n.map(function(n){var a,r=t(n[0][0],n[0][1])[0],e=t(n[2][0],n[2][1])[0],o=t(n[1][0],n[0][1])[1],i=t(n[1][0],n[1][1])[1];return o>i&&(a=o,o=i,i=a),[[r,o],[e,i]]})})}function r(){for(var t=1e-6,n=[],a=0,r=h[0].length;r>a;++a){var o=h[0][a],i=180*o[0][0]/pa,u=180*o[0][1]/pa,M=180*o[1][1]/pa,s=180*o[2][0]/pa,c=180*o[2][1]/pa;n.push(e([[i+t,u+t],[i+t,M-t],[s-t,M-t],[s-t,c+t]],30))}for(var a=h[1].length-1;a>=0;--a){var o=h[1][a],i=180*o[0][0]/pa,u=180*o[0][1]/pa,M=180*o[1][1]/pa,s=180*o[2][0]/pa,c=180*o[2][1]/pa;n.push(e([[s-t,c-t],[s-t,M+t],[i+t,M+t],[i+t,u-t]],30))}return{type:"Polygon",coordinates:[d3.merge(n)]}}function e(t,n){for(var a,r,e,o=-1,i=t.length,h=t[0],u=[];++oM;++M)u.push([h[0]+M*r,h[1]+M*e]);h=a}return u.push(a),u}function o(t,n){return Math.abs(t[0]-n[0])r)],u=h[+(0>r)],M=0,s=e.length;s>M;++M){var c=e[M];if(c[0][0]<=a&&apa*pa+da)){var a=t,r=n,e=25;do{var o,i=Math.sin(a),h=Math.sin(a/2),M=Math.cos(a/2),s=Math.sin(r),c=Math.cos(r),f=Math.sin(2*r),v=s*s,l=c*c,g=h*h,d=1-l*M*M,b=d?u(c*M)*Math.sqrt(o=1/d):o=0,p=2*b*c*h-t,w=b*s-n,q=o*(l*g+b*c*M*v),m=o*(.5*i*f-2*b*s*h),y=.25*o*(f*h-b*s*l*i),S=o*(v*M+b*g*c),Q=m*y-S*q;if(!Q)break;var R=(w*m-p*S)/Q,T=(p*y-w*q)/Q;a-=R,r-=T}while((Math.abs(R)>da||Math.abs(T)>da)&&--e>0);return[a,r]}},(d3.geo.aitoff=function(){return Sa(f)}).raw=f,(d3.geo.armadillo=l).raw=v,q.invert=function(t,n){if(t*=3/8,n*=3/8,!t&&Math.abs(n)>1)return null;var a=t*t,r=n*n,e=1+a+r,o=Math.sqrt(.5*(e-Math.sqrt(e*e-4*n*n))),u=h(o)/3,M=o?w(Math.abs(n/o))/3:p(Math.abs(t))/3,s=Math.cos(u),c=b(M),f=c*c-s*s;return[2*i(t)*Math.atan2(d(M)*s,.25-f),2*i(n)*Math.atan2(c*Math.sin(u),.25+f)]},(d3.geo.august=function(){return Sa(q)}).raw=q;var Ra=Math.log(1+Math.SQRT2);m.invert=function(t,n){if((r=Math.abs(n))ba&&--h>0);return[t/(Math.cos(o)*(e-1/Math.sin(o))),i(n)*o]},(d3.geo.baker=function(){return Sa(m)}).raw=m;var Ta=d3.geo.azimuthalEquidistant.raw;(d3.geo.berghaus=S).raw=y;var xa=Q(pa),Ea=R(Math.SQRT2/wa,Math.SQRT2,pa);(d3.geo.mollweide=function(){return Sa(Ea)}).raw=Ea,T.invert=function(t,n){var a,r,e=2.00276,o=e*n,i=0>n?-pa/4:pa/4,h=25;do r=o-Math.SQRT2*Math.sin(i),i-=a=(Math.sin(2*i)+2*i-pa*Math.sin(r))/(2*Math.cos(2*i)+2+pa*Math.cos(r)*Math.SQRT2*Math.cos(i));while(Math.abs(a)>da&&--h>0);return r=o-Math.SQRT2*Math.sin(i),[t*(1/Math.cos(r)+1.11072/Math.cos(i))/e,r]},(d3.geo.boggs=function(){return Sa(T)}).raw=T,E.invert=function(t,n){return[t/Math.cos(n),n]},(d3.geo.sinusoidal=function(){return Sa(E)}).raw=E,(d3.geo.bonne=function(){return x(k).parallel(45)}).raw=k,(d3.geo.bottomley=function(){var t=pa/6,n=d3.geo.projectionMutator(P),a=n(t);return a.variant=function(a){return arguments.length?n(t=+a):t},a}).raw=P;var ka=R(1,4/pa,pa);(d3.geo.bromley=function(){return Sa(ka)}).raw=ka,(d3.geo.chamberlin=z).raw=_,G.invert=function(t,n){var a=(a=n/qa-1)*a;return[a>0?t*Math.sqrt(pa/a)/2:0,h(1-a)]},(d3.geo.collignon=function(){return Sa(G)}).raw=G,(d3.geo.craig=function(){return x(C)}).raw=C,D.invert=function(t,n){var a=Math.sqrt(3),r=3*h(n/(a*qa));return[qa*t/(a*(2*Math.cos(2*r/3)-1)),r]},(d3.geo.craster=function(){return Sa(D)}).raw=D,(d3.geo.cylindricalEqualArea=function(){return x(L)}).raw=L,(d3.geo.cylindricalStereographic=function(){return x(O)}).raw=O,H.invert=function(t,n){var a=Math.sqrt(8/(3*pa)),r=n/a;return[t/(a*(1-Math.abs(r)/pa)),r]},(d3.geo.eckert1=function(){return Sa(H)}).raw=H,I.invert=function(t,n){var a=2-Math.abs(n)/Math.sqrt(2*pa/3);return[t*Math.sqrt(6*pa)/(2*a),i(n)*h((4-a*a)/3)]},(d3.geo.eckert2=function(){return Sa(I)}).raw=I,J.invert=function(t,n){var a=Math.sqrt(pa*(4+pa))/2;return[t*a/(1+M(1-n*n*(4+pa)/(4*pa))),n*a/2]},(d3.geo.eckert3=function(){return Sa(J)}).raw=J,K.invert=function(t,n){var a=.5*n*Math.sqrt((4+pa)/pa),r=h(a),e=Math.cos(r);return[t/(2/Math.sqrt(pa*(4+pa))*(1+e)),h((r+a*(e+2))/(2+wa))]},(d3.geo.eckert4=function(){return Sa(K)}).raw=K,N.invert=function(t,n){var a=Math.sqrt(2+pa),r=n*a/2;return[a*t/(1+Math.cos(r)),r]},(d3.geo.eckert5=function(){return Sa(N)}).raw=N,U.invert=function(t,n){var a=1+wa,r=Math.sqrt(a/2);return[2*t*r/(1+Math.cos(n*=r)),h((n+Math.sin(n))/a)]},(d3.geo.eckert6=function(){return Sa(U)}).raw=U,V.invert=function(t,n){var a=d3.geo.august.raw.invert(t/1.2,1.065*n);if(!a)return null;var r=a[0],e=a[1],o=20;t/=Pa,n/=Pa;do{var i=r/2,h=e/2,u=Math.sin(i),M=Math.cos(i),s=Math.sin(h),c=Math.cos(h),f=Math.cos(e),v=Math.sqrt(f),l=s/(c+Math.SQRT2*M*v),g=l*l,d=Math.sqrt(2/(1+g)),b=Math.SQRT2*c+(M+u)*v,p=Math.SQRT2*c+(M-u)*v,w=b/p,q=Math.sqrt(w),m=q-1/q,y=q+1/q,S=d*m-2*Math.log(q)-t,Q=d*l*y-2*Math.atan(l)-n,R=s&&Math.SQRT1_2*v*u*g/s,T=(Math.SQRT2*M*c+v)/(2*(c+Math.SQRT2*M*v)*(c+Math.SQRT2*M*v)*v),x=-.5*l*d*d*d,E=x*R,k=x*T,P=(P=2*c+Math.SQRT2*v*(M-u))*P*q,_=(Math.SQRT2*M*c*v+f)/P,z=-(Math.SQRT2*u*s)/(v*P),B=m*E-2*_/q+d*(_+_/w),F=m*k-2*z/q+d*(z+z/w),j=l*y*E-2*R/(1+g)+d*y*R+d*l*(_-_/w),A=l*y*k-2*T/(1+g)+d*y*T+d*l*(z-z/w),G=F*j-A*B;if(!G)break;var C=(Q*F-S*A)/G,D=(S*j-Q*B)/G;r-=C,e=Math.max(-wa,Math.min(wa,e-D))}while((Math.abs(C)>da||Math.abs(D)>da)&&--o>0);return Math.abs(Math.abs(e)-wa)da&&--o>0);o=50,t/=1-.162388*i;do{var h=(h=r*r)*h;r-=a=(r*(.87-952426e-9*h)-t)/(.87-.00476213*h)}while(Math.abs(a)>da&&--o>0);return[r,e]},(d3.geo.ginzburg8=function(){return Sa(an)}).raw=an;var Aa=nn(2.6516,-.76534,.19123,-.047094,1.36289,-.13965,.031762);(d3.geo.ginzburg9=function(){return Sa(Aa)}).raw=Aa,en.invert=function(t,n){var a=i(t),r=i(n),e=-a*t,o=-r*n,u=1>o/e,M=hn(u?o:e,u?e:o),s=M[0],c=M[1];u&&(s=-wa-s);var f=Math.cos(c),t=Math.cos(s)*f,n=Math.sin(s)*f,v=Math.sin(c);return[a*(Math.atan2(n,-v)+pa),r*h(t)]},d3.geo.gringorten=rn(en),fn.invert=function(t,n){var a=(Math.SQRT2-1)/(Math.SQRT2+1),r=Math.sqrt(1-a*a),e=cn(wa,r*r),o=-1,i=un(.5*e-n,-t,r*r),h=ln(i[0],i[1]),u=Math.atan2(h[1],h[0])/o;return[u,2*Math.atan(Math.exp(.5/o*Math.log(a*h[0]*h[0]+a*h[1]*h[1])))-wa]},d3.geo.guyou=rn(fn),(d3.geo.hammerRetroazimuthal=bn).raw=gn;var Ga=d3.geo.azimuthalEqualArea.raw;qn.invert=function(t,n){var a=2*h(n/2);return[t*Math.cos(a/2)/Math.cos(a),a]},(d3.geo.hammer=wn).raw=pn,mn.invert=function(t,n){var a=Math.abs(a=n*(0>n?.5179951515653813:.5686373742600607))>1-da?a>0?wa:-wa:h(a);return[1.1764705882352942*t/Math.cos(a),Math.abs(a=((a+=a)+Math.sin(a))*(0>n?.4102345310814193:.3736990601468637))>1-da?a>0?wa:-wa:h(a)]},(d3.geo.hatano=function(){return Sa(mn)}).raw=mn;var Ca=41+48/36+37/3600;(d3.geo.healpix=Sn).raw=yn,(d3.geo.hill=Rn).raw=Qn;var Da=.7109889596207567,La=.0528035274542;Tn.invert=function(t,n){return n>-Da?Ea.invert(t,n-La):E.invert(t,n)},(d3.geo.sinuMollweide=function(){return Sa(Tn).rotate([-20,-55])}).raw=Tn,xn.invert=function(t,n){return Math.abs(n)>Da?Ea.invert(t,n+(n>0?La:-La)):E.invert(t,n)},(d3.geo.homolosine=function(){return Sa(xn)}).raw=xn,En.invert=function(t,n){return[2/3*pa*t/Math.sqrt(pa*pa/3-n*n),n]},(d3.geo.kavrayskiy7=function(){return Sa(En)}).raw=En,(d3.geo.lagrange=Pn).raw=kn,_n.invert=function(t,n){var a=Math.abs(t),r=Math.abs(n),e=pa/Math.SQRT2,o=da,i=wa;e>r?i*=r/e:o+=6*u(e/r);for(var h=0;25>h;h++){var s=Math.sin(i),c=M(Math.cos(i)),f=Math.sin(i/2),v=Math.cos(i/2),l=Math.sin(o/6),g=Math.cos(o/6),d=.5*o*(1+c)-a,b=i/(v*g)-r,p=c?-.25*o*s/c:0,w=.5*(1+c),q=(1+.5*i*f/v)/(v*g),m=i/v*(l/6)/(g*g),y=p*m-q*w,S=(d*m-b*w)/y,Q=(b*p-d*q)/y;if(i-=S,o-=Q,Math.abs(S)t?-o:o,0>n?-i:i]},(d3.geo.larrivee=function(){return Sa(_n)}).raw=_n,zn.invert=function(t,n){var a=i(t)*pa,r=n/2,e=50;do{var o=a*a,h=r*r,u=a*r,M=a*(.975534+h*(-.119161+o*-.0143059+h*-.0547009))-t,s=r*(1.00384+o*(.0802894+h*-.02855+199025e-9*o)+h*(.0998909+h*-.0491032))-n,c=.975534-h*(.119161+.0143059*3*o+.0547009*h),f=-u*(.238322+.2188036*h+.0286118*o),v=u*(.1605788+7961e-7*o+-0.0571*h),l=1.00384+o*(.0802894+199025e-9*o)+h*(3*(.0998909-.02855*o)-.245516*h),g=f*v-l*c,d=(s*f-M*l)/g,b=(M*v-s*c)/g;a-=d,r-=b}while((Math.abs(d)>da||Math.abs(b)>da)&&--e>0);return e&&[a,r]},(d3.geo.laskowski=function(){return Sa(zn)}).raw=zn,Bn.invert=function(t,n){var a=t*t,r=n*n,e=r+1,o=t?Math.SQRT1_2*Math.sqrt((e-Math.sqrt(a*a+2*a*(r-1)+e*e))/a+1):1/Math.sqrt(e);return[h(t*o),i(n)*u(o)]},(d3.geo.littrow=function(){return Sa(Bn)}).raw=Bn,(d3.geo.loximuthal=function(){return x(Fn).parallel(40)}).raw=Fn,jn.invert=function(t,n){return[t,2.5*Math.atan(Math.exp(.8*n))-.625*pa]},(d3.geo.miller=function(){return Sa(jn)}).raw=jn;var Oa={alaska:[[.9972523,0],[.0052513,-.0041175],[.0074606,.0048125],[-.0153783,-.1968253],[.0636871,-.1408027],[.3660976,-.2937382]],gs48:[[.98879,0],[0,0],[-.050909,0],[0,0],[.075528,0]],gs50:[[.984299,0],[.0211642,.0037608],[-.1036018,-.0575102],[-.0329095,-.0320119],[.0499471,.1223335],[.026046,.0899805],[7388e-7,-.1435792],[.0075848,-.1334108],[-.0216473,.0776645],[-.0225161,.0853673]],miller:[[.9245,0],[0,0],[.01943,0]],lee:[[.721316,0],[0,0],[-.00881625,-.00617325]]};(d3.geo.modifiedStereographic=Gn).raw=An,Cn.invert=function(t,n){var a=Math.sqrt(6),r=Math.sqrt(7),e=3*h(n*r/9);return[t*r/(a*(2*Math.cos(2*e/3)-1)),h(3*Math.sin(e)*a/7)]},(d3.geo.mtFlatPolarParabolic=function(){return Sa(Cn)}).raw=Cn,Dn.invert=function(t,n){var a=n*Math.sqrt(2+Math.SQRT2)/(2*Math.sqrt(3)),r=2*h(a);return[3*Math.SQRT2*t/(1+2*Math.cos(r)/Math.cos(r/2)),h((a+Math.sin(r))/(1+Math.SQRT1_2))]},(d3.geo.mtFlatPolarQuartic=function(){return Sa(Dn)}).raw=Dn,Ln.invert=function(t,n){var a=Math.sqrt(6/(4+pa)),r=n/a;return Math.abs(Math.abs(r)-wa)r?-wa:wa),[1.5*t/(a*(.5+Math.cos(r))),h((r/2+Math.sin(r))/(1+pa/4))]},(d3.geo.mtFlatPolarSinusoidal=function(){return Sa(Ln)}).raw=Ln,On.invert=function(t,n){var a,r=n,e=25;do{var o=r*r,i=o*o;r-=a=(r*(1.007226+o*(.015085+i*(-.044475+.028874*o-.005916*i)))-n)/(1.007226+o*(.045255+i*(-0.311325+.259866*o-.005916*11*i)))}while(Math.abs(a)>da&&--e>0);return[t/(.8707+(o=r*r)*(-.131979+o*(-.013791+o*o*o*(.003971-.001529*o)))),r]},(d3.geo.naturalEarth=function(){return Sa(On)}).raw=On,Hn.invert=function(t,n){for(var a=n/2,r=0,e=1/0;10>r&&Math.abs(e)>da;r++){var o=Math.cos(n/2);n-=e=(n-Math.tan(n/2)-a)/(1-.5/(o*o))}return[2*t/(1+Math.cos(n)),n]},(d3.geo.nellHammer=function(){return Sa(Hn)}).raw=Hn;var Ha=1.0148,Ia=.23185,Ja=-.14499,Ka=.02406,Na=Ha,Ua=5*Ia,Va=7*Ja,Wa=9*Ka,Xa=1.790857183;In.invert=function(t,n){n>Xa?n=Xa:-Xa>n&&(n=-Xa);var a,r=n;do{var e=r*r;r-=a=(r*(Ha+e*e*(Ia+e*(Ja+Ka*e)))-n)/(Na+e*e*(Ua+e*(Va+Wa*e)))}while(Math.abs(a)>da);return[t,r]},(d3.geo.patterson=function(){return Sa(In)}).raw=In;var Ya=rn(fn);(d3.geo.peirceQuincuncial=function(){return Ya().quincuncial(!0).rotate([-90,-90,45]).clipAngle(180-1e-6)}).raw=Ya.raw,Jn.invert=function(t,n){if(Math.abs(n)da&&--o>0);return M=Math.tan(e),[(Math.abs(n)=0||1===o){r=(n>=0?5:-5)*(v+e);var l,g=50;do e=Math.min(18,Math.abs(r)/5),o=Math.floor(e),v=e-o,i=Za[o][1],h=Za[o+1][1],u=Za[Math.min(19,o+2)][1],r-=(l=(n>=0?wa:-wa)*(h+v*(u-i)/2+v*v*(u-2*h+i)/2)-n)*ya;while(Math.abs(l)>ba&&--g>0);break}}while(--o>=0);var d=Za[o][0],b=Za[o+1][0],p=Za[Math.min(19,o+2)][0];return[t/(b+v*(p-d)/2+v*v*(p-2*b+d)/2),r*ma]},(d3.geo.robinson=function(){return Sa(Nn)}).raw=Nn,(d3.geo.satellite=Wn).raw=Vn,Xn.invert=function(t,n){var a=n/1.70711,r=Math.sin(pa/4*a);return[t/(.74482-.34588*r*r),2*Math.atan(a)]},(d3.geo.times=function(){return Sa(Xn)}).raw=Xn,(d3.geo.twoPointEquidistant=Zn).raw=Yn,(d3.geo.twoPointAzimuthal=ta).raw=$n,na.invert=function(t,n){if(Math.abs(n)da&&--h>0);return[i(t)*(Math.sqrt(r*r+4)+r)*pa/4,wa*o]},(d3.geo.vanDerGrinten4=function(){return Sa(ea)}).raw=ea;var $a=function(){var t=4*pa+3*Math.sqrt(3),n=2*Math.sqrt(2*pa*Math.sqrt(3)/t);return R(n*Math.sqrt(3)/pa,n,t/6)}();(d3.geo.wagner4=function(){return Sa($a)}).raw=$a,oa.invert=function(t,n){return[t/Math.sqrt(1-3*n*n/(pa*pa)),n]},(d3.geo.wagner6=function(){return Sa(oa)}).raw=oa,ia.invert=function(t,n){var a=t/2.66723,r=n/1.24104,e=Math.sqrt(a*a+r*r),o=2*h(e/2);return[3*Math.atan2(t*Math.tan(o),2.66723*e),e&&h(n*Math.sin(o)/(1.24104*.90631*e))]},(d3.geo.wagner7=function(){return Sa(ia)}).raw=ia,ha.invert=function(t,n){var a=-.5*(t*t+n*n),r=Math.sqrt(-a*(2+a)),e=n*a+t*r,o=t*a-n*r,i=Math.sqrt(o*o+e*e);return[Math.atan2(r*e,i*(1+a)),i?-h(r*o/i):0]},(d3.geo.wiechel=function(){return Sa(ha)}).raw=ha,ua.invert=function(t,n){var a=t,r=n,e=25;do{var o,i=Math.cos(r),h=Math.sin(r),M=Math.sin(2*r),s=h*h,c=i*i,f=Math.sin(a),v=Math.cos(a/2),l=Math.sin(a/2),g=l*l,d=1-c*v*v,b=d?u(i*v)*Math.sqrt(o=1/d):o=0,p=.5*(2*b*i*l+a/wa)-t,w=.5*(b*h+r)-n,q=.5*o*(c*g+b*i*v*s)+.5/wa,m=o*(f*M/4-b*h*l),y=.125*o*(M*l-b*h*c*f),S=.5*o*(s*v+b*g*i)+.5,Q=m*y-S*q,R=(w*m-p*S)/Q,T=(p*y-w*q)/Q;a-=R,r-=T}while((Math.abs(R)>da||Math.abs(T)>da)&&--e>0);return[a,r]},(d3.geo.winkel3=function(){return Sa(ua)}).raw=ua}(); \ No newline at end of file diff --git a/third_party/d3/LICENSE b/third_party/d3/LICENSE new file mode 100644 index 000000000000..ff3f2e5419a8 --- /dev/null +++ b/third_party/d3/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2010-2016, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/d3/README.amp b/third_party/d3/README.amp new file mode 100644 index 000000000000..dfb4fcd25c87 --- /dev/null +++ b/third_party/d3/README.amp @@ -0,0 +1,10 @@ +URL: https://github.com/d3/d3/tree/v3.5.17 +License: BSD +License File: https://github.com/d3/d3/blob/v3.5.17/LICENSE + +Description: +Copy of minified (d3.min.js) from release v3.5.17 +https://github.com/d3/d3/tree/v3.5.17 + +Local Modifications: +None diff --git a/third_party/d3/d3.js b/third_party/d3/d3.js new file mode 100644 index 000000000000..166487309a77 --- /dev/null +++ b/third_party/d3/d3.js @@ -0,0 +1,5 @@ +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++ie;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t0&&(n=n.slice(0,a));var c=To.get(n);return c&&(n=c,l=B),a?t?i:r:t?b:u}function $(n,t){return function(e){var r=ao.event;ao.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ao.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Do,i="click"+r,u=ao.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ro&&(Ro="onselectstart"in e?!1:x(e.style,"userSelect")),Ro){var o=n(e).style,a=o[Ro];o[Ro]="none"}return function(n){if(u.on(r,null),Ro&&(o[Ro]=a),n){var t=function(){u.on(i,null)};u.on(i,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var i=r.createSVGPoint();if(0>Po){var u=t(n);if(u.scrollX||u.scrollY){r=ao.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Po=!(o.f||o.e),r.remove()}}return Po?(i.x=e.pageX,i.y=e.pageY):(i.x=e.clientX,i.y=e.clientY),i=i.matrixTransform(n.getScreenCTM().inverse()),[i.x,i.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ao.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Fo:Math.acos(n)}function tn(n){return n>1?Io:-1>n?-Io:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function on(n){return(n=Math.sin(n/2))*n}function an(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(o-u)*n/60:180>n?o:240>n?u+(o-u)*(240-n)/60:u}function i(n){return Math.round(255*r(n))}var u,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,u=2*e-o,new mn(i(n+120),i(n),i(n-120))}function fn(n,t,e){return this instanceof fn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof fn?new fn(n.h,n.c,n.l):n instanceof hn?gn(n.l,n.a,n.b):gn((n=Sn((n=ao.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new fn(n,t,e)}function sn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Yo)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof fn?sn(n.h,n.c,n.l):Sn((n=mn(n)).r,n.g,n.b):new hn(n,t,e)}function pn(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=vn(i)*na,r=vn(r)*ta,u=vn(u)*ea,new mn(yn(3.2404542*i-1.5371385*r-.4985314*u),yn(-.969266*i+1.8760108*r+.041556*u),yn(.0556434*i-.2040259*r+1.0572252*u))}function gn(n,t,e){return n>0?new fn(Math.atan2(e,t)*Zo,Math.sqrt(t*t+e*e),n):new fn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function yn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mn(n,t,e){return this instanceof mn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mn?new mn(n.r,n.g,n.b):_n(""+n,mn,cn):new mn(n,t,e)}function Mn(n){return new mn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,i,u,o=0,a=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Nn(i[0]),Nn(i[1]),Nn(i[2]))}return(u=ua.get(n))?t(u.r,u.g,u.b):(null==n||"#"!==n.charAt(0)||isNaN(u=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&u)>>4,o=o>>4|o,a=240&u,a=a>>4|a,l=15&u,l=l<<4|l):7===n.length&&(o=(16711680&u)>>16,a=(65280&u)>>8,l=255&u)),t(o,a,l))}function wn(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-u,l=(o+u)/2;return a?(i=.5>l?a/(o+u):a/(2-o-u),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=NaN,i=l>0&&1>l?0:r),new ln(r,i,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/na),i=dn((.2126729*n+.7151522*t+.072175*e)/ta),u=dn((.0193339*n+.119192*t+.9503041*e)/ea);return hn(116*i-16,500*(r-i),200*(i-u))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function i(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(u,l)}catch(r){return void o.error.call(u,r)}o.load.call(u,n)}else o.error.call(u,l)}var u={},o=ao.dispatch("beforesend","progress","load","error"),a={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=i:l.onreadystatechange=function(){l.readyState>3&&i()},l.onprogress=function(n){var t=ao.event;ao.event=n;try{o.progress.call(u,l)}finally{ao.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(c=n,u):c},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(co(arguments)))}}),u.send=function(e,r,i){if(2===arguments.length&&"function"==typeof r&&(i=r,r=null),l.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),l.setRequestHeader)for(var f in a)l.setRequestHeader(f,a[f]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),o.beforesend.call(u,l),l.send(null==r?null:r),u},u.abort=function(){return l.abort(),u},ao.rebind(u,o,"on"),null==r?u:u.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var i=e+t,u={c:n,t:i,n:null};return aa?aa.n=u:oa=u,aa=u,la||(ca=clearTimeout(ca),la=1,fa(Tn)),u}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(ca),ca=setTimeout(Tn,t)),la=0):(la=1,fa(Tn))}function Rn(){for(var n=Date.now(),t=oa;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=oa,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,i=n.currency,u=r&&e?function(n,t){for(var i=n.length,u=[],o=0,a=r[0],l=0;i>0&&a>0&&(l+a+1>t&&(a=Math.max(1,t-l)),u.push(n.substring(i-=a,i+a)),!((l+=a+1)>t));)a=r[o=(o+1)%r.length];return u.reverse().join(e)}:m;return function(n){var e=ha.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",l=e[4]||"",c=e[5],f=+e[6],s=e[7],h=e[8],p=e[9],g=1,v="",d="",y=!1,m=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===o)&&(c=r="0",o="="),p){case"n":s=!0,p="g";break;case"%":g=100,d="%",p="f";break;case"p":g=100,d="%",p="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+p.toLowerCase());case"c":m=!1;case"d":y=!0,h=0;break;case"s":g=-1,p="r"}"$"===l&&(v=i[0],d=i[1]),"r"!=p||h||(p="g"),null!=h&&("g"==p?h=Math.max(1,Math.min(21,h)):"e"!=p&&"f"!=p||(h=Math.max(0,Math.min(20,h)))),p=pa.get(p)||Fn;var M=c&&s;return function(n){var e=d;if(y&&n%1)return"";var i=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>g){var l=ao.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=g;n=p(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=m?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&s&&(x=u(x,1/0));var S=v.length+x.length+b.length+(M?0:i.length),k=f>S?new Array(S=f-S+1).join(r):"";return M&&(x=u(k+x,k.length?f-b.length:1/0)),i+=v,n=x+b,("<"===o?i+n+k:">"===o?k+i+n:"^"===o?k.substring(0,S>>=1)+i+n+k.substring(S):i+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new va(e-1)),1),e}function u(n,e){return t(n=new va(+n),e),n}function o(n,r,u){var o=i(n),a=[];if(u>1)for(;r>o;)e(o)%u||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{va=Hn;var r=new Hn;return r._=n,o(r,t,e)}finally{va=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=o;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(i),l.offset=In(u),l.range=a,n}function In(n){return function(t,e){try{va=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{va=Date}}}function Yn(n){function t(n){function t(t){for(var e,i,u,o=[],a=-1,l=0;++aa;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(o=t.charAt(a++),u=C[o in ya?t.charAt(a++):o],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function f(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var s=n.dateTime,h=n.date,p=n.time,g=n.periods,v=n.days,d=n.shortDays,y=n.months,m=n.shortMonths;t.utc=function(n){function e(n){try{va=Hn;var t=new va;return t._=n,r(t)}finally{va=Date}}var r=t(n);return e.parse=function(n){try{va=Hn;var t=r.parse(n);return t&&t._}finally{va=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=ao.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(y),k=Xn(y),N=Vn(m),E=Xn(m);g.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return m[n.getMonth()]},B:function(n){return y[n.getMonth()]},c:t(s),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ga.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return g[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ga.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ga.mondayOfYear(n),t,2)},x:t(h),X:t(p),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:at,"%":function(){return"%"}},C={a:r,A:i,b:u,B:o,c:a,d:tt,e:tt,H:rt,I:rt,j:et,L:ot,m:nt,M:it,p:f,S:ut,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?new Array(e-u+1).join(t)+i:i)}function Vn(n){return new RegExp("^(?:"+n.map(ao.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function it(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function ut(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ot(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function at(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=xo(t)/60|0,i=xo(t)%60;return e+Zn(r,"0",2)+Zn(i,"0",2)}function lt(n,t,e){Ma.lastIndex=0;var r=Ma.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,l=Math.cos(t),c=Math.sin(t),f=u*c,s=i*l+f*Math.cos(a),h=f*o*Math.sin(a);ka.add(Math.atan2(h,s)),r=n,i=l,u=c}var t,e,r,i,u;Na.point=function(o,a){Na.point=n,r=(t=o)*Yo,i=Math.cos(a=(e=a)*Yo/2+Fo/4),u=Math.sin(a)},Na.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function yt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function mt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return xo(n[0]-t[0])a;++a)i.point((e=n[a])[0],e[1]);return void i.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,u.push(l),o.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,u.push(l),o.push(c)}}),o.sort(t),qt(u),qt(o),u.length){for(var a=0,l=e,c=o.length;c>a;++a)o[a].e=l=!l;for(var f,s,h=u[0];;){for(var p=h,g=!0;p.v;)if((p=p.n)===h)return;f=p.z,i.lineStart();do{if(p.v=p.o.v=!0,p.e){if(g)for(var a=0,c=f.length;c>a;++a)i.point((s=f[a])[0],s[1]);else r(p.x,p.n.x,1,i);p=p.n}else{if(g){f=p.p.z;for(var a=f.length-1;a>=0;--a)i.point((s=f[a])[0],s[1])}else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,f=p.z,g=!g}while(!p.v);i.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,i=n[0];++r0){for(b||(u.polygonStart(),b=!0),u.lineStart();++o1&&2&t&&e.push(e.pop().concat(e.shift())),p.push(e.filter(Dt))}var p,g,v,d=t(u),y=i.invert(r[0],r[1]),m={point:o,lineStart:l,lineEnd:c,polygonStart:function(){m.point=f,m.lineStart=s,m.lineEnd=h,p=[],g=[]},polygonEnd:function(){m.point=o,m.lineStart=l,m.lineEnd=c,p=ao.merge(p);var n=Ot(y,g);p.length?(b||(u.polygonStart(),b=!0),Lt(p,Ut,n,e,u)):n&&(b||(u.polygonStart(),b=!0),u.lineStart(),e(null,null,1,u),u.lineEnd()),b&&(u.polygonEnd(),b=!1),p=g=null},sphere:function(){u.polygonStart(),u.lineStart(),e(null,null,1,u),u.lineEnd(),u.polygonEnd()}},M=Pt(),x=t(M),b=!1;return m}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Io-Uo:Io-n[1])-((t=t.x)[0]<0?t[1]-Io-Uo:Io-t[1])}function jt(n){var t,e=NaN,r=NaN,i=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(u,o){var a=u>0?Fo:-Fo,l=xo(u-e);xo(l-Fo)0?Io:-Io),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(u,r),t=0):i!==a&&l>=Fo&&(xo(e-i)Uo?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*o)):(t+r)/2}function Ht(n,t,e,r){var i;if(null==n)i=e*Io,r.point(-Fo,i),r.point(0,i),r.point(Fo,i),r.point(Fo,0),r.point(Fo,-i),r.point(0,-i),r.point(-Fo,-i),r.point(-Fo,0),r.point(-Fo,i);else if(xo(n[0]-t[0])>Uo){var u=n[0]a;++a){var c=t[a],f=c.length;if(f)for(var s=c[0],h=s[0],p=s[1]/2+Fo/4,g=Math.sin(p),v=Math.cos(p),d=1;;){d===f&&(d=0),n=c[d];var y=n[0],m=n[1]/2+Fo/4,M=Math.sin(m),x=Math.cos(m),b=y-h,_=b>=0?1:-1,w=_*b,S=w>Fo,k=g*M;if(ka.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),u+=S?b+_*Ho:b,S^h>=e^y>=e){var N=mt(dt(s),dt(n));bt(N);var E=mt(i,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=y,g=M,v=x,s=n}}return(-Uo>u||Uo>u&&-Uo>ka)^1&o}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>u}function e(n){var e,u,l,c,f;return{lineStart:function(){c=l=!1,f=1},point:function(s,h){var p,g=[s,h],v=t(s,h),d=o?v?0:i(s,h):v?i(s+(0>s?Fo:-Fo),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(p=r(e,g),(wt(e,p)||wt(g,p))&&(g[0]+=Uo,g[1]+=Uo,v=t(g[0],g[1]))),v!==l)f=0,v?(n.lineStart(),p=r(g,e),n.point(p[0],p[1])):(p=r(e,g),n.point(p[0],p[1]),n.lineEnd()),e=p;else if(a&&e&&o^v){var y;d&u||!(y=r(g,e,!0))||(f=0,o?(n.lineStart(),n.point(y[0][0],y[0][1]),n.point(y[1][0],y[1][1]),n.lineEnd()):(n.point(y[1][0],y[1][1]),n.lineEnd(),n.lineStart(),n.point(y[0][0],y[0][1])))}!v||e&&wt(e,g)||n.point(g[0],g[1]),e=g,l=v,u=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return f|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),i=dt(t),o=[1,0,0],a=mt(r,i),l=yt(a,a),c=a[0],f=l-c*c;if(!f)return!e&&n;var s=u*l/f,h=-u*c/f,p=mt(o,a),g=xt(o,s),v=xt(a,h);Mt(g,v);var d=p,y=yt(g,d),m=yt(d,d),M=y*y-m*(yt(g,g)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-y-x)/m);if(Mt(b,g),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=xo(E-Fo)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(xo(b[0]-w)Fo^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-y+x)/m);return Mt(z,g),[b,_t(z)]}}}function i(t,e){var r=o?n:Fo-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}var u=Math.cos(n),o=u>0,a=xo(u)>Uo,l=ve(n,6*Yo);return Rt(t,e,l,o?[0,-n]:[-Fo,n-Fo])}function Yt(n,t,e,r){return function(i){var u,o=i.a,a=i.b,l=o.x,c=o.y,f=a.x,s=a.y,h=0,p=1,g=f-l,v=s-c;if(u=n-l,g||!(u>0)){if(u/=g,0>g){if(h>u)return;p>u&&(p=u)}else if(g>0){if(u>p)return;u>h&&(h=u)}if(u=e-l,g||!(0>u)){if(u/=g,0>g){if(u>p)return;u>h&&(h=u)}else if(g>0){if(h>u)return;p>u&&(p=u)}if(u=t-c,v||!(u>0)){if(u/=v,0>v){if(h>u)return;p>u&&(p=u)}else if(v>0){if(u>p)return;u>h&&(h=u)}if(u=r-c,v||!(0>u)){if(u/=v,0>v){if(u>p)return;u>h&&(h=u)}else if(v>0){if(h>u)return;p>u&&(p=u)}return h>0&&(i.a={x:l+h*g,y:c+h*v}),1>p&&(i.b={x:l+p*g,y:c+p*v}),i}}}}}}function Zt(n,t,e,r){function i(r,i){return xo(r[0]-n)0?0:3:xo(r[0]-e)0?2:1:xo(r[1]-t)0?1:0:i>0?3:2}function u(n,t){return o(n.x,t.x)}function o(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function l(n){for(var t=0,e=d.length,r=n[1],i=0;e>i;++i)for(var u,o=1,a=d[i],l=a.length,c=a[0];l>o;++o)u=a[o],c[1]<=r?u[1]>r&&Q(c,u,n)>0&&++t:u[1]<=r&&Q(c,u,n)<0&&--t,c=u;return 0!==t}function c(u,a,l,c){var f=0,s=0;if(null==u||(f=i(u,l))!==(s=i(a,l))||o(u,a)<0^l>0){do c.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+l+4)%4)!==s)}else c.point(a[0],a[1])}function f(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function s(n,t){f(n,t)&&a.point(n,t)}function h(){C.point=g,d&&d.push(y=[]),S=!0,w=!1,b=_=NaN}function p(){v&&(g(m,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=s,w&&a.lineEnd()}function g(n,t){n=Math.max(-Ha,Math.min(Ha,n)),t=Math.max(-Ha,Math.min(Ha,t));var e=f(n,t);if(d&&y.push([n,t]),S)m=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,y,m,M,x,b,_,w,S,k,N=a,E=Pt(),A=Yt(n,t,e,r),C={point:s,lineStart:h,lineEnd:p,polygonStart:function(){a=E,v=[],d=[],k=!0},polygonEnd:function(){a=N,v=ao.merge(v);var t=l([n,r]),e=k&&t,i=v.length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),c(null,null,1,a),a.lineEnd()),i&&Lt(v,u,t,c,a),a.polygonEnd()),v=d=y=null}};return C}}function Vt(n){var t=0,e=Fo/3,r=ae(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*Fo/180,e=n[1]*Fo/180):[t/Fo*180,e/Fo*180]},i}function Xt(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),o-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),o=Math.sqrt(u)/i;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/i,tn((u-(n*n+e*e)*i*i)/(2*i))]},e}function $t(){function n(n,t){Ia+=i*n-r*t,r=n,i=t}var t,e,r,i;$a.point=function(u,o){$a.point=n,t=r=u,e=i=o},$a.lineEnd=function(){n(t,e)}}function Bt(n,t){Ya>n&&(Ya=n),n>Va&&(Va=n),Za>t&&(Za=t),t>Xa&&(Xa=t)}function Wt(){function n(n,t){o.push("M",n,",",t,u)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function i(){o.push("Z")}var u=Jt(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return u=Jt(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ca+=n,za+=t,++La}function Kt(){function n(n,r){var i=n-t,u=r-e,o=Math.sqrt(i*i+u*u);qa+=o*(t+n)/2,Ta+=o*(e+r)/2,Ra+=o,Gt(t=n,e=r)}var t,e;Wa.point=function(r,i){Wa.point=n,Gt(t=r,e=i)}}function Qt(){Wa.point=Gt}function ne(){function n(n,t){var e=n-r,u=t-i,o=Math.sqrt(e*e+u*u);qa+=o*(r+n)/2,Ta+=o*(i+t)/2,Ra+=o,o=i*n-r*t,Da+=o*(r+n),Pa+=o*(i+t),Ua+=3*o,Gt(r=n,i=t)}var t,e,r,i;Wa.point=function(u,o){Wa.point=n,Gt(t=r=u,e=i=o)},Wa.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,Ho)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function i(){a.point=t}function u(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=i,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function ee(n){function t(n){return(a?r:e)(n)}function e(t){return ue(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=u,t.lineStart()}function u(e,r){var u=dt([e,r]),o=n(e,r);i(M,x,m,b,_,w,M=o[0],x=o[1],m=e,b=u[0],_=u[1],w=u[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function l(){ +r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=o-t,r=K(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(o/r,1/u))-Io]},e):Ne}function ke(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return xo(i)i;i++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var i=n[0],u=e[0],o=t[0]-i,a=r[0]-u,l=n[1],c=e[1],f=t[1]-l,s=r[1]-c,h=(a*(l-c)-s*(i-u))/(s*o-a*f);return[i+h*o,l+h*f]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),ol.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,i={x:e,y:r},u=n.P,o=n.N,a=[n];je(n);for(var l=u;l.circle&&xo(e-l.circle.x)f;++f)c=a[f],l=a[f-1],nr(c.edge,l.site,c.site,i);l=a[0],c=a[s-1],c.edge=Ke(l.site,c.site,null,i),$e(l),$e(c)}function He(n){for(var t,e,r,i,u=n.x,o=n.y,a=ol._;a;)if(r=Oe(a,o)-u,r>Uo)a=a.L;else{if(i=u-Ie(a,o),!(i>Uo)){r>-Uo?(t=a.P,e=a):i>-Uo?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var l=Ue(n);if(ol.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),ol.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,f=c.x,s=c.y,h=n.x-f,p=n.y-s,g=e.site,v=g.x-f,d=g.y-s,y=2*(h*d-p*v),m=h*h+p*p,M=v*v+d*d,x={x:(d*m-p*M)/y+f,y:(h*M-v*m)/y+s};nr(e.edge,c,g,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,g,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,i=e.y,u=i-t;if(!u)return r;var o=n.P;if(!o)return-(1/0);e=o.site;var a=e.x,l=e.y,c=l-t;if(!c)return a;var f=a-r,s=1/u-1/c,h=f/c;return s?(-h+Math.sqrt(h*h-2*s*(f*f/(-2*c)-l+c/2+i-u/2)))/s+r:(r+a)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,i,u,o,a,l,c,f,s=n[0][0],h=n[1][0],p=n[0][1],g=n[1][1],v=ul,d=v.length;d--;)if(u=v[d],u&&u.prepare())for(a=u.edges,l=a.length,o=0;l>o;)f=a[o].end(),r=f.x,i=f.y,c=a[++o%l].start(),t=c.x,e=c.y,(xo(r-t)>Uo||xo(i-e)>Uo)&&(a.splice(o,0,new tr(Qe(u.site,f,xo(r-s)Uo?{x:s,y:xo(t-s)Uo?{x:xo(e-g)Uo?{x:h,y:xo(t-h)Uo?{x:xo(e-p)=-jo)){var p=l*l+c*c,g=f*f+s*s,v=(s*p-c*g)/h,d=(l*g-f*p)/h,s=d+a,y=fl.pop()||new Xe;y.arc=n,y.site=i,y.x=v+o,y.y=s+Math.sqrt(v*v+d*d),y.cy=s,n.circle=y;for(var m=null,M=ll._;M;)if(y.yd||d>=a)return;if(h>g){if(u){if(u.y>=c)return}else u={x:d,y:l};e={x:d,y:c}}else{if(u){if(u.yr||r>1)if(h>g){if(u){if(u.y>=c)return}else u={x:(l-i)/r,y:l};e={x:(c-i)/r,y:c}}else{if(u){if(u.yp){if(u){if(u.x>=a)return}else u={x:o,y:r*o+i};e={x:a,y:r*a+i}}else{if(u){if(u.xu||s>o||r>h||i>p)){if(g=n.point){var g,v=t-n.x,d=e-n.y,y=v*v+d*d;if(l>y){var m=Math.sqrt(l=y);r=t-m,i=e-m,u=t+m,o=e+m,a=g}}for(var M=n.nodes,x=.5*(f+h),b=.5*(s+p),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,f,s,x,b);break;case 1:c(n,x,s,h,b);break;case 2:c(n,f,b,x,p);break;case 3:c(n,x,b,h,p)}}}(n,r,i,u,o),a}function vr(n,t){n=ao.rgb(n),t=ao.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,o=t.g-r,a=t.b-i;return function(n){return"#"+bn(Math.round(e+u*n))+bn(Math.round(r+o*n))+bn(Math.round(i+a*n))}}function dr(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function yr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function mr(n,t){var e,r,i,u=hl.lastIndex=pl.lastIndex=0,o=-1,a=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=pl.exec(t));)(i=r.index)>u&&(i=t.slice(u,i),a[o]?a[o]+=i:a[++o]=i),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,l.push({i:o,x:yr(e,r)})),u=pl.lastIndex;return ur;++r)a[(e=l[r]).i]=e.x(n);return a.join("")})}function Mr(n,t){for(var e,r=ao.interpolators.length;--r>=0&&!(e=ao.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],i=[],u=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(Mr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;o>e;++e)i[e]=t[e];return function(n){for(e=0;a>e;++e)i[e]=r[e](n);return i}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Io)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ho*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ho/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=ao.hcl(n),t=ao.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,o=t.c-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return sn(e+u*n,r+o*n,i+a*n)+""}}function Dr(n,t){n=ao.hsl(n),t=ao.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,o=t.s-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return cn(e+u*n,r+o*n,i+a*n)+""}}function Pr(n,t){n=ao.lab(n),t=ao.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,o=t.a-r,a=t.b-i;return function(n){return pn(e+u*n,r+o*n,i+a*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),i=Fr(t,e),u=Hr(Or(e,t,-i))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:yr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:yr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var i=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:i-4,x:yr(n[0],t[0])},{i:i-2,x:yr(n[1],t[1])})}else 1===t[0]&&1===t[1]||e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=ao.transform(n),t=ao.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,i=-1,u=r.length;++i=0;)e.push(i[r])}function oi(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(u=n.children)&&(i=u.length))for(var i,u,o=-1;++oe;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function yi(n){return n.reduce(mi,0)}function mi(n,t){return n+t[1]}function Mi(n,t){return xi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function bi(n){return[ao.min(n),ao.max(n)]}function _i(n,t){return n.value-t.value}function wi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Si(n,t){n._pack_next=t,t._pack_prev=n}function ki(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ni(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),p=Math.max(n.y+n.r,p)}if((e=n.children)&&(c=e.length)){var e,r,i,u,o,a,l,c,f=1/0,s=-(1/0),h=1/0,p=-(1/0);if(e.forEach(Ei),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(i=e[1],i.x=i.r,i.y=0,t(i),c>2))for(u=e[2],zi(r,i,u),t(u),wi(r,u),r._pack_prev=u,wi(u,i),i=r._pack_next,o=3;c>o;o++){zi(r,i,u=e[o]);var g=0,v=1,d=1;for(a=i._pack_next;a!==i;a=a._pack_next,v++)if(ki(a,u)){g=1;break}if(1==g)for(l=r._pack_prev;l!==a._pack_prev&&!ki(l,u);l=l._pack_prev,d++);g?(d>v||v==d&&i.ro;o++)u=e[o],u.x-=y,u.y-=m,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(Ai)}}function Ei(n){n._pack_next=n._pack_prev=n}function Ai(n){delete n._pack_next,delete n._pack_prev}function Ci(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,o=i.length;++u=0;)t=i[u],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pi(n,t,e){return n.a.parent===t.parent?n.a:e}function Ui(n){return 1+ao.max(n,function(n){return n.y})}function ji(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Hi(n){var t,e=n.children;return e&&(t=e.length)?Hi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ii(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zi(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Vi(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Xi(n,t){var e,r=0,i=n.length-1,u=n[r],o=n[i];return u>o&&(e=r,r=i,i=e,e=u,u=o,o=e),n[r]=t.floor(u),n[i]=t.ceil(o),n}function $i(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bi(n,t,e,r){var i=[],u=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?Bi:Vi,l=r?Wr:Br;return o=i(n,t,l,e),a=i(t,n,l,Mr),u}function u(n){return o(n)}var o,a;return u.invert=function(n){return a(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Qi(n,t)},u.tickFormat=function(t,e){return nu(n,t,e)},u.nice=function(t){return Gi(n,t),i()},u.copy=function(){return Wi(n,t,e,r)},i()}function Ji(n,t){return ao.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gi(n,t){return Xi(n,$i(Ki(n,t)[2])),Xi(n,$i(Ki(n,t)[2])),n}function Ki(n,t){null==t&&(t=10);var e=Yi(n),r=e[1]-e[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Qi(n,t){return ao.range.apply(ao,Ki(n,t))}function nu(n,t,e){var r=Ki(n,t);if(e){var i=ha.exec(e);if(i.shift(),"s"===i[8]){var u=ao.formatPrefix(Math.max(xo(r[0]),xo(r[1])));return i[7]||(i[7]="."+tu(u.scale(r[2]))),i[8]="f",e=ao.format(i.join("")),function(n){return e(u.scale(n))+u.symbol}}i[7]||(i[7]="."+eu(i[8],r)),e=i.join("")}else e=",."+tu(r[2])+"f";return ao.format(e)}function tu(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function eu(n,t){var e=tu(t[2]);return n in kl?Math.abs(e-tu(Math.max(xo(t[0]),xo(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ru(n,t,e,r){function i(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function u(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(i(t))}return o.invert=function(t){return u(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(i)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(i)),o):t},o.nice=function(){var t=Xi(r.map(i),e?Math:El);return n.domain(t),r=t.map(u),o},o.ticks=function(){var n=Yi(r),o=[],a=n[0],l=n[1],c=Math.floor(i(a)),f=Math.ceil(i(l)),s=t%1?2:t;if(isFinite(f-c)){if(e){for(;f>c;c++)for(var h=1;s>h;h++)o.push(u(c)*h);o.push(u(c))}else for(o.push(u(c));c++0;h--)o.push(u(c)*h);for(c=0;o[c]l;f--);o=o.slice(c,f)}return o},o.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=ao.format(e));var r=Math.max(1,t*n/o.ticks().length);return function(n){var o=n/u(Math.round(i(n)));return t-.5>o*t&&(o*=t),r>=o?e(n):""}},o.copy=function(){return ru(n.copy(),t,e,r)},Ji(o,n)}function iu(n,t,e){function r(t){return n(i(t))}var i=uu(t),u=uu(1/t);return r.invert=function(t){return u(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Qi(e,n)},r.tickFormat=function(n,t){return nu(e,n,t)},r.nice=function(n){return r.domain(Gi(e,n))},r.exponent=function(o){return arguments.length?(i=uu(t=o),u=uu(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return iu(n.copy(),t,e)},Ji(r,n)}function uu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(e){return u[((i.get(e)||("range"===t.t?i.set(e,n.push(e)):NaN))-1)%u.length]}function r(t,e){return ao.range(n.length).map(function(n){return t+e*n})}var i,u,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new c;for(var u,o=-1,a=r.length;++oe?[NaN,NaN]:[e>0?a[e-1]:n[0],et?NaN:t/u+n,[t,t+1/u]},r.copy=function(){return lu(n,t,e)},i()}function cu(n,t){function e(e){return e>=e?t[ao.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return cu(n,t)},e}function fu(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qi(n,t)},t.tickFormat=function(t,e){return nu(n,t,e)},t.copy=function(){return fu(n)},t}function su(){return 0}function hu(n){return n.innerRadius}function pu(n){return n.outerRadius}function gu(n){return n.startAngle}function vu(n){return n.endAngle}function du(n){return n&&n.padAngle}function yu(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function mu(n,t,e,r,i){var u=n[0]-t[0],o=n[1]-t[1],a=(i?r:-r)/Math.sqrt(u*u+o*o),l=a*o,c=-a*u,f=n[0]+l,s=n[1]+c,h=t[0]+l,p=t[1]+c,g=(f+h)/2,v=(s+p)/2,d=h-f,y=p-s,m=d*d+y*y,M=e-r,x=f*p-h*s,b=(0>y?-1:1)*Math.sqrt(Math.max(0,M*M*m-x*x)),_=(x*y-d*b)/m,w=(-x*d-y*b)/m,S=(x*y+d*b)/m,k=(-x*d+y*b)/m,N=_-g,E=w-v,A=S-g,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mu(n){function t(t){function o(){c.push("M",u(n(f),a))}for(var l,c=[],f=[],s=-1,h=t.length,p=En(e),g=En(r);++s1?n.join("L"):n+"Z"}function bu(n){return n.join("L")+"Z"}function _u(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1&&i.push("H",r[0]),i.join("")}function wu(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1){a=t[1],u=n[l],l++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(u[0]-a[0])+","+(u[1]-a[1])+","+u[0]+","+u[1];for(var c=2;c9&&(i=3*t/Math.sqrt(i),o[a]=i*e,o[a+1]=i*r));for(a=-1;++a<=l;)i=(n[Math.min(l,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),u.push([i||0,o[a]*i||0]);return u}function Fu(n){return n.length<3?xu(n):n[0]+Au(n,ju(n))}function Hu(n){for(var t,e,r,i=-1,u=n.length;++i=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},ao.max=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},ao.extent=function(n,t){var e,r,i,u=-1,o=n.length;if(1===arguments.length){for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}else{for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}return[e,i]},ao.sum=function(n,t){var e,r=0,u=n.length,o=-1;if(1===arguments.length)for(;++o1?l/(f-1):void 0},ao.deviation=function(){var n=ao.variance.apply(this,arguments);return n?Math.sqrt(n):n};var Mo=u(e);ao.bisectLeft=Mo.left,ao.bisect=ao.bisectRight=Mo.right,ao.bisector=function(n){return u(1===n.length?function(t,r){return e(n(t),r)}:n)},ao.shuffle=function(n,t,e){(u=arguments.length)<3&&(e=n.length,2>u&&(t=0));for(var r,i,u=e-t;u;)i=Math.random()*u--|0,r=n[u+t],n[u+t]=n[i+t],n[i+t]=r;return n},ao.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ao.pairs=function(n){for(var t,e=0,r=n.length-1,i=n[0],u=new Array(0>r?0:r);r>e;)u[e]=[t=i,i=n[++e]];return u},ao.transpose=function(n){if(!(i=n.length))return[];for(var t=-1,e=ao.min(n,o),r=new Array(e);++t=0;)for(r=n[i],t=r.length;--t>=0;)e[--o]=r[t];return e};var xo=Math.abs;ao.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,i=[],u=a(xo(e)),o=-1;if(n*=u,t*=u,e*=u,0>e)for(;(r=n+e*++o)>t;)i.push(r/u);else for(;(r=n+e*++o)=u.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,p=-1,g=o.length,v=u[a++],d=new c;++p=u.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,i={},u=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ao.map,e,0),0)},i.key=function(n){return u.push(n),i},i.sortKeys=function(n){return o[u.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ao.set=function(n){var t=new y;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(y,{has:h,add:function(n){return this._[f(n+="")]=!0,n},remove:p,values:g,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,s(t))}}),ao.behavior={},ao.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ao.event=null,ao.requote=function(n){return n.replace(So,"\\$&")};var So=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ko={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},No=function(n,t){return t.querySelector(n)},Eo=function(n,t){return t.querySelectorAll(n)},Ao=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ao=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(No=function(n,t){return Sizzle(n,t)[0]||null},Eo=Sizzle,Ao=Sizzle.matchesSelector),ao.selection=function(){return ao.select(fo.documentElement)};var Co=ao.selection.prototype=[];Co.select=function(n){var t,e,r,i,u=[];n=A(n);for(var o=-1,a=this.length;++o=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Lo.hasOwnProperty(e)?{space:Lo[e],local:n}:n}},Co.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ao.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Co.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,i=-1;if(t=e.classList){for(;++ii){if("string"!=typeof n){2>i&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>i){var u=this.node();return t(u).getComputedStyle(u,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Co.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Co.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Co.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Co.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Co.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Co.remove=function(){return this.each(F)},Co.data=function(n,t){function e(n,e){var r,i,u,o=n.length,s=e.length,h=Math.min(o,s),p=new Array(s),g=new Array(s),v=new Array(o);if(t){var d,y=new c,m=new Array(o);for(r=-1;++rr;++r)g[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}g.update=p,g.parentNode=p.parentNode=v.parentNode=n.parentNode,a.push(g),l.push(p),f.push(v)}var r,i,u=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++uu;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return E(i)},Co.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Co.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Co.size=function(){var n=0;return Y(this,function(){++n}),n};var qo=[];ao.selection.enter=Z,ao.selection.enter.prototype=qo,qo.append=Co.append,qo.empty=Co.empty,qo.node=Co.node,qo.call=Co.call,qo.size=Co.size,qo.select=function(n){for(var t,e,r,i,u,o=[],a=-1,l=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var To=ao.map({mouseenter:"mouseover",mouseleave:"mouseout"});fo&&To.forEach(function(n){"on"+n in fo&&To.remove(n)});var Ro,Do=0;ao.mouse=function(n){return J(n,k())};var Po=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ao.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,i=0,u=t.length;u>i;++i)if((r=t[i]).identifier===e)return J(n,r)},ao.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",o)}function e(n,t,e,u,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],g|=n|e,M=r,p({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(y.on(u+d,null).on(o+d,null),m(g),p({type:"dragend"}))}var c,f=this,s=ao.event.target.correspondingElement||ao.event.target,h=f.parentNode,p=r.of(f,arguments),g=0,v=n(),d=".drag"+(null==v?"":"-"+v),y=ao.select(e(s)).on(u+d,a).on(o+d,l),m=W(s),M=t(h,v);i?(c=i.apply(f,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],p({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),i=null,u=e(b,ao.mouse,t,"mousemove","mouseup"),o=e(G,ao.touch,m,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},ao.rebind(n,r,"on")},ao.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?co(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Uo=1e-6,jo=Uo*Uo,Fo=Math.PI,Ho=2*Fo,Oo=Ho-Uo,Io=Fo/2,Yo=Fo/180,Zo=180/Fo,Vo=Math.SQRT2,Xo=2,$o=4;ao.interpolateZoom=function(n,t){var e,r,i=n[0],u=n[1],o=n[2],a=t[0],l=t[1],c=t[2],f=a-i,s=l-u,h=f*f+s*s;if(jo>h)r=Math.log(c/o)/Vo,e=function(n){return[i+n*f,u+n*s,o*Math.exp(Vo*n*r)]};else{var p=Math.sqrt(h),g=(c*c-o*o+$o*h)/(2*o*Xo*p),v=(c*c-o*o-$o*h)/(2*c*Xo*p),d=Math.log(Math.sqrt(g*g+1)-g),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-d)/Vo,e=function(n){var t=n*r,e=rn(d),a=o/(Xo*p)*(e*un(Vo*t+d)-en(d));return[i+a*f,u+a*s,o*e/rn(Vo*t+d)]}}return e.duration=1e3*r,e},ao.behavior.zoom=function(){function n(n){n.on(L,s).on(Wo+".zoom",p).on("dblclick.zoom",g).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function i(n){k.k=Math.max(A[0],Math.min(A[1],n))}function u(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},i(Math.pow(2,o)),u(d=e,r),t=ao.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function f(n){--z||(n({type:"zoomend"}),d=null)}function s(){function n(){a=1,u(ao.mouse(i),h),c(o)}function r(){s.on(q,null).on(T,null),p(a),f(o)}var i=this,o=D.of(i,arguments),a=0,s=ao.select(t(i)).on(q,n).on(T,r),h=e(ao.mouse(i)),p=W(i);Il.call(i),l(o)}function h(){function n(){var n=ao.touches(g);return p=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ao.event.target;ao.select(t).on(x,r).on(b,a),_.push(t);for(var e=ao.event.changedTouches,i=0,u=e.length;u>i;++i)d[e[i].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var f=l[0];o(g,f,d[f.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var f=l[0],s=l[1],h=f[0]-s[0],p=f[1]-s[1];y=h*h+p*p}}function r(){var n,t,e,r,o=ao.touches(g);Il.call(g);for(var a=0,l=o.length;l>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var f=(f=e[0]-n[0])*f+(f=e[1]-n[1])*f,s=y&&Math.sqrt(f/y);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],i(s*p)}M=null,u(n,t),c(v)}function a(){if(ao.event.touches.length){for(var t=ao.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var i in d)return void n()}ao.selectAll(_).on(m,null),w.on(L,s).on(R,h),N(),f(v)}var p,g=this,v=D.of(g,arguments),d={},y=0,m=".zoom-"+ao.event.changedTouches[0].identifier,x="touchmove"+m,b="touchend"+m,_=[],w=ao.select(g),N=W(g);t(),l(v),w.on(L,null).on(R,t)}function p(){var n=D.of(this,arguments);m?clearTimeout(m):(Il.call(this),v=e(d=y||ao.mouse(this)),l(n)),m=setTimeout(function(){m=null,f(n)},50),S(),i(Math.pow(2,.002*Bo())*k.k),u(d,v),c(n)}function g(){var n=ao.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ao.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,y,m,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Jo,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Wo||(Wo="onwheel"in fo?(Bo=function(){return-ao.event.deltaY*(ao.event.deltaMode?120:1)},"wheel"):"onmousewheel"in fo?(Bo=function(){return ao.event.wheelDelta},"mousewheel"):(Bo=function(){return-ao.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?ao.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],i=d?d[0]:e/2,u=d?d[1]:r/2,o=ao.interpolateZoom([(i-k.x)/k.k,(u-k.y)/k.k,e/k.k],[(i-t.x)/t.k,(u-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:i-r[0]*a,y:u-r[1]*a,k:a},c(n)}}).each("interrupt.zoom",function(){f(n)}).each("end.zoom",function(){f(n)}):(this.__chart__=k,l(n),c(n),f(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},i(+t),a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Jo:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(y=t&&[+t[0],+t[1]],n):y},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ao.rebind(n,D,"on")};var Bo,Wo,Jo=[0,1/0];ao.color=an,an.prototype.toString=function(){return this.rgb()+""},ao.hsl=ln;var Go=ln.prototype=new an;Go.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Go.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Go.rgb=function(){return cn(this.h,this.s,this.l)},ao.hcl=fn;var Ko=fn.prototype=new an;Ko.brighter=function(n){return new fn(this.h,this.c,Math.min(100,this.l+Qo*(arguments.length?n:1)))},Ko.darker=function(n){return new fn(this.h,this.c,Math.max(0,this.l-Qo*(arguments.length?n:1)))},Ko.rgb=function(){return sn(this.h,this.c,this.l).rgb()},ao.lab=hn;var Qo=18,na=.95047,ta=1,ea=1.08883,ra=hn.prototype=new an;ra.brighter=function(n){return new hn(Math.min(100,this.l+Qo*(arguments.length?n:1)),this.a,this.b)},ra.darker=function(n){return new hn(Math.max(0,this.l-Qo*(arguments.length?n:1)),this.a,this.b)},ra.rgb=function(){return pn(this.l,this.a,this.b)},ao.rgb=mn;var ia=mn.prototype=new an;ia.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),new mn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mn(i,i,i)},ia.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mn(n*this.r,n*this.g,n*this.b)},ia.hsl=function(){return wn(this.r,this.g,this.b)},ia.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ua=ao.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ua.forEach(function(n,t){ua.set(n,Mn(t))}),ao.functor=En,ao.xhr=An(m),ao.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var o=Cn(n,t,null==e?r:i(e),u);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:i(n)):e},o}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function u(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return o;if(i)return i=!1,u;var t=f;if(34===n.charCodeAt(t)){for(var e=t;e++f;){var r=n.charCodeAt(f++),a=1;if(10===r)i=!0;else if(13===r)i=!0,10===n.charCodeAt(f)&&(++f,++a);else if(r!==l)continue;return n.slice(t,f-a)}return n.slice(t)}for(var r,i,u={},o={},a=[],c=n.length,f=0,s=0;(r=e())!==o;){for(var h=[];r!==u&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,s++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new y,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(u).join("\n")},e},ao.csv=ao.dsv(",","text/csv"),ao.tsv=ao.dsv(" ","text/tab-separated-values");var oa,aa,la,ca,fa=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ao.timer=function(){qn.apply(this,arguments)},ao.timer.flush=function(){Rn(),Dn()},ao.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var sa=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);ao.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=ao.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),sa[8+e/3]};var ha=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,pa=ao.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ao.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ga=ao.time={},va=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){da.setUTCDate.apply(this._,arguments)},setDay:function(){da.setUTCDay.apply(this._,arguments)},setFullYear:function(){da.setUTCFullYear.apply(this._,arguments)},setHours:function(){da.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){da.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){da.setUTCMinutes.apply(this._,arguments)},setMonth:function(){da.setUTCMonth.apply(this._,arguments)},setSeconds:function(){da.setUTCSeconds.apply(this._,arguments)},setTime:function(){da.setTime.apply(this._,arguments)}};var da=Date.prototype;ga.year=On(function(n){return n=ga.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ga.years=ga.year.range,ga.years.utc=ga.year.utc.range,ga.day=On(function(n){var t=new va(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ga.days=ga.day.range,ga.days.utc=ga.day.utc.range,ga.dayOfYear=function(n){var t=ga.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ga[n]=On(function(n){return(n=ga.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ga[n+"s"]=e.range,ga[n+"s"].utc=e.utc.range,ga[n+"OfYear"]=function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)}}),ga.week=ga.sunday,ga.weeks=ga.sunday.range,ga.weeks.utc=ga.sunday.utc.range,ga.weekOfYear=ga.sundayOfYear;var ya={"-":"",_:" ",0:"0"},ma=/^\s*\d+/,Ma=/^%/;ao.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xa=ao.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], +shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++rn?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),o.precision(t),a.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),o.scale(.35*t),a.scale(t),n.translate(u.translate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var c=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*c,s-.238*c],[f+.455*c,s+.238*c]]).stream(l).point,r=o.translate([f-.307*c,s+.201*c]).clipExtent([[f-.425*c+Uo,s+.12*c+Uo],[f-.214*c-Uo,s+.234*c-Uo]]).stream(l).point,i=a.translate([f-.205*c,s+.212*c]).clipExtent([[f-.214*c+Uo,s+.166*c+Uo],[f-.115*c-Uo,s+.234*c-Uo]]).stream(l).point,n},n.scale(1070)};var Oa,Ia,Ya,Za,Va,Xa,$a={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Ia=0,$a.lineStart=$t},polygonEnd:function(){$a.lineStart=$a.lineEnd=$a.point=b,Oa+=xo(Ia/2)}},Ba={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wa={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wa.lineStart=ne},polygonEnd:function(){Wa.point=Gt,Wa.lineStart=Kt,Wa.lineEnd=Qt}};ao.geo.path=function(){function n(n){return n&&("function"==typeof a&&u.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=i(u)),ao.geo.stream(n,o)),u.result()}function t(){return o=null,n}var e,r,i,u,o,a=4.5;return n.area=function(n){return Oa=0,ao.geo.stream(n,i($a)),Oa},n.centroid=function(n){return Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,i(Wa)),Ua?[Da/Ua,Pa/Ua]:Ra?[qa/Ra,Ta/Ra]:La?[Ca/La,za/La]:[NaN,NaN]},n.bounds=function(n){return Va=Xa=-(Ya=Za=1/0),ao.geo.stream(n,i(Ba)),[[Ya,Za],[Va,Xa]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||re(n):m,t()):e},n.context=function(n){return arguments.length?(u=null==(r=n)?new Wt:new te(n),"function"!=typeof a&&u.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(u.pointRadius(+t),+t),n):a},n.projection(ao.geo.albersUsa()).context(null)},ao.geo.transform=function(n){return{stream:function(t){var e=new ie(t);for(var r in n)e[r]=n[r];return e}}},ie.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ao.geo.projection=oe,ao.geo.projectionMutator=ae,(ao.geo.equirectangular=function(){return oe(ce)}).raw=ce.invert=ce,ao.geo.rotation=function(n){function t(t){return t=n(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t}return n=se(n[0]%360*Yo,n[1]*Yo,n.length>2?n[2]*Yo:0),t.invert=function(t){return t=n.invert(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t},t},fe.invert=ce,ao.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=se(-n[0]*Yo,-n[1]*Yo,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Zo,n[1]*=Zo}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Yo,i*Yo),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Yo,(i=+r)*Yo),n):i},n.angle(90)},ao.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Yo,i=n[1]*Yo,u=t[1]*Yo,o=Math.sin(r),a=Math.cos(r),l=Math.sin(i),c=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*o)*e+(e=c*f-l*s*a)*e),l*f+c*s*a)},ao.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ao.range(Math.ceil(u/d)*d,i,d).map(h).concat(ao.range(Math.ceil(c/y)*y,l,y).map(p)).concat(ao.range(Math.ceil(r/g)*g,e,g).filter(function(n){return xo(n%d)>Uo}).map(f)).concat(ao.range(Math.ceil(a/v)*v,o,v).filter(function(n){return xo(n%y)>Uo}).map(s))}var e,r,i,u,o,a,l,c,f,s,h,p,g=10,v=g,d=90,y=360,m=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(p(l).slice(1),h(i).reverse().slice(1),p(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],c=+t[0][1],l=+t[1][1],u>i&&(t=u,u=i,i=t),c>l&&(t=c,c=l,l=t),n.precision(m)):[[u,c],[i,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(m)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],y=+t[1],n):[d,y]},n.minorStep=function(t){return arguments.length?(g=+t[0],v=+t[1],n):[g,v]},n.precision=function(t){return arguments.length?(m=+t,f=ye(a,o,90),s=me(r,e,m),h=ye(c,l,90),p=me(u,i,m),n):m},n.majorExtent([[-180,-90+Uo],[180,90-Uo]]).minorExtent([[-180,-80-Uo],[180,80+Uo]])},ao.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Me,i=xe;return n.distance=function(){return ao.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},ao.geo.interpolate=function(n,t){return be(n[0]*Yo,n[1]*Yo,t[0]*Yo,t[1]*Yo)},ao.geo.length=function(n){return Ja=0,ao.geo.stream(n,Ga),Ja};var Ja,Ga={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ka=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ao.geo.azimuthalEqualArea=function(){return oe(Ka)}).raw=Ka;var Qa=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},m);(ao.geo.azimuthalEquidistant=function(){return oe(Qa)}).raw=Qa,(ao.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(ao.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(ao.geo.gnomonic=function(){return oe(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Io]},(ao.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(ao.geo.orthographic=function(){return oe(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ao.geo.stereographic=function(){return oe(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Io]},(ao.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,ao.geom={},ao.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i=En(e),u=En(r),o=n.length,a=[],l=[];for(t=0;o>t;t++)a.push([+i.call(this,n[t],t),+u.call(this,n[t],t),t]);for(a.sort(qe),t=0;o>t;t++)l.push([a[t][0],-a[t][1]]);var c=Le(a),f=Le(l),s=f[0]===c[0],h=f[f.length-1]===c[c.length-1],p=[];for(t=c.length-1;t>=0;--t)p.push(n[a[c[t]][2]]);for(t=+s;t=r&&c.x<=u&&c.y>=i&&c.y<=o?[[r,o],[u,o],[u,i],[r,i]]:[];f.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(u(n,t)/Uo)*Uo,y:Math.round(o(n,t)/Uo)*Uo,i:t}})}var r=Ce,i=ze,u=r,o=i,a=sl;return n?t(n):(t.links=function(n){return ar(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return ar(e(n)).cells.forEach(function(e,r){for(var i,u,o=e.site,a=e.edges.sort(Ve),l=-1,c=a.length,f=a[c-1].edge,s=f.l===o?f.r:f.l;++l=c,h=r>=f,p=h<<1|s;n.leaf=!1,n=n.nodes[p]||(n.nodes[p]=hr()),s?i=c:a=c,h?o=f:l=f,u(n,t,e,r,i,o,a,l)}var f,s,h,p,g,v,d,y,m,M=En(a),x=En(l);if(null!=t)v=t,d=e,y=r,m=i;else if(y=m=-(v=d=1/0),s=[],h=[],g=n.length,o)for(p=0;g>p;++p)f=n[p],f.xy&&(y=f.x),f.y>m&&(m=f.y),s.push(f.x),h.push(f.y);else for(p=0;g>p;++p){var b=+M(f=n[p],p),_=+x(f,p);v>b&&(v=b),d>_&&(d=_),b>y&&(y=b),_>m&&(m=_),s.push(b),h.push(_)}var w=y-v,S=m-d;w>S?m=d+w:y=v+S;var k=hr();if(k.add=function(n){u(k,n,+M(n,++p),+x(n,p),v,d,y,m)},k.visit=function(n){pr(n,k,v,d,y,m)},k.find=function(n){return gr(k,n[0],n[1],v,d,y,m)},p=-1,null==t){for(;++p=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||gl,r=dl.get(r)||m,br(r(e.apply(null,lo.call(arguments,1))))},ao.interpolateHcl=Rr,ao.interpolateHsl=Dr,ao.interpolateLab=Pr,ao.interpolateRound=Ur,ao.transform=function(n){var t=fo.createElementNS(ao.ns.prefix.svg,"g");return(ao.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:yl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var yl={a:1,b:0,c:0,d:1,e:0,f:0};ao.interpolateTransform=$r,ao.layout={},ao.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/y){if(v>l){var c=t.charge/l;n.px-=u*c,n.py-=o*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=u*c,n.py-=o*c}}return!t.charge}}function t(n){n.px=ao.event.x,n.py=ao.event.y,l.resume()}var e,r,i,u,o,a,l={},c=ao.dispatch("start","tick","end"),f=[1,1],s=.9,h=ml,p=Ml,g=-30,v=xl,d=.1,y=.64,M=[],x=[];return l.tick=function(){if((i*=.99)<.005)return e=null,c.end({type:"end",alpha:i=0}),!0;var t,r,l,h,p,v,y,m,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,p=l.target,m=p.x-h.x,b=p.y-h.y,(v=m*m+b*b)&&(v=i*o[r]*((v=Math.sqrt(v))-u[r])/v,m*=v,b*=v,p.x-=m*(y=h.weight+p.weight?h.weight/(h.weight+p.weight):.5),p.y-=b*y,h.x+=m*(y=1-y),h.y+=b*y);if((y=i*d)&&(m=f[0]/2,b=f[1]/2,r=-1,y))for(;++r<_;)l=M[r],l.x+=(m-l.x)*y,l.y+=(b-l.y)*y;if(g)for(ri(t=ao.geom.quadtree(M),i,a),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*s,l.y-=(l.py-(l.py=l.y))*s);c.tick({type:"tick",alpha:i})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(f=n,l):f},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.friction=function(n){return arguments.length?(s=+n,l):s},l.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(y=n*n,l):Math.sqrt(y)},l.alpha=function(n){return arguments.length?(n=+n,i?n>0?i=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:i=0})):n>0&&(c.start({type:"start",alpha:i=n}),e=qn(l.tick)),l):i},l.start=function(){function n(n,r){if(!e){for(e=new Array(i),l=0;i>l;++l)e[l]=[];for(l=0;c>l;++l){var u=x[l];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var o,a=e[t],l=-1,f=a.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;i>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",s)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof h)for(t=0;c>t;++t)u[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)u[t]=h;if(o=[],"function"==typeof p)for(t=0;c>t;++t)o[t]=+p.call(this,x[t],t);else for(t=0;c>t;++t)o[t]=p;if(a=[],"function"==typeof g)for(t=0;i>t;++t)a[t]=+g.call(this,M[t],t);else for(t=0;i>t;++t)a[t]=g;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=ao.behavior.drag().origin(m).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",ni)),arguments.length?void this.on("mouseover.force",ti).on("mouseout.force",ei).call(r):r},ao.rebind(l,c,"on")};var ml=20,Ml=1,xl=1/0;ao.layout.hierarchy=function(){function n(i){var u,o=[i],a=[];for(i.depth=0;null!=(u=o.pop());)if(a.push(u),(c=e.call(n,u,u.depth))&&(l=c.length)){for(var l,c,f;--l>=0;)o.push(f=c[l]),f.parent=u,f.depth=u.depth+1;r&&(u.value=0),u.children=c}else r&&(u.value=+r.call(n,u,u.depth)||0),delete u.children;return oi(i,function(n){var e,i;t&&(e=n.children)&&e.sort(t),r&&(i=n.parent)&&(i.value+=n.value)}),a}var t=ci,e=ai,r=li;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(ui(t,function(n){n.children&&(n.value=0)}),oi(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ao.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(o=u.length)){var o,a,l,c=-1;for(r=t.value?r/t.value:0;++cs?-1:1),g=ao.sum(c),v=g?(s-l*p)/g:0,d=ao.range(l),y=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(o[n],o[t])}),d.forEach(function(n){y[n]={data:o[n],value:a=c[n],startAngle:f,endAngle:f+=a*v+p,padAngle:h}}),y}var t=Number,e=bl,r=0,i=Ho,u=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n.padAngle=function(t){return arguments.length?(u=t,n):u},n};var bl={};ao.layout.stack=function(){function n(a,l){if(!(h=a.length))return a;var c=a.map(function(e,r){return t.call(n,e,r)}),f=c.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),o.call(n,t,e)]})}),s=e.call(n,f,l);c=ao.permute(c,s),f=ao.permute(f,s);var h,p,g,v,d=r.call(n,f,l),y=c[0].length;for(g=0;y>g;++g)for(i.call(n,c[0][g],v=d[g],f[0][g][1]),p=1;h>p;++p)i.call(n,c[p][g],v+=f[p-1][g][1],f[p][g][1]);return a}var t=m,e=gi,r=vi,i=pi,u=si,o=hi;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||gi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vi,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(i=t,n):i},n};var _l=ao.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(di),u=n.map(yi),o=ao.range(r).sort(function(n,t){return i[n]-i[t]}),a=0,l=0,c=[],f=[];for(t=0;r>t;++t)e=o[t],l>a?(a+=u[e],c.push(e)):(l+=u[e],f.push(e));return f.reverse().concat(c)},reverse:function(n){return ao.range(n.length).reverse()},"default":gi}),wl=ao.map({silhouette:function(n){var t,e,r,i=n.length,u=n[0].length,o=[],a=0,l=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;u>e;++e)l[e]=(a-o[e])/2;return l},wiggle:function(n){var t,e,r,i,u,o,a,l,c,f=n.length,s=n[0],h=s.length,p=[];for(p[0]=l=c=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,a=s[e][0]-s[e-1][0];f>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;u+=o*n[t][e][1]}p[e]=l-=i?u/i*a:0,c>l&&(c=l)}for(e=0;h>e;++e)p[e]-=c;return p},expand:function(n){var t,e,r,i=n.length,u=n[0].length,o=1/i,a=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=o}for(e=0;u>e;++e)a[e]=0;return a},zero:vi});ao.layout.histogram=function(){function n(n,u){for(var o,a,l=[],c=n.map(e,this),f=r.call(this,c,u),s=i.call(this,f,c,u),u=-1,h=c.length,p=s.length-1,g=t?1:1/h;++u0)for(u=-1;++u=f[0]&&a<=f[1]&&(o=l[ao.bisect(s,a,1,p)-1],o.y+=g,o.push(n[u]));return l}var t=!0,e=Number,r=bi,i=Mi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return xi(n,t)}:En(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ao.layout.pack=function(){function n(n,u){var o=e.call(this,n,u),a=o[0],l=i[0],c=i[1],f=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,oi(a,function(n){n.r=+f(n.value)}),oi(a,Ni),r){var s=r*(t?1:Math.max(2*a.r/l,2*a.r/c))/2;oi(a,function(n){n.r+=s}),oi(a,Ni),oi(a,function(n){n.r-=s})}return Ci(a,l/2,c/2,t?1:1/Math.max(2*a.r/l,2*a.r/c)),o}var t,e=ao.layout.hierarchy().sort(_i),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},ii(n,e)},ao.layout.tree=function(){function n(n,i){var f=o.call(this,n,i),s=f[0],h=t(s);if(oi(h,e),h.parent.m=-h.z,ui(h,r),c)ui(s,u);else{var p=s,g=s,v=s;ui(s,function(n){n.xg.x&&(g=n),n.depth>v.depth&&(v=n)});var d=a(p,g)/2-p.x,y=l[0]/(g.x+a(g,p)/2+d),m=l[1]/(v.depth||1);ui(s,function(n){n.x=(n.x+d)*y,n.y=n.depth*m})}return f}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var i,u=t.children,o=0,a=u.length;a>o;++o)r.push((u[o]=i={_:u[o],parent:t,children:(i=u[o].children)&&i.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=i);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Di(n);var u=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-u):n.z=u}else r&&(n.z=r.z+a(n._,r._));n.parent.A=i(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function i(n,t,e){if(t){for(var r,i=n,u=n,o=t,l=i.parent.children[0],c=i.m,f=u.m,s=o.m,h=l.m;o=Ti(o),i=qi(i),o&&i;)l=qi(l),u=Ti(u),u.a=n,r=o.z+s-i.z-c+a(o._,i._),r>0&&(Ri(Pi(o,n,e),n,r),c+=r,f+=r),s+=o.m,c+=i.m,h+=l.m,f+=u.m;o&&!Ti(u)&&(u.t=o,u.m+=s-f),i&&!qi(l)&&(l.t=i,l.m+=c-h,e=n)}return e}function u(n){n.x*=l[0],n.y=n.depth*l[1]}var o=ao.layout.hierarchy().sort(null).value(null),a=Li,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(c=null==(l=t)?u:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:u,n):c?l:null},ii(n,o)},ao.layout.cluster=function(){function n(n,u){var o,a=t.call(this,n,u),l=a[0],c=0;oi(l,function(n){var t=n.children;t&&t.length?(n.x=ji(t),n.y=Ui(t)):(n.x=o?c+=e(n,o):0,n.y=0,o=n)});var f=Fi(l),s=Hi(l),h=f.x-e(f,s)/2,p=s.x+e(s,f)/2;return oi(l,i?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(p-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),a}var t=ao.layout.hierarchy().sort(null).value(null),e=Li,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=null==(r=t),n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=null!=(r=t),n):i?r:null},ii(n,t)},ao.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++it?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var o,a,l,c=s(e),f=[],h=u.slice(),g=1/0,v="slice"===p?c.dx:"dice"===p?c.dy:"slice-dice"===p?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),f.area=0;(l=h.length)>0;)f.push(o=h[l-1]),f.area+=o.area,"squarify"!==p||(a=r(f,v))<=g?(h.pop(),g=a):(f.area-=f.pop().area,i(f,v,c,!1),v=Math.min(c.dx,c.dy),f.length=f.area=0,g=1/0);f.length&&(i(f,v,c,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,o=s(t),a=r.slice(),l=[];for(n(a,o.dx*o.dy/t.value),l.area=0;u=a.pop();)l.push(u),l.area+=u.area,null!=u.z&&(i(l,u.z?o.dx:o.dy,o,!a.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,o=-1,a=n.length;++oe&&(u=e),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*g/r,r/(t*u*g)):1/0}function i(n,t,e,r){var i,u=-1,o=n.length,a=e.x,c=e.y,f=t?l(n.area/t):0; +if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ue.dx)&&(f=e.dx);++ue&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-p," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",m,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",p," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-p," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hu,r=pu,i=su,u=ql,o=gu,a=vu,l=du;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(i=En(t),n):i},n.padRadius=function(t){return arguments.length?(u=t==ql?ql:En(t),n):u},n.startAngle=function(t){return arguments.length?(o=En(t),n):o},n.endAngle=function(t){return arguments.length?(a=En(t),n):a},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Io;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";ao.svg.line=function(){return Mu(m)};var Tl=ao.map({linear:xu,"linear-closed":bu,step:_u,"step-before":wu,"step-after":Su,basis:zu,"basis-open":Lu,"basis-closed":qu,bundle:Tu,cardinal:Eu,"cardinal-open":ku,"cardinal-closed":Nu,monotone:Fu});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];ao.svg.line.radial=function(){var n=Mu(Hu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wu.reverse=Su,Su.reverse=wu,ao.svg.area=function(){return Ou(m)},ao.svg.area.radial=function(){var n=Ou(Hu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ao.svg.chord=function(){function n(n,a){var l=t(this,u,n,a),c=t(this,o,n,a);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?i(l.r,l.p1,l.r,l.p0):i(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+i(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=a.call(n,i,r),o=l.call(n,i,r)-Io,f=c.call(n,i,r)-Io;return{r:u,a0:o,a1:f,p0:[u*Math.cos(o),u*Math.sin(o)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fo)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Me,o=xe,a=Iu,l=gu,c=vu;return n.radius=function(t){return arguments.length?(a=En(t),n):a},n.source=function(t){return arguments.length?(u=En(t),n):u},n.target=function(t){return arguments.length?(o=En(t),n):o},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},ao.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),o=e.call(this,n,i),a=(u.y+o.y)/2,l=[u,{x:u.x,y:a},{x:o.x,y:a},o];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yu;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ao.svg.diagonal.radial=function(){var n=ao.svg.diagonal(),t=Yu,e=n.projection;return n.projection=function(n){return arguments.length?e(Zu(t=n)):t},n},ao.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$u)(e.call(this,n,r))}var t=Xu,e=Vu;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=ao.map({circle:$u,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ao.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Yo);Co.transition=function(n){for(var t,e,r=Hl||++Zl,i=Ku(n),u=[],o=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},a=-1,l=this.length;++au;u++){i.push(t=[]);for(var e=this[u],a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return Wu(i,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(i){i[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?$r:Mr,a=ao.ns.qualify(n);return Ju(this,"attr."+n,t,a.local?u:i)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=ao.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Yl.style=function(n,e,r){function i(){this.style.removeProperty(n)}function u(e){return null==e?i:(e+="",function(){var i,u=t(this).getComputedStyle(this,null).getPropertyValue(n);return u!==e&&(i=Mr(u,e),function(t){this.style.setProperty(n,i(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ju(this,"style."+n,e,u)},Yl.styleTween=function(n,e,r){function i(i,u){var o=e.call(this,i,u,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,i)},Yl.text=function(n){return Ju(this,"text",n,Gu)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ao.ease.apply(ao,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,i,u){r[e][t].delay=+n.call(r,r.__data__,i,u)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,i,u){r[e][t].duration=Math.max(1,n.call(r,r.__data__,i,u))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var i=Ol,u=Hl;try{Hl=e,Y(this,function(t,i,u){Ol=t[r][e],n.call(t,t.__data__,i,u)})}finally{Ol=i,Hl=u}}else Y(this,function(i){var u=i[r][e];(u.event||(u.event=ao.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,i=this.id,u=++Zl,o=this.namespace,a=[],l=0,c=this.length;c>l;l++){a.push(n=[]);for(var t=this[l],f=0,s=t.length;s>f;f++)(e=t[f])&&(r=e[o][i],Qu(e,f,o,u,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wu(a,o,u)},ao.svg.axis=function(){function n(n){n.each(function(){var n,c=ao.select(this),f=this.__chart__||e,s=this.__chart__=e.copy(),h=null==l?s.ticks?s.ticks.apply(s,a):s.domain():l,p=null==t?s.tickFormat?s.tickFormat.apply(s,a):m:t,g=c.selectAll(".tick").data(h,s),v=g.enter().insert("g",".domain").attr("class","tick").style("opacity",Uo),d=ao.transition(g.exit()).style("opacity",Uo).remove(),y=ao.transition(g.order()).style("opacity",1),M=Math.max(i,0)+o,x=Zi(s),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ao.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=y.select("line"),C=g.select("text").text(p),z=v.select("text"),L=y.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=no,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*u+"V0H"+x[1]+"V"+q*u)):(n=to,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*u+","+x[0]+"H0V"+x[1]+"H"+q*u)),E.attr(N,q*i),z.attr(k,q*M),A.attr(S,0).attr(N,q*i),L.attr(w,0).attr(k,q*M),s.rangeBand){var T=s,R=T.rangeBand()/2;f=s=function(n){return T(n)+R}}else f.rangeBand?f=s:d.call(n,s,f);v.call(n,f,s),y.call(n,s,s)})}var t,e=ao.scale.linear(),r=Vl,i=6,u=6,o=3,a=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(a=co(arguments),n):a},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(i=+t,u=+arguments[e-1],n):i},n.innerTickSize=function(t){return arguments.length?(i=+t,n):i},n.outerTickSize=function(t){return arguments.length?(u=+t,n):u},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};ao.svg.brush=function(){function n(t){t.each(function(){var t=ao.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,m);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,s=ao.transition(t),h=ao.transition(o);c&&(l=Zi(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(s)),f&&(l=Zi(f),h.attr("y",l[0]).attr("height",l[1]-l[0]),i(s)),e(s)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",s[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1]-s[0])}function i(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function u(){function u(){32==ao.event.keyCode&&(C||(M=null,L[0]-=s[1],L[1]-=h[1],C=2),S())}function v(){32==ao.event.keyCode&&2==C&&(L[0]+=s[1],L[1]+=h[1],C=0,S())}function d(){var n=ao.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ao.event.altKey?(M||(M=[(s[0]+s[1])/2,(h[0]+h[1])/2]),L[0]=s[+(n[0]f?(i=r,r=f):i=f),v[0]!=r||v[1]!=i?(e?a=null:o=null,v[0]=r,v[1]=i,!0):void 0}function m(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ao.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ao.select(ao.event.target),w=l.of(b,arguments),k=ao.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&f,C=_.classed("extent"),z=W(b),L=ao.mouse(b),q=ao.select(t(b)).on("keydown.brush",u).on("keyup.brush",v);if(ao.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",m):q.on("mousemove.brush",d).on("mouseup.brush",m),k.interrupt().selectAll("*").interrupt(),C)L[0]=s[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[s[1-T]-L[0],h[1-R]-L[1]],L[0]=s[T],L[1]=h[R]}else ao.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ao.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,l=N(n,"brushstart","brush","brushend"),c=null,f=null,s=[0,0],h=[0,0],p=!0,g=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:s,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Hl?ao.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,s=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(s,t.x),r=xr(h,t.y);return o=a=null,function(i){s=t.x=e(i),h=t.y=r(i),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!f],n):c},n.y=function(t){return arguments.length?(f=t,v=Bl[!c<<1|!f],n):f},n.clamp=function(t){return arguments.length?(c&&f?(p=!!t[0],g=!!t[1]):c?p=!!t:f&&(g=!!t),n):c&&f?[p,g]:c?p:f?g:null},n.extent=function(t){var e,r,i,u,l;return arguments.length?(c&&(e=t[0],r=t[1],f&&(e=e[0],r=r[0]),o=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),e==s[0]&&r==s[1]||(s=[e,r])),f&&(i=t[0],u=t[1],c&&(i=i[1],u=u[1]),a=[i,u],f.invert&&(i=f(i),u=f(u)),i>u&&(l=i,i=u,u=l),i==h[0]&&u==h[1]||(h=[i,u])),n):(c&&(o?(e=o[0],r=o[1]):(e=s[0],r=s[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),f&&(a?(i=a[0],u=a[1]):(i=h[0],u=h[1],f.invert&&(i=f.invert(i),u=f.invert(u)),i>u&&(l=i,i=u,u=l))),c&&f?[[e,i],[r,u]]:c?[e,r]:f&&[i,u])},n.clear=function(){return n.empty()||(s=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!c&&s[0]==s[1]||!!f&&h[0]==h[1]},ao.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=ga.format=xa.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?eo:Gl,eo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},eo.toString=Gl.toString,ga.second=On(function(n){return new va(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ga.seconds=ga.second.range,ga.seconds.utc=ga.second.utc.range,ga.minute=On(function(n){return new va(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ga.minutes=ga.minute.range,ga.minutes.utc=ga.minute.utc.range,ga.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new va(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ga.hours=ga.hour.range,ga.hours.utc=ga.hour.utc.range,ga.month=On(function(n){return n=ga.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ga.months=ga.month.range,ga.months.utc=ga.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[ga.second,1],[ga.second,5],[ga.second,15],[ga.second,30],[ga.minute,1],[ga.minute,5],[ga.minute,15],[ga.minute,30],[ga.hour,1],[ga.hour,3],[ga.hour,6],[ga.hour,12],[ga.day,1],[ga.day,2],[ga.week,1],[ga.month,1],[ga.month,3],[ga.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return ao.range(Math.ceil(n/e)*e,+t,e).map(io)},floor:m,ceil:m};Ql.year=ga.year,ga.scale=function(){return ro(ao.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=ga.year.utc,ga.scale.utc=function(){return ro(ao.scale.linear(),ec,rc)},ao.text=An(function(n){return n.responseText}),ao.json=function(n,t){return Cn(n,"application/json",uo,t)},ao.html=function(n,t){return Cn(n,"text/html",oo,t)},ao.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=ao,define(ao)):"object"==typeof module&&module.exports?module.exports=ao:this.d3=ao}(); \ No newline at end of file diff --git a/third_party/optimized-svg-icons/amp-social-share-svgs.css b/third_party/optimized-svg-icons/amp-social-share-svgs.css index bc46e92bb5ea..7ecf30887f7c 100644 --- a/third_party/optimized-svg-icons/amp-social-share-svgs.css +++ b/third_party/optimized-svg-icons/amp-social-share-svgs.css @@ -1,23 +1,41 @@ -amp-social-share[type=facebook] { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M211.9%20197.4h-36.7v59.9h36.7V433.1h70.5V256.5h49.2l5.2-59.1h-54.4c0%200%200-22.1%200-33.7%200-13.9%202.8-19.5%2016.3-19.5%2010.9%200%2038.2%200%2038.2%200V82.9c0%200-40.2%200-48.8%200%20-52.5%200-76.1%2023.1-76.1%2067.3C211.9%20188.8%20211.9%20197.4%20211.9%20197.4z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); +/** + * Note: Attribute selectors were used initially here but we switched to using + * class-based selector to style each type because of a bug on iOS Safari 8. + * See https://github.com/ampproject/amphtml/issues/4277 for more details. + */ + +.amp-social-share-facebook { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M211.9%20197.4h-36.7v59.9h36.7V433.1h70.5V256.5h49.2l5.2-59.1h-54.4c0%200%200-22.1%200-33.7%200-13.9%202.8-19.5%2016.3-19.5%2010.9%200%2038.2%200%2038.2%200V82.9c0%200-40.2%200-48.8%200%20-52.5%200-76.1%2023.1-76.1%2067.3C211.9%20188.8%20211.9%20197.4%20211.9%20197.4z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); +} + +.amp-social-share-pinterest { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M266.6%2076.5c-100.2%200-150.7%2071.8-150.7%20131.7%200%2036.3%2013.7%2068.5%2043.2%2080.6%204.8%202%209.2%200.1%2010.6-5.3%201-3.7%203.3-13%204.3-16.9%201.4-5.3%200.9-7.1-3-11.8%20-8.5-10-13.9-23-13.9-41.3%200-53.3%2039.9-101%20103.8-101%2056.6%200%2087.7%2034.6%2087.7%2080.8%200%2060.8-26.9%20112.1-66.8%20112.1%20-22.1%200-38.6-18.2-33.3-40.6%206.3-26.7%2018.6-55.5%2018.6-74.8%200-17.3-9.3-31.7-28.4-31.7%20-22.5%200-40.7%2023.3-40.7%2054.6%200%2019.9%206.7%2033.4%206.7%2033.4s-23.1%2097.8-27.1%20114.9c-8.1%2034.1-1.2%2075.9-0.6%2080.1%200.3%202.5%203.6%203.1%205%201.2%202.1-2.7%2028.9-35.9%2038.1-69%202.6-9.4%2014.8-58%2014.8-58%207.3%2014%2028.7%2026.3%2051.5%2026.3%2067.8%200%20113.8-61.8%20113.8-144.5C400.1%20134.7%20347.1%2076.5%20266.6%2076.5z%22%2F%3E%3C%2Fsvg%3E'); +} + +.amp-social-share-linkedin { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M186.4%20142.4c0%2019-15.3%2034.5-34.2%2034.5%20-18.9%200-34.2-15.4-34.2-34.5%200-19%2015.3-34.5%2034.2-34.5C171.1%20107.9%20186.4%20123.4%20186.4%20142.4zM181.4%20201.3h-57.8V388.1h57.8V201.3zM273.8%20201.3h-55.4V388.1h55.4c0%200%200-69.3%200-98%200-26.3%2012.1-41.9%2035.2-41.9%2021.3%200%2031.5%2015%2031.5%2041.9%200%2026.9%200%2098%200%2098h57.5c0%200%200-68.2%200-118.3%200-50-28.3-74.2-68-74.2%20-39.6%200-56.3%2030.9-56.3%2030.9v-25.2H273.8z%22%2F%3E%3C%2Fsvg%3E'); +} + +.amp-social-share-gplus { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M179.7%20237.6L179.7%20284.2%20256.7%20284.2C253.6%20304.2%20233.4%20342.9%20179.7%20342.9%20133.4%20342.9%2095.6%20304.4%2095.6%20257%2095.6%20209.6%20133.4%20171.1%20179.7%20171.1%20206.1%20171.1%20223.7%20182.4%20233.8%20192.1L270.6%20156.6C247%20134.4%20216.4%20121%20179.7%20121%20104.7%20121%2044%20181.8%2044%20257%2044%20332.2%20104.7%20393%20179.7%20393%20258%20393%20310%20337.8%20310%20260.1%20310%20251.2%20309%20244.4%20307.9%20237.6L179.7%20237.6%20179.7%20237.6ZM468%20236.7L429.3%20236.7%20429.3%20198%20390.7%20198%20390.7%20236.7%20352%20236.7%20352%20275.3%20390.7%20275.3%20390.7%20314%20429.3%20314%20429.3%20275.3%20468%20275.3%22%2F%3E%3C%2Fsvg%3E'); } -amp-social-share[type=pinterest] { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M266.6%2076.5c-100.2%200-150.7%2071.8-150.7%20131.7%200%2036.3%2013.7%2068.5%2043.2%2080.6%204.8%202%209.2%200.1%2010.6-5.3%201-3.7%203.3-13%204.3-16.9%201.4-5.3%200.9-7.1-3-11.8%20-8.5-10-13.9-23-13.9-41.3%200-53.3%2039.9-101%20103.8-101%2056.6%200%2087.7%2034.6%2087.7%2080.8%200%2060.8-26.9%20112.1-66.8%20112.1%20-22.1%200-38.6-18.2-33.3-40.6%206.3-26.7%2018.6-55.5%2018.6-74.8%200-17.3-9.3-31.7-28.4-31.7%20-22.5%200-40.7%2023.3-40.7%2054.6%200%2019.9%206.7%2033.4%206.7%2033.4s-23.1%2097.8-27.1%20114.9c-8.1%2034.1-1.2%2075.9-0.6%2080.1%200.3%202.5%203.6%203.1%205%201.2%202.1-2.7%2028.9-35.9%2038.1-69%202.6-9.4%2014.8-58%2014.8-58%207.3%2014%2028.7%2026.3%2051.5%2026.3%2067.8%200%20113.8-61.8%20113.8-144.5C400.1%20134.7%20347.1%2076.5%20266.6%2076.5z%22%2F%3E%3C%2Fsvg%3E'); +.amp-social-share-email { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M101.3%20141.6v228.9h0.3%20308.4%200.8V141.6H101.3zM375.7%20167.8l-119.7%2091.5%20-119.6-91.5H375.7zM127.6%20194.1l64.1%2049.1%20-64.1%2064.1V194.1zM127.8%20344.2l84.9-84.9%2043.2%2033.1%2043-32.9%2084.7%2084.7L127.8%20344.2%20127.8%20344.2zM384.4%20307.8l-64.4-64.4%2064.4-49.3V307.8z%22%2F%3E%3C%2Fsvg%3E'); } -amp-social-share[type=linkedin] { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M186.4%20142.4c0%2019-15.3%2034.5-34.2%2034.5%20-18.9%200-34.2-15.4-34.2-34.5%200-19%2015.3-34.5%2034.2-34.5C171.1%20107.9%20186.4%20123.4%20186.4%20142.4zM181.4%20201.3h-57.8V388.1h57.8V201.3zM273.8%20201.3h-55.4V388.1h55.4c0%200%200-69.3%200-98%200-26.3%2012.1-41.9%2035.2-41.9%2021.3%200%2031.5%2015%2031.5%2041.9%200%2026.9%200%2098%200%2098h57.5c0%200%200-68.2%200-118.3%200-50-28.3-74.2-68-74.2%20-39.6%200-56.3%2030.9-56.3%2030.9v-25.2H273.8z%22%2F%3E%3C%2Fsvg%3E'); +.amp-social-share-twitter { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22-49%20141%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M432.9%2C256.9c-16.6%2C7.4-34.5%2C12.4-53.2%2C14.6c19.2-11.5%2C33.8-29.7%2C40.8-51.3c-17.9%2C10.6-37.8%2C18.4-58.9%2C22.5%20c-16.9-18-41-29.2-67.7-29.2c-51.2%2C0-92.7%2C41.5-92.7%2C92.7c0%2C7.2%2C0.8%2C14.3%2C2.4%2C21.1c-77-3.9-145.3-40.8-191.1-96.9%20C4.6%2C244%2C0%2C259.9%2C0%2C276.9C0%2C309%2C16.4%2C337.4%2C41.3%2C354c-15.2-0.4-29.5-4.7-42-11.6c0%2C0.4%2C0%2C0.8%2C0%2C1.1c0%2C44.9%2C31.9%2C82.4%2C74.4%2C90.9%20c-7.8%2C2.1-16%2C3.3-24.4%2C3.3c-6%2C0-11.7-0.6-17.5-1.7c11.8%2C36.8%2C46.1%2C63.6%2C86.6%2C64.4c-31.8%2C24.9-71.7%2C39.7-115.2%2C39.7%20c-7.5%2C0-14.8-0.4-22.2-1.3c41.1%2C26.4%2C89.8%2C41.7%2C142.2%2C41.7c170.5%2C0%2C263.8-141.3%2C263.8-263.8c0-4.1-0.1-8-0.3-12%20C404.8%2C291.8%2C420.5%2C275.5%2C432.9%2C256.9z%22%2F%3E%3C%2Fsvg%3E'); } -amp-social-share[type=gplus] { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M179.7%20237.6L179.7%20284.2%20256.7%20284.2C253.6%20304.2%20233.4%20342.9%20179.7%20342.9%20133.4%20342.9%2095.6%20304.4%2095.6%20257%2095.6%20209.6%20133.4%20171.1%20179.7%20171.1%20206.1%20171.1%20223.7%20182.4%20233.8%20192.1L270.6%20156.6C247%20134.4%20216.4%20121%20179.7%20121%20104.7%20121%2044%20181.8%2044%20257%2044%20332.2%20104.7%20393%20179.7%20393%20258%20393%20310%20337.8%20310%20260.1%20310%20251.2%20309%20244.4%20307.9%20237.6L179.7%20237.6%20179.7%20237.6ZM468%20236.7L429.3%20236.7%20429.3%20198%20390.7%20198%20390.7%20236.7%20352%20236.7%20352%20275.3%20390.7%20275.3%20390.7%20314%20429.3%20314%20429.3%20275.3%20468%20275.3%22%2F%3E%3C%2Fsvg%3E'); +.amp-social-share-tumblr { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M210.8%2080.3c-2.3%2018.3-6.4%2033.4-12.4%2045.2%20-6%2011.9-13.9%2022-23.9%2030.5%20-9.9%208.5-21.8%2014.9-35.7%2019.5v50.6h38.9v124.5c0%2016.2%201.7%2028.6%205.1%2037.1%203.4%208.5%209.5%2016.6%2018.3%2024.2%208.8%207.6%2019.4%2013.4%2031.9%2017.5%2012.5%204.1%2026.8%206.1%2043%206.1%2014.3%200%2027.6-1.4%2039.9-4.3%2012.3-2.9%2026-7.9%2041.2-15v-55.9c-17.8%2011.7-35.7%2017.5-53.7%2017.5%20-10.1%200-19.1-2.4-27-7.1%20-5.9-3.5-10-8.2-12.2-14%20-2.2-5.8-3.3-19.1-3.3-39.7v-91.1H345.5v-55.8h-84.4v-90H210.8z%22%2F%3E%3C%2Fsvg%3E'); } -amp-social-share[type=email] { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M101.3%20141.6v228.9h0.3%20308.4%200.8V141.6H101.3zM375.7%20167.8l-119.7%2091.5%20-119.6-91.5H375.7zM127.6%20194.1l64.1%2049.1%20-64.1%2064.1V194.1zM127.8%20344.2l84.9-84.9%2043.2%2033.1%2043-32.9%2084.7%2084.7L127.8%20344.2%20127.8%20344.2zM384.4%20307.8l-64.4-64.4%2064.4-49.3V307.8z%22%2F%3E%3C%2Fsvg%3E'); +.amp-social-share-whatsapp { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20width%3D%22419%22%20height%3D%22421%22%20viewBox%3D%220%200%20419%20421%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Ctitle%3Elogo%3C%2Ftitle%3E%3Cdefs%3E%3Cpath%20d%3D%22M299.558%20247.553c-5.111-2.558-30.242-14.925-34.928-16.633-4.685-1.706-8.092-2.557-11.5%202.559-3.408%205.117-13.204%2016.634-16.186%2020.046-2.98%203.41-5.963%203.84-11.074%201.279-5.112-2.559-21.582-7.956-41.106-25.375-15.195-13.556-25.455-30.296-28.436-35.414-2.982-5.118-.318-7.885%202.24-10.434%202.3-2.291%205.113-5.971%207.668-8.957%202.557-2.984%203.408-5.118%205.112-8.528%201.704-3.413.852-6.397-.426-8.956-1.278-2.558-11.5-27.723-15.76-37.96-4.15-9.968-8.363-8.618-11.501-8.776-2.978-.148-6.39-.18-9.796-.18-3.408%200-8.946%201.28-13.632%206.397-4.685%205.118-17.89%2017.487-17.89%2042.649%200%2025.165%2018.316%2049.473%2020.872%2052.885%202.556%203.413%2036.044%2055.05%2087.321%2077.193%2012.195%205.269%2021.716%208.414%2029.14%2010.77%2012.245%203.891%2023.388%203.342%2032.195%202.025%209.821-1.466%2030.243-12.366%2034.503-24.307%204.259-11.944%204.259-22.18%202.98-24.311-1.276-2.133-4.684-3.412-9.796-5.972m-93.266%20127.364h-.069c-30.511-.012-60.436-8.21-86.542-23.705l-6.21-3.685-64.353%2016.884%2017.177-62.753-4.043-6.435c-17.02-27.075-26.01-58.368-25.997-90.501.038-93.761%2076.315-170.043%20170.104-170.043%2045.416.015%2088.107%2017.727%20120.212%2049.872%2032.102%2032.143%2049.77%2074.87%2049.753%20120.308-.038%2093.77-76.314%20170.058-170.032%20170.058M351.003%2060.126C312.38%2021.452%20261.016.145%20206.29.122%2093.532.122%201.76%2091.901%201.715%20204.71c-.015%2036.06%209.405%2071.257%2027.307%20102.286L0%20413.02l108.447-28.452c29.88%2016.3%2063.523%2024.892%2097.761%2024.903h.084c112.747%200%20204.527-91.787%20204.573-204.597.02-54.67-21.239-106.074-59.862-144.747%22%20id%3D%22b%22%2F%3E%3Cfilter%20x%3D%22-50%25%22%20y%3D%22-50%25%22%20width%3D%22200%25%22%20height%3D%22200%25%22%20filterUnits%3D%22objectBoundingBox%22%20id%3D%22a%22%3E%3CfeMorphology%20radius%3D%221%22%20operator%3D%22dilate%22%20in%3D%22SourceAlpha%22%20result%3D%22shadowSpreadOuter1%22%2F%3E%3CfeOffset%20in%3D%22shadowSpreadOuter1%22%20result%3D%22shadowOffsetOuter1%22%2F%3E%3CfeColorMatrix%20values%3D%220%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200.04%200%22%20in%3D%22shadowOffsetOuter1%22%2F%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M117.472%20352.386l6.209%203.685c26.106%2015.494%2056.031%2023.693%2086.542%2023.704h.07c93.717%200%20169.993-76.288%20170.032-170.058.017-45.438-17.653-88.165-49.755-120.308-32.103-32.145-74.795-49.856-120.21-49.872-93.79%200-170.068%2076.282-170.105%20170.043-.013%2032.133%208.976%2063.427%2025.997%2090.501l4.043%206.435-17.177%2062.753%2064.354-16.883z%22%20fill%3D%22%2325D366%22%2F%3E%3Cg%20transform%3D%22translate(4%204.858)%22%3E%3Cuse%20fill%3D%22%23000%22%20filter%3D%22url(%23a)%22%20xlink%3Ahref%3D%22%23b%22%2F%3E%3Cuse%20fill%3D%22%23FFF%22%20xlink%3Ahref%3D%22%23b%22%2F%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'); } -amp-social-share[type=twitter] { - background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22-49%20141%20512%20512%22%3E%3Cpath%20fill%3D%22%23ffffff%22%20d%3D%22M432.9%2C256.9c-16.6%2C7.4-34.5%2C12.4-53.2%2C14.6c19.2-11.5%2C33.8-29.7%2C40.8-51.3c-17.9%2C10.6-37.8%2C18.4-58.9%2C22.5%20c-16.9-18-41-29.2-67.7-29.2c-51.2%2C0-92.7%2C41.5-92.7%2C92.7c0%2C7.2%2C0.8%2C14.3%2C2.4%2C21.1c-77-3.9-145.3-40.8-191.1-96.9%20C4.6%2C244%2C0%2C259.9%2C0%2C276.9C0%2C309%2C16.4%2C337.4%2C41.3%2C354c-15.2-0.4-29.5-4.7-42-11.6c0%2C0.4%2C0%2C0.8%2C0%2C1.1c0%2C44.9%2C31.9%2C82.4%2C74.4%2C90.9%20c-7.8%2C2.1-16%2C3.3-24.4%2C3.3c-6%2C0-11.7-0.6-17.5-1.7c11.8%2C36.8%2C46.1%2C63.6%2C86.6%2C64.4c-31.8%2C24.9-71.7%2C39.7-115.2%2C39.7%20c-7.5%2C0-14.8-0.4-22.2-1.3c41.1%2C26.4%2C89.8%2C41.7%2C142.2%2C41.7c170.5%2C0%2C263.8-141.3%2C263.8-263.8c0-4.1-0.1-8-0.3-12%20C404.8%2C291.8%2C420.5%2C275.5%2C432.9%2C256.9z%22%2F%3E%3C%2Fsvg%3E'); +.amp-social-share-system { + background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20fill=%22#ffffff%22%20height=%2224%22%20viewBox=%220%200%2024%2024%22%20width=%2224%22%20xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cpath%20d=%22M0%200h24v24H0z%22%20fill=%22none%22/%3E%3Cpath%20d=%22M18%2016.08c-.76%200-1.44.3-1.96.77L8.91%2012.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5%201.25.81%202.04.81%201.66%200%203-1.34%203-3s-1.34-3-3-3-3%201.34-3%203c0%20.24.04.47.09.7L8.04%209.81C7.5%209.31%206.79%209%206%209c-1.66%200-3%201.34-3%203s1.34%203%203%203c.79%200%201.5-.31%202.04-.81l7.12%204.16c-.05.21-.08.43-.08.65%200%201.61%201.31%202.92%202.92%202.92%201.61%200%202.92-1.31%202.92-2.92s-1.31-2.92-2.92-2.92z%22/%3E%3C/svg%3E'); } diff --git a/third_party/vega/LICENSE b/third_party/vega/LICENSE new file mode 100644 index 000000000000..b5b5d2301905 --- /dev/null +++ b/third_party/vega/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2013, Trifacta Inc. +Copyright (c) 2015, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/vega/README.amp b/third_party/vega/README.amp new file mode 100644 index 000000000000..815babf6efd5 --- /dev/null +++ b/third_party/vega/README.amp @@ -0,0 +1,10 @@ +URL: https://github.com/vega/vega/tree/v2.6.1 +License: BSD +License File: https://github.com/vega/vega/blob/v2.6.1/LICENSE + +Description: +Copy of minified (vega.min.js) from release v2.6.1 +https://github.com/vega/vega/tree/v2.6.1 + +Local Modifications: +None diff --git a/third_party/vega/vega.js b/third_party/vega/vega.js new file mode 100644 index 000000000000..ad83b17e1105 --- /dev/null +++ b/third_party/vega/vega.js @@ -0,0 +1,10 @@ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.vg=t()}}(function(){var t;return function t(e,n,r){function i(o,s){if(!n[o]){if(!e[o]){var u="function"==typeof require&&require;if(!s&&u)return u(o,!0);if(a)return a(o,!0);var l=new Error("Cannot find module '"+o+"'");throw l.code="MODULE_NOT_FOUND",l}var c=n[o]={exports:{}};e[o][0].call(c.exports,function(t){var n=e[o][1][t];return i(n?n:t)},c,c.exports,t,e,n,r)}return n[o].exports}for(var a="function"==typeof require&&require,o=0;o=l)return o;if(i)return i=!1,a;var e,n=c;if(34===t.charCodeAt(n)){for(var r=n;r++1?r[0]+r.slice(2):r,+t.slice(n+1)]}function n(t){return t=e(Math.abs(t)),t?t[1]:NaN}function r(t,e){return function(n,r){for(var i=n.length,a=[],o=0,s=t[0],u=0;i>0&&s>0&&(u+s+1>r&&(s=Math.max(1,r-u)),a.push(n.substring(i-=s,i+s)),!((u+=s+1)>r));)s=t[o=(o+1)%t.length];return a.reverse().join(e)}}function i(t,n){var r=e(t,n);if(!r)return t+"";var i=r[0],a=r[1],o=a-(p=3*Math.max(-8,Math.min(8,Math.floor(a/3))))+1,s=i.length;return o===s?i:o>s?i+new Array(o-s+1).join("0"):o>0?i.slice(0,o)+"."+i.slice(o):"0."+new Array(1-o).join("0")+e(t,Math.max(0,n+o-1))[0]}function a(t,n){var r=e(t,n);if(!r)return t+"";var i=r[0],a=r[1];return a<0?"0."+new Array((-a)).join("0")+i:i.length>a+1?i.slice(0,a+1)+"."+i.slice(a+1):i+new Array(a-i.length+2).join("0")}function o(t,e){t=t.toPrecision(e);t:for(var n,r=t.length,i=1,a=-1;i0&&(a=0)}return a>0?t.slice(0,a)+t.slice(n+1):t}function s(t){return new u(t)}function u(t){if(!(e=m.exec(t)))throw new Error("invalid format: "+t);var e,n=e[1]||" ",r=e[2]||">",i=e[3]||"-",a=e[4]||"",o=!!e[5],s=e[6]&&+e[6],u=!!e[7],l=e[8]&&+e[8].slice(1),c=e[9]||"";"n"===c?(u=!0,c="g"):g[c]||(c=""),(o||"0"===n&&"="===r)&&(o=!0,n="0",r="="),this.fill=n,this.align=r,this.sign=i,this.symbol=a,this.zero=o,this.width=s,this.comma=u,this.precision=l,this.type=c}function l(t){return t}function c(t){function e(t){t=s(t);var e=t.fill,n=t.align,r=t.sign,i=t.symbol,l=t.zero,c=t.width,d=t.comma,f=t.precision,h=t.type,m="$"===i?o[0]:"#"===i&&/[boxX]/.test(h)?"0"+h.toLowerCase():"",y="$"===i?o[1]:/[%p]/.test(h)?"%":"",_=g[h],b=!h||/[defgprs%]/.test(h);return f=null==f?h?6:12:/[gprs]/.test(h)?Math.max(1,Math.min(21,f)):Math.max(0,Math.min(20,f)),function(t){var i=m,o=y;if("c"===h)o=_(t)+o,t="";else{t=+t;var s=(t<0||1/t<0)&&(t*=-1,!0);if(t=_(t,f),s){var g,x=-1,w=t.length;for(s=!1;++xg||g>57){o=(46===g?u+t.slice(x+1):t.slice(x))+o,t=t.slice(0,x);break}}d&&!l&&(t=a(t,1/0));var k=i.length+t.length+o.length,M=k>1)+i+t+o+M.slice(k)}return M+i+t+o}}function i(t,r){var i=e((t=s(t),t.type="f",t)),a=3*Math.max(-8,Math.min(8,Math.floor(n(r)/3))),o=Math.pow(10,-a),u=v[8+a/3];return function(t){return i(o*t)+u}}var a=t.grouping&&t.thousands?r(t.grouping,t.thousands):l,o=t.currency,u=t.decimal;return{format:e,formatPrefix:i}}function d(t){return Math.max(0,-n(Math.abs(t)))}function f(t,e){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(n(e)/3)))-n(Math.abs(t)))}function h(t,e){return t=Math.abs(t),e=Math.abs(e)-t,Math.max(0,n(e)-n(t))+1}var p,g={"":o,"%":function(t,e){return(100*t).toFixed(e)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,e){return t.toExponential(e)},f:function(t,e){return t.toFixed(e)},g:function(t,e){return t.toPrecision(e)},o:function(t){return Math.round(t).toString(8)},p:function(t,e){return a(100*t,e)},r:a,s:i,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},m=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;u.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var v=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],y=c({decimal:".",thousands:",",grouping:[3],currency:["$",""]}),_=c({decimal:",",thousands:".",grouping:[3],currency:[""," €"]}),b=c({decimal:",",thousands:" ",grouping:[3],currency:[""," Kč"]}),x=c({decimal:",",thousands:"'",grouping:[3],currency:[""," CHF"]}),w=c({decimal:",",thousands:".",grouping:[3],currency:[""," €"]}),k=c({decimal:".",thousands:",",grouping:[3],currency:["$",""]}),M=c({decimal:".",thousands:",",grouping:[3],currency:["£",""]}),S=c({decimal:",",thousands:".",grouping:[3],currency:[""," €"]}),T=c({decimal:",",thousands:" ",grouping:[3],currency:[""," €"]}),E=c({decimal:",",thousands:" ",grouping:[3],currency:["","$"]}),A=c({decimal:",",thousands:".",grouping:[3],currency:[""," €"]}),L=c({decimal:".",thousands:",",grouping:[3],currency:["₪",""]}),C=c({decimal:",",thousands:" ",grouping:[3],currency:[""," Ft"]}),D=c({decimal:",",thousands:".",grouping:[3],currency:["€",""]}),P=c({decimal:".",thousands:",",grouping:[3],currency:["","円"]}),I=c({decimal:".",thousands:",",grouping:[3],currency:["₩",""]}),N=c({decimal:",",thousands:".",grouping:[3],currency:[""," ден."]}),O=c({decimal:",",thousands:".",grouping:[3],currency:["€ ",""]}),z=c({decimal:",",thousands:".",grouping:[3],currency:["","zł"]}),j=c({decimal:",",thousands:".",grouping:[3],currency:["R$",""]}),F=c({decimal:",",thousands:" ",grouping:[3],currency:[""," руб."]}),U=c({decimal:",",thousands:" ",grouping:[3],currency:["","SEK"]}),R=c({decimal:".",thousands:",",grouping:[3],currency:["¥",""]}),q=y.format,B=y.formatPrefix,G="0.4.2";t.version=G,t.format=q,t.formatPrefix=B,t.locale=c,t.localeCaEs=_,t.localeCsCz=b,t.localeDeCh=x,t.localeDeDe=w,t.localeEnCa=k,t.localeEnGb=M,t.localeEnUs=y,t.localeEsEs=S,t.localeFiFi=T,t.localeFrCa=E,t.localeFrFr=A,t.localeHeIl=L,t.localeHuHu=C,t.localeItIt=D,t.localeJaJp=P,t.localeKoKr=I,t.localeMkMk=N,t.localeNlNl=O,t.localePlPl=z,t.localePtBr=j,t.localeRuRu=F,t.localeSvSe=U,t.localeZhCn=R,t.formatSpecifier=s,t.precisionFixed=d,t.precisionPrefix=f,t.precisionRound=h})},{}],5:[function(e,n,r){!function(i,a){"object"==typeof r&&"undefined"!=typeof n?a(r,e("d3-time")):"function"==typeof t&&t.amd?t("d3-time-format",["exports","d3-time"],a):a(i.d3_time_format={},i.d3_time)}(this,function(t,e){"use strict";function n(t){if(0<=t.y&&t.y<100){var e=new Date((-1),t.m,t.d,t.H,t.M,t.S,t.L);return e.setFullYear(t.y),e}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function r(t){if(0<=t.y&&t.y<100){var e=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return e.setUTCFullYear(t.y),e}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function i(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function a(t){function e(t,e){return function(n){var r,i,a,o=[],s=-1,u=0,l=t.length;for(n instanceof Date||(n=new Date((+n)));++s=u)return-1;if(i=e.charCodeAt(o++),37===i){if(i=e.charAt(o++),a=jt[i in tt?e.charAt(o++):i],!a||(r=a(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}function s(t,e,n){var r=St.exec(e.slice(n));return r?(t.p=Tt[r[0].toLowerCase()],n+r[0].length):-1}function Q(t,e,n){var r=Lt.exec(e.slice(n));return r?(t.w=Ct[r[0].toLowerCase()],n+r[0].length):-1}function et(t,e,n){var r=Et.exec(e.slice(n));return r?(t.w=At[r[0].toLowerCase()],n+r[0].length):-1}function nt(t,e,n){var r=It.exec(e.slice(n));return r?(t.m=Nt[r[0].toLowerCase()],n+r[0].length):-1}function rt(t,e,n){var r=Dt.exec(e.slice(n));return r?(t.m=Pt[r[0].toLowerCase()],n+r[0].length):-1}function it(t,e,n){return o(t,vt,e,n)}function at(t,e,n){return o(t,yt,e,n)}function ot(t,e,n){return o(t,_t,e,n)}function st(t){return wt[t.getDay()]}function ut(t){return xt[t.getDay()]}function lt(t){return Mt[t.getMonth()]}function ct(t){return kt[t.getMonth()]}function dt(t){return bt[+(t.getHours()>=12)]}function ft(t){return wt[t.getUTCDay()]}function ht(t){return xt[t.getUTCDay()]}function pt(t){return Mt[t.getUTCMonth()]}function gt(t){return kt[t.getUTCMonth()]}function mt(t){return bt[+(t.getUTCHours()>=12)]}var vt=t.dateTime,yt=t.date,_t=t.time,bt=t.periods,xt=t.days,wt=t.shortDays,kt=t.months,Mt=t.shortMonths,St=u(bt),Tt=l(bt),Et=u(xt),At=l(xt),Lt=u(wt),Ct=l(wt),Dt=u(kt),Pt=l(kt),It=u(Mt),Nt=l(Mt),Ot={a:st,A:ut,b:lt,B:ct,c:null,d:M,e:M,H:S,I:T,j:E,L:A,m:L,M:C,p:dt,S:D,U:P,w:I,W:N,x:null,X:null,y:O,Y:z,Z:j,"%":Z},zt={a:ft,A:ht,b:pt,B:gt,c:null,d:F,e:F,H:U,I:R,j:q,L:B,m:G,M:$,p:mt,S:H,U:Y,w:V,W:W,x:null,X:null,y:X,Y:J,Z:K,"%":Z},jt={a:Q,A:et,b:nt,B:rt,c:it,d:v,e:v,H:_,I:_,j:y,L:w,m:m,M:b,p:s,S:x,U:d,w:c,W:f,x:at,X:ot,y:p,Y:h,Z:g,"%":k};return Ot.x=e(yt,Ot),Ot.X=e(_t,Ot),Ot.c=e(vt,Ot),zt.x=e(yt,zt),zt.X=e(_t,zt),zt.c=e(vt,zt),{format:function(t){var r=e(t+="",Ot);return r.parse=a(t,n),r.toString=function(){return t},r},utcFormat:function(t){var n=e(t+="",zt);return n.parse=a(t,r),n.toString=function(){return t},n}}}function o(t,e,n){var r=t<0?"-":"",i=(r?-t:t)+"",a=i.length;return r+(a68?1900:2e3),n+r[0].length):-1}function g(t,e,n){var r=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function m(t,e,n){var r=et.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function v(t,e,n){var r=et.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function y(t,e,n){var r=et.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function _(t,e,n){var r=et.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function b(t,e,n){var r=et.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function x(t,e,n){var r=et.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function w(t,e,n){var r=et.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function k(t,e,n){var r=nt.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function M(t,e){return o(t.getDate(),e,2)}function S(t,e){return o(t.getHours(),e,2)}function T(t,e){return o(t.getHours()%12||12,e,2)}function E(t,n){return o(1+e.day.count(e.year(t),t),n,3)}function A(t,e){return o(t.getMilliseconds(),e,3)}function L(t,e){return o(t.getMonth()+1,e,2)}function C(t,e){return o(t.getMinutes(),e,2)}function D(t,e){return o(t.getSeconds(),e,2)}function P(t,n){return o(e.sunday.count(e.year(t),t),n,2)}function I(t){return t.getDay()}function N(t,n){return o(e.monday.count(e.year(t),t),n,2)}function O(t,e){return o(t.getFullYear()%100,e,2)}function z(t,e){return o(t.getFullYear()%1e4,e,4)}function j(t){var e=t.getTimezoneOffset();return(e>0?"-":(e*=-1,"+"))+o(e/60|0,"0",2)+o(e%60,"0",2)}function F(t,e){return o(t.getUTCDate(),e,2)}function U(t,e){return o(t.getUTCHours(),e,2)}function R(t,e){return o(t.getUTCHours()%12||12,e,2)}function q(t,n){return o(1+e.utcDay.count(e.utcYear(t),t),n,3)}function B(t,e){return o(t.getUTCMilliseconds(),e,3)}function G(t,e){return o(t.getUTCMonth()+1,e,2)}function $(t,e){return o(t.getUTCMinutes(),e,2)}function H(t,e){return o(t.getUTCSeconds(),e,2)}function Y(t,n){return o(e.utcSunday.count(e.utcYear(t),t),n,2)}function V(t){return t.getUTCDay()}function W(t,n){return o(e.utcMonday.count(e.utcYear(t),t),n,2)}function X(t,e){return o(t.getUTCFullYear()%100,e,2)}function J(t,e){return o(t.getUTCFullYear()%1e4,e,4)}function K(){return"+0000"}function Z(){return"%"}function Q(t){return t.toISOString()}var tt={"-":"",_:" ",0:"0"},et=/^\s*\d+/,nt=/^%/,rt=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,it=a({dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}),at=a({dateTime:"%A, %e de %B de %Y, %X",date:"%d/%m/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],shortDays:["dg.","dl.","dt.","dc.","dj.","dv.","ds."],months:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],shortMonths:["gen.","febr.","març","abr.","maig","juny","jul.","ag.","set.","oct.","nov.","des."]}),ot=a({dateTime:"%A, der %e. %B %Y, %X",date:"%d.%m.%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],shortDays:["So","Mo","Di","Mi","Do","Fr","Sa"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],shortMonths:["Jan","Feb","Mrz","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]}),st=a({dateTime:"%A, der %e. %B %Y, %X",date:"%d.%m.%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],shortDays:["So","Mo","Di","Mi","Do","Fr","Sa"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],shortMonths:["Jan","Feb","Mrz","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]}),ut=a({dateTime:"%a %b %e %X %Y",date:"%Y-%m-%d",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}),lt=a({dateTime:"%a %e %b %X %Y",date:"%d/%m/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}),ct=a({dateTime:"%A, %e de %B de %Y, %X",date:"%d/%m/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],shortDays:["dom","lun","mar","mié","jue","vie","sáb"],months:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],shortMonths:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"]}),dt=a({dateTime:"%A, %-d. %Bta %Y klo %X",date:"%-d.%-m.%Y",time:"%H:%M:%S",periods:["a.m.","p.m."],days:["sunnuntai","maanantai","tiistai","keskiviikko","torstai","perjantai","lauantai"],shortDays:["Su","Ma","Ti","Ke","To","Pe","La"],months:["tammikuu","helmikuu","maaliskuu","huhtikuu","toukokuu","kesäkuu","heinäkuu","elokuu","syyskuu","lokakuu","marraskuu","joulukuu"],shortMonths:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"]}),ft=a({dateTime:"%a %e %b %Y %X",date:"%Y-%m-%d",time:"%H:%M:%S",periods:["",""],days:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],shortDays:["dim","lun","mar","mer","jeu","ven","sam"],months:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],shortMonths:["jan","fév","mar","avr","mai","jui","jul","aoû","sep","oct","nov","déc"]}),ht=a({dateTime:"%A, le %e %B %Y, %X",date:"%d/%m/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],shortDays:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],months:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],shortMonths:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."]}),pt=a({dateTime:"%A, %e ב%B %Y %X",date:"%d.%m.%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת"],shortDays:["א׳","ב׳","ג׳","ד׳","ה׳","ו׳","ש׳"],months:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],shortMonths:["ינו׳","פבר׳","מרץ","אפר׳","מאי","יוני","יולי","אוג׳","ספט׳","אוק׳","נוב׳","דצמ׳"]}),gt=a({dateTime:"%Y. %B %-e., %A %X",date:"%Y. %m. %d.",time:"%H:%M:%S",periods:["de.","du."],days:["vasárnap","hétfő","kedd","szerda","csütörtök","péntek","szombat"],shortDays:["V","H","K","Sze","Cs","P","Szo"],months:["január","február","március","április","május","június","július","augusztus","szeptember","október","november","december"],shortMonths:["jan.","feb.","már.","ápr.","máj.","jún.","júl.","aug.","szept.","okt.","nov.","dec."]}),mt=a({dateTime:"%A %e %B %Y, %X",date:"%d/%m/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],shortDays:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],months:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],shortMonths:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"]}),vt=a({dateTime:"%Y %b %e %a %X",date:"%Y/%m/%d",time:"%H:%M:%S",periods:["AM","PM"],days:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],shortDays:["日","月","火","水","木","金","土"],months:["睦月","如月","弥生","卯月","皐月","水無月","文月","葉月","長月","神無月","霜月","師走"],shortMonths:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]}),yt=a({dateTime:"%Y/%m/%d %a %X",date:"%Y/%m/%d",time:"%H:%M:%S",periods:["오전","오후"],days:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],shortDays:["일","월","화","수","목","금","토"],months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],shortMonths:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"]}),_t=a({dateTime:"%A, %e %B %Y г. %X",date:"%d.%m.%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["недела","понеделник","вторник","среда","четврток","петок","сабота"],shortDays:["нед","пон","вто","сре","чет","пет","саб"],months:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],shortMonths:["јан","фев","мар","апр","мај","јун","јул","авг","сеп","окт","ное","дек"]}),bt=a({dateTime:"%a %e %B %Y %T",date:"%d-%m-%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],shortDays:["zo","ma","di","wo","do","vr","za"],months:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],shortMonths:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"]}),xt=a({dateTime:"%A, %e %B %Y, %X",date:"%d/%m/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],shortDays:["Niedz.","Pon.","Wt.","Śr.","Czw.","Pt.","Sob."],months:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],shortMonths:["Stycz.","Luty","Marz.","Kwie.","Maj","Czerw.","Lipc.","Sierp.","Wrz.","Paźdz.","Listop.","Grudz."]}),wt=a({dateTime:"%A, %e de %B de %Y. %X",date:"%d/%m/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado"],shortDays:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],shortMonths:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"]}),kt=a({dateTime:"%A, %e %B %Y г. %X",date:"%d.%m.%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],shortDays:["вс","пн","вт","ср","чт","пт","сб"],months:["января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"],shortMonths:["янв","фев","мар","апр","май","июн","июл","авг","сен","окт","ноя","дек"]}),Mt=a({dateTime:"%A den %d %B %Y %X",date:"%Y-%m-%d",time:"%H:%M:%S",periods:["fm","em"],days:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],shortDays:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],months:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],shortMonths:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"]}),St=a({dateTime:"%a %b %e %X %Y",date:"%Y/%-m/%-d",time:"%H:%M:%S",periods:["上午","下午"],days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],shortDays:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],shortMonths:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]}),Tt="%Y-%m-%dT%H:%M:%S.%LZ";Q.parse=function(t){var e=new Date(t);return isNaN(e)?null:e},Q.toString=function(){return Tt};var Et=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Q:it.utcFormat(Tt),At=it.format,Lt=it.utcFormat,Ct="0.2.1";t.version=Ct,t.format=At,t.utcFormat=Lt,t.locale=a,t.localeCaEs=at,t.localeDeCh=ot,t.localeDeDe=st,t.localeEnCa=ut,t.localeEnGb=lt,t.localeEnUs=it,t.localeEsEs=ct,t.localeFiFi=dt,t.localeFrCa=ft,t.localeFrFr=ht,t.localeHeIl=pt,t.localeHuHu=gt,t.localeItIt=mt,t.localeJaJp=vt,t.localeKoKr=yt,t.localeMkMk=_t,t.localeNlNl=bt,t.localePlPl=xt,t.localePtBr=wt,t.localeRuRu=kt,t.localeSvSe=Mt,t.localeZhCn=St,t.isoFormat=Et})},{"d3-time":6}],6:[function(e,n,r){!function(e,i){"object"==typeof r&&"undefined"!=typeof n?i(r):"function"==typeof t&&t.amd?t("d3-time",["exports"],i):i(e.d3_time={})}(this,function(t){"use strict";function e(t,n,r,o){function s(e){return t(e=new Date((+e))),e}return s.floor=s,s.round=function(e){var r=new Date((+e)),i=new Date(e-1);return t(r),t(i),n(i,1),e-r0))return a;for(n(e,1),t(e),e=0;)for(;n(t,1),!r(t););})},r&&(s.count=function(e,n){return i.setTime(+e),a.setTime(+n),t(i),t(a),Math.floor(r(i,a))},s.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?s.filter(o?function(e){return o(e)%t===0}:function(e){return s.count(0,e)%t===0}):s:null}),s}function n(t){return e(function(e){e.setHours(0,0,0,0),e.setDate(e.getDate()-(e.getDay()+7-t)%7)},function(t,e){t.setDate(t.getDate()+7*e)},function(t,e){return(e-t-6e4*(e.getTimezoneOffset()-t.getTimezoneOffset()))/6048e5})}function r(t){return e(function(e){e.setUTCHours(0,0,0,0),e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7)},function(t,e){t.setUTCDate(t.getUTCDate()+7*e)},function(t,e){return(e-t)/6048e5})}var i=new Date,a=new Date,o=e(function(){},function(t,e){t.setTime(+t+e)},function(t,e){return e-t});o.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?e(function(e){e.setTime(Math.floor(e/t)*t)},function(e,n){e.setTime(+e+n*t)},function(e,n){return(n-e)/t}):o:null};var s=e(function(t){t.setMilliseconds(0)},function(t,e){t.setTime(+t+1e3*e)},function(t,e){return(e-t)/1e3},function(t){return t.getSeconds()}),u=e(function(t){t.setSeconds(0,0)},function(t,e){t.setTime(+t+6e4*e)},function(t,e){return(e-t)/6e4},function(t){return t.getMinutes()}),l=e(function(t){t.setMinutes(0,0,0)},function(t,e){t.setTime(+t+36e5*e)},function(t,e){return(e-t)/36e5},function(t){return t.getHours()}),c=e(function(t){t.setHours(0,0,0,0)},function(t,e){t.setDate(t.getDate()+e)},function(t,e){return(e-t-6e4*(e.getTimezoneOffset()-t.getTimezoneOffset()))/864e5},function(t){return t.getDate()-1}),d=n(0),f=n(1),h=n(2),p=n(3),g=n(4),m=n(5),v=n(6),y=e(function(t){t.setHours(0,0,0,0),t.setDate(1)},function(t,e){t.setMonth(t.getMonth()+e)},function(t,e){return e.getMonth()-t.getMonth()+12*(e.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),_=e(function(t){t.setHours(0,0,0,0),t.setMonth(0,1)},function(t,e){t.setFullYear(t.getFullYear()+e)},function(t,e){return e.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()}),b=e(function(t){t.setUTCMilliseconds(0)},function(t,e){t.setTime(+t+1e3*e)},function(t,e){return(e-t)/1e3},function(t){return t.getUTCSeconds()}),x=e(function(t){t.setUTCSeconds(0,0)},function(t,e){t.setTime(+t+6e4*e)},function(t,e){return(e-t)/6e4},function(t){return t.getUTCMinutes()}),w=e(function(t){t.setUTCMinutes(0,0,0)},function(t,e){t.setTime(+t+36e5*e)},function(t,e){return(e-t)/36e5},function(t){return t.getUTCHours()}),k=e(function(t){t.setUTCHours(0,0,0,0)},function(t,e){t.setUTCDate(t.getUTCDate()+e)},function(t,e){return(e-t)/864e5},function(t){return t.getUTCDate()-1}),M=r(0),S=r(1),T=r(2),E=r(3),A=r(4),L=r(5),C=r(6),D=e(function(t){t.setUTCHours(0,0,0,0),t.setUTCDate(1)},function(t,e){t.setUTCMonth(t.getUTCMonth()+e)},function(t,e){return e.getUTCMonth()-t.getUTCMonth()+12*(e.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),P=e(function(t){t.setUTCHours(0,0,0,0),t.setUTCMonth(0,1)},function(t,e){t.setUTCFullYear(t.getUTCFullYear()+e)},function(t,e){return e.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()}),I=o.range,N=s.range,O=u.range,z=l.range,j=c.range,F=d.range,U=f.range,R=h.range,q=p.range,B=g.range,G=m.range,$=v.range,H=d.range,Y=y.range,V=_.range,W=o,X=I,J=b.range,K=x.range,Z=w.range,Q=k.range,tt=M.range,et=S.range,nt=T.range,rt=E.range,it=A.range,at=L.range,ot=C.range,st=M.range,ut=D.range,lt=P.range,ct="0.1.1";t.version=ct,t.milliseconds=I,t.seconds=N,t.minutes=O,t.hours=z,t.days=j,t.sundays=F,t.mondays=U,t.tuesdays=R,t.wednesdays=q,t.thursdays=B,t.fridays=G,t.saturdays=$,t.weeks=H,t.months=Y,t.years=V,t.utcMillisecond=W,t.utcMilliseconds=X,t.utcSeconds=J,t.utcMinutes=K,t.utcHours=Z,t.utcDays=Q,t.utcSundays=tt,t.utcMondays=et,t.utcTuesdays=nt,t.utcWednesdays=rt,t.utcThursdays=it,t.utcFridays=at,t.utcSaturdays=ot,t.utcWeeks=st,t.utcMonths=ut,t.utcYears=lt,t.millisecond=o,t.second=s,t.minute=u,t.hour=l,t.day=c,t.sunday=d,t.monday=f,t.tuesday=h,t.wednesday=p,t.thursday=g,t.friday=m,t.saturday=v,t.week=d,t.month=y,t.year=_,t.utcSecond=b,t.utcMinute=x,t.utcHour=w,t.utcDay=k,t.utcSunday=M,t.utcMonday=S,t.utcTuesday=T,t.utcWednesday=E,t.utcThursday=A,t.utcFriday=L,t.utcSaturday=C,t.utcWeek=M,t.utcMonth=D,t.utcYear=P,t.interval=e})},{}],7:[function(t,e,n){var r=t("./util"),i=t("./time"),a=i.utc,o=e.exports;o.$year=r.$func("year",i.year.unit),o.$month=r.$func("month",i.months.unit),o.$date=r.$func("date",i.dates.unit),o.$day=r.$func("day",i.weekdays.unit),o.$hour=r.$func("hour",i.hours.unit),o.$minute=r.$func("minute",i.minutes.unit),o.$second=r.$func("second",i.seconds.unit),o.$utcYear=r.$func("utcYear",a.year.unit),o.$utcMonth=r.$func("utcMonth",a.months.unit),o.$utcDate=r.$func("utcDate",a.dates.unit),o.$utcDay=r.$func("utcDay",a.weekdays.unit),o.$utcHour=r.$func("utcHour",a.hours.unit),o.$utcMinute=r.$func("utcMinute",a.minutes.unit),o.$utcSecond=r.$func("utcSecond",a.seconds.unit)},{"./time":29,"./util":30}],8:[function(t,e,n){function r(){this._cells={},this._aggr=[],this._stream=!1}function i(t){if(a.isArray(t))return t;if(null==t)return[];var e,n,r=[];for(e in t)n=a.array(t[e]),r.push({name:e,ops:n});return r}var a=t("../util"),o=t("./measures"),s=t("./collector"),u=r.Flags={ +ADD_CELL:1,MOD_CELL:2},l=r.prototype;l.stream=function(t){return null==t?this._stream:(this._stream=!!t,this._aggr=[],this)},l.key=function(t){return null==t?this._key:(this._key=a.$(t),this)},l.groupby=function(t){return this._dims=a.array(t).map(function(t,e){if(t=a.isString(t)?{name:t,get:a.$(t)}:a.isFunction(t)?{name:a.name(t)||t.name||"_"+e,get:t}:t.name&&a.isFunction(t.get)?t:null,null==t)throw"Invalid groupby argument: "+t;return t}),this.clear()},l.summarize=function(t){t=i(t),this._count=!0;var e,n,r,s,u,l,c,d=this._aggr=[];for(r=0;r0){for(t.collect&&t.data.values(),e=0;e0?r[s[t]]-=1:c[e++]=s[t];return this._rem=[],this._add=c},s.extent=function(t){if(this._get!==t||!this._ext){var e=this.values(),n=a.extent.index(e,t);this._ext=[e[n[0]],e[n[1]]],this._get=t}return this._ext},s.argmin=function(t){return this.extent(t)[0]},s.argmax=function(t){return this.extent(t)[1]},s.min=function(t){var e=this.extent(t)[0];return null!=e?t(e):+(1/0)},s.max=function(t){var e=this.extent(t)[1];return null!=e?t(e):-(1/0)},s.quartile=function(t){return this._get===t&&this._q||(this._q=a.quartile(this.values(),t),this._get=t),this._q},s.q1=function(t){return this.quartile(t)[0]},s.q2=function(t){return this.quartile(t)[1]},s.q3=function(t){return this.quartile(t)[2]},e.exports=r},{"../stats":27,"../util":30}],10:[function(t,e,n){var r=t("../util"),i=t("./aggregator");e.exports=function(){var t=[].reduce.call(arguments,function(t,e){return t.concat(r.array(e))},[]);return(new i).groupby(t).summarize({"*":"values"})}},{"../util":30,"./aggregator":8}],11:[function(t,e,n){function r(t){return function(e){var n=o.extend({init:"",add:"",rem:"",idx:0},t);return n.out=e||t.name,n}}function i(t,e){function n(t,r){function i(e){t[e]||n(t,t[e]=s[e]())}return r.req&&r.req.forEach(i),e&&r.str&&r.str.forEach(i),t}var r=t.reduce(n,t.reduce(function(t,e){return t[e.name]=e,t},{}));return o.vals(r).sort(function(t,e){return t.idx-e.idx})}function a(e,n,r,a){var s=i(e,n),u="this.cell = cell; this.tuple = t; this.valid = 0; this.missing = 0;",l="if (v==null) this.missing++; if (!this.isValid(v)) return; ++this.valid;",c="if (v==null) this.missing--; if (!this.isValid(v)) return; --this.valid;",d="var t = this.tuple; var cell = this.cell;";return s.forEach(function(t){t.idx<0?(u=t.init+u,l=t.add+l,c=t.rem+c):(u+=t.init,l+=t.add,c+=t.rem)}),e.slice().sort(function(t,e){return t.idx-e.idx}).forEach(function(t){d+="this.assign(t,'"+t.out+"',"+t.set+");"}),d+="return t;",u=Function("cell","t",u),u.prototype.assign=a,u.prototype.add=Function("t","var v = this.get(t);"+l),u.prototype.rem=Function("t","var v = this.get(t);"+c),u.prototype.set=Function(d),u.prototype.get=r,u.prototype.distinct=t("../stats").count.distinct,u.prototype.isValid=o.isValid,u.fields=e.map(o.$("out")),u}var o=t("../util"),s={values:r({name:"values",init:"cell.collect = true;",set:"cell.data.values()",idx:-1}),count:r({name:"count",set:"cell.num"}),missing:r({name:"missing",set:"this.missing"}),valid:r({name:"valid",set:"this.valid"}),sum:r({name:"sum",init:"this.sum = 0;",add:"this.sum += v;",rem:"this.sum -= v;",set:"this.sum"}),mean:r({name:"mean",init:"this.mean = 0;",add:"var d = v - this.mean; this.mean += d / this.valid;",rem:"var d = v - this.mean; this.mean -= this.valid ? d / this.valid : this.mean;",set:"this.mean"}),average:r({name:"average",set:"this.mean",req:["mean"],idx:1}),variance:r({name:"variance",init:"this.dev = 0;",add:"this.dev += d * (v - this.mean);",rem:"this.dev -= d * (v - this.mean);",set:"this.valid > 1 ? this.dev / (this.valid-1) : 0",req:["mean"],idx:1}),variancep:r({name:"variancep",set:"this.valid > 1 ? this.dev / this.valid : 0",req:["variance"],idx:2}),stdev:r({name:"stdev",set:"this.valid > 1 ? Math.sqrt(this.dev / (this.valid-1)) : 0",req:["variance"],idx:2}),stdevp:r({name:"stdevp",set:"this.valid > 1 ? Math.sqrt(this.dev / this.valid) : 0",req:["variance"],idx:2}),stderr:r({name:"stderr",set:"this.valid > 1 ? Math.sqrt(this.dev / (this.valid * (this.valid-1))) : 0",req:["variance"],idx:2}),median:r({name:"median",set:"cell.data.q2(this.get)",req:["values"],idx:3}),q1:r({name:"q1",set:"cell.data.q1(this.get)",req:["values"],idx:3}),q3:r({name:"q3",set:"cell.data.q3(this.get)",req:["values"],idx:3}),distinct:r({name:"distinct",set:"this.distinct(cell.data.values(), this.get)",req:["values"],idx:3}),argmin:r({name:"argmin",add:"if (v < this.min) this.argmin = t;",rem:"if (v <= this.min) this.argmin = null;",set:"this.argmin = this.argmin || cell.data.argmin(this.get)",req:["min"],str:["values"],idx:3}),argmax:r({name:"argmax",add:"if (v > this.max) this.argmax = t;",rem:"if (v >= this.max) this.argmax = null;",set:"this.argmax = this.argmax || cell.data.argmax(this.get)",req:["max"],str:["values"],idx:3}),min:r({name:"min",init:"this.min = +Infinity;",add:"if (v < this.min) this.min = v;",rem:"if (v <= this.min) this.min = NaN;",set:"this.min = (isNaN(this.min) ? cell.data.min(this.get) : this.min)",str:["values"],idx:4}),max:r({name:"max",init:"this.max = -Infinity;",add:"if (v > this.max) this.max = v;",rem:"if (v >= this.max) this.max = NaN;",set:"this.max = (isNaN(this.max) ? cell.data.max(this.get) : this.max)",str:["values"],idx:4}),modeskew:r({name:"modeskew",set:"this.dev===0 ? 0 : (this.mean - cell.data.q2(this.get)) / Math.sqrt(this.dev/(this.valid-1))",req:["mean","variance","median"],idx:5})};s.create=a,e.exports=s},{"../stats":27,"../util":30}],12:[function(t,e,n){function r(t){if(!t)throw Error("Missing binning options.");var e,n,r,s,u,l,c,d=t.maxbins||15,f=t.base||10,h=Math.log(f),p=t.div||[5,2],g=t.min,m=t.max,v=m-g;if(t.step)e=t.step;else if(t.steps)e=t.steps[Math.min(t.steps.length-1,i(t.steps,v/d,0,t.steps.length))];else{for(n=Math.ceil(Math.log(d)/h),r=t.minstep||0,e=Math.max(r,Math.pow(f,Math.round(Math.log(v)/h)-n));Math.ceil(v/e)>d;)e*=f;for(l=0;l=r&&v/u<=d&&(e=u)}return u=Math.log(e),s=u>=0?0:~~(-u/h)+1,c=Math.pow(f,-s-1),g=Math.min(g,Math.floor(g/e+c)*e),m=Math.ceil(m/e)*e,{start:g,stop:m,step:e,unit:{precision:s},value:a,index:o}}function i(t,e,n,r){for(;n>>1;l.cmp(t[i],e)<0?n=i+1:r=i}return n}function a(t){return this.step*Math.floor(t/this.step+d)}function o(t){return Math.floor((t-this.start)/this.step+d)}function s(t){return this.unit.date(a.call(this,t))}function u(t){return o.call(this,this.unit.unit(t))}var l=t("../util"),c=t("../time"),d=1e-15;r.date=function(t){if(!t)throw Error("Missing date binning options.");var e=t.utc?c.utc:c,n=t.min,i=t.max,a=t.maxbins||20,o=t.minbins||4,l=+i-+n,d=t.unit?e[t.unit]:e.find(l,o,a),f=r({min:null!=d.min?d.min:d.unit(n),max:null!=d.max?d.max:d.unit(i),maxbins:a,minstep:d.minstep,steps:d.step});return f.unit=d,f.index=u,t.raw||(f.value=s),f},e.exports=r},{"../time":29,"../util":30}],13:[function(t,e,n){function r(t,e,n){n=o(t,e,n);var r=a(n);return r?f.$func("bin",r.unit.unit?function(t){return r.value(r.unit.unit(t))}:function(t){return r.value(t)})(n.accessor):n.accessor||f.identity}function i(t,e,n){n=o(t,e,n);var r=a(n);return r?s(t,n.accessor,r):u(t,n.accessor,n&&n.sort)}function a(t){var e=t.type,n=null;return(null==e||p[e])&&("integer"===e&&null==t.minstep&&(t.minstep=1),n="date"===e?l.date(t):l(t)),n}function o(){var t=arguments,e=0,n=f.isArray(t[e])?t[e++]:null,r=f.isFunction(t[e])||f.isString(t[e])?f.$(t[e++]):null,i=f.extend({},t[e]);if(n&&(i.type=i.type||d(n,r),p[i.type])){var a=h.extent(n,r);i=f.extend({min:a[0],max:a[1]},i)}return r&&(i.accessor=r),i}function s(t,e,n){for(var r,i,a=c.range(n.start,n.stop+n.step/2,n.step).map(function(t){return{value:n.value(t),count:0}}),o=0;o=a.length||!isFinite(i))continue;a[i].count+=1}return a.bins=n,a}function u(t,e,n){var r=h.unique(t,e),i=h.count.map(t,e);return r.map(function(t){return{value:t,count:i[t]}}).sort(f.comparator(n?"-count":"+value"))}var l=t("./bins"),c=t("../generate"),d=t("../import/type"),f=t("../util"),h=t("../stats"),p={integer:1,number:1,date:1};e.exports={$bin:r,histogram:i}},{"../generate":16,"../import/type":25,"../stats":27,"../util":30,"./bins":12}],14:[function(t,e,n){function r(t,e){e=s.extend({separator:" ",minwidth:8,maxwidth:15},e);var n=e.fields||s.keys(t[0]),r=u.all(t);if(e.start||e.limit){var i=e.start||0,a=e.limit?i+e.limit:t.length;t=t.slice(i,a)}var o=n.map(function(n){var i=d[r[n]]||"",a=c("{{"+n+i+"}}"),o=l.max(t,function(t){return a(t).length});return o=Math.max(Math.min(n.length,e.minwidth),o),e.maxwidth>0?Math.min(o,e.maxwidth):o}),h=n.map(function(t,e){return s.truncate(s.pad(t,o[e],"center"),o[e])}).join(e.separator),p=c(n.map(function(t,e){return"{{"+t+(d[r[t]]||"")+("|pad:"+o[e]+","+(f[r[t]]||"right"))+("|truncate:"+o[e])+"}}"}).join(e.separator));return h+"\n"+t.map(p).join("\n")}function i(t){t=t?t.__summary__?t:l.summary(t):this;var e,n,r=[];for(e=0,n=t.length;e4&&"locale"+(t[0].toUpperCase()+t[1].toLowerCase()+t[3].toUpperCase()+t[4].toLowerCase())}function i(t){var e=x.isString(t)?M[r(t)]:M.locale(t);if(null==e)throw Error("Unrecognized locale: "+t);S=e}function a(t){var e=x.isString(t)?k[r(t)]:k.locale(t);if(null==e)throw Error("Unrecognized locale: "+t);T=e,v=y=_=b=null}function o(t,e){t.length||(t=[0]),null==e&&(e=10);var n=t[0],r=t[t.length-1];r=A?a*=10:o>=L?a*=5:o>=C&&(a*=2),[Math.ceil(n/a)*a,Math.floor(r/a)*a+a/2,a]}function s(t,e){return function(n){var r=t(n),i=r.indexOf(e);if(i<0)return r;for(var a=u(r,i),o=ai;)if("0"!==r[a]){++a;break}return r.slice(0,a)+o}}function u(t,e){var n,r=t.lastIndexOf("e");if(r>0)return r;for(r=t.length;--r>e;)if(n=t.charCodeAt(r),n>=48&&n<=57)return r+1}function l(t){var e=S.format(".1f")(1)[1];switch(null==t&&(t=","),t=M.formatSpecifier(t),null==t.precision&&(t.precision=12),t.type){case"%":t.precision-=2;break;case"e":t.precision-=1}return s(S.format(t),e)}function c(t,e,n){var r=o(t,e);switch(null==n&&(n=",f"),n=M.formatSpecifier(n),n.type){case"s":var i=Math.max(Math.abs(r[0]),Math.abs(r[1]));return null==n.precision&&(n.precision=M.precisionPrefix(r[2],i)),S.formatPrefix(n,i);case"":case"e":case"g":case"p":case"r":null==n.precision&&(n.precision=M.precisionRound(r[2],Math.max(Math.abs(r[0]),Math.abs(r[1])))-("e"===n.type));break;case"f":case"%":null==n.precision&&(n.precision=M.precisionFixed(r[2])-2*("%"===n.type))}return S.format(n)}function d(){var t=T.format,e=t(".%L"),n=t(":%S"),r=t("%I:%M"),i=t("%I %p"),a=t("%a %d"),o=t("%b %d"),s=t("%B"),u=t("%Y");return function(t){var l=+t;return(w.second(t)e;)i.push(r);else for(;(r=t+n*++a)=t&&r<=e?1/n:0},r.cdf=function(r){return re?1:(r-t)/n},r.icdf=function(e){return e>=0&&e<=1?t+e*n:NaN},r},i.random.integer=function(t,e){void 0===e&&(e=t,t=0);var n=e-t,r=function(){return t+Math.floor(n*Math.random())};return r.samples=function(t){return i.zeros(t).map(r)},r.pdf=function(r){return r===Math.floor(r)&&r>=t&&r=e?1:(i-t+1)/n},r.icdf=function(e){return e>=0&&e<=1?t-1+Math.floor(e*n):NaN},r},i.random.normal=function(t,e){t=t||0,e=e||1;var n,r=function(){var r,i,a=0,o=0;if(void 0!==n)return a=n,n=void 0,a;do a=2*Math.random()-1,o=2*Math.random()-1,r=a*a+o*o;while(0===r||r>1);return i=Math.sqrt(-2*Math.log(r)/r),n=t+o*i*e,t+a*i*e};return r.samples=function(t){return i.zeros(t).map(r)},r.pdf=function(n){var r=Math.exp(Math.pow(n-t,2)/(-2*Math.pow(e,2)));return 1/(e*Math.sqrt(2*Math.PI))*r},r.cdf=function(n){var r,i=(n-t)/e,a=Math.abs(i);if(a>37)r=0;else{var o,s=Math.exp(-a*a/2);a<7.07106781186547?(o=.0352624965998911*a+.700383064443688,o=o*a+6.37396220353165,o=o*a+33.912866078383,o=o*a+112.079291497871,o=o*a+221.213596169931,o=o*a+220.206867912376,r=s*o,o=.0883883476483184*a+1.75566716318264,o=o*a+16.064177579207,o=o*a+86.7807322029461,o=o*a+296.564248779674,o=o*a+637.333633378831,o=o*a+793.826512519948,o=o*a+440.413735824752,r/=o):(o=a+.65,o=a+4/o,o=a+3/o,o=a+2/o,o=a+1/o,r=s/o/2.506628274631)}return i>0?1-r:r},r.icdf=function(n){if(n<=0||n>=1)return NaN;var r=2*n-1,i=8*(Math.PI-3)/(3*Math.PI*(4-Math.PI)),a=2/(Math.PI*i)+Math.log(1-Math.pow(r,2))/2,o=Math.log(1-r*r)/i,s=(r>0?1:-1)*Math.sqrt(Math.sqrt(a*a-o)-a);return t+e*Math.SQRT2*s},r},i.random.bootstrap=function(t,e){var n=t.filter(r.isValid),a=n.length,o=e?i.random.normal(0,e):null,s=function(){return n[~~(Math.random()*a)]+(o?o():0)};return s.samples=function(t){return i.zeros(t).map(s)},s}},{"./util":30}],17:[function(t,e,n){function r(t,e){if(t){var n=e.header;t=(n?n.join(e.delimiter)+"\n":"")+t}return a.dsv(e.delimiter).parse(t)}var i=t("../../util"),a=t("d3-dsv");r.delimiter=function(t){var e={delimiter:t};return function(t,n){return r(t,n?i.extend(n,e):e)}},e.exports=r},{"../../util":30,"d3-dsv":3}],18:[function(t,e,n){var r=t("./dsv");e.exports={json:t("./json"),topojson:t("./topojson"),treejson:t("./treejson"),dsv:r,csv:r.delimiter(","),tsv:r.delimiter("\t")}},{"./dsv":17,"./json":19,"./topojson":20,"./treejson":21}],19:[function(t,e,n){var r=t("../../util");e.exports=function(t,e){var n=r.isObject(t)&&!r.isBuffer(t)?t:JSON.parse(t);return e&&e.property&&(n=r.accessor(e.property)(n)),n}},{"../../util":30}],20:[function(t,e,n){var r=t("./json"),i=function(t,e){var n=i.topojson;if(null==n)throw Error("TopoJSON library not loaded.");var a,o=r(t,e);if(e&&e.feature){if(a=o.objects[e.feature])return n.feature(o,a).features;throw Error("Invalid TopoJSON object: "+e.feature)}if(e&&e.mesh){if(a=o.objects[e.mesh])return[n.mesh(o,o.objects[e.mesh])];throw Error("Invalid TopoJSON object: "+e.mesh)}throw Error("Missing TopoJSON feature or mesh parameter.")};i.topojson=t("topojson"),e.exports=i},{"./json":19,topojson:31}],21:[function(t,e,n){function r(t,e){function n(t,e){t[i]=e,a.push(t);var o=t[r];if(o)for(var s=0;s1&&"."===r[e-1]&&r.lastIndexOf(t)===e});if(!u)throw"URL is not whitelisted: "+n}}return n}function i(t,e){return i.loader(t,e)}function a(t,e){var n,r=e||function(t){throw t};try{n=i.sanitizeUrl(t)}catch(t){return void r(t)}return n?i.useXHR?i.xhr(n,t,e):c(n,h)?i.file(n.slice(h.length),t,e):n.indexOf("://")<0?i.file(n,t,e):i.http(n,t,e):void r("Invalid URL: "+t.url)}function o(t){var e=t.responseType;return e&&"text"!==e?t.response:t.responseText}function s(t,e,n){function r(){var t=s.status;!t&&o(s)||t>=200&&t<300||304===t?n(null,s.responseText):n(s,null)}var a=!!n,s=new XMLHttpRequest;if("undefined"==typeof XDomainRequest||"withCredentials"in s||!/^(http(s)?:)?\/\//.test(t)||(s=new XDomainRequest),a&&("onload"in s?s.onload=s.onerror=r:s.onreadystatechange=function(){s.readyState>3&&r()}),s.open("GET",t,a),s.setRequestHeader){var u=d.extend({},i.headers,e.headers);for(var l in u)s.setRequestHeader(l,u[l])}if(s.send(),!a&&o(s))return s.responseText}function u(e,n,r){var i=t("fs");return r?void i.readFile(e,r):i.readFileSync(e,"utf8")}function l(e,n,r){var a=d.extend({},i.headers,n.headers),o={url:e,encoding:null,gzip:!0,headers:a};return r?void t("request")(o,function(t,e,n){t||200!==e.statusCode?(t=t||"Load failed with response code "+e.statusCode+".",r(t,null)):r(null,n)}):t("sync-request")("GET",e,o).getBody()}function c(t,e){return null!=t&&0===t.lastIndexOf(e,0)}var d=t("../util"),f=/^([A-Za-z]+:)?\/\//,h="file://";i.loader=a,i.sanitizeUrl=r,i.xhr=s,i.file=u,i.http=l,i.useXHR="undefined"!=typeof XMLHttpRequest,i.headers={},e.exports=i},{"../util":30,fs:2,request:2,"sync-request":2,url:2}],23:[function(t,e,n){function r(t,e){var n=e&&e.type||"json";return t=s[n](t,e),e&&e.parse&&i(t,e.parse),t}function i(t,e){var n,r,i,s,l,c,d=t.length;for(e="auto"===e?o.inferAll(t):a.duplicate(e),n=a.keys(e),r=n.map(function(t){var n=e[t];if(n&&0===n.indexOf("date:")){var r=n.split(/:(.+)?/,2),i=r[1];if(!("'"===i[0]&&"'"===i[i.length-1]||'"'===i[0]&&'"'===i[i.length-1]))throw Error("Format pattern must be quoted: "+i);return i=i.slice(1,-1),i=u(i),function(t){return i.parse(t)}}if(!o.parsers[n])throw Error("Illegal format pattern: "+t+":"+n);return o.parsers[n]}),s=0,c=n.length;s0?Math.pow(s,1/n):0},l.mean.harmonic=function(t,e){e=o.$(e);var n,r,i,a,s=0;for(a=0,n=0,r=t.length;ar&&(r=i));return[n,r]},l.extent.index=function(t,e){e=o.$(e);var n,r,i,a,s=-1,u=-1,l=t.length;for(a=0;ar&&(r=i,u=a));return[s,u]},l.dot=function(t,e,n){var r,i,a=0;if(n)for(e=o.$(e),n=o.$(n),r=0;r-1&&c!==r){for(i=1+(n-1+l)/2;l-1)for(i=1+(s-1+l)/2;lg)&&(g=u),n=u-c,c+=n/++d,m+=n*(u-c),v.push(u));return m/=d-1,r=Math.sqrt(m),v.sort(o.cmp),{type:s(t,e),unique:y,count:t.length,valid:d,missing:f,distinct:h,min:p,max:g,mean:c,stdev:r,median:a=l.quantile(v,.5),q1:l.quantile(v,.25),q3:l.quantile(v,.75),modeskew:0===r?0:(c-a)/r}},l.summary=function(t,e){e=e||o.keys(t[0]);var n=e.map(function(e){var n=l.profile(t,o.$(e));return n.field=e,n});return n.__summary__=!0,n}},{"./generate":16,"./import/type":25,"./util":30}],28:[function(t,e,n){function r(t){var e=i(t,"d");return e="var __t; return "+e+";",new Function("d",e).bind(d)}function i(t,e,n){e=e||"obj";var r=0,i="'",s=f;return t.replace(s,function(s,u,l){return i+=t.slice(r,l).replace(m,o),r=l+s.length,u&&(i+="'\n+((__t=("+a(u,e,n)+"))==null?'':__t)+\n'"),s}),i+"'"}function a(t,e,n){function i(t){return t=t||"",d?(d=!1,f="String("+f+")"+t):f+=t,f}function a(){return"(typeof "+f+'==="number"?new Date('+f+"):"+f+")"}function o(t){var e=b[0];if(!("'"===e[0]&&"'"===e[e.length-1]||'"'===e[0]&&'"'===e[e.length-1]))throw Error("Format pattern must be quoted: "+e);e=e.slice(1,-1),v=s(e,t),d=!1;var n="number"===t?f:a();f="this.formats["+v+"]("+n+")"}var u=t.match(h),c=u.shift().trim(),d=!0;n&&(n[c]=1);for(var f=r.property(e,c),g=0;g0&&(_=_.slice(0,m),b=u[g].slice(m+1).match(p).map(function(t){return t.trim()})),_=_.trim()){case"length":i(".length");break;case"lower":i(".toLowerCase()");break;case"upper":i(".toUpperCase()");break;case"lower-locale":i(".toLocaleLowerCase()");break;case"upper-locale":i(".toLocaleUpperCase()");break;case"trim":i(".trim()");break;case"left":v=l.number(b[0]),i(".slice(0,"+v+")");break;case"right":v=l.number(b[0]),i(".slice(-"+v+")");break;case"mid":v=l.number(b[0]),y=v+l.number(b[1]),i(".slice(+"+v+","+y+")");break;case"slice":v=l.number(b[0]),i(".slice("+v+(b.length>1?","+l.number(b[1]):"")+")");break;case"truncate":v=l.number(b[0]),y=b[1],y="left"!==y&&"middle"!==y&&"center"!==y?"right":y,f="this.truncate("+i()+","+v+",'"+y+"')";break;case"pad":v=l.number(b[0]),y=b[1],y="left"!==y&&"middle"!==y&&"center"!==y?"right":y,f="this.pad("+i()+","+v+",'"+y+"')";break;case"number":o("number");break;case"time":o("time");break;case"time-utc":o("utc");break;case"month":f="this.month("+f+")";break;case"month-abbrev":f="this.month("+f+",true)";break;case"day":f="this.day("+f+")";break;case"day-abbrev":f="this.day("+f+",true)";break;case"quarter":f="this.quarter("+f+")";break;case"quarter-utc":f="this.utcQuarter("+f+")";break;default:throw Error("Unrecognized template filter: "+_)}}return f}function o(t){return"\\"+g[t]}function s(t,e){var n=e+":"+t;if(null==d.format_map[n]){var r=c[e](t),i=d.formats.length;return d.formats.push(r),d.format_map[n]=i,i}return d.format_map[n]}function u(t,e){return d.formats[s(t,e)]}var l=t("./util"),c=t("./format"),d={formats:[],format_map:{},truncate:l.truncate,pad:l.pad,day:c.day,month:c.month,quarter:c.quarter,utcQuarter:c.utcQuarter};r.source=i,r.context=d,r.format=u,e.exports=r,r.clearFormatCache=function(){d.formats=[],d.format_map={}},r.property=function(t,e){var n=l.field(e).map(l.str).join("][");return t+"["+n+"]"};var f=/\{\{(.+?)\}\}|$/g,h=/(?:"[^"]*"|\'[^\']*\'|[^\|"]+|[^\|\']+)+/g,p=/(?:"[^"]*"|\'[^\']*\'|[^,"]+|[^,\']+)+/g,g={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},m=/\\|'|\r|\n|\u2028|\u2029/g},{"./format":15,"./util":30}],29:[function(t,e,n){function r(t){return l.setTime(+t),l}function i(t,e,n,r,i,a){var o={type:t,date:e,unit:n};return r?o.step=r:o.minstep=1,null!=i&&(o.min=i),null!=a&&(o.max=a),o}function a(t,e,n,r,a,o){return i(t,function(t){return e.offset(n,t)},function(t){return e.count(n,t)},r,a,o)}function o(t,e,n,r){var i,a,o,s=p[0];for(i=1,a=p.length;is[0]){if(o=e/s[0],o>r)return t[p[i-1][1]];if(o>=n)return t[s[1]]}return t[p[a-1][1]]}function s(t){var e,n,r={};for(e=0,n=t.length;e1?function(t,n){for(var r=0;re||null==e)&&null!=t?1:(e=e instanceof Date?+e:e,(t=t instanceof Date?+t:t)!==t&&e===e?-1:e!==e&&t===t?1:0)},i.numcmp=function(t,e){return t-e},i.stablesort=function(t,e,n){var r=t.reduce(function(t,e,r){return t[n(e)]=r,t},{});return t.sort(function(t,i){var a=e(t),o=e(i);return ao?1:r[n(t)]-r[n(i)]}),t},i.permute=function(t){for(var e,n,r=t.length;r;)n=Math.floor(Math.random()*r--),e=t[r],t[r]=t[n],t[n]=e},i.pad=function(t,e,r,i){i=i||" ";var a=e-t.length;if(a<=0)return t;switch(r){case"left":return n(a,i)+t;case"middle":case"center":return n(Math.floor(a/2),i)+t+n(Math.ceil(a/2),i);default:return t+n(a,i)}},i.truncate=function(t,e,n,i,a){var o=t.length;if(o<=e)return t;a=void 0!==a?String(a):"…";var s=Math.max(0,e-a.length);switch(n){case"left":return a+(i?r(t,s,1):t.slice(o-s));case"middle":case"center":var u=Math.ceil(s/2),l=Math.floor(s/2);return(i?r(t,u):t.slice(0,u))+a+(i?r(t,l,1):t.slice(o-l));default:return(i?r(t,s):t.slice(0,s))+a}};var u=/([\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF])/}).call(this,t("buffer").Buffer)},{buffer:2}],31:[function(e,n,r){!function(e,i){"object"==typeof r&&"undefined"!=typeof n?i(r):"function"==typeof t&&t.amd?t(["exports"],i):i(e.topojson=e.topojson||{})}(this,function(t){"use strict";function e(){}function n(t){if(!t)return e;var n,r,i=t.scale[0],a=t.scale[1],o=t.translate[0],s=t.translate[1];return function(t,e){e||(n=r=0),t[0]=(n+=t[0])*i+o,t[1]=(r+=t[1])*a+s}}function r(t){if(!t)return e;var n,r,i=t.scale[0],a=t.scale[1],o=t.translate[0],s=t.translate[1];return function(t,e){e||(n=r=0);var u=Math.round((t[0]-o)/i),l=Math.round((t[1]-s)/a);t[0]=u-n,t[1]=l-r,n=u,r=l}}function i(t,e){for(var n,r=t.length,i=r-e;i<--r;)n=t[i],t[i++]=t[r],t[r]=n}function a(t,e){for(var n=0,r=t.length;n>>1;t[i]1){var u,c=[],d={LineString:i,MultiLineString:a,Polygon:a,MultiPolygon:function(t){t.forEach(a)}};o(e),c.forEach(arguments.length<3?function(t){s.push(t[0].i)}:function(t){n(t[0].g,t[t.length-1].g)&&s.push(t[0].i)})}else for(var f=0,h=t.arcs.length;f1)for(var o,s,u=1,c=r(a[0]);uc&&(s=a[0],a[0]=a[u],a[u]=s,c=o);return a})}}function m(t){function e(t,e){t.forEach(function(t){t<0&&(t=~t);var n=i[t];n?n.push(e):i[t]=[e]})}function n(t,n){t.forEach(function(t){e(t,n)})}function r(t,e){"GeometryCollection"===t.type?t.geometries.forEach(function(t){r(t,e)}):t.type in s&&s[t.type](t.arcs,e)}var i={},o=t.map(function(){return[]}),s={LineString:e,MultiLineString:n,Polygon:n,MultiPolygon:function(t,e){t.forEach(function(t){n(t,e)})}};t.forEach(r);for(var u in i)for(var l=i[u],c=l.length,d=0;d0;){var n=(e+1>>1)-1,i=r[n];if(v(t,i)>=0)break;r[i._=e]=i,r[t._=e=n]=t}}function e(t,e){for(;;){var n=e+1<<1,a=n-1,o=e,s=r[o];if(a0&&(t=r[i],e(r[t._=0]=t,0)),n}},n.remove=function(n){var a,o=n._;if(r[o]===n)return o!==--i&&(a=r[i],(v(a,n)<0?t:e)(r[a._=o]=a,o)),o},n}function _(t,e){function i(t){s.remove(t),t[1][2]=e(t),s.push(t)}var a=n(t.transform),o=r(t.transform),s=y();return e||(e=f),t.arcs.forEach(function(t){var n,r,u,l,c=[],d=0;for(r=0,u=t.length;r1)for(var n=1,r=t.length;n0;)if(e=m.peek(),p=e instanceof d,t=g[e._id],e.rank()!==e.qrank())m.replace(e.qrank(!0));else if(m.pop(),g[e._id]=null,i=e._listeners,(!p||p&&!r)&&(t=this.evaluate(t,e)),t!==this.doNotPropagate)for(!t.reflow&&e.reflows()&&(t=s.create(t,!0)),f=0,h=i.length;f0&&t[o-1].addListener(n)}return t},h.disconnect=function(t){var e,n,r,i,a,o,s,u;for(a=0,o=t.length;a=t.stamp,r=e.router()||t.add.length||t.rem.length;return r||!n||e.reevaluate(t)},h.evaluate=function(t,e){return this.reevaluate(t,e)?(t=e.evaluate(t),e.last(t.stamp),t):t},e.exports=r},{"./ChangeSet":32,"./Collector":33,"./DataSource":34,"./Dependencies":35,"./Heap":37,"./Signal":39,"./Tuple":40,datalib:26,"vega-logging":48}],37:[function(t,e,n){function r(t){this.cmp=t,this.nodes=[]}function i(t,e,n,r){var i,a,o;for(i=t[n];n>e&&(o=n-1>>1,a=t[o],r(i,a)<0);)t[n]=a,n=o;return t[n]=i}function a(t,e,n){for(var r,a=e,o=t.length,s=t[e],u=2*e+1;u=0&&(u=r),t[e]=t[u],e=u,u=2*e+1;return t[e]=s,i(t,a,e,n)}var o=r.prototype;o.size=function(){return this.nodes.length},o.clear=function(){return this.nodes=[],this},o.peek=function(){return this.nodes[0]},o.push=function(t){var e=this.nodes;return e.push(t),i(e,0,e.length-1,this.cmp)},o.pop=function(){var t,e=this.nodes,n=e.pop();return e.length?(t=e[0],e[0]=n,a(e,0,this.cmp)):t=n,t},o.replace=function(t){var e=this.nodes,n=e[0];return e[0]=t,a(e,0,this.cmp),n},o.pushpop=function(t){var e=this.nodes,n=e[0];return e.length&&this.cmp(n,t)<0&&(e[0]=t,t=n,a(e,0,this.cmp)),t},e.exports=r},{}],38:[function(t,e,n){function r(t){t&&this.init(t)}var i=t("./Dependencies").ALL,a=0,o=r.Flags={Router:1,Collector:2,Produces:4,Mutates:8,Reflows:16,Batch:32},s=r.prototype;s.init=function(t){this._id=++a,this._graph=t,this._rank=t.rank(),this._qrank=null,this._stamp=0,this._listeners=[],this._listeners._ids={},this._deps={};for(var e=0,n=i.length;et._rank&&t.rerank(),this)},s.removeListener=function(t){if(!this._listeners._ids[t._id])return!1;var e=this._listeners.indexOf(t),n=e>=0;return n&&(this._listeners.splice(e,1),this._listeners._ids[t._id]=null),n},s.disconnect=function(){this._listeners=[],this._listeners._ids={}},s.evaluate=function(t){return t},s.reevaluate=function(t){var e,n,r,a,o,s;for(r=0,a=i.length;r=0;)t&&r[e].handler!==t||(n=r.splice(e,1)[0],this.removeListener(n.node));return this},e.exports=r},{"./ChangeSet":32,"./Node":38}],40:[function(t,e,n){function r(t){return t=t===Object(t)?t:{data:t},t._id=++o,t._prev&&(t._prev=null),t}function i(t,e){e=e||{};for(var n=0,r=t.length;n0;)i(arguments[n],e);return t.filter(function(t){return!e[t._id]})}}},{}],41:[function(t,e,n){e.exports={ChangeSet:t("./ChangeSet"),Collector:t("./Collector"),DataSource:t("./DataSource"),Dependencies:t("./Dependencies"),Graph:t("./Graph"),Node:t("./Node"),Signal:t("./Signal"),Tuple:t("./Tuple"),debug:t("vega-logging").debug}},{"./ChangeSet":32,"./Collector":33,"./DataSource":34,"./Dependencies":35,"./Graph":36,"./Node":38,"./Signal":39,"./Tuple":40,"vega-logging":48}],42:[function(t,e,n){e.exports=function(){"use strict";function t(t,e){function n(){this.constructor=t}n.prototype=e.prototype,t.prototype=new n}function e(t,n,r,i){this.message=t,this.expected=n,this.found=r,this.location=i,this.name="SyntaxError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,e)}function n(t){function n(e){var n,r,i=pe[e];if(i)return i;for(n=e-1;!pe[n];)n--;for(i=pe[n],i={line:i.line,column:i.column,seenCR:i.seenCR};nge&&(ge=fe,me=[]),me.push(t))}function a(t,n,r,i){function a(t){var e=1;for(t.sort(function(t,e){return t.descriptione.description?1:0});e1?o.slice(0,-1).join(", ")+" or "+o[t.length-1]:o[0],i=e?'"'+n(e)+'"':"end of input","Expected "+r+" but "+i+" found."}return null!==n&&a(n),new e(null!==t?t:o(n,r),n,r,i)}function o(){var t;return t=s()}function s(){var e,n,r,a,o,l;return e=fe,n=u(),n!==b?(r=v(),r!==b?(44===t.charCodeAt(fe)?(a=k,fe++):(a=b,0===ve&&i(M)),a!==b?(o=v(),o!==b?(l=s(),l!==b?(he=e,n=S(n,l),e=n):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b),e===b&&(e=fe,n=u(),n!==b&&(he=e,n=T(n)),e=n),e}function u(){var e,n,r,a,o,s,c,d,f,h,p,g,m,y;return e=fe,91===t.charCodeAt(fe)?(n=E,fe++):(n=b,0===ve&&i(A)),n!==b?(r=v(),r!==b?(a=l(),a!==b?(o=v(),o!==b?(44===t.charCodeAt(fe)?(s=k,fe++):(s=b,0===ve&&i(M)),s!==b?(c=v(),c!==b?(d=l(),d!==b?(f=v(),f!==b?(93===t.charCodeAt(fe)?(h=L,fe++):(h=b,0===ve&&i(C)),h!==b?(p=v(),p!==b?(62===t.charCodeAt(fe)?(g=D,fe++):(g=b,0===ve&&i(P)),g!==b?(m=v(),m!==b?(y=u(),y!==b?(he=e,n=I(a,d,y),e=n):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b),e===b&&(e=l()),e}function l(){var t,e,n,r;if(t=fe,e=c(),e!==b){if(n=[],r=h(),r!==b)for(;r!==b;)n.push(r),r=h();else n=b;n!==b?(he=t,e=N(e,n),t=e):(fe=t,t=b)}else fe=t,t=b;return t===b&&(t=fe,e=c(),e!==b&&(he=t,e=O(e)),t=e),t}function c(){var e,n,r,a,o;return e=fe,40===t.charCodeAt(fe)?(n=z,fe++):(n=b,0===ve&&i(j)),n!==b?(r=s(),r!==b?(41===t.charCodeAt(fe)?(a=F,fe++):(a=b,0===ve&&i(U)),a!==b?(he=e,n=R(r),e=n):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b),e===b&&(e=fe,64===t.charCodeAt(fe)?(n=q,fe++):(n=b,0===ve&&i(B)),n!==b?(r=p(),r!==b?(58===t.charCodeAt(fe)?(a=G,fe++):(a=b,0===ve&&i($)),a!==b?(o=f(),o!==b?(he=e,n=H(r,o),e=n):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b),e===b&&(e=fe,n=d(),n!==b?(58===t.charCodeAt(fe)?(r=G,fe++):(r=b,0===ve&&i($)),r!==b?(a=f(),a!==b?(he=e,n=Y(n,a),e=n):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b),e===b&&(e=fe,n=g(),n!==b?(58===t.charCodeAt(fe)?(r=G,fe++):(r=b,0===ve&&i($)),r!==b?(a=f(),a!==b?(he=e,n=V(n,a),e=n):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b),e===b&&(e=fe,n=f(),n!==b&&(he=e,n=W(n)),e=n,e===b&&(e=fe,n=p(),n!==b&&(he=e,n=X(n)),e=n))))),e}function d(){var e;return t.substr(fe,4)===J?(e=J,fe+=4):(e=b,0===ve&&i(K)),e===b&&(t.substr(fe,6)===Z?(e=Z,fe+=6):(e=b,0===ve&&i(Q)),e===b&&(t.substr(fe,4)===tt?(e=tt,fe+=4):(e=b,0===ve&&i(et)),e===b&&(t.substr(fe,3)===nt?(e=nt,fe+=3):(e=b,0===ve&&i(rt)),e===b&&(t.substr(fe,4)===it?(e=it,fe+=4):(e=b,0===ve&&i(at)),e===b&&(t.substr(fe,4)===ot?(e=ot,fe+=4):(e=b,0===ve&&i(st)),e===b&&(t.substr(fe,4)===ut?(e=ut,fe+=4):(e=b,0===ve&&i(lt)),e===b&&(t.substr(fe,5)===ct?(e=ct,fe+=5):(e=b,0===ve&&i(dt)),e===b&&(t.substr(fe,4)===ft?(e=ft,fe+=4):(e=b,0===ve&&i(ht)),e===b&&(t.substr(fe,5)===pt?(e=pt,fe+=5):(e=b,0===ve&&i(gt))))))))))),e}function f(){var e;return t.substr(fe,9)===mt?(e=mt,fe+=9):(e=b,0===ve&&i(vt)),e===b&&(t.substr(fe,7)===yt?(e=yt,fe+=7):(e=b,0===ve&&i(_t)),e===b&&(t.substr(fe,5)===bt?(e=bt,fe+=5):(e=b,0===ve&&i(xt)),e===b&&(t.substr(fe,8)===wt?(e=wt,fe+=8):(e=b,0===ve&&i(kt)),e===b&&(t.substr(fe,5)===Mt?(e=Mt,fe+=5):(e=b,0===ve&&i(St)),e===b&&(t.substr(fe,7)===Tt?(e=Tt,fe+=7):(e=b,0===ve&&i(Et)),e===b&&(t.substr(fe,8)===At?(e=At,fe+=8):(e=b,0===ve&&i(Lt)),e===b&&(t.substr(fe,5)===Ct?(e=Ct,fe+=5):(e=b,0===ve&&i(Dt)),e===b&&(t.substr(fe,10)===Pt?(e=Pt,fe+=10):(e=b,0===ve&&i(It)),e===b&&(t.substr(fe,9)===Nt?(e=Nt,fe+=9):(e=b,0===ve&&i(Ot)),e===b&&(t.substr(fe,8)===zt?(e=zt,fe+=8):(e=b,0===ve&&i(jt)),e===b&&(t.substr(fe,9)===Ft?(e=Ft,fe+=9):(e=b,0===ve&&i(Ut)),e===b&&(t.substr(fe,10)===Rt?(e=Rt,fe+=10):(e=b,0===ve&&i(qt)),e===b&&(t.substr(fe,10)===Bt?(e=Bt,fe+=10):(e=b,0===ve&&i(Gt)),e===b&&(t.substr(fe,9)===$t?(e=$t,fe+=9):(e=b,0===ve&&i(Ht)),e===b&&(t.substr(fe,8)===Yt?(e=Yt,fe+=8):(e=b,0===ve&&i(Vt)),e===b&&(t.substr(fe,9)===Wt?(e=Wt,fe+=9):(e=b,0===ve&&i(Xt)),e===b&&(t.substr(fe,8)===Jt?(e=Jt,fe+=8):(e=b,0===ve&&i(Kt)),e===b&&(t.substr(fe,9)===Zt?(e=Zt,fe+=9):(e=b,0===ve&&i(Qt)))))))))))))))))))),e}function h(){var e,n,r,a;return e=fe,91===t.charCodeAt(fe)?(n=E,fe++):(n=b,0===ve&&i(A)),n!==b?(r=m(),r!==b?(93===t.charCodeAt(fe)?(a=L,fe++):(a=b,0===ve&&i(C)),a!==b?(he=e,n=te(r),e=n):(fe=e,e=b)):(fe=e,e=b)):(fe=e,e=b),e}function p(){var e,n,r;if(e=fe,n=[],ee.test(t.charAt(fe))?(r=t.charAt(fe),fe++):(r=b,0===ve&&i(ne)),r!==b)for(;r!==b;)n.push(r),ee.test(t.charAt(fe))?(r=t.charAt(fe),fe++):(r=b,0===ve&&i(ne));else n=b;return n!==b&&(he=e,n=re(n)),e=n}function g(){var e,n,r;if(e=fe,n=[],ie.test(t.charAt(fe))?(r=t.charAt(fe),fe++):(r=b,0===ve&&i(ae)),r!==b)for(;r!==b;)n.push(r),ie.test(t.charAt(fe))?(r=t.charAt(fe),fe++):(r=b,0===ve&&i(ae));else n=b;return n!==b&&(he=e,n=oe(n)),e=n}function m(){var e,n,r;if(e=fe,n=[],se.test(t.charAt(fe))?(r=t.charAt(fe),fe++):(r=b,0===ve&&i(ue)),r!==b)for(;r!==b;)n.push(r),se.test(t.charAt(fe))?(r=t.charAt(fe),fe++):(r=b,0===ve&&i(ue));else n=b;return n!==b&&(he=e,n=le(n)),e=n}function v(){var e,n;for(e=[],ce.test(t.charAt(fe))?(n=t.charAt(fe),fe++):(n=b,0===ve&&i(de));n!==b;)e.push(n),ce.test(t.charAt(fe))?(n=t.charAt(fe),fe++):(n=b,0===ve&&i(de));return e}var y,_=arguments.length>1?arguments[1]:{},b={},x={start:o},w=o,k=",",M={type:"literal",value:",",description:'","'},S=function(t,e){return[t].concat(e)},T=function(t){return[t]},E="[",A={type:"literal",value:"[",description:'"["'},L="]",C={type:"literal",value:"]",description:'"]"'},D=">",P={type:"literal",value:">",description:'">"'},I=function(t,e,n){return{start:t,middle:n,end:e,str:"["+t.str+", "+e.str+"] > "+n.str}},N=function(t,e){return t.filters=e,t.str+=e.map(function(t){return"["+t+"]"}).join(""),t},O=function(t){return t},z="(",j={type:"literal",value:"(",description:'"("'},F=")",U={type:"literal",value:")",description:'")"'},R=function(t){return{stream:t,str:"("+t.map(function(t){return t.str}).join(", ")+")"}},q="@",B={type:"literal",value:"@",description:'"@"'},G=":",$={type:"literal",value:":",description:'":"'},H=function(t,e){return{event:e,name:t,str:"@"+t+":"+e}},Y=function(t,e){return{event:e,mark:t,str:t+":"+e}},V=function(t,e){return{event:e,target:t,str:t+":"+e}},W=function(t){return{event:t,str:t}},X=function(t){return{signal:t,str:t}},J="rect",K={type:"literal",value:"rect",description:'"rect"'},Z="symbol",Q={type:"literal",value:"symbol",description:'"symbol"'},tt="path",et={type:"literal",value:"path",description:'"path"'},nt="arc",rt={type:"literal",value:"arc",description:'"arc"'},it="area",at={type:"literal",value:"area",description:'"area"'},ot="line",st={type:"literal",value:"line",description:'"line"'},ut="rule",lt={type:"literal",value:"rule",description:'"rule"'},ct="image",dt={type:"literal",value:"image",description:'"image"'},ft="text",ht={type:"literal",value:"text",description:'"text"'},pt="group",gt={type:"literal",value:"group",description:'"group"'},mt="mousedown",vt={type:"literal",value:"mousedown",description:'"mousedown"'},yt="mouseup",_t={type:"literal",value:"mouseup",description:'"mouseup"'},bt="click",xt={type:"literal",value:"click",description:'"click"'},wt="dblclick",kt={type:"literal",value:"dblclick",description:'"dblclick"'},Mt="wheel",St={type:"literal",value:"wheel",description:'"wheel"'},Tt="keydown",Et={type:"literal",value:"keydown",description:'"keydown"'},At="keypress",Lt={type:"literal",value:"keypress",description:'"keypress"'},Ct="keyup",Dt={type:"literal",value:"keyup",description:'"keyup"'},Pt="mousewheel",It={type:"literal",value:"mousewheel",description:'"mousewheel"'},Nt="mousemove",Ot={type:"literal",value:"mousemove",description:'"mousemove"'},zt="mouseout",jt={type:"literal",value:"mouseout",description:'"mouseout"'},Ft="mouseover",Ut={type:"literal",value:"mouseover",description:'"mouseover"'},Rt="mouseenter",qt={type:"literal",value:"mouseenter",description:'"mouseenter"'},Bt="touchstart",Gt={type:"literal",value:"touchstart",description:'"touchstart"'},$t="touchmove",Ht={type:"literal",value:"touchmove",description:'"touchmove"'},Yt="touchend",Vt={type:"literal",value:"touchend",description:'"touchend"'},Wt="dragenter",Xt={type:"literal",value:"dragenter",description:'"dragenter"'},Jt="dragover",Kt={type:"literal",value:"dragover",description:'"dragover"'},Zt="dragleave",Qt={type:"literal",value:"dragleave",description:'"dragleave"'},te=function(t){return t},ee=/^[a-zA-Z0-9_\-]/,ne={type:"class",value:"[a-zA-Z0-9_-]",description:"[a-zA-Z0-9_-]"},re=function(t){return t.join("")},ie=/^[a-zA-Z0-9\-_ #.>+~[\]=|\^$*]/,ae={type:"class",value:"[a-zA-Z0-9-_ #\\.\\>\\+~\\[\\]=|\\^\\$\\*]",description:"[a-zA-Z0-9-_ #\\.\\>\\+~\\[\\]=|\\^\\$\\*]"},oe=function(t){return t.join("")},se=/^['"a-zA-Z0-9_().><=! \t-&|~]/,ue={type:"class",value:"['\"a-zA-Z0-9_\\(\\)\\.\\>\\<\\=\\! \\t-&|~]",description:"['\"a-zA-Z0-9_\\(\\)\\.\\>\\<\\=\\! \\t-&|~]"},le=function(t){return t.join("")},ce=/^[ \t\r\n]/,de={type:"class",value:"[ \\t\\r\\n]",description:"[ \\t\\r\\n]"},fe=0,he=0,pe=[{line:1,column:1,seenCR:!1}],ge=0,me=[],ve=0;if("startRule"in _){if(!(_.startRule in x))throw new Error("Can't start parsing from rule \""+_.startRule+'".');w=x[_.startRule]}if(y=w(),y!==b&&fe===t.length)return y;throw y!==b&&fe0)return e;if(o.hasOwnProperty(e))return o[e];if(l)return l.hasOwnProperty(e)?e:(p[e]=1,v(e));if(c&&c.hasOwnProperty(e))throw new Error("Illegal identifier: "+e);return e},Program:function(t){return t.body.map(a).join("\n")},MemberExpression:function(t){var e=!t.computed,n=a(t.object);e&&(d+=1);var r=a(t.property);return n===f&&(g[r]=1),e&&(d-=1),n+(e?"."+r:"["+r+"]")},CallExpression:function(t){if("Identifier"!==t.callee.type)throw new Error("Illegal callee type: "+t.callee.type);var e=t.callee.name,n=t.arguments,r=s.hasOwnProperty(e)&&s[e];if(!r)throw new Error("Unrecognized function: "+e);return r instanceof Function?r(n,p,g,m):r+"("+n.map(a).join(",")+")"},ArrayExpression:function(t){return"["+t.elements.map(a).join(",")+"]"},BinaryExpression:function(t){return"("+a(t.left)+t.operator+a(t.right)+")"},UnaryExpression:function(t){return"("+t.operator+a(t.argument)+")"},ConditionalExpression:function(t){return"("+a(t.test)+"?"+a(t.consequent)+":"+a(t.alternate)+")"},LogicalExpression:function(t){return"("+a(t.left)+t.operator+a(t.right)+")"},ObjectExpression:function(t){return"{"+t.properties.map(a).join(",")+"}"},Property:function(t){d+=1;var e=a(t.key);return d-=1,e+":"+a(t.value)},ExpressionStatement:function(t){return a(t.expression)}};return n.functions=s,n.functionDefs=u,n.constants=o,n}},{"./constants":44,"./functions":45}],44:[function(t,e,n){e.exports={NaN:"NaN",E:"Math.E",LN2:"Math.LN2",LN10:"Math.LN10",LOG2E:"Math.LOG2E",LOG10E:"Math.LOG10E",PI:"Math.PI",SQRT1_2:"Math.SQRT1_2",SQRT2:"Math.SQRT2"}},{}],45:[function(t,e,n){e.exports=function(t){function e(e,n,r,i){var a=t(n[0]);return r&&(a=r+"("+a+")",0===r.lastIndexOf("new ",0)&&(a="("+a+")")),a+"."+e+(i<0?"":0===i?"()":"("+n.slice(1).map(t).join(",")+")")}function n(t,n,r){return function(i){return e(t,i,n,r)}}var r="new Date",i="String",a="RegExp";return{isNaN:"isNaN",isFinite:"isFinite",abs:"Math.abs",acos:"Math.acos",asin:"Math.asin",atan:"Math.atan",atan2:"Math.atan2",ceil:"Math.ceil",cos:"Math.cos",exp:"Math.exp",floor:"Math.floor",log:"Math.log",max:"Math.max",min:"Math.min",pow:"Math.pow",random:"Math.random",round:"Math.round",sin:"Math.sin",sqrt:"Math.sqrt",tan:"Math.tan",clamp:function(e){if(e.length<3)throw new Error("Missing arguments to clamp function.");if(e.length>3)throw new Error("Too many arguments to clamp function.");var n=e.map(t);return"Math.max("+n[1]+", Math.min("+n[2]+","+n[0]+"))"},now:"Date.now",utc:"Date.UTC",datetime:r,date:n("getDate",r,0),day:n("getDay",r,0),year:n("getFullYear",r,0),month:n("getMonth",r,0),hours:n("getHours",r,0),minutes:n("getMinutes",r,0),seconds:n("getSeconds",r,0),milliseconds:n("getMilliseconds",r,0),time:n("getTime",r,0),timezoneoffset:n("getTimezoneOffset",r,0),utcdate:n("getUTCDate",r,0),utcday:n("getUTCDay",r,0),utcyear:n("getUTCFullYear",r,0),utcmonth:n("getUTCMonth",r,0),utchours:n("getUTCHours",r,0),utcminutes:n("getUTCMinutes",r,0),utcseconds:n("getUTCSeconds",r,0),utcmilliseconds:n("getUTCMilliseconds",r,0),length:n("length",null,-1),indexof:n("indexOf",null),lastindexof:n("lastIndexOf",null),parseFloat:"parseFloat",parseInt:"parseInt",upper:n("toUpperCase",i,0),lower:n("toLowerCase",i,0),slice:n("slice",i),substring:n("substring",i),replace:n("replace",i),regexp:a,test:n("test",a),if:function(e){if(e.length<3)throw new Error("Missing arguments to if function.");if(e.length>3)throw new Error("Too many arguments to if function.");var n=e.map(t);return n[0]+"?"+n[1]+":"+n[2]}}}},{}],46:[function(t,e,n){var r=t("./parser"),i=t("./codegen"),a=e.exports={parse:function(t,e){return r.parse("("+t+")",e)},code:function(t){return i(t)},compiler:function(t,e){t=t.slice();var n=i(e),r=t.length,o=function(e){var i=n(a.parse(e));t[r]='"use strict"; return ('+i.code+");";var o=Function.apply(null,t);return i.fn=t.length>8?function(){return o.apply(i,arguments)}:function(t,e,n,r,a,s,u){return o.call(i,t,e,n,r,a,s,u)},i};return o.codegen=n,o},functions:t("./functions"),constants:t("./constants")}},{"./codegen":43,"./constants":44,"./functions":45,"./parser":47}],47:[function(t,e,n){e.exports=function(){"use strict";function t(t,e){if(!t)throw new Error("ASSERT: "+e)}function e(t){return t>=48&&t<=57}function n(t){return"0123456789abcdefABCDEF".indexOf(t)>=0}function r(t){return"01234567".indexOf(t)>=0}function i(t){return 32===t||9===t||11===t||12===t||160===t||t>=5760&&[5760,6158,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8239,8287,12288,65279].indexOf(t)>=0}function a(t){return 10===t||13===t||8232===t||8233===t}function o(t){return 36===t||95===t||t>=65&&t<=90||t>=97&&t<=122||92===t||t>=128&&Mt.NonAsciiIdentifierStart.test(String.fromCharCode(t))}function s(t){return 36===t||95===t||t>=65&&t<=90||t>=97&&t<=122||t>=48&&t<=57||92===t||t>=128&&Mt.NonAsciiIdentifierPart.test(String.fromCharCode(t))}function u(t){switch(t){case"class":case"enum":case"export":case"extends":case"import":case"super":return!0;default:return!1}}function l(t){switch(t){case"implements":case"interface":case"package":case"private":case"protected":case"public":case"static":case"yield":case"let":return!0;default:return!1}}function c(t){if(Tt&&l(t))return!0;switch(t.length){case 2:return"if"===t||"in"===t||"do"===t;case 3:return"var"===t||"for"===t||"new"===t||"try"===t||"let"===t;case 4:return"this"===t||"else"===t||"case"===t||"void"===t||"with"===t||"enum"===t;case 5:return"while"===t||"break"===t||"catch"===t||"throw"===t||"const"===t||"yield"===t||"class"===t||"super"===t;case 6:return"return"===t||"typeof"===t||"delete"===t||"switch"===t||"export"===t||"import"===t;case 7:return"default"===t||"finally"===t||"extends"===t;case 8:return"function"===t||"continue"===t||"debugger"===t;case 10:return"instanceof"===t;default:return!1}}function d(){var t,e;for(e=0===Et;Et1114111||"}"!==t)&&U({},kt.UnexpectedToken,"ILLEGAL"),e<=65535?String.fromCharCode(e):(r=(e-65536>>10)+55296,i=(e-65536&1023)+56320,String.fromCharCode(r,i))}function p(){var t,e;for(t=St.charCodeAt(Et++),e=String.fromCharCode(t),92===t&&(117!==St.charCodeAt(Et)&&U({},kt.UnexpectedToken,"ILLEGAL"),++Et,t=f("u"),t&&"\\"!==t&&o(t.charCodeAt(0))||U({},kt.UnexpectedToken,"ILLEGAL"),e=t);Et>>="===r?(Et+=4,{type:_t.Punctuator,value:r,lineNumber:At,lineStart:Lt,start:i,end:Et}):(n=r.substr(0,3),">>>"===n||"<<="===n||">>="===n?(Et+=3,{type:_t.Punctuator,value:n,lineNumber:At,lineStart:Lt,start:i,end:Et}):(e=n.substr(0,2),o===e[1]&&"+-<>&|".indexOf(o)>=0||"=>"===e?(Et+=2,{type:_t.Punctuator,value:e,lineNumber:At,lineStart:Lt,start:i,end:Et}):"<>=!+-*%&|^/".indexOf(o)>=0?(++Et,{type:_t.Punctuator,value:o,lineNumber:At,lineStart:Lt,start:i,end:Et}):void U({},kt.UnexpectedToken,"ILLEGAL")))}function y(t){for(var e="";Et=0&&Et=0&&(r=r.replace(/\\u\{([0-9a-fA-F]+)\}/g,function(t,e){return parseInt(e,16)<=1114111?"x":void U({},kt.InvalidRegExp)}).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,"x"));try{n=new RegExp(r)}catch(t){U({},kt.InvalidRegExp)}try{return new RegExp(t,e)}catch(t){return null}}function k(){var e,n,r,i,o;for(e=St[Et],t("/"===e,"Regular expression literal must start with a slash"),n=St[Et++],r=!1,i=!1;Et0&&(r=It.tokens[It.tokens.length-1],r.range[0]===t&&"Punctuator"===r.type&&("/"!==r.value&&"/="!==r.value||It.tokens.pop())),It.tokens.push({type:"RegularExpression",value:n.literal,regex:n.regex,range:[t,Et],loc:e})),n}function E(t){return t.type===_t.Identifier||t.type===_t.Keyword||t.type===_t.BooleanLiteral||t.type===_t.NullLiteral}function A(){var t,e;if(t=It.tokens[It.tokens.length-1],!t)return T();if("Punctuator"===t.type){if("]"===t.value)return v();if(")"===t.value)return e=It.tokens[It.openParenToken-1],!e||"Keyword"!==e.type||"if"!==e.value&&"while"!==e.value&&"for"!==e.value&&"with"!==e.value?v():T();if("}"===t.value){if(It.tokens[It.openCurlyToken-3]&&"Keyword"===It.tokens[It.openCurlyToken-3].type){if(e=It.tokens[It.openCurlyToken-4],!e)return v()}else{if(!It.tokens[It.openCurlyToken-4]||"Keyword"!==It.tokens[It.openCurlyToken-4].type)return v();if(e=It.tokens[It.openCurlyToken-5],!e)return T()}return v()}return T()}return"Keyword"===t.type&&"this"!==t.value?T():v()}function L(){var t;return d(),Et>=Ct?{type:_t.EOF,lineNumber:At,lineStart:Lt,start:Et,end:Et}:(t=St.charCodeAt(Et),o(t)?m():40===t||41===t||59===t?v():39===t||34===t?x():46===t?e(St.charCodeAt(Et+1))?b():v():e(t)?b():It.tokenize&&47===t?A():v())}function C(){var t,e,n,r;return d(),t={start:{line:At,column:Et-Lt}},e=L(),t.end={line:At,column:Et-Lt},e.type!==_t.EOF&&(n=St.slice(e.start,e.end),r={type:bt[e.type],value:n,range:[e.start,e.end],loc:t},e.regex&&(r.regex={pattern:e.regex.pattern,flags:e.regex.flags}),It.tokens.push(r)),e}function D(){var t;return t=Dt,Et=t.end,At=t.lineNumber,Lt=t.lineStart,Dt="undefined"!=typeof It.tokens?C():L(),Et=t.end,At=t.lineNumber,Lt=t.lineStart,t}function P(){var t,e,n;t=Et,e=At,n=Lt,Dt="undefined"!=typeof It.tokens?C():L(),Et=t,At=e,Lt=n}function I(){this.line=At,this.column=Et-Lt}function N(){this.start=new I,this.end=null}function O(t){t.type===_t.StringLiteral?this.start={line:t.startLineNumber,column:t.start-t.startLineStart}:this.start={line:t.lineNumber,column:t.start-t.lineStart},this.end=null}function z(){Et=Dt.start,Dt.type===_t.StringLiteral?(At=Dt.startLineNumber,Lt=Dt.startLineStart):(At=Dt.lineNumber,Lt=Dt.lineStart),It.range&&(this.range=[Et,0]),It.loc&&(this.loc=new N)}function j(t){It.range&&(this.range=[t.start,0]),It.loc&&(this.loc=new O(t))}function F(){var t,e,n,r;return t=Et,e=At,n=Lt,d(),r=At!==e,Et=t,At=e,Lt=n,r}function U(e,n){var r,i=Array.prototype.slice.call(arguments,2),a=n.replace(/%(\d)/g,function(e,n){return t(n":case"<=":case">=":case"instanceof":n=7;break;case"in":n=e?7:0;break;case"<<":case">>":case">>>":n=8;break;case"+":case"-":n=9;break;case"*":case"/":case"%":n=11}return n}function st(){var t,e,n,r,i,a,o,s,u,l;if(t=Dt,u=at(),r=Dt,i=ot(r,Pt.allowIn),0===i)return u;for(r.prec=i,D(),e=[t,Dt],o=at(),a=[u,r,o];(i=ot(Dt,Pt.allowIn))>0;){for(;a.length>2&&i<=a[a.length-2].prec;)o=a.pop(),s=a.pop().value,u=a.pop(),e.pop(),n=new j(e[e.length-1]).finishBinaryExpression(s,u,o),a.push(n);r=D(),r.prec=i,a.push(r),e.push(Dt),n=at(),a.push(n)}for(l=a.length-1,n=a[l],e.pop();l>1;)n=new j(e.pop()).finishBinaryExpression(a[l-1].value,a[l-2],n),l-=2;return n}function ut(){var t,e,n,r,i;return i=Dt,t=st(),$("?")&&(D(),e=Pt.allowIn,Pt.allowIn=!0,n=lt(),Pt.allowIn=e,B(":"),r=lt(),t=new j(i).finishConditionalExpression(t,n,r)),t}function lt(){var t,e,n,r;return t=Pt.parenthesisCount,r=Dt,e=Dt,n=ut()}function ct(){var t=lt();if($(","))throw new Error("Disabled.");return t}function dt(t){var e=ct();return Y(),t.finishExpressionStatement(e)}function ft(){var t,e,n=Dt.type;if(n===_t.EOF&&q(Dt),n===_t.Punctuator&&"{"===Dt.value)throw new Error("Disabled.");if(e=new z,n===_t.Punctuator)switch(Dt.value){case";":throw new Error("Disabled.");case"(":return dt(e)}else if(n===_t.Keyword)throw new Error("Disabled.");return t=ct(),Y(),e.finishExpressionStatement(t)}function ht(){if(Dt.type===_t.Keyword)switch(Dt.value){case"const":case"let":throw new Error("Disabled.");case"function":throw new Error("Disabled.");default:return ft()}if(Dt.type!==_t.EOF)return ft()}function pt(){for(var t,e,n,r,i=[];Et0?1:0,Lt=0,Ct=St.length,Dt=null,Pt={allowIn:!0,labelSet:{},inFunctionBody:!1,inIteration:!1,inSwitch:!1,lastCommentStart:-1},It={},e=e||{},e.tokens=!0,It.tokens=[],It.tokenize=!0,It.openParenToken=-1,It.openCurlyToken=-1,It.range="boolean"==typeof e.range&&e.range,It.loc="boolean"==typeof e.loc&&e.loc,"boolean"==typeof e.tolerant&&e.tolerant&&(It.errors=[]);try{if(P(),Dt.type===_t.EOF)return It.tokens;for(D();Dt.type!==_t.EOF;)try{D()}catch(t){if(It.errors){It.errors.push(t);break}throw t}mt(),r=It.tokens,"undefined"!=typeof It.errors&&(r.errors=It.errors)}catch(t){throw t}finally{It={}}return r}function yt(t,e){var n,r;r=String,"string"==typeof t||t instanceof String||(t=r(t)),St=t,Et=0,At=St.length>0?1:0,Lt=0,Ct=St.length,Dt=null,Pt={allowIn:!0,labelSet:{},parenthesisCount:0,inFunctionBody:!1,inIteration:!1,inSwitch:!1,lastCommentStart:-1},It={},"undefined"!=typeof e&&(It.range="boolean"==typeof e.range&&e.range,It.loc="boolean"==typeof e.loc&&e.loc,It.loc&&null!==e.source&&void 0!==e.source&&(It.source=r(e.source)),"boolean"==typeof e.tokens&&e.tokens&&(It.tokens=[]),"boolean"==typeof e.tolerant&&e.tolerant&&(It.errors=[]));try{n=gt(),"undefined"!=typeof It.tokens&&(mt(),n.tokens=It.tokens),"undefined"!=typeof It.errors&&(n.errors=It.errors)}catch(t){throw t}finally{It={}}return n}var _t,bt,xt,wt,kt,Mt,St,Tt,Et,At,Lt,Ct,Dt,Pt,It;_t={BooleanLiteral:1,EOF:2,Identifier:3,Keyword:4,NullLiteral:5,NumericLiteral:6,Punctuator:7,StringLiteral:8,RegularExpression:9},bt={},bt[_t.BooleanLiteral]="Boolean",bt[_t.EOF]="",bt[_t.Identifier]="Identifier",bt[_t.Keyword]="Keyword",bt[_t.NullLiteral]="Null",bt[_t.NumericLiteral]="Numeric",bt[_t.Punctuator]="Punctuator",bt[_t.StringLiteral]="String",bt[_t.RegularExpression]="RegularExpression",xt={AssignmentExpression:"AssignmentExpression",ArrayExpression:"ArrayExpression",BinaryExpression:"BinaryExpression",CallExpression:"CallExpression",ConditionalExpression:"ConditionalExpression",ExpressionStatement:"ExpressionStatement",Identifier:"Identifier",Literal:"Literal",LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",ObjectExpression:"ObjectExpression",Program:"Program",Property:"Property",UnaryExpression:"UnaryExpression"},wt={Data:1,Get:2,Set:4},kt={UnexpectedToken:"Unexpected token %0",UnexpectedNumber:"Unexpected number",UnexpectedString:"Unexpected string",UnexpectedIdentifier:"Unexpected identifier",UnexpectedReserved:"Unexpected reserved word",UnexpectedEOS:"Unexpected end of input",NewlineAfterThrow:"Illegal newline after throw",InvalidRegExp:"Invalid regular expression",UnterminatedRegExp:"Invalid regular expression: missing /",InvalidLHSInAssignment:"Invalid left-hand side in assignment",InvalidLHSInForIn:"Invalid left-hand side in for-in",MultipleDefaultsInSwitch:"More than one default clause in switch statement",NoCatchOrFinally:"Missing catch or finally after try",UnknownLabel:"Undefined label '%0'",Redeclaration:"%0 '%1' has already been declared",IllegalContinue:"Illegal continue statement",IllegalBreak:"Illegal break statement",IllegalReturn:"Illegal return statement",StrictModeWith:"Strict mode code may not include a with statement",StrictCatchVariable:"Catch variable may not be eval or arguments in strict mode",StrictVarName:"Variable name may not be eval or arguments in strict mode",StrictParamName:"Parameter name eval or arguments is not allowed in strict mode",StrictParamDupe:"Strict mode function may not have duplicate parameter names",StrictFunctionName:"Function name may not be eval or arguments in strict mode",StrictOctalLiteral:"Octal literals are not allowed in strict mode.",StrictDelete:"Delete of an unqualified identifier in strict mode.",StrictDuplicateProperty:"Duplicate data property in object literal not allowed in strict mode",AccessorDataProperty:"Object literal may not have data and accessor property with the same name",AccessorGetSet:"Object literal may not have multiple get/set accessors with the same name",StrictLHSAssignment:"Assignment to eval or arguments is not allowed in strict mode",StrictLHSPostfix:"Postfix increment/decrement may not have eval or arguments operand in strict mode",StrictLHSPrefix:"Prefix increment/decrement may not have eval or arguments operand in strict mode",StrictReservedWord:"Use of future reserved word in strict mode"},Mt={NonAsciiIdentifierStart:new RegExp("[ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠ-ࢲऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘౙౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൠൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᧁ-ᧇᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々-〇〡-〩〱-〵〸-〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿌ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛯꜗ-ꜟꜢ-ꞈꞋ-ꞎꞐ-ꞭꞰꞱꟷ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭟꭤꭥꯀ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]"),NonAsciiIdentifierPart:new RegExp("[ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮ̀-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁ҃-҇Ҋ-ԯԱ-Ֆՙա-և֑-ׇֽֿׁׂׅׄא-תװ-ײؐ-ؚؠ-٩ٮ-ۓە-ۜ۟-۪ۨ-ۼۿܐ-݊ݍ-ޱ߀-ߵߺࠀ-࠭ࡀ-࡛ࢠ-ࢲࣤ-ॣ०-९ॱ-ঃঅ-ঌএঐও-নপ-রলশ-হ়-ৄেৈো-ৎৗড়ঢ়য়-ৣ০-ৱਁ-ਃਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹ਼ਾ-ੂੇੈੋ-੍ੑਖ਼-ੜਫ਼੦-ੵઁ-ઃઅ-ઍએ-ઑઓ-નપ-રલળવ-હ઼-ૅે-ૉો-્ૐૠ-ૣ૦-૯ଁ-ଃଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହ଼-ୄେୈୋ-୍ୖୗଡ଼ଢ଼ୟ-ୣ୦-୯ୱஂஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹா-ூெ-ைொ-்ௐௗ௦-௯ఀ-ఃఅ-ఌఎ-ఐఒ-నప-హఽ-ౄె-ైొ-్ౕౖౘౙౠ-ౣ౦-౯ಁ-ಃಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹ಼-ೄೆ-ೈೊ-್ೕೖೞೠ-ೣ೦-೯ೱೲഁ-ഃഅ-ഌഎ-ഐഒ-ഺഽ-ൄെ-ൈൊ-ൎൗൠ-ൣ൦-൯ൺ-ൿංඃඅ-ඖක-නඳ-රලව-ෆ්ා-ුූෘ-ෟ෦-෯ෲෳก-ฺเ-๎๐-๙ກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ູົ-ຽເ-ໄໆ່-ໍ໐-໙ໜ-ໟༀ༘༙༠-༩༹༵༷༾-ཇཉ-ཬཱ-྄྆-ྗྙ-ྼ࿆က-၉ၐ-ႝႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚ፝-፟ᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛸᜀ-ᜌᜎ-᜔ᜠ-᜴ᝀ-ᝓᝠ-ᝬᝮ-ᝰᝲᝳក-៓ៗៜ៝០-៩᠋-᠍᠐-᠙ᠠ-ᡷᢀ-ᢪᢰ-ᣵᤀ-ᤞᤠ-ᤫᤰ-᤻᥆-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉ᧐-᧙ᨀ-ᨛᨠ-ᩞ᩠-᩿᩼-᪉᪐-᪙ᪧ᪰-᪽ᬀ-ᭋ᭐-᭙᭫-᭳ᮀ-᯳ᰀ-᰷᱀-᱉ᱍ-ᱽ᳐-᳔᳒-ᳶ᳸᳹ᴀ-᷵᷼-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼ‌‍‿⁀⁔ⁱⁿₐ-ₜ⃐-⃥⃜⃡-⃰ℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯ⵿-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⷠ-ⷿⸯ々-〇〡-〯〱-〵〸-〼ぁ-ゖ゙゚ゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿌ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘫꙀ-꙯ꙴ-꙽ꙿ-ꚝꚟ-꛱ꜗ-ꜟꜢ-ꞈꞋ-ꞎꞐ-ꞭꞰꞱꟷ-ꠧꡀ-ꡳꢀ-꣄꣐-꣙꣠-ꣷꣻ꤀-꤭ꤰ-꥓ꥠ-ꥼꦀ-꧀ꧏ-꧙ꧠ-ꧾꨀ-ꨶꩀ-ꩍ꩐-꩙ꩠ-ꩶꩺ-ꫂꫛ-ꫝꫠ-ꫯꫲ-꫶ꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭟꭤꭥꯀ-ꯪ꯬꯭꯰-꯹가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻ︀-️︠-︭︳︴﹍-﹏ﹰ-ﹴﹶ-ﻼ0-9A-Z_a-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]")},j.prototype=z.prototype={finish:function(){It.range&&(this.range[1]=Et),It.loc&&(this.loc.end=new I,It.source&&(this.loc.source=It.source))},finishArrayExpression:function(t){return this.type=xt.ArrayExpression,this.elements=t,this.finish(),this},finishAssignmentExpression:function(t,e,n){return this.type=xt.AssignmentExpression,this.operator=t,this.left=e,this.right=n,this.finish(),this},finishBinaryExpression:function(t,e,n){return this.type="||"===t||"&&"===t?xt.LogicalExpression:xt.BinaryExpression,this.operator=t,this.left=e,this.right=n,this.finish(),this},finishCallExpression:function(t,e){return this.type=xt.CallExpression,this.callee=t,this.arguments=e,this.finish(),this},finishConditionalExpression:function(t,e,n){return this.type=xt.ConditionalExpression,this.test=t,this.consequent=e,this.alternate=n,this.finish(),this},finishExpressionStatement:function(t){return this.type=xt.ExpressionStatement,this.expression=t,this.finish(),this},finishIdentifier:function(t){return this.type=xt.Identifier,this.name=t,this.finish(),this},finishLiteral:function(t){return this.type=xt.Literal,this.value=t.value,this.raw=St.slice(t.start,t.end),t.regex&&("//"==this.raw&&(this.raw="/(?:)/"),this.regex=t.regex),this.finish(),this},finishMemberExpression:function(t,e,n){return this.type=xt.MemberExpression,this.computed="["===t,this.object=e,this.property=n,this.finish(),this},finishObjectExpression:function(t){return this.type=xt.ObjectExpression,this.properties=t,this.finish(),this},finishProgram:function(t){return this.type=xt.Program,this.body=t,this.finish(),this},finishProperty:function(t,e,n){return this.type=xt.Property,this.key=e,this.value=n,this.kind=t,this.finish(),this},finishUnaryExpression:function(t,e){return this.type=xt.UnaryExpression,this.operator=t,this.argument=e,this.prefix=!0,this.finish(),this}};var Nt={if:1,this:1};return{tokenize:vt,parse:yt}}()},{}],48:[function(t,e,n){function r(t){console.log("[Vega Log]",t)}function i(t){console.error("[Vega Err]",t)}function a(t,e){if(a.enable){var n=Function.prototype.bind.call(console.log,console),r={prevTime:Date.now()-o,stamp:t.stamp};t.add&&(r.add=t.add.length,r.mod=t.mod.length,r.rem=t.rem.length,r.reflow=!!t.reflow),n.apply(console,(e.push(JSON.stringify(r)),e)),o=Date.now()}}var o=Date.now();e.exports={log:r,error:i,debug:(a.enable=!1,a)}},{}],49:[function(t,e,n){e.exports={path:t("./path"),render:t("./render"),Item:t("./util/Item"),bound:t("./util/bound"),Bounds:t("./util/Bounds"),canvas:t("./util/canvas"),Gradient:t("./util/Gradient"),toJSON:t("./util/scene").toJSON,fromJSON:t("./util/scene").fromJSON}},{"./path":51,"./render":71,"./util/Bounds":77,"./util/Gradient":79,"./util/Item":81,"./util/bound":82,"./util/canvas":83,"./util/scene":85}],50:[function(t,e,n){function r(t,e,n,r,i,o,u,l,c){var d=s.call(arguments);if(a[d])return a[d];var f=u*(Math.PI/180),h=Math.sin(f),p=Math.cos(f);n=Math.abs(n),r=Math.abs(r);var g=p*(l-t)*.5+h*(c-e)*.5,m=p*(c-e)*.5-h*(l-t)*.5,v=g*g/(n*n)+m*m/(r*r);v>1&&(v=Math.sqrt(v),n*=v,r*=v);var y=p/n,_=h/n,b=-h/r,x=p/r,w=y*l+_*c,k=b*l+x*c,M=y*t+_*e,S=b*t+x*e,T=(M-w)*(M-w)+(S-k)*(S-k),E=1/T-.25;E<0&&(E=0);var A=Math.sqrt(E);o==i&&(A=-A);var L=.5*(w+M)-A*(S-k),C=.5*(k+S)+A*(M-w),D=Math.atan2(k-C,w-L),P=Math.atan2(S-C,M-L),I=P-D;I<0&&1===o?I+=2*Math.PI:I>0&&0===o&&(I-=2*Math.PI);for(var N=Math.ceil(Math.abs(I/(.5*Math.PI+.001))),O=[],z=0;zl)for(d=1,h=o.length;d=0;)i[n].type===t&&(e&&i[n].handler!==e||i.splice(n,1));return this}},c.pickEvent=function(t){var e,n,r=this._canvas.getBoundingClientRect(),i=this._padding;return this.pick(this._scene,e=t.clientX-r.left,n=t.clientY-r.top,e-i.left,n-i.top)},c.pick=function(t,e,n,r,i){var a=this.context(),o=u[t.marktype];return o.pick.call(this,a,t,e,n,r,i)},e.exports=r},{"../../util/dom":84,"../Handler":54,"./marks":63}],57:[function(t,e,n){function r(t){c.call(this),this._loader=new u(t)}function i(t,e){if(!e)return null;var n,r,i,o,u,l=new s;for(n=0,r=e.length;n0&&(r.fill&&a.fill(t,r,h)&&t.fillRect(l,c,d,f),r.stroke&&a.stroke(t,r,h)&&t.strokeRect(l,c,d,f))),t.save(),t.translate(l,c),r.clip&&(t.beginPath(),t.rect(0,0,d,f),t.clip()),n&&n.translate(-l,-c),m=0,v=s.length;m=0;)if(u=y[g],v=u.bounds,!v||v.contains(i,a)){for(l=u.axisItems||o,c=u.items||o,d=u.legendItems||o,h=u.x||0,p=u.y||0,t.save(),t.translate(h,p),h=i-h,p=a-p,m=d.length;--m>=0;)if(s=d[m],s.interactive!==!1&&(f=this.pick(s,n,r,h,p)))return t.restore(),f;for(m=l.length;--m>=0;)if(s=l[m],s.interactive!==!1&&"back"!==s.layer&&(f=this.pick(s,n,r,h,p)))return t.restore(),f;for(m=c.length;--m>=0;)if(s=c[m],s.interactive!==!1&&(f=this.pick(s,n,r,h,p)))return t.restore(),f;for(m=l.length;--m>=0;)if(s=l[m],s.interative!==!1&&"back"===s.layer&&(f=this.pick(s,n,r,h,p)))return t.restore(),f;if(t.restore(),e.interactive!==!1&&(u.fill||u.stroke)&&h>=0&&h<=u.width&&p>=0&&p<=u.height)return u}return null}var a=t("./util"),o=[];e.exports={draw:r,pick:i}},{"./util":70}],62:[function(t,e,n){function r(t,e,n){if(e.items&&e.items.length)for(var r,i=this,a=e.items,o=0,s=a.length;o=0;)if(s=n.items[l],u=s.bounds,(!u||u.contains(a,o))&&u&&t(e,s,r,i,a,o))return s;return null}}function u(t,e){return function(n,r,i,a){var o,s,u=Array.isArray(r)?r[0]:r,l=null==e?u.fill:e,c=u.stroke&&n.isPointInStroke;return c&&(o=u.strokeWidth,s=u.strokeCap,n.lineWidth=null!=o?o:1,n.lineCap=null!=s?s:"butt"),!t(n,r)&&(l&&n.isPointInPath(i,a)||c&&n.isPointInStroke(i,a))}}function l(t){return s(u(t))}function c(t,e,n){return n*=null==e.fillOpacity?1:e.fillOpacity,n>0&&(t.globalAlpha=n,t.fillStyle=f(t,e,e.fill),!0)}function d(t,e,n){var r,i=null!=(i=e.strokeWidth)?i:1;return!(i<=0)&&(n*=null==e.strokeOpacity?1:e.strokeOpacity,n>0&&(t.globalAlpha=n,t.strokeStyle=f(t,e,e.stroke),t.lineWidth=i,t.lineCap=null!=(r=e.strokeCap)?r:"butt",t.vgLineDash(e.strokeDash||null),t.vgLineDashOffset(e.strokeDashOffset||0),!0))}function f(t,e,n){return n.id?h(t,n,e.bounds):n}function h(t,e,n){var r,i,a=n.width(),o=n.height(),s=n.x1+e.x1*a,u=n.y1+e.y1*o,l=n.x1+e.x2*a,c=n.y1+e.y2*o,d=t.createLinearGradient(s,u,l,c),f=e.stops;for(r=0,i=f.length;r=0;)(a[n].type===t&&!e||a[n].handler===e)&&(i.removeEventListener(r,a[n].listener),a.splice(n,1));return this}},e.exports=r},{"../../util/dom":84,"../Handler":54}],73:[function(t,e,n){function r(t){d.call(this),this._loader=new c(t),this._dirtyID=0}function i(t,e,n){var r,i,a;for(t=h.child(t,n,"linearGradient",g),t.setAttribute("id",e.id),t.setAttribute("x1",e.x1),t.setAttribute("x2",e.x2),t.setAttribute("y1",e.y1),t.setAttribute("y2",e.y2),r=0,i=e.stops.length;r/g,">")}var s=t("../Renderer"),u=t("../../util/ImageLoader"),l=t("../../util/svg"),c=t("../../util/text"),d=t("../../util/dom"),f=d.openTag,h=d.closeTag,p=t("./marks"),g=s.prototype,m=r.prototype=Object.create(g);m.constructor=r,m.resize=function(t,e,n){g.resize.call(this,t,e,n);var r=this._padding,i=this._text,a={class:"marks",width:this._width+r.left+r.right,height:this._height+r.top+r.bottom};for(var o in l.metadata)a[o]=l.metadata[o];return i.head=f("svg",a),i.root=f("g",{transform:"translate("+r.left+","+r.top+")"}),i.foot=h("g")+h("svg"),this},m.svg=function(){var t=this._text;return t.head+t.defs+t.root+t.body+t.foot},m.render=function(t){return this._text.body=this.mark(t),this._text.defs=this.buildDefs(),this},m.reset=function(){return this._defs.clip_id=0,this},m.buildDefs=function(){var t,e,n,r,i=this._defs,a="";for(e in i.gradient){for(n=i.gradient[e],r=n.stops,a+=f("linearGradient",{id:e,x1:n.x1,x2:n.x2,y1:n.y1,y2:n.y2}),t=0;t0?f("defs")+a+h("defs"):""},m.imageURL=function(t){return this._loader.imageURL(t)};var v;m.attributes=function(t,e){return v={},t(i,e,this),v},m.mark=function(t){var e,n,r,i=p[t.marktype],s=i.tag,u=i.attr,l=i.nest||!1,g=l?t.items&&t.items.length?[t.items[0]]:[]:t.items||[],m=this._defs,v="";for("g"!==s&&t.interactive===!1&&(e='style="pointer-events: none;"'),v+=f("g",{class:d.cssClass(t)},e),n=0;nthis.x2&&(this.x2=t),e>this.y2&&(this.y2=e),this},i.expand=function(t){return this.x1-=t,this.y1-=t,this.x2+=t,this.y2+=t,this},i.round=function(){return this.x1=Math.floor(this.x1),this.y1=Math.floor(this.y1),this.x2=Math.ceil(this.x2),this.y2=Math.ceil(this.y2),this},i.translate=function(t,e){return this.x1+=t,this.x2+=t,this.y1+=e,this.y2+=e,this},i.rotate=function(t,e,n){var r=Math.cos(t),i=Math.sin(t),a=e-e*r+n*i,o=n-e*i-n*r,s=this.x1,u=this.x2,l=this.y1,c=this.y2;return this.clear().add(r*s-i*l+a,i*s+r*l+o).add(r*s-i*c+a,i*s+r*c+o).add(r*u-i*l+a,i*u+r*l+o).add(r*u-i*c+a,i*u+r*c+o)},i.union=function(t){return t.x1this.x2&&(this.x2=t.x2),t.y2>this.y2&&(this.y2=t.y2),this},i.encloses=function(t){return t&&this.x1<=t.x1&&this.x2>=t.x2&&this.y1<=t.y1&&this.y2>=t.y2},i.alignsWith=function(t){return t&&(this.x1==t.x1||this.x2==t.x2||this.y1==t.y1||this.y2==t.y2)},i.intersects=function(t){return t&&!(this.x2t.x2||this.y2t.y2)},i.contains=function(t,e){return!(tthis.x2||ethis.y2)},i.width=function(){return this.x2-this.x1},i.height=function(){return this.y2-this.y1},e.exports=r},{}],78:[function(t,e,n){e.exports=function(t){function e(){}function n(e,n){t.add(e,n)}return{bounds:function(e){return arguments.length?(t=e,this):t},beginPath:e,closePath:e,moveTo:n,lineTo:n,quadraticCurveTo:function(e,n,r,i){t.add(e,n),t.add(r,i)},bezierCurveTo:function(e,n,r,i,a,o){t.add(e,n),t.add(r,i),t.add(a,o)}}}},{}],79:[function(t,e,n){function r(t){this.id="gradient_"+i++,this.type=t||"linear",this.stops=[],this.x1=0,this.x2=1,this.y1=0,this.y2=0}var i=0,a=r.prototype;a.stop=function(t,e){return this.stops.push({offset:t,color:e}),this},e.exports=r},{}],80:[function(t,e,n){(function(n){function r(t){this._pending=0,this._config=t||r.Config}function i(t,e){var n=o.sanitizeUrl(this.params(t));if(!n)return e&&e(t,null),null;var r=this,i=new Image;return r._pending+=1,i.onload=function(){r._pending-=1,i.loaded=!0,e&&e(null,i)},i.src=n,i}function a(t,e){var r=this,i=new("undefined"!=typeof window?window.canvas:"undefined"!=typeof n?n.canvas:null).Image;return r._pending+=1,o(this.params(t),function(t,n){return r._pending-=1,t?(e&&e(t,null),null):(i.src=n,i.loaded=!0,void(e&&e(null,i)))}),i}var o=t("datalib/src/import/load");r.Config=null;var s=r.prototype;s.pending=function(){return this._pending},s.params=function(t){var e,n={url:t};for(e in this._config)n[e]=this._config[e];return n},s.imageURL=function(t){return o.sanitizeUrl(this.params(t))},s.loadImage=function(t,e){return o.useXHR?i.call(this,t,e):a.call(this,t,e)},e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"datalib/src/import/load":22}],81:[function(t,e,n){function r(t){this.mark=t}var i=r.prototype;i.hasPropertySet=function(t){var e=this.mark.def.properties;return e&&null!=e[t]},i.cousin=function(t,e){if(0===t)return this;t=t||-1;var n=this.mark,r=n.group,i=null==e?n.items.indexOf(this):e,a=r.items.indexOf(n)+t;return r.items[a].items[i]},i.sibling=function(t){if(0===t)return this;t=t||-1;var e=this.mark,n=e.items.indexOf(this)+t;return e.items[n]},i.remove=function(){var t=this,e=t.mark.items,n=e.indexOf(t);return n>=0&&(n===e.length-1?e.pop():e.splice(n,1)),t},i.touch=function(){this.pathCache&&(this.pathCache=null)},e.exports=r},{}],82:[function(t,e,n){function r(){return D||(D=b.instance(1,1).getContext("2d"))}function i(t,e){return t.stroke&&0!==t.opacity&&0!==t.stokeOpacity&&e.expand(null!=t.strokeWidth?t.strokeWidth:1),e}function a(t,e,n,r,a){return null==e?n.set(0,0,0,0):(S(P.bounds(n),e,r,a),i(t,n)),n}function o(t,e){var n=t.path?t.pathCache||(t.pathCache=M(t.path)):null;return a(t,n,e,t.x,t.y)}function s(t,e){if(0===t.items.length)return e;var n=t.items,r=n[0],i=r.pathCache||(r.pathCache=M(T(n)));return a(r,i,e)}function u(t,e){if(0===t.items.length)return e;var n=t.items,r=n[0],i=r.pathCache||(r.pathCache=M(E(n)));return a(r,i,e)}function l(t,e){var n,r;return i(t,e.set(n=t.x||0,r=t.y||0,n+t.width||0,r+t.height||0))}function c(t,e){var n=t.x||0,r=t.y||0,i=t.width||0,a=t.height||0;return n-="center"===t.align?i/2:"right"===t.align?i:0,r-="middle"===t.baseline?a/2:"bottom"===t.baseline?a:0,e.set(n,r,n+i,r+a)}function d(t,e){var n,r;return i(t,e.set(n=t.x||0,r=t.y||0,null!=t.x2?t.x2:n,null!=t.y2?t.y2:r))}function f(t,e){var n,r,a,o,s,u,l,c,d,f=t.x||0,h=t.y||0,p=t.innerRadius||0,g=t.outerRadius||0,m=(t.startAngle||0)-A,v=(t.endAngle||0)-A,y=1/0,_=-(1/0),b=1/0,x=-(1/0),w=[m,v],k=m-m%A;for(r=0;r<4&&ke;)t.removeChild(t.childNodes[--n]);return t},remove:i,cssClass:function(t){return"mark-"+t.marktype+(t.name?" "+t.name:"")},openTag:function(t,e,n){var r,i,a="<"+t;if(e)for(r in e)i=e[r],null!=i&&(a+=" "+r+'="'+i+'"');return n&&(a+=" "+n),a+">"},closeTag:function(t){return""}}},{}],85:[function(t,e,n){function r(t,e){return JSON.stringify(t,u,e)}function i(t){var e="string"==typeof t?JSON.parse(t):t;return a(e)}function a(t){var e,n,r,i,u,l=t.marktype;for(r=0,i=s.length;r0?e():t(this.canvas()))},u.svg=function(){return"svg"===this._type?this._renderer.svg():null},u.initialize=function(){var t=this._width,e=this._height,n=this._bgcolor,r=this._padding,i=this.model().config();return this._viewport&&(t=this._viewport[0]-(r?r.left+r.right:0),e=this._viewport[1]-(r?r.top+r.bottom:0)),this._renderer=(this._renderer||new this._io.Renderer(i.load)).initialize(null,t,e,r).background(n),this},e.exports=r},{"./View":90,"vega-scenegraph":49}],89:[function(t,e,n){function r(t){this._defs={},this._predicates={},this._scene=null,this._groups=null,this._node=null,this._builder=null,this._reset={axes:!1,legends:!1},this.config(t),this.expr=f(this),u.init.call(this)}function i(t){var e=this,n={};return a.isArray(t)?(t.forEach(function(t){n[t]=e._predicates[t]}),n):this._predicates[t]}var a=t("datalib"),o=t("vega-dataflow"),s=o.ChangeSet,u=o.Graph.prototype,l=o.Node,c=t("../scene/GroupBuilder"),d=t("../scene/visit"),f=t("../parse/expr"),h=t("./config"),p=r.prototype=Object.create(u);p.constructor=r,p.defs=function(t){return arguments.length?(this._defs=t,this):this._defs},p.config=function(t){if(!arguments.length)return this._config;this._config=Object.create(h);for(var e in t){var n=t[e],r=this._config[e];a.isObject(n)&&a.isObject(r)?this._config[e]=a.extend({},r,n):this._config[e]=n}return this},p.width=function(t){return this._defs&&(this._defs.width=t),this._defs&&this._defs.marks&&(this._defs.marks.width=t),this._scene&&(this._scene.items[0].width=t,this._scene.items[0]._dirty=!0),this._reset.axes=!0,this},p.height=function(t){return this._defs&&(this._defs.height=t),this._defs&&this._defs.marks&&(this._defs.marks.height=t),this._scene&&(this._scene.items[0].height=t,this._scene.items[0]._dirty=!0),this._reset.axes=!0,this},p.node=function(){return this._node||(this._node=new l(this))},p.data=function(){var t=u.data.apply(this,arguments);return arguments.length>1&&this.node().addListener(t.pipeline()[0]),t},p.predicate=function(t,e){return 1===arguments.length?i.call(this,t):this._predicates[t]=e},p.predicates=function(){return this._predicates},p.scene=function(t){if(!arguments.length)return this._scene;this._builder&&(this.node().removeListener(this._builder),this._builder._groupBuilder.disconnect());var e=this,n=this._builder=new l(this);return n.evaluate=function(r){if(n._groupBuilder)return r;var i=n._groupBuilder=new c(e,e._defs.marks,e._scene={}),a=i.pipeline();return e._groups={},this.addListener(i.connect()),a[a.length-1].addListener(t),r},this.addListener(n),this},p.group=function(t,e){var n=this._groups;return 1===arguments.length?n[t]:(n[t]=e,this)},p.reset=function(){return this._scene&&this._reset.axes&&(d(this._scene,function(t){t.axes&&t.axes.forEach(function(t){t.reset()})}),this._reset.axes=!1),this._scene&&this._reset.legends&&(d(this._scene,function(t){t.legends&&t.legends.forEach(function(t){t.reset()})}),this._reset.legends=!1),this},p.addListener=function(t){this.node().addListener(t)},p.removeListener=function(t){this.node().removeListener(t)},p.fire=function(t){t||(t=s.create()),this.propagate(t,this.node())},e.exports=r},{"../parse/expr":96,"../scene/GroupBuilder":112,"../scene/visit":117,"./config":91,datalib:26,"vega-dataflow":41}],90:[function(t,e,n){(function(n){function r(t,e,n){this._el=null,this._model=null,this._width=this.__width=e||500,this._height=this.__height=n||300,this._bgcolor=null,this._cursor=!0,this._autopad=1,this._padding={top:0,left:0,bottom:0,right:0},this._viewport=null,this._renderer=null,this._handler=null,this._streamer=null,this._skipSignals=!1,this._changeset=null,this._repaint=!0,this._renderers=c,this._io=null,this._api={}}function i(t){var e=this,n=this._model.data(t);if(!n)return d.error('Data source "'+t+'" is not defined.');var r=n.pipeline()[0],i=this._streamer,a={};return this._api[t]?this._api[t]:(a.insert=function(o){return n.insert(u.duplicate(o)),i.addListener(r),e._changeset.data[t]=1,a},a.update=function(){return i.addListener(r),e._changeset.data[t]=1,n.update.apply(n,arguments),a},a.remove=function(){return i.addListener(r),e._changeset.data[t]=1,n.remove.apply(n,arguments),a},a.values=function(){return n.values()},this._api[t]=a)}function a(t,e){var n=this._changeset,r=this._model.signal(t);return r?(this._streamer.addListener(r.value(e)),n.signals[t]=1,void(n.reflow=!0)):d.error('Signal "'+t+'" is not defined.')}function o(){var t=this;return t._renderNode=new l.Node(t._model).router(!0),t._renderNode.evaluate=function(e){d.debug(e,["rendering"]);var n=t._model.scene(),r=t._handler;return r&&r.scene&&r.scene(n),e.trans?e.trans.start(function(e){t._renderer.render(n,e)}):t._repaint?t._renderer.render(n):e.dirty.length&&t._renderer.render(n,e.dirty),e.dirty.length&&(e.dirty.forEach(function(t){t._dirty=!1}),n.items[0]._dirty=!1),t._repaint=t._skipSignals=!1,e},t._model.scene(t._renderNode),!0}var s="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,u=t("datalib"),l=t("vega-dataflow"),c=t("vega-scenegraph").render,d=t("vega-logging"),f=l.Dependencies,h=t("../parse/streams"),p=t("../scene/Encoder"),g=t("../scene/Transition"),m=r.prototype;m.model=function(t){return arguments.length?(this._model!==t&&(this._model=t,this._streamer=new l.Node(t),this._streamer._rank=-1,this._changeset=l.ChangeSet.create(),this._handler&&this._handler.model(t)),this):this._model},m.data=function(t){var e=this;return arguments.length?u.isString(t)?i.call(e,t):(u.isObject(t)&&u.keys(t).forEach(function(n){var r=i.call(e,n);t[n](r)}),this):e._model.values()};var v=u.toMap(["width","height","padding"]);m.signal=function(t,e,n){var r,i,o=this._model;if(!arguments.length)return o.values(f.SIGNALS);if(1===arguments.length&&u.isString(t))return o.values(f.SIGNALS,t);u.isObject(t)?(i=t,n=e):(i={},i[t]=e);for(r in i)v[r]?this[r](i[r]):a.call(this,r,i[r]);return this._skipSignals=n,this},m.width=function(t){return arguments.length?(this.__width!==t&&(this._width=this.__width=t,this.model().width(t),this.initialize(),this._strict&&(this._autopad=1),a.call(this,"width",t)),this):this.__width},m.height=function(t){return arguments.length?(this.__height!==t&&(this._height=this.__height=t,this.model().height(t),this.initialize(),this._strict&&(this._autopad=1),a.call(this,"height",t)),this):this.__height},m.background=function(t){return arguments.length?(this._bgcolor!==t&&(this._bgcolor=t,this.initialize()),this):this._bgcolor},m.padding=function(t){return arguments.length?(this._padding!==t&&(u.isString(t)?(this._autopad=1,this._padding={top:0,left:0,bottom:0,right:0},this._strict="strict"===t):(this._autopad=0,this._padding=t,this._strict=!1),this._renderer&&this._renderer.resize(this._width,this._height,this._padding),this._handler&&this._handler.padding(this._padding),a.call(this,"padding",this._padding)),this._repaint=!0,this):this._padding},m.autopad=function(t){if(this._autopad<1)return this;this._autopad=0;var e=this.model().scene().bounds,n=this._padding,r=this.model().config(),i=r.autopadInset,o=e.x1<0?Math.ceil(-e.x1)+i:0,s=e.y1<0?Math.ceil(-e.y1)+i:0,u=e.x2>this._width?Math.ceil(+e.x2-this._width)+i:0;return e=e.y2>this._height?Math.ceil(+e.y2-this._height)+i:0,n={left:o,top:s,right:u,bottom:e},this._strict?(this._autopad=0,this._padding=n,this._width=Math.max(0,this.__width-(o+u)),this._height=Math.max(0,this.__height-(s+e)),this._model.width(this._width).height(this._height).reset(),a.call(this,"width",this._width),a.call(this,"height",this._height),a.call(this,"padding",n),this.initialize().update({props:"enter"}).update({props:"update"})):this.padding(n).update(t),this},m.viewport=function(t){return arguments.length?(this._viewport!==t&&(this._viewport=t,this.initialize()),this):this._viewport},m.renderer=function(t){if(!arguments.length)return this._renderer;if(this._renderers[t])t=this._renderers[t];else{if(u.isString(t))throw new Error("Unknown renderer: "+t);if(!t)throw new Error("No renderer specified")}return this._io!==t&&(this._io=t,this._renderer=null,this.initialize(),this._build&&this.render()),this},m.initialize=function(t){var e,n=this,r=n._width,i=n._height,a=n._padding,o=n._bgcolor,u=this.model().config();return arguments.length&&null!==t||(t=this._el?this._el.parentNode:null)?(s.select(t).select("div.vega").remove(),this._el=t=s.select(t).append("div").attr("class","vega").style("position","relative").node(),n._viewport&&s.select(t).style("width",(n._viewport[0]||r)+"px").style("height",(n._viewport[1]||i)+"px").style("overflow","auto"), +c.canvas.Renderer.RETINA=u.render.retina,n._renderer=(n._renderer||new this._io.Renderer(u.load)).initialize(t,r,i,a).background(o),e=n._handler,n._handler=(new this._io.Handler).initialize(t,a,n),e?e.handlers().forEach(function(t){n._handler.on(t.type,t.handler)}):n._detach=h(this),this._repaint=!0,this):this},m.destroy=function(){this._detach&&this._detach()},m.update=function(t){t=t||{};var e=this,n=this._model,r=this._streamer,i=this._changeset,a=t.duration?new g(t.duration,t.ease):null;if(a&&(i.trans=a),void 0!==t.props){if(u.keys(i.data).length>0)throw Error("New data values are not reflected in the visualization. Please call view.update() before updating a specified property set.");i.reflow=!0,i.request=t.props}var s=e._build;return e._build=e._build||o.call(this),t.items&&s?(p.update(n,t.trans,t.props,t.items,i.dirty),e._renderNode.evaluate(i)):r.listeners().length&&s?(this._repaint&&r.addListener(n.node()),n.propagate(i,r,null,this._skipSignals),r.disconnect()):n.fire(i),e._changeset=l.ChangeSet.create(),e.autopad(t)},m.toImageURL=function(t){var e,n=this;switch(t||"png"){case"canvas":case"png":e=c.canvas.Renderer;break;case"svg":e=c.svg.string.Renderer;break;default:throw Error("Unrecognized renderer type: "+t)}var r=c.canvas.Renderer.RETINA;c.canvas.Renderer.RETINA=!1;var i=new e(n._model.config.load).initialize(null,n._width,n._height,n._padding).background(n._bgcolor).render(n._model.scene());if(c.canvas.Renderer.RETINA=r,"svg"===t){var a=new Blob([i.svg()],{type:"image/svg+xml"});return window.URL.createObjectURL(a)}return i.canvas().toDataURL("image/png")},m.render=function(t){return this._renderer.render(this._model.scene(),t),this},m.on=function(){return this._handler.on.apply(this._handler,arguments),this},m.onSignal=function(t,e){var n=this._model.signal(t);return n?n.on(e):d.error('Signal "'+t+'" is not defined.'),this},m.off=function(){return this._handler.off.apply(this._handler,arguments),this},m.offSignal=function(t,e){var n=this._model.signal(t);return n?n.off(e):d.error('Signal "'+t+'" is not defined.'),this},r.factory=function(e){var n=t("./HeadlessView");return function(t){t=t||{};var i=e.defs(),a=(t.el?new r:new n).model(e).renderer(t.renderer||"canvas").width(i.width).height(i.height).background(i.background).padding(i.padding).viewport(i.viewport).initialize(t.el);return t.data&&a.data(t.data),t.el&&(t.hover!==!1&&a.on("mouseover",function(t,e){e&&e.hasPropertySet("hover")&&this.update({props:"hover",items:e})}).on("mouseout",function(t,e){e&&e.hasPropertySet("hover")&&this.update({props:"update",items:e})}),t.cursor!==!1&&a.onSignal("cursor",function(t,e){var n=s.select("body");u.isString(e)?(a._cursor="default"===e,n.style("cursor",e)):u.isObject(e)&&a._cursor&&n.style("cursor",e.default)})),a}},e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../parse/streams":107,"../scene/Encoder":111,"../scene/Transition":114,"./HeadlessView":88,datalib:26,"vega-dataflow":41,"vega-logging":48,"vega-scenegraph":49}],91:[function(t,e,n){(function(t){var n="undefined"!=typeof window?window.d3:"undefined"!=typeof t?t.d3:null,r={};r.load={baseURL:"",domainWhiteList:!1},r.autopadInset=5,r.scale={time:n.time.scale,utc:n.time.scale.utc},r.render={retina:!0},r.scene={fill:void 0,fillOpacity:void 0,stroke:void 0,strokeOpacity:void 0,strokeWidth:void 0,strokeDash:void 0,strokeDashOffset:void 0},r.axis={layer:"back",ticks:10,padding:3,axisColor:"#000",axisWidth:1,gridColor:"#000",gridOpacity:.15,tickColor:"#000",tickLabelColor:"#000",tickWidth:1,tickSize:6,tickLabelFontSize:11,tickLabelFont:"sans-serif",titleColor:"#000",titleFont:"sans-serif",titleFontSize:11,titleFontWeight:"bold",titleOffset:"auto",titleOffsetAutoMin:30,titleOffsetAutoMax:1e4,titleOffsetAutoMargin:4},r.legend={orient:"right",offset:20,padding:3,margin:2,gradientStrokeColor:"#888",gradientStrokeWidth:1,gradientHeight:16,gradientWidth:100,labelColor:"#000",labelFontSize:10,labelFont:"sans-serif",labelAlign:"left",labelBaseline:"middle",labelOffset:8,symbolShape:"circle",symbolSize:50,symbolColor:"#888",symbolStrokeWidth:1,titleColor:"#000",titleFont:"sans-serif",titleFontSize:11,titleFontWeight:"bold"},r.color={rgb:[128,128,128],lab:[50,0,0],hcl:[0,0,50],hsl:[0,0,.5]},r.range={category10:n.scale.category10().range(),category20:n.scale.category20().range(),category20b:n.scale.category20b().range(),category20c:n.scale.category20c().range(),shapes:["circle","cross","diamond","square","triangle-down","triangle-up"]},e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],92:[function(t,e,n){function r(t,e,n){var r=t.schema;r&&(r.refs&&i.extend(n.refs,r.refs),r.defs&&i.extend(n.defs,r.defs))}var i=t("datalib"),a=t("../parse"),o=t("../scene/Scale"),s=t("./config");e.exports=function(t){var e=null;return t=t||{},t.url?e=i.json(i.extend({url:t.url},s.load)):(e={$schema:"http://json-schema.org/draft-04/schema#",title:"Vega Visualization Specification Language",defs:{},refs:{},$ref:"#/defs/spec"},i.keys(a).forEach(function(n){r(a[n],t,e)}),r(o,t,e)),t.properties&&i.keys(t.properties).forEach(function(n){e.defs.propset.properties[n]={$ref:"#/refs/"+t.properties[n]+"Value"}}),t.propertySets&&i.keys(t.propertySets).forEach(function(t){e.defs.mark.properties.properties.properties[t]={$ref:"#/defs/propset"}}),e}},{"../parse":97,"../scene/Scale":113,"./config":91,datalib:26}],93:[function(t,e,n){function r(t,e,n,r){var o=a(t);(e||[]).forEach(function(e,a){n[a]=n[a]||s(t,o[e.type]),i(o[e.type],e,a,n[a],r)})}function i(t,e,n,r,i){var a;void 0!==e.scale&&r.scale(a=i.scale(e.scale));var s=t.grid;o.isObject(s)&&(t.grid=void 0!==s[a.type]?s[a.type]:s.default),r.orient(u(e,t,"orient",l[e.type])),r.offset(u(e,t,"offset",0)),r.layer(u(e,t,"layer","front")),r.grid(u(e,t,"grid",!1)),r.title(e.title||null),r.titleOffset(u(e,t,"titleOffset")),r.tickValues(e.values||null),r.tickFormat(e.format||null),r.tickFormatType(e.formatType||null),r.tickSubdivide(e.subdivide||0),r.tickPadding(u(e,t,"tickPadding",t.padding));var c=u(e,t,"tickSize"),d=[c,c,c];d[0]=u(e,t,"tickSizeMajor",d[0]),d[1]=u(e,t,"tickSizeMinor",d[1]),d[2]=u(e,t,"tickSizeEnd",d[2]),d.length&&r.tickSize.apply(r,d),r.tickCount(u(e,t,"ticks"));var f=e.properties;f&&f.ticks?(r.majorTickProperties(f.majorTicks?o.extend({},f.ticks,f.majorTicks):f.ticks),r.minorTickProperties(f.minorTicks?o.extend({},f.ticks,f.minorTicks):f.ticks)):(r.majorTickProperties(f&&f.majorTicks||{}),r.minorTickProperties(f&&f.minorTicks||{})),r.tickLabelProperties(f&&f.labels||{}),r.titleProperties(f&&f.title||{}),r.gridLineProperties(f&&f.grid||{}),r.domainProperties(f&&f.axis||{})}function a(t){var e=t.config(),n=e.axis;return{x:o.extend(o.duplicate(n),e.axis_x),y:o.extend(o.duplicate(n),e.axis_y)}}var o=t("datalib"),s=t("../scene/axis"),u=t("../util/theme-val"),l={x:"bottom",y:"left",top:"top",bottom:"bottom",left:"left",right:"right"};e.exports=r},{"../scene/axis":115,"../util/theme-val":149,datalib:26}],94:[function(t,e,n){(function(t){function n(t){return null==t?null:r.rgb(t)+""}var r="undefined"!=typeof window?window.d3:"undefined"!=typeof t?t.d3:null;e.exports=n}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],95:[function(t,e,n){function r(t,e,n){function o(t,e){a.error("PARSE DATA FAILED: "+e.name+" "+t),l=-1,n(t)}function s(e){return function(r,a){if(r)o(r,e);else if(l>0)try{t.data(e.name).values(i.read(a,e.format)),0===--l&&n()}catch(t){o(t,e)}}}var u=t.config(),l=0;return(e||[]).forEach(function(e){e.url&&(l+=1,i.load(i.extend({url:e.url},u.load),s(e)));try{r.datasource(t,e)}catch(t){o(t,e)}}),0===l&&setTimeout(n,1),e}var i=t("datalib"),a=t("vega-logging"),o=t("./transforms"),s=t("./modify");r.datasource=function(t,e){var n=(e.transform||[]).map(function(e){return o(t,e)}),r=(e.modify||[]).map(function(n){return s(t,n,e)}),a=t.data(e.name,r.concat(n));return e.values?a.values(i.read(e.values,e.format)):e.source&&(a.source(e.source).addListener(a),t.removeListener(a.pipeline()[0])),a},e.exports=r},{"./modify":101,"./transforms":108,datalib:26,"vega-logging":48}],96:[function(t,e,n){function r(t){return function(e){e=e.map(t);var n=e.length;if(n<1||n>2)throw Error("open takes exactly 1 or 2 arguments.");return"this.defs.open(this.model, "+e[0]+(n>1?","+e[1]:"")+")"}}function i(t,e,n){if("undefined"==typeof window||!window||!window.open)throw Error("Open function can only be invoked in a browser.");var r=p.extend({type:"open",url:e,name:n},t.config().load),i=p.load.sanitizeUrl(r);if(!i)throw Error("Invalid URL: "+r.url);window.open(i,n)}function a(t,e){return function(n){n=n.map(t);var r=n.length;if(r<2||r>3)throw Error("scale takes exactly 2 or 3 arguments.");return"this.defs.scale(this.model, "+e+", "+n[0]+","+n[1]+(r>2?","+n[2]:"")+")"}}function o(t,e,n,r,i){if(i&&i.scale||(i=i&&i.mark?i.mark.group:t.scene().items[0]),t.group(i._id)!==i)throw Error('Scope for scale "'+n+'" is not a valid group item.');var a=i.scale(n);return a?e?a.invert(r):a(r):r}function s(t,e,n,r){var i=e,a=n;return e>n&&(i=n,a=e),r?it:i<=t&&a>=t}function u(t){return function(e,n,r,i){var a;if(3!==e.length)throw Error("indata takes 3 arguments.");if("Literal"!==e[0].type)throw Error("Data source name must be a literal for indata.");return a=e[0].value,i[a]=1,"Literal"===e[2].type&&u.model.requestIndex(a,e[2].value),e=e.map(t),"this.defs.indata(this.model,"+e[0]+","+e[1]+","+e[2]+")"}}function l(t,e,n,r){var i=t.data(e),a=i.getIndex(r);return a[n]>0}function c(t,e){return g.format(t,"number")(e)}function d(t,e){return g.format(t,"time")(e)}function f(t,e){return g.format(t,"utc")(e)}function h(t){return function(e){u.model=t;var n=y(e);return n.model=t,n.sig=t?t._signals:{},n}}var p=t("datalib"),g=p.template,m=t("vega-expression"),v=["datum","parent","event","signals"],y=m.compiler(v,{idWhiteList:v,fieldVar:v[0],globalVar:function(t){return"this.sig["+p.str(t)+"]._value"},functions:function(t){var e=m.functions(t);return e.eventItem="event.vg.getItem",e.eventGroup="event.vg.getGroup",e.eventX="event.vg.getX",e.eventY="event.vg.getY",e.open=r(t),e.scale=a(t,!1),e.iscale=a(t,!0),e.inrange="this.defs.inrange",e.indata=u(t),e.format="this.defs.format",e.timeFormat="this.defs.timeFormat",e.utcFormat="this.defs.utcFormat",e},functionDefs:function(){return{scale:o,inrange:s,indata:l,format:c,timeFormat:d,utcFormat:f,open:i}}});h.scale=o,h.codegen=y.codegen,e.exports=h},{datalib:26,"vega-expression":46}],97:[function(t,e,n){e.exports={axes:t("./axes"),background:t("./background"),data:t("./data"),events:t("vega-event-selector"),expr:t("./expr"),legends:t("./legends"),mark:t("./mark"),marks:t("./marks"),modify:t("./modify"),padding:t("./padding"),predicates:t("./predicates"),properties:t("./properties"),signals:t("./signals"),spec:t("./spec"),streams:t("./streams"),transforms:t("./transforms")}},{"./axes":93,"./background":94,"./data":95,"./expr":96,"./legends":98,"./mark":99,"./marks":100,"./modify":101,"./padding":102,"./predicates":103,"./properties":104,"./signals":105,"./spec":106,"./streams":107,"./transforms":108,"vega-event-selector":42}],98:[function(t,e,n){function r(t,e,n,r){(e||[]).forEach(function(e,o){n[o]=n[o]||a(t),i(e,o,n[o],r)})}function i(t,e,n,r){n.size(t.size?r.scale(t.size):null),n.shape(t.shape?r.scale(t.shape):null),n.fill(t.fill?r.scale(t.fill):null),n.stroke(t.stroke?r.scale(t.stroke):null),n.opacity(t.opacity?r.scale(t.opacity):null),t.orient&&n.orient(t.orient),null!=t.offset&&n.offset(t.offset),n.title(t.title||null),n.values(t.values||null),n.format(void 0!==t.format?t.format:null),n.formatType(t.formatType||null);var i=t.properties;n.titleProperties(i&&i.title||{}),n.labelProperties(i&&i.labels||{}),n.legendProperties(i&&i.legend||{}),n.symbolProperties(i&&i.symbols||{}),n.gradientProperties(i&&i.gradient||{})}var a=t("../scene/legend");e.exports=r},{"../scene/legend":116}],99:[function(t,e,n){function r(t,e,n){var o=e.properties||n&&(e.properties={}),s=o.enter||n&&(o.enter={}),u=e.marks,l=t.config().marks||{};if(n){"symbol"===e.type&&!s.size&&l.symbolSize&&(s.size={value:l.symbolSize});var c={arc:"fill",area:"fill",rect:"fill",symbol:"fill",text:"fill",line:"stroke",path:"stroke",rule:"stroke"},d=c[e.type];!s[d]&&l.color&&(s[d]={value:l.color})}return i.keys(o).forEach(function(n){o[n]=a(t,e.type,o[n])}),e.delay&&(e.delay=a(t,e.type,{delay:e.delay})),u&&(e.marks=u.map(function(e){return r(t,e,!0)})),e}var i=t("datalib"),a=t("./properties");e.exports=r},{"./properties":104,datalib:26}],100:[function(t,e,n){function r(t,e,n,r){return{type:"group",width:n,height:r,properties:i(e.scene||{},t),scales:e.scales||[],axes:e.axes||[],legends:e.legends||[],marks:(e.marks||[]).map(function(e){return a(t,e,!0)})}}function i(t,e){var n,r,i,a,u,l=e.config().scene,c={};for(n=0,r=i=s.length;n=0;--i){for(a=0;a 0;"}else e.range&&(e.scale&&(r+="if (scale.length == 2) {\n ordSet = scale(o1, o2);\n} else {\n o1 = scale(o1);\no2 = scale(o2);\n}"),r+="return ordSet !== null ? ordSet.indexOf(o0) !== -1 :\n o1 < o2 ? o1 <= o0 && o0 <= o2 : o2 <= o0 && o0 <= o1;");return{code:r,signals:i.signals,data:i.data.concat(e.data?[e.data]:[])}}function l(t,e){var n="var scale = ",r=e.length;return c.isString(t)?(e.push({value:t}),n+="this.root().scale(o"+r+")"):t.arg?(e.push(t),n+="o"+r):t.name&&(e.push(c.isString(t.name)?{value:t.name}:t.name),n+="(this.isFunction(o"+r+") ? o"+r+" : ",t.scope?(e.push(t.scope),n+="((o"+(r+1)+".scale || this.root().scale)(o"+r+") || this.nullScale)"):n+="this.root().scale(o"+r+")",n+=")"),t.invert===!0&&(n+=".invert"),n+";\n"}var c=t("datalib"),d={"=":o,"==":o,"!=":o,">":o,">=":o,"<":o,"<=":o,and:s,"&&":s,or:s,"||":s,in:u},f=function(){return 0};f.invert=f,e.exports=r},{datalib:26}],104:[function(t,e,n){(function(n){function r(t,e,n){function r(t){if(null!=v[t]){var e,n,r=h.array(v[t]);for(e=0,n=r.length;e0?"\n ":" ",v.rule?(v=s(t,d,v.rule,x),_+="\n "+v.code):h.isArray(v)?(v=s(t,d,v,x),_+="\n "+v.code):(v=u(y,d,v),_+="d += set(o, "+h.str(d)+", "+v.val+");"),w[d]=!0,m.forEach(r),k.reflow=k.reflow||v.reflow,v.nested.length&&v.nested.forEach(i);h.keys(k._nRefs).forEach(function(t){k.nested.push(k._nRefs[t])}),k.nested.sort(function(t,e){return t=t.level,e=e.level,te?1:t>=e?0:NaN}),w.x2&&(w.x?(_+="\n if (o.x > o.x2) { \n t = o.x;\n d += set(o, 'x', o.x2);\n d += set(o, 'x2', t); \n };",_+="\n d += set(o, 'width', (o.x2 - o.x));"):_+=w.width?"\n d += set(o, 'x', (o.x2 - o.width));":"\n d += set(o, 'x', o.x2);"),w.xc&&(_+=w.width?"\n d += set(o, 'x', (o.xc - o.width/2));":"\n d += set(o, 'x', o.xc);"),w.y2&&(w.y?(_+="\n if (o.y > o.y2) { \n t = o.y;\n d += set(o, 'y', o.y2);\n d += set(o, 'y2', t);\n };",_+="\n d += set(o, 'height', (o.y2 - o.y));"):_+=w.height?"\n d += set(o, 'y', (o.y2 - o.height));":"\n d += set(o, 'y', o.y2);"),w.yc&&(_+=w.height?"\n d += set(o, 'y', (o.yc - o.height/2));":"\n d += set(o, 'y', o.yc);"),a(e,w)&&(_+="\n d += (item.touch(), 1);"),_+="\n if (trans) trans.interpolate(item, o);",_+="\n return d > 0;";try{var M=Function("item","group","trans","db","signals","predicates",_);return M.tpl=g,M.exprs=x,M.util=h,M.d3=f,h.extend(M,h.template.context),{encode:M,signals:h.keys(k.signals),scales:h.keys(k.scales),data:h.keys(k.data),fields:h.keys(k.fields),nested:k.nested,reflow:k.reflow}}catch(t){p.error(t),p.log(_)}}function i(t,e){return h.isObject(t)||(t={reflow:!1,nested:[]},m.forEach(function(e){t[e]=[]})),h.isObject(e)&&(t.reflow=t.reflow||e.reflow,t.nested.push.apply(t.nested,e.nested),m.forEach(function(n){t[n].push.apply(t[n],e[n])})),t}function a(t,e){return e.path||("area"===t||"line"===t)&&(e.x||e.x2||e.width||e.y||e.y2||e.height||e.tension||e.interpolate)}function o(t,e,n){var r,i,a=n.shape,o=0;if(a&&(r=a.value)){for(e.shape&&e.shape[r]&&(r=e.shape[r]),a="";null!==(i=v.exec(r));)a+=r.substring(o,i.index),a+=t.expr(i[1]).fn(),o=v.lastIndex;n.shape.value=a+r.substring(o)}}function s(t,e,n,r){var a=t.config(),o=i(),s=[],l="";return(n||[]).forEach(function(c,d){var f=u(a,e,c);if(i(o,f),c.test){var p=t.expr(c.test);o.signals.push.apply(o.signals,p.globals),o.data.push.apply(o.data,p.dataSources),l+="if (exprs["+r.length+"](item.datum, item.mark.group.datum, null)) {\n d += set(o, "+h.str(e)+", "+f.val+");",l+=n[d+1]?"\n } else ":" }",r.push(p.fn)}else{var g=c.predicate,m=g&&(g.name||g),v=t.predicate(m),y="predicates["+h.str(m)+"]",_=[],b=e+"_arg"+d;h.isObject(g)&&h.keys(g).forEach(function(t){if("name"!==t){var e=u(a,d,g[t],!0);_.push(h.str(t)+": "+e.val),i(o,e)}}),m?(o.signals.push.apply(o.signals,v.signals),o.data.push.apply(o.data,v.data),s.push(b+" = {\n "+_.join(",\n ")+"\n }"),l+="if ("+y+".call("+y+","+b+", db, signals, predicates)) {\n d += set(o, "+h.str(e)+", "+f.val+");",l+=n[d+1]?"\n } else ":" }"):l+="{\n d += set(o, "+h.str(e)+", "+f.val+");\n }\n"}}),s.length&&(l="var "+s.join(",\n ")+";\n "+l),o.code=l,o}function u(t,e,n,r){if(null==n)return null;if("fill"===e||"stroke"===e){if(n.c)return l(t,"hcl",n.h,n.c,n.l);if(n.h||n.s)return l(t,"hsl",n.h,n.s,n.l);if(n.l||n.a)return l(t,"lab",n.l,n.a,n.b);if(n.r||n.g||n.b)return l(t,"rgb",n.r,n.g,n.b)}var a=null,o=null,s=i(),u=null,f=null,p=null,g={};return void 0!==n.template&&(a=h.template.source(n.template,"tmpl",g),h.keys(g).forEach(function(t){var e=h.field(t),n=e.shift();"parent"===n||"group"===n?s.nested.push({parent:"parent"===n,group:"group"===n,level:1}):"datum"===n?s.fields.push(e[0]):s.signals.push(n)})),void 0!==n.value&&(a=h.str(n.value)),void 0!==n.signal&&(u=h.field(n.signal),a="signals["+u.map(h.str).join("][")+"]",s.signals.push(u.shift())),void 0!==n.field&&(n.field=h.isString(n.field)?{datum:n.field}:n.field,f=c(n.field),a=f.val,i(s,f)),void 0!==n.scale&&(p=d(n.scale),o=p.val,i(s,p),s.scales.push(n.scale.name||n.scale),null!==a||n.band||n.mult||n.offset||!r?a=o+(n.band?".rangeBand()":"("+(null!==a?a:"item.datum.data")+")"):r&&(a=o)),a="("+(n.mult?h.number(n.mult)+" * ":"")+a+")"+(n.offset?" + "+h.number(n.offset):""),s.val=a,s}function l(t,e,n,r,a){var o=n?u(t,"",n):t.color[e][0],s=r?u(t,"",r):t.color[e][1],l=a?u(t,"",a):t.color[e][2],c=i();[o,s,l].forEach(function(t){h.isArray||i(c,t)});var d="(this.d3."+e+"("+[o.val,s.val,l.val].join(",")+') + "")';return c.val=d,c}function c(t){if(h.isString(t))return{val:h.field(t).map(h.str).join("][")};var e=t.level||1,n=(t.group||t.parent)&&e,r=n?Array(e).join("group.mark."):"",a=c(t.datum||t.group||t.parent||t.signal),o=a.val,s=i(null,a);return t.datum?(o="item.datum["+o+"]",s.fields.push(t.datum)):t.group?(o=r+"group["+o+"]",s.nested.push({level:e,group:!0})):t.parent?(o=r+"group.datum["+o+"]",s.nested.push({level:e,parent:!0})):t.signal&&(o="signals["+o+"]",s.signals.push(h.field(t.signal)[0]),s.reflow=!0),s.val=o,s}function d(t){var e=null,n=null,r=i();return e=h.isString(t)?h.str(t):t.name?h.isString(t.name)?h.str(t.name):(n=c(t.name)).val:(n=c(t)).val,e="(item.mark._scaleRefs["+e+"] = 1, group.scale("+e+"))",t.invert&&(e+=".invert"),n&&n.nested.forEach(function(t){t.scale=!0}),n?(n.val=e,n):(r.val=e,r)}var f="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,h=t("datalib"),p=t("vega-logging"),g=t("vega-dataflow").Tuple,m=["signals","scales","data","fields"],v=/{{(.*?)}}/g;e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{datalib:26,"vega-dataflow":41,"vega-logging":48}],105:[function(t,e,n){function r(t,e){return(e||[]).forEach(function(e){if(u.indexOf(e.name)!==-1)throw Error('Signal name "'+e.name+'" is a reserved keyword ('+u.join(", ")+").");var n=t.signal(e.name,e.init).verbose(e.verbose);e.init&&e.init.expr&&(e.init.expr=t.expr(e.init.expr),n.value(i(t,e.init))),e.expr&&(e.expr=t.expr(e.expr),n.evaluate=function(r){var a=i(t,e),o=r.signals;return(a!==n.value()||n.verbose())&&(n.value(a),o[e.name]=1),o[e.name]?r:t.doNotPropagate},n.dependency(s,e.expr.globals),e.expr.globals.forEach(function(e){t.signal(e).addListener(n)}))}),e}function i(t,e){var n=e.expr,i=n.fn();return e.scale?r.scale(t,e,i):i}var a=t("datalib"),o=t("./expr"),s=t("vega-dataflow").Dependencies.SIGNALS,u=["datum","event","signals","width","height","padding"].concat(a.keys(o.codegen.functions));r.scale=function(t,e,n,r,i){var s,u=e.scale,l=u.name||u.signal||u,c=u.scope;return c&&(c.signal?c=t.signalRef(c.signal):a.isString(c)&&(s=u._expr=u._expr||t.expr(c),c=s.fn(r,i))),o.scale(t,u.invert,l,n,c)},e.exports=r},{"./expr":96,datalib:26,"vega-dataflow":41}],106:[function(t,e,n){function r(e){function n(e){try{e=i.duplicate(e);var n=t("./"),a=o(e,c,"width",500),s=o(e,c,"height",500),u=n.padding(o(e,c,"padding")),d=o(e,c,"background");p.signal("width",a),p.signal("height",s),p.signal("padding",u),r(e),p.defs({width:a,height:s,padding:u,viewport:e.viewport||null,background:n.background(d),signals:n.signals(p,e.signals),predicates:n.predicates(p,e.predicates),marks:n.marks(p,e,a,s),data:n.data(p,e.data,l)})}catch(t){l(t)}}function r(t){var e,n=t.signals||(t.signals=[]);n.some(function(t){return"cursor"===t.name&&(e=t,!0)}),e||n.push(e={name:"cursor",streams:[]}),e.init=e.init||{},e.streams.unshift({type:"mousemove",expr:"eventItem().cursor === cursor.default ? cursor : {default: eventItem().cursor}"})}function l(t){var e;t?a.error(t):e=g(p.buildIndexes()),h&&(h.length>1?h(t,e):t||h(e),h=null)}var c,d=arguments.length,f=2,h=arguments[d-1],p=new s,g=u.factory;if(d>f&&i.isFunction(arguments[d-f])&&(g=arguments[d-f],++f),d>f&&i.isObject(arguments[d-f])&&p.config(arguments[d-f]),c=p.config(),i.isObject(e))n(e);else if(i.isString(e)){var m=i.extend({url:e},c.load);i.json(m,function(t,e){t?l("SPECIFICATION LOAD FAILED: "+t):n(e)})}else l("INVALID SPECIFICATION: Must be a valid JSON object or URL.")}var i=t("datalib"),a=t("vega-logging"),o=t("../util/theme-val"),s=t("../core/Model"),u=t("../core/View");e.exports=r},{"../core/Model":89,"../core/View":90,"../util/theme-val":149,"./":97,datalib:26,"vega-logging":48}],107:[function(t,e,n){(function(n){function r(t){function e(e,n){var r,a,o,s=i.mouse((i.event=e,t.renderer().scene())),u=t.padding(),l={};if(n)for(r=n.mark,a="group"===r.marktype?n:r.group,o=n;null!=o;o=o.mark.group)o.mark.def.name&&(l[o.mark.def.name]=o);l.root=t.model().scene().items[0],e.vg=Object.create(d),e.vg.group=a,e.vg.item=n||{},e.vg.name=l,e.vg.x=s[0]-u.left,e.vg.y=s[1]-u.top}function n(t,e,n,r,i){function a(t){return!t.fn(n,r,i)}var s,l,c,d,f=t.handlers[e],h=t.nodes[e],p=o.ChangeSet.create(null,!0),m=!1;for(l=0,c=f.length;l0,y=h.bounds,_=!y||t.rem.length;if("line"===p||"area"===p)l.mark(h,null,g&&!v);else if(t.add.forEach(function(t){l.item(t),_=_||y&&!y.encloses(t.bounds)}),t.mod.forEach(function(t){_=_||y&&y.alignsWith(t.bounds),l.item(t)}),_)for(y=h.bounds&&h.bounds.clear()||(h.bounds=new c),e=0,n=m.length;e0&&(n+="|"),n+=String(e[r](t));return n}}var u=t("datalib"),l=t("vega-logging"),c=t("vega-scenegraph").Item,d=t("vega-dataflow"),f=d.Node,h=d.Dependencies,p=d.Tuple,g=d.ChangeSet,m={},v=t("./Encoder"),y=t("./Bounder"),_=t("../parse/data"),b=r.STATUS={ENTER:"enter",UPDATE:"update",EXIT:"exit"},x=1,w=2,k=r.prototype=new f;k.init=function(t,e,n,r,a,o){return f.prototype.init.call(this,t).router(!0).collector(!0),this._def=e,this._mark=n,this._from=(e.from?e.from.data:null)||o,this._ds=u.isString(this._from)?t.data(this._from):null,this._map={},this._status=null,n.def=e,n.marktype=e.type,n.interactive=e.interactive!==!1,n.items=[],u.isValid(e.name)&&(n.name=e.name),this._parent=r,this._parent_id=a,e.from&&(e.from.mark||e.from.transform||e.from.modify)&&i.call(this),this._isSuper="group"!==this._def.type,this._encoder=new v(this._graph,this._mark,this),this._bounder=new y(this._graph,this._mark),this._output=null,this._ds&&this._encoder.dependency(h.DATA,this._from),this.dependency(h.DATA,this._encoder.dependency(h.DATA)),this.dependency(h.SCALES,this._encoder.dependency(h.SCALES)),this.dependency(h.SIGNALS,this._encoder.dependency(h.SIGNALS)),this},k.ds=function(){return this._ds},k.parent=function(){return this._parent},k.encoder=function(){return this._encoder},k.pipeline=function(){return[this]},k.connect=function(){var t=this;return this._graph.connect(this.pipeline()),this._encoder._scales.forEach(function(e){(e=t._parent.scale(e))&&e.addListener(t)}),this._parent&&(this._isSuper?this.addListener(this._parent._collector):this._bounder.addListener(this._parent._collector)),this._status=x,this},k.disconnect=function(){function t(t){for(var n,r=0,i=t.length;rthis._stamp?o.call(this,n,f,this._ds.values(),!0,e):e&&(f.mod=this._mark.items.slice())}else r=u.isFunction(this._def.from)?this._def.from():[m],o.call(this,t,f,r);return this._output=f=this._graph.evaluate(f,this._encoder),d.nested&&d.nested.length&&this._status===x&&u.keys(this._mark._scaleRefs).forEach(function(t){var e=a._parent.scale(t);e&&(e.addListener(a),a.dependency(h.SCALES,t),a._encoder.dependency(h.SCALES,t))}),this._isSuper&&(f.mod=f.mod.filter(function(t){return t._dirty}),f=this._graph.evaluate(f,this._bounder)),f},e.exports=r},{"../parse/data":95,"./Bounder":109,"./Encoder":111,datalib:26,"vega-dataflow":41,"vega-logging":48,"vega-scenegraph":49}],111:[function(t,e,n){function r(t,e,n){var r=e.def.properties||{},i=r.enter,a=r.update,o=r.exit;c.prototype.init.call(this,t),this._mark=e,this._builder=n;var s=this._scales=[];return i&&s.push.apply(s,i.scales),a&&(this.dependency(d.DATA,a.data),this.dependency(d.SIGNALS,a.signals),this.dependency(d.FIELDS,a.fields),this.dependency(d.SCALES,a.scales),s.push.apply(s,a.scales)),o&&s.push.apply(s,o.scales),this.mutates(!0)}function i(t,e,n,r){var i,a,o,s=n.add.length;return(i=r.enter)&&(a=i[t]).length&&s&&(o=e.values(t,a,o=o||{})),(i=r.exit)&&(a=i[t]).length&&n.rem.length&&(o=e.values(t,a,o=o||{})),(i=r.update)&&(a=i[t]).length&&(s||n.mod.length)&&(o=e.values(t,a,o=o||{})),o||h}function a(t,e,n,r,i,a,o){var s=t.encode,u=e._dirty,l=s.call(s,e,e.mark.group||e,n,r,i,a);e._dirty=l||u,l&&!u&&o.push(e)}function o(){for(var t,e,n,r=this._mark.def.properties.update.nested,i=this._builder,a=0,o=0,s=r.length;othis._stamp)return!0}return!1}var s=t("datalib"),u=t("vega-logging"),l=t("vega-dataflow"),c=l.Node,d=l.Dependencies,f=t("vega-scenegraph").bound,h={},p=r.prototype=new c;p.evaluate=function(e){u.debug(e,["encoding",this._mark.def.type]);var n,r,o,s,l=this._graph,c=this._mark.def.properties||{},f=this._mark.items,p=c.enter,g=c.update,m=c.exit,v=e.dirty,y=l.predicates(),_=e.request,b=this._mark.group,x=b&&(b.mark.axis||b.mark.legend),w=h,k=h;if(_&&!x){if((s=c[_])&&e.mod.length)for(w=s.data?l.values(d.DATA,s.data):null,k=s.signals?l.values(d.SIGNALS,s.signals):null,n=0,r=e.mod.length;n0,y=c.array(this._def.axes).length>0,_=c.array(this._def.legends).length>0,b=!1;for(a=0,f=t.add.length;a=0;--a)for(h=t.add[a],o=this._children[h._id].length-1;o>=0;--o)d=this._children[h._id][o],d.builder.connect(),p=d.builder.pipeline(),g=d.builder._def,b=g.type!==x.GROUP,b=b&&void 0!==this._graph.data(d.from),b=b&&1===p[p.length-1].listeners().length,b=b&&g.from&&!g.from.mark,d.inline=b,b?this._graph.evaluate(t,d.builder):this._recursor.addListener(d.builder);for(a=0,f=t.mod.length;ae[1]?(p=e[1]||0,e=[p+(v*y+_),p]):(p=e[0]||0,e=[p,p+(v*y+_)]),a.reverse&&(e=e.reverse())}i="string"==typeof e[0],i||e.length>2||1===e.length||u?(t.range(e),h=!1):d&&f?t.rangeRoundPoints(e,l):d?t.rangePoints(e,l):f?t.rangeRoundBands(e,l,c):t.rangeBands(e,l,c),o.range=e,this._updated=!0}!t.invert&&h&&s(t)}function s(t){t.invert=function(e,n){var r=t.range(),i=r[0]c&&(l=c),(i?b.range(u,l+1):b.range(l,u-1,-1)).map(function(t){return s[t]})}}}function u(t,e,n){var r,i,a=this._def,o=t._prev,s=m.call(this,a.round),u=m.call(this,a.exponent),l=m.call(this,a.clamp),c=m.call(this,a.nice);r=a.type===E.QUANTILE?g.call(this,A.DOMAIN,a.domain,t,n):v.call(this,t,n),r&&!b.equal(o.domain,r)&&(t.domain(r),o.domain=r,this._updated=!0),"height"===m.call(this,a.range)&&(e=e.reverse()),e&&!b.equal(o.range,e)&&(t[s&&t.rangeRound?"rangeRound":"range"](e),o.range=e,this._updated=!0),u&&a.type===E.POWER&&t.exponent(u),l&&t.clamp(!0),c&&(a.type===E.TIME?(i=_.time[c],i||w.error("Unrecognized interval: "+i),t.nice(i)):t.nice())}function l(t){return t.type===E.ORDINAL||t.type===E.QUANTILE}function c(t){return t.fields||b.array(t)}function d(t){return t.some(function(t){return!t.data||t.data&&b.array(t.field).some(function(t){return t.parent})})}function f(t,e){return b.array(t.field).map(function(t){return t.parent?b.accessor(t.parent)(e.datum):t})}function h(t,e){var n=c(t);return 1==n.length&&1==b.array(n[0].field).length?S.TYPES.TUPLE:l(e)&&b.isObject(t.sort)?S.TYPES.MULTI:S.TYPES.VALUE}function p(t,e,n,r){var i=c(e),a=d(i),o=h(e,n),s=l(n),u=e.sort,p="_"+t,g=f(i[0],r);if(n[p]||this[p])return n[p]||this[p];var m,v,y=new S(this._graph).type(o);return a?n[p]=y:this[p]=y,s?o===S.TYPES.VALUE?(m=[{name:A.GROUPBY,get:b.identity}],v={"*":A.COUNT}):o===S.TYPES.TUPLE?(m=[{name:A.GROUPBY,get:b.$(g[0])}],v=b.isObject(u)?[{field:A.VALUE,get:b.$(u.field),ops:[u.op]}]:{"*":A.COUNT}):(m=A.GROUPBY,v=[{field:A.VALUE,ops:[u.op]}]):(m=[],v=[{field:A.VALUE,get:o==S.TYPES.TUPLE?b.$(g[0]):b.identity,ops:[A.MIN,A.MAX],as:[A.MIN,A.MAX]}]),y.param("groupby",m).param("summarize",v),y._lastUpdate=-1,y}function g(t,e,n,r){function i(t){k.dependency(M.SIGNALS,t)}if(null==e)return[];if(b.isArray(e))return e.map(m.bind(this));var a,o,s,u,g,v,y,_,x,w,k=this,T=this._graph,E=c(e),L=d(E),C=h(e,n),D=p.apply(this,arguments),P=e.sort,I=l(n);if(L||!L&&D._lastUpdate1&&(r=1),i=d.ease(r),a=0,s=d.length;a1?+e:I,o=r>0?+arguments[r]:I;return I===i&&N===a&&O===o||n(),I=i,N=a,O=o,X},X.tickSubdivide=function(t){return arguments.length?(R=+t,X):R},X.offset=function(t){return arguments.length?(E=m.isObject(t)?t:+t,X):E},X.tickPadding=function(t){return arguments.length?(z!==+t&&(z=+t,n()),X):z},X.titleOffset=function(t){return arguments.length?(A!==t&&(A=t,n()),X):A},X.layer=function(t){return arguments.length?(C!==t&&(C=t,n()),X):C},X.grid=function(t){return arguments.length?(D!==t&&(D=t,n()),X):D},X.gridLineProperties=function(t){return arguments.length?(B!==t&&(B=t),X):B},X.majorTickProperties=function(t){return arguments.length?($!==t&&($=t),X):$},X.minorTickProperties=function(t){return arguments.length?(H!==t&&(H=t),X):H},X.tickLabelProperties=function(t){return arguments.length?(G!==t&&(G=t),X):G},X.titleProperties=function(t){return arguments.length?(Y!==t&&(Y=t),X):Y},X.domainProperties=function(t){return arguments.length?(V!==t&&(V=t),X):V},X.reset=function(){return n(),X},X}function i(t,e,n){var r=[];if(n&&e.length>1){for(var i,o,s=a(t.domain()),u=-1,l=e.length,c=(e[1]-e[0])/++n;++u0;)(o=+e[u]-i*c)>=s[0]&&r.push(o);for(--u,i=0;++i=0&&(i.y={value:o*r})):(i.y={value:a},i.angle={value:"left"===t?-90:90},r>=0&&(i.x={value:o*r}))}function c(t,e,n,r){var i;"top"!==t&&"left"!==t||(r=-1*r),i="bottom"===t||"top"===t?"M"+n[0]+","+r+"V0H"+n[1]+"V"+r:"M"+r+","+n[0]+"H0V"+n[1]+"H"+r,e.properties.update.path={value:i}}function d(t,e,n){var r=n?{}:t,i=t.mark.def.offset,a=t.mark.def.orient,o=e.width,s=e.height;if(m.isArray(i)){var u=i[0],l=i[1];switch(a){case"left":v.set(r,"x",-u),v.set(r,"y",l);break;case"right":v.set(r,"x",o+u),v.set(r,"y",l);break;case"bottom":v.set(r,"x",u),v.set(r,"y",s+l);break;case"top":v.set(r,"x",u),v.set(r,"y",-l);break;default:v.set(r,"x",u),v.set(r,"y",l)}}else switch(m.isObject(i)&&(i=-e.scale(i.scale)(i.value)),a){case"left":v.set(r,"x",-i),v.set(r,"y",0);break;case"right":v.set(r,"x",o+i),v.set(r,"y",0);break;case"bottom":v.set(r,"x",0),v.set(r,"y",s+i);break;case"top":v.set(r,"x",0),v.set(r,"y",-i);break;default:v.set(r,"x",0),v.set(r,"y",0)}return n&&n.interpolate(t,r),!0}function f(t){return{type:"rule",interactive:!1,key:"data",properties:{enter:{stroke:{value:t.tickColor},strokeWidth:{value:t.tickWidth},opacity:{value:1e-6}},exit:{opacity:{value:1e-6}},update:{opacity:{value:1}}}}}function h(t){return{type:"text",interactive:!0,key:"data",properties:{enter:{fill:{value:t.tickLabelColor},font:{value:t.tickLabelFont},fontSize:{value:t.tickLabelFontSize},opacity:{value:1e-6},text:{field:"label"}},exit:{opacity:{value:1e-6}},update:{opacity:{value:1}}}}}function p(t){return{type:"text",interactive:!0,properties:{enter:{font:{value:t.titleFont},fontSize:{value:t.titleFontSize},fontWeight:{value:t.titleFontWeight},fill:{value:t.titleColor},align:{value:"center"},baseline:{value:"middle"},text:{field:"data"}},update:{}}}}function g(t){return{type:"path",interactive:!1,properties:{enter:{x:{value:.5},y:{value:.5},stroke:{value:t.axisColor},strokeWidth:{value:t.axisWidth}},update:{}}}}var m=t("datalib"),v=t("vega-dataflow").Tuple,y=t("../parse/mark"),_=t("../util"),b=new(t("vega-scenegraph").Bounds),x="ordinal",w={top:1,right:1,bottom:1,left:1},k={bottom:"center",top:"center",left:"right",right:"left"},M={bottom:"top",top:"bottom",left:"middle",right:"middle"};e.exports=r},{"../parse/mark":99,"../util":148,datalib:26,"vega-dataflow":41,"vega-scenegraph":49}],116:[function(t,e,n){(function(n){function r(t){function e(){$.type=null}function n(t,e){return{data:t,index:e}}function r(t){return"ordinal"===t||"quantize"===t||"quantile"===t||"threshold"===t}function y(t){var e,r,i,a=_(w,k,M,S,T),o=(null==A?t.ticks?t.ticks.apply(t,z):t.domain():A).map(n),s=m.getTickFormat(t,o.length,C,L),u=5,l=d.range(o.length);w?(r=o.map(function(t){return Math.sqrt(w(t.data))}),i=d.max(r),r=r.reduce(function(t,e,n,r){return n>0&&(t[n]=t[n-1]+r[n-1]/2+u),t[n]+=e/2,t},[0]).map(Math.round)):(i=Math.round(Math.sqrt(P.symbolSize)),r=E||(e=q.fontSize)&&e.value+u||P.labelFontSize+u,r=l.map(function(t,e){return Math.round(i/2+e*r)}));var c,h=O;D&&(c=R.fontSize,h+=5+(c&&c.value||P.titleFontSize));for(var p=0,g=r.length;p"},summarize:{type:"custom",set:function(t){function e(t){t.signal&&(l[t.signal]=1)}var n,r,i,o,s,u,l={},d=this._transform;if(!a.isArray(o=t)){o=[];for(s in t)u=a.array(t[s]),o.push({field:s,ops:u})}for(n=0,r=o.length;n",default:[5,2]}}),this._output={start:"bin_start",end:"bin_end",mid:"bin_mid"},this.mutates(!0)}var i=t("datalib"),a=t("vega-dataflow").Tuple,o=t("vega-logging"),s=t("./Transform"),u=t("./BatchTransform"),l=r.prototype=Object.create(u.prototype);l.constructor=r,l.extent=function(t){var e,n=[this.param("min"),this.param("max")];return null!=n[0]&&null!=n[1]||(e=i.extent(t,this.param("field").accessor),null==n[0]&&(n[0]=e[0]),null==n[1]&&(n[1]=e[1])),n},l.batchTransform=function(t,e){function n(t){var e=d(t);e=null==e?null:h.start+p*~~((e-h.start)/p),a.set(t,s.start,e),a.set(t,s.end,e+p),a.set(t,s.mid,e+p/2)}o.debug(t,["binning"]);var r=this.extent(e),s=this._output,u=this.param("step"),l=this.param("steps"),c=this.param("minstep"),d=this.param("field").accessor,f={min:r[0],max:r[1],base:this.param("base"),maxbins:this.param("maxbins"),div:this.param("div")};u&&(f.step=u),l&&(f.steps=l),c&&(f.minstep=c);var h=i.bins(f),p=h.step;return t.add.forEach(n),t.mod.forEach(n),t.rem.forEach(n),t.fields[s.start]=1,t.fields[s.end]=1,t.fields[s.mid]=1,t},e.exports=r},{"./BatchTransform":119,"./Transform":140,datalib:26,"vega-dataflow":41,"vega-logging":48}],121:[function(t,e,n){function r(t){return s.prototype.init.call(this,t),s.addParameters(this,{field:{type:"field",default:"data"},pattern:{type:"value",default:"[\\w']+"},case:{type:"value",default:"lower"},stopwords:{type:"value",default:""}}),this._output={text:"text",count:"count"},this.router(!0).produces(!0)}var i=t("vega-dataflow"),a=i.Tuple,o=t("vega-logging"),s=t("./Transform"),u=r.prototype=Object.create(s.prototype);u.constructor=r,u.transform=function(t,e){function n(t){return a.prev_init(t),i(t)}function r(t){return i(a.prev(t))}o.debug(t,["countpattern"]);var i=this.param("field").accessor,s=this.param("pattern"),u=this.param("stopwords"),l=!1;return this._stop!==u&&(this._stop=u,this._stop_re=new RegExp("^"+u+"$","i"),e=!0),this._pattern!==s&&(this._pattern=s,this._match=new RegExp(this._pattern,"g"),e=!0),e&&(this._counts={}),this._add(t.add,n),e||this._rem(t.rem,r),(e||(l=t.fields[i.field]))&&(l&&this._rem(t.mod,r),this._add(t.mod,n)),this._changeset(t)},u._changeset=function(t){var e,n,r,o=this._counts,s=this._tuples||(this._tuples={}),u=i.ChangeSet.create(t),l=this._output;for(e in o)n=s[e],r=o[e]||0,!n&&r?(s[e]=n=a.ingest({}),n[l.text]=e,n[l.count]=r,u.add.push(n)):0===r?(n&&u.rem.push(n),delete o[e],delete s[e]):n[l.count]!==r&&(a.set(n,l.count,r),u.mod.push(n));return u},u._tokenize=function(t){switch(this.param("case")){case"upper":t=t.toUpperCase();break;case"lower":t=t.toLowerCase()}return t.match(this._match)},u._add=function(t,e){var n,r,i,a,o=this._counts,s=this._stop_re;for(i=0;i=0;--c)if(d=_[c],h=l===d[g.left],f=h?d[g.right]:d[g.left],p=a(h,l,f),m[f._id])if(!i||i(d)){if(s[d._id])continue;x.push(d),s[d._id]=1}else u[d._id]||w.push.apply(w,_.splice(c,1)),u[d._id]=1,v[p]=!1,y.f=!0;else v[p]=!1,_.splice(c,1);i&&b&&o.call(this,t,e,n,r,i,s,l)}function u(t,e,n,r){var i,o,s,u,l,c=this._output,d=this._cache[r._id],f=this._cids,h=t.rem;if(d){for(i=0,o=d.c.length;ithis._lastWith&&(h.rem.forEach(u.bind(this,m,!1,y)),h.add.forEach(o.bind(this,m,!1,e,i,c,v)),h.mod.forEach(s.bind(this,m,!1,e,i,c,v,y)),this._lastWith=h.stamp),t.mod.forEach(s.bind(this,m,!0,g,i,c,v,y))),m.fields[a.left]=1,m.fields[a.right]=1,m},e.exports=r},{"./BatchTransform":119,"./Transform":140,datalib:26,"vega-dataflow":41,"vega-logging":48}],123:[function(t,e,n){function r(e){return i.addParameters(this,{transform:{type:"custom",set:function(t){return this._transform._pipeline=t,this._transform},get:function(){var e=t("../parse/transforms"),n=this._transform;return n._pipeline.map(function(t){return e(n._graph,t)})}}}),this._pipeline=[],a.call(this,e)}var i=t("./Transform"),a=t("./Aggregate"),o=r.prototype=Object.create(a.prototype);o.constructor=r,o.aggr=function(){return a.prototype.aggr.call(this).facet(this)},o.transform=function(t,e){var n=a.prototype.transform.call(this,t,e);return t.add.length&&this.listeners()[0].rerank(),n},e.exports=r},{"../parse/transforms":108,"./Aggregate":118,"./Transform":140}],124:[function(t,e,n){function r(){o.call(this),this._facet=null,this._facetID=++d}function i(t){c.debug({},["disconnecting cell",this.tuple._id]);var e=this.ds.pipeline();t.removeListener(e[0]),t._graph.removeListener(e[0]),t._graph.disconnect(e)}var a=t("datalib"),o=a.Aggregator,s=o.prototype,u=t("vega-dataflow"),l=u.Tuple,c=t("vega-logging"),d=0,f=r.prototype=Object.create(s);f.constructor=r,f.facet=function(t){return arguments.length?(this._facet=t,this):this._facet},f._ingest=function(t){return l.ingest(t,null)},f._assign=l.set,f._newcell=function(t,e){var n=s._newcell.call(this,t,e),r=this._facet;if(r){var a=r._graph,o=n.tuple,u=r.param("transform");n.ds=a.data(o._facetID,u,o),n.disconnect=i,r.addListener(u[0])}return n},f._newtuple=function(t,e){var n=s._newtuple.call(this,t);return this._facet&&(l.set(n,"key",e),l.set(n,"_facetID",this._facetID+"_"+e)),n},f.clear=function(){if(this._facet)for(var t in this._cells)this._cells[t].disconnect(this._facet);return s.clear.call(this)},f._on_add=function(t,e){this._facet&&e.ds._input.add.push(t)},f._on_rem=function(t,e){this._facet&&e.ds._input.rem.push(t)},f._on_mod=function(t,e,n,r){this._facet&&(n===r?n.ds._input.mod.push(t):(n.ds._input.rem.push(t),r.ds._input.add.push(t)))},f._on_drop=function(t){this._facet&&t.disconnect(this._facet)},f._on_keep=function(t){this._facet&&u.ChangeSet.copy(this._input,t.ds._input)},e.exports=r},{datalib:26,"vega-dataflow":41,"vega-logging":48}],125:[function(t,e,n){function r(t){return o.prototype.init.call(this,t),o.addParameters(this,{test:{type:"expr"}}),this._skip={},this.router(!0)}var i=t("vega-dataflow"),a=t("vega-logging"),o=t("./Transform"),s=r.prototype=Object.create(o.prototype);s.constructor=r,s.transform=function(t){a.debug(t,["filtering"]);var e=i.ChangeSet.create(t),n=this._skip,r=this.param("test");return t.rem.forEach(function(t){1!==n[t._id]?e.rem.push(t):n[t._id]=0}),t.add.forEach(function(t){r(t)?e.add.push(t):n[t._id]=1}),t.mod.forEach(function(t){var i=r(t),a=1===n[t._id];i&&a?(n[t._id]=0,e.add.push(t)):i&&!a?e.mod.push(t):!i&&a||(e.rem.push(t),n[t._id]=1)}),e},e.exports=r},{"./Transform":140,"vega-dataflow":41,"vega-logging":48}],126:[function(t,e,n){function r(t){return s.prototype.init.call(this,t),s.addParameters(this,{fields:{type:"array"}}),this._output={key:"key",value:"value"},this._cache={},this.router(!0).produces(!0)}var i=t("vega-dataflow"),a=i.Tuple,o=t("vega-logging"),s=t("./Transform"),u=r.prototype=Object.create(s.prototype);u.constructor=r,u._reset=function(t,e){for(var n in this._cache)e.rem.push.apply(e.rem,this._cache[n]);this._cache={}},u._tuple=function(t,e,n){var r=this._cache[t._id]||(this._cache[t._id]=Array(n));return r[e]?a.rederive(t,r[e]):r[e]=a.derive(t)},u._fn=function(t,e,n){var r,i,o,s,u,l;for(r=0,o=t.length;r",default:t("./screen").size},bound:{type:"value",default:!0},links:{type:"data"},linkStrength:{type:"value",default:1},linkDistance:{type:"value",default:20},charge:{type:"value",default:-30},chargeDistance:{type:"value",default:1/0},friction:{type:"value",default:.9},theta:{type:"value",default:.8},gravity:{type:"value",default:.1},alpha:{type:"value",default:.1},iterations:{type:"value",default:500},interactive:{type:"value",default:this._interactive},active:{type:"value",default:this._prev},fixed:{type:"data"}}),this._output={x:"layout_x",y:"layout_y"},this.mutates(!0)}var i="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,a=t("vega-dataflow"),o=a.Tuple,s=a.ChangeSet,u=t("vega-logging"),l=t("./Transform"),c=r.prototype=Object.create(l.prototype);c.constructor=r,c.transform=function(t,e){u.debug(t,["force"]),e-=t.signals.active?1:0;var n=this.param("interactive"),r=this.param("links").source,i=r.last(),a=this.param("active"),s=this._output,l=this._layout,c=this._nodes,d=this._links;if(i.stamp"},translate:{type:"array",default:t("./screen").center},rotate:{type:"array"},scale:{type:"value"},precision:{type:"value"},clipAngle:{type:"value"},clipExtent:{type:"value"}},r.d3Projection=function(){var t,e,n,o=this.param("projection"),s=r.Parameters;o!==this._mode&&(this._mode=o,this._projection=i.geo[o]()),t=this._projection;for(e in s)"projection"!==e&&t[e]&&(n=this.param(e),void 0===n||a.isArray(n)&&0===n.length||n!==t[e]()&&t[e](n));return t};var l=r.prototype=Object.create(u.prototype);l.constructor=r,l.transform=function(t){function e(t){var e=[i(t),a(t)],r=u(e)||[null,null];o.set(t,n.x,r[0]),o.set(t,n.y,r[1])}s.debug(t,["geo"]);var n=this._output,i=this.param("lon").accessor,a=this.param("lat").accessor,u=r.d3Projection.call(this);return t.add.forEach(e),this.reevaluate(t)&&(t.mod.forEach(e),t.rem.forEach(e)),t.fields[n.x]=1,t.fields[n.y]=1,t},e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./Transform":140,"./screen":146,datalib:26,"vega-dataflow":41,"vega-logging":48}],130:[function(t,e,n){(function(n){function r(t){return l.prototype.init.call(this,t),l.addParameters(this,u.Parameters),l.addParameters(this,{field:{type:"field",default:null}}),this._output={path:"layout_path"},this.mutates(!0)}var i="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,a=t("datalib"),o=t("vega-dataflow").Tuple,s=t("vega-logging"),u=t("./Geo"),l=t("./Transform"),c=r.prototype=Object.create(l.prototype);c.constructor=r,c.transform=function(t){function e(t){o.set(t,n.path,c(r(t)))}s.debug(t,["geopath"]);var n=this._output,r=this.param("field").accessor||a.identity,l=u.d3Projection.call(this),c=i.geo.path().projection(l);return t.add.forEach(e),this.reevaluate(t)&&(t.mod.forEach(e),t.rem.forEach(e)),t.fields[n.path]=1,t},e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./Geo":129,"./Transform":140,datalib:26,"vega-dataflow":41,"vega-logging":48}],131:[function(t,e,n){(function(n){function r(e){return l.prototype.init.call(this,e),u.addParameters(this,{sort:{type:"array",default:null},children:{type:"field",default:"children"},parent:{type:"field",default:"parent"},field:{type:"value",default:null},mode:{type:"value",default:"tidy"},size:{type:"array",default:t("./screen").size},nodesize:{type:"array",default:null},orient:{type:"value",default:"cartesian"}}),this._mode=null,this._output={x:"layout_x",y:"layout_y",width:"layout_width",height:"layout_height",depth:"layout_depth"},this.mutates(!0)}var i="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,a=t("datalib"),o=t("vega-dataflow").Tuple,s=t("vega-logging"),u=t("./Transform"),l=t("./BatchTransform"),c="partition",d={cartesian:function(t,e){return t.parent===e.parent?1:2},radial:function(t,e){return(t.parent===e.parent?1:2)/t.depth}},f=r.prototype=Object.create(l.prototype);f.constructor=r,f.batchTransform=function(t,e){s.debug(t,["hierarchy layout"]);var n=this._layout,r=this._output,u=this.param("mode"),l=this.param("sort"),f=this.param("nodesize"),h=this.param("parent").accessor,p=e.filter(function(t){return null===h(t)})[0];return u!==this._mode&&(this._mode=u,"tidy"===u&&(u="tree"),n=this._layout=i.layout[u]()),t.fields[r.x]=1,t.fields[r.y]=1,t.fields[r.depth]=1,u===c?(t.fields[r.width]=1,t.fields[r.height]=1,n.value(this.param("field").accessor)):n.separation(d[this.param("orient")]),f.length&&u!==c?n.nodeSize(f):n.size(this.param("size")),n.sort(l.field.length?a.comparator(l.field):null).children(this.param("children").accessor).nodes(p),e.forEach(function(t){o.set(t,r.x,t.x),o.set(t,r.y,t.y),o.set(t,r.depth,t.depth),u===c&&(o.set(t,r.width,t.dx),o.set(t,r.height,t.dy))}),t},e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./BatchTransform":119,"./Transform":140,"./screen":146,datalib:26,"vega-dataflow":41,"vega-logging":48}],132:[function(t,e,n){function r(t){return c.prototype.init.call(this,t),l.addParameters(this,{groupby:{type:"array"},orderby:{type:"array"},field:{type:"field"},method:{type:"value",default:"value"},value:{type:"value",default:0}}),this.router(!0).produces(!0)}function i(t,e,n,r){var i,a={_imputed:!0};for(i=0;iMath.PI?n<=t:n>t;return"M"+e*i+","+e*a+"A"+e+","+e+" 0 0,"+(u?1:0)+" "+e*o+","+e*s+"L"+r*o+","+r*s}function l(t,e,n,r){var i=(t+n)/2;return"M"+t+","+e+"C"+i+","+e+" "+i+","+r+" "+n+","+r}function c(t,e,n,r){var i=(e+r)/2;return"M"+t+","+e+"C"+t+","+i+" "+n+","+i+" "+n+","+r}function d(t,e,n,r){var i=Math.cos(t),a=Math.sin(t),o=Math.cos(n),s=Math.sin(n),u=(e+r)/2;return"M"+e*i+","+e*a+"C"+u*i+","+u*a+" "+u*o+","+u*s+" "+r*o+","+r*s}var f=t("vega-dataflow").Tuple,h=t("vega-logging"),p=t("./Transform"),g=r.prototype=Object.create(p.prototype);g.constructor=r;var m={line:i,curve:a,cornerX:o,cornerY:s,cornerR:u,diagonalX:l,diagonalY:c,diagonalR:d};g.transform=function(t){function e(t){var e=r(i(t),a(t),o(t),s(t),u);f.set(t,n.path,e)}h.debug(t,["linkpath"]);var n=this._output,r=m[this.param("shape")]||m.line,i=this.param("sourceX").accessor,a=this.param("sourceY").accessor,o=this.param("targetX").accessor,s=this.param("targetY").accessor,u=this.param("tension");return t.add.forEach(e),this.reevaluate(t)&&(t.mod.forEach(e),t.rem.forEach(e)),t.fields[n.path]=1,t},e.exports=r},{"./Transform":140,"vega-dataflow":41,"vega-logging":48}],134:[function(t,e,n){function r(t){return o.prototype.init.call(this,t),o.addParameters(this,{on:{type:"data"},onKey:{type:"field",default:null},as:{type:"array"},keys:{type:"array",default:["data"]},default:{type:"value"}}),this.mutates(!0)}var i=t("vega-dataflow").Tuple,a=t("vega-logging"),o=t("./Transform"),s=r.prototype=Object.create(o.prototype);s.constructor=r,s.transform=function(t,e){function n(t){for(var e=0;e"}}),this.router(!0)}var i=t("datalib"),a=t("vega-logging"),o=t("./Transform"),s=r.prototype=Object.create(o.prototype);s.constructor=r,s.transform=function(t){return a.debug(t,["sorting"]),(t.add.length||t.mod.length||t.rem.length)&&(t.sort=i.comparator(this.param("by").field)),t},e.exports=r},{"./Transform":140,datalib:26,"vega-logging":48}],139:[function(t,e,n){function r(t){return l.prototype.init.call(this,t),u.addParameters(this,{groupby:{type:"array"},sortby:{type:"array"},field:{type:"field"},offset:{type:"value",default:"zero"}}),this._output={start:"layout_start",end:"layout_end",mid:"layout_mid"},this.mutates(!0)}function i(t,e,n,r){var i,a,o,s,u,l,c,d=[],f=function(t){return t(o)};if(null==e)d.push(t.slice());else for(i={},a=0;ac&&(c=l),null!=n&&u.sort(n)}return d.max=c,d}var a=t("datalib"),o=t("vega-dataflow").Tuple,s=t("vega-logging"),u=t("./Transform"),l=t("./BatchTransform"),c=r.prototype=Object.create(l.prototype);c.constructor=r,c.batchTransform=function(t,e){s.debug(t,["stacking"]);for(var n=this.param("groupby").accessor,r=a.comparator(this.param("sortby").field),u=this.param("field").accessor,l=this.param("offset"),c=this._output,d=i(e,n,r,u),f=0,h=d.max;f"}}),this._output={children:"children",parent:"parent"},this.router(!0).produces(!0)}var i=t("datalib"),a=t("vega-dataflow").Tuple,o=t("vega-logging"),s=t("./Transform"),u=t("./BatchTransform"),l=r.prototype=Object.create(u.prototype);l.constructor=r,l.batchTransform=function(t,e){function n(t,e,r){var i=f[t].execute(r);e[l]=i,i.forEach(function(r){r[c]=e,p.push(a.ingest(r)),t+1",default:["-value"]},children:{type:"field",default:"children"},parent:{type:"field",default:"parent"},field:{type:"field",default:"value"},size:{type:"array",default:t("./screen").size},round:{type:"value",default:!0},sticky:{type:"value",default:!1},ratio:{type:"value",default:c},padding:{type:"value",default:null},mode:{type:"value",default:"squarify"}}),this._layout=i.layout.treemap(),this._output={x:"layout_x",y:"layout_y",width:"layout_width",height:"layout_height",depth:"layout_depth"},this.mutates(!0)}var i="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,a=t("datalib"),o=t("vega-dataflow").Tuple,s=t("vega-logging"),u=t("./Transform"),l=t("./BatchTransform"),c=.5*(1+Math.sqrt(5)),d=r.prototype=Object.create(l.prototype);d.constructor=r,d.batchTransform=function(t,e){s.debug(t,["treemap"]);var n=this._layout,r=this._output,i=this.param("sticky"),u=this.param("parent").accessor,l=e.filter(function(t){return null===u(t)})[0];return n.sticky()!==i&&n.sticky(i),n.sort(a.comparator(this.param("sort").field)).children(this.param("children").accessor).value(this.param("field").accessor).size(this.param("size")).round(this.param("round")).ratio(this.param("ratio")).padding(this.param("padding")).mode(this.param("mode")).nodes(l),e.forEach(function(t){o.set(t,r.x,t.x),o.set(t,r.y,t.y),o.set(t,r.width,t.dx),o.set(t,r.height,t.dy),o.set(t,r.depth,t.depth)}),t.fields[r.x]=1,t.fields[r.y]=1,t.fields[r.width]=1,t.fields[r.height]=1,t.fields[r.depth]=1,t},e.exports=r}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./BatchTransform":119,"./Transform":140,"./screen":146,datalib:26,"vega-dataflow":41,"vega-logging":48}],143:[function(t,e,n){(function(n){function r(e){return u.prototype.init.call(this,e),s.addParameters(this,{clipExtent:{type:"array",default:t("./screen").extent},x:{type:"field",default:"layout_x"},y:{type:"field",default:"layout_y"}}),this._layout=i.geom.voronoi(),this._output={path:"layout_path"},this.mutates(!0)}var i="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,a=t("vega-dataflow/src/Tuple"),o=t("vega-logging"),s=t("./Transform"),u=t("./BatchTransform"),l=r.prototype=Object.create(u.prototype);l.constructor=r,l.batchTransform=function(t,e){o.debug(t,["voronoi"]);for(var n=this._output.path,r=this._layout.clipExtent(this.param("clipExtent")).x(this.param("x").accessor).y(this.param("y").accessor)(e),i=0;i",default:t("./screen").size},text:{type:"field",default:"data"},rotate:{type:"field|value",default:0},font:{type:"field|value",default:{value:"sans-serif"}},fontSize:{type:"field|value",default:14},fontStyle:{type:"field|value",default:{value:"normal"}},fontWeight:{type:"field|value",default:{value:"normal"}},fontScale:{type:"array",default:[10,50]},padding:{type:"value",default:1},spiral:{type:"value",default:"archimedean"}}),this._layout=u().canvas(l.instance),this._output={x:"layout_x",y:"layout_y",font:"layout_font",fontSize:"layout_fontSize",fontStyle:"layout_fontStyle",fontWeight:"layout_fontWeight",rotate:"layout_rotate"},this.mutates(!0)}function i(t){return t&&t.accessor||t}function a(t){var e=Object.create(t);return e._tuple=t,e}var o=t("datalib"),s="undefined"!=typeof window?window.d3:"undefined"!=typeof n?n.d3:null,u="undefined"!=typeof window?window.d3.layout.cloud:"undefined"!=typeof n?n.d3.layout.cloud:null,l=t("vega-scenegraph").canvas,c=t("vega-dataflow/src/Tuple"),d=t("vega-logging"),f=t("./Transform"),h=t("./BatchTransform"),p=r.prototype=Object.create(h.prototype);p.constructor=r,p.batchTransform=function(t,e){d.debug(t,["wordcloud"]);var n,r,u=this._layout,l=this._output,f=this.param("fontSize"),h=f.accessor&&this.param("fontScale");f=f.accessor||s.functor(f),h.length&&(r=s.scale.sqrt().domain(o.extent(e,n=f)).range(h),f=function(t){return r(n(t))}),u.size(this.param("size")).text(i(this.param("text"))).padding(this.param("padding")).spiral(this.param("spiral")).rotate(i(this.param("rotate"))).font(i(this.param("font"))).fontStyle(i(this.param("fontStyle"))).fontWeight(i(this.param("fontWeight"))).fontSize(f).words(e.map(a)).on("end",function(t){var e,n,r,i,a=u.size(),o=a[0]>>1,s=a[1]>>1;for(r=0,i=t.length;r0?0:-t):Math.log(t<0?0:t))/Math.log(s)}function a(t){return e[0]<0?-Math.pow(s,-t):Math.pow(s,t)}if(null==n)return r;var o,s=t.base(),u=Math.min(s,t.ticks().length/n),l=e[0]>0?(o=1e-12,Math.ceil):(o=-1e-12,Math.floor);return function(t){return a(l(i(t)+o))/t>=u?r(t):""}}function o(t,e,n,r){var i,o=s.format,c="log"===t.type;switch(n){case f:return i=t.domain(),c?a(t,i,e,o.auto.number(r||null)):o.auto.linear(i,e,r||null);case u:return(r?o:o.auto).time(r);case l:return(r?o:o.auto).utc(r);default:return String}}var s=t("datalib"),u="time",l="utc",c="string",d="ordinal",f="number";e.exports={getTickFormat:r}},{datalib:26}],148:[function(t,e,n){var r=t("datalib"),i={};r.extend(i,t("./format")),e.exports=r.extend(i,r)},{"./format":147,datalib:26}],149:[function(t,e,n){e.exports=function(t,e,n,r){return void 0!==t[n]?t[n]:void 0!==e&&void 0!==e[n]?e[n]:void 0!==r?r:void 0}},{}]},{},[1])(1)}); diff --git a/third_party/webcomponentsjs/LICENSE b/third_party/webcomponentsjs/LICENSE new file mode 100644 index 000000000000..9353f6f46d72 --- /dev/null +++ b/third_party/webcomponentsjs/LICENSE @@ -0,0 +1,19 @@ +# License + +Everything in this repo is BSD style license unless otherwise specified. + +Copyright (c) 2015 The Polymer Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/webcomponentsjs/README.amp b/third_party/webcomponentsjs/README.amp new file mode 100644 index 000000000000..0c2573cb9d98 --- /dev/null +++ b/third_party/webcomponentsjs/README.amp @@ -0,0 +1,9 @@ +URL: https://github.com/webcomponents/webcomponentsjs +License: BSD +License File: https://github.com/webcomponents/webcomponentsjs/blob/master/LICENSE.md + +Description: +Shadow DOM-related polyfills + +Local Modifications: +Stripped to only perform necessary Shadow CSS polyfilling. diff --git a/third_party/webcomponentsjs/ShadowCSS.js b/third_party/webcomponentsjs/ShadowCSS.js new file mode 100644 index 000000000000..5ce37d799d1a --- /dev/null +++ b/third_party/webcomponentsjs/ShadowCSS.js @@ -0,0 +1,312 @@ +/** + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +/* + This is a limited shim for ShadowDOM css styling. + https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles + + The intention here is to support only the styling features which can be + relatively simply implemented. The goal is to allow users to avoid the + most obvious pitfalls and do so without compromising performance significantly. + For ShadowDOM styling that's not covered here, a set of best practices + can be provided that should allow users to accomplish more complex styling. + + The following is a list of specific ShadowDOM styling features and a brief + discussion of the approach used to shim. + + Shimmed features: + + * :host, :host-context: ShadowDOM allows styling of the shadowRoot's host + element using the :host rule. To shim this feature, the :host styles are + reformatted and prefixed with a given scope name and promoted to a + document level stylesheet. + For example, given a scope name of .foo, a rule like this: + + :host { + background: red; + } + } + + becomes: + + .foo { + background: red; + } + + * encapsultion: Styles defined within ShadowDOM, apply only to + dom inside the ShadowDOM. Polymer uses one of two techniques to imlement + this feature. + + By default, rules are prefixed with the host element tag name + as a descendant selector. This ensures styling does not leak out of the 'top' + of the element's ShadowDOM. For example, + + div { + font-weight: bold; + } + + becomes: + + x-foo div { + font-weight: bold; + } + + becomes: + + + Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then + selectors are scoped by adding an attribute selector suffix to each + simple selector that contains the host element tag name. Each element + in the element's ShadowDOM template is also given the scope attribute. + Thus, these rules match only elements that have the scope attribute. + For example, given a scope name of x-foo, a rule like this: + + div { + font-weight: bold; + } + + becomes: + + div[x-foo] { + font-weight: bold; + } + + Note that elements that are dynamically added to a scope must have the scope + selector added to them manually. + + * upper/lower bound encapsulation: Styles which are defined outside a + shadowRoot should not cross the ShadowDOM boundary and should not apply + inside a shadowRoot. + + This styling behavior is not emulated. Some possible ways to do this that + were rejected due to complexity and/or performance concerns include: (1) reset + every possible property for every possible selector for a given scope name; + (2) re-implement css in javascript. + + As an alternative, users should make sure to use selectors + specific to the scope in which they are working. + + * ::distributed: This behavior is not emulated. It's often not necessary + to style the contents of a specific insertion point and instead, descendants + of the host element can be styled selectively. Users can also create an + extra node around an insertion point and style that node's contents + via descendent selectors. For example, with a shadowRoot like this: + + + + + could become: + + +
    + +
    + + Note the use of @polyfill in the comment above a ShadowDOM specific style + declaration. This is a directive to the styling shim to use the selector + in comments in lieu of the next selector when running under polyfill. +*/ + +export const ShadowCSS = { + strictStyling: false, + // change a selector like 'div' to 'name div' + /** @this {ShadowCSS} */ + scopeRules: function(cssRules, scopeSelector, opt_transformer) { + var cssText = ''; + if (cssRules) { + Array.prototype.forEach.call(cssRules, function(rule) { + if (rule.selectorText && (rule.style && rule.style.cssText !== undefined)) { + cssText += this.scopeSelector(rule.selectorText, scopeSelector, + this.strictStyling, opt_transformer) + ' {\n\t'; + cssText += this.propertiesFromRule(rule) + '\n}\n\n'; + } else if (rule.type === CSSRule.MEDIA_RULE) { + cssText += '@media ' + rule.media.mediaText + ' {\n'; + cssText += this.scopeRules(rule.cssRules, scopeSelector); + cssText += '\n}\n\n'; + } else { + // KEYFRAMES_RULE in IE throws when we query cssText + // when it contains a -webkit- property. + // if this happens, we fallback to constructing the rule + // from the CSSRuleSet + // https://connect.microsoft.com/IE/feedbackdetail/view/955703/accessing-csstext-of-a-keyframe-rule-that-contains-a-webkit-property-via-cssom-generates-exception + try { + if (rule.cssText) { + cssText += rule.cssText + '\n\n'; + } + } catch(x) { + if (rule.type === CSSRule.KEYFRAMES_RULE && rule.cssRules) { + cssText += this.ieSafeCssTextFromKeyFrameRule(rule); + } + } + } + }, this); + } + return cssText; + }, + /** @this {ShadowCSS} */ + ieSafeCssTextFromKeyFrameRule: function(rule) { + var cssText = '@keyframes ' + rule.name + ' {'; + Array.prototype.forEach.call(rule.cssRules, function(rule) { + cssText += ' ' + rule.keyText + ' {' + rule.style.cssText + '}'; + }); + cssText += ' }'; + return cssText; + }, + /** @this {ShadowCSS} */ + scopeSelector: function(selector, scopeSelector, strict, opt_transformer) { + var r = [], parts = selector.split(','); + parts.forEach(function(p) { + p = p.trim(); + if (opt_transformer) { + p = opt_transformer(p); + } + if (this.selectorNeedsScoping(p, scopeSelector)) { + p = (strict && !p.match(polyfillHostNoCombinator)) ? + this.applyStrictSelectorScope(p, scopeSelector) : + this.applySelectorScope(p, scopeSelector); + } + r.push(p); + }, this); + return r.join(', '); + }, + /** @this {ShadowCSS} */ + selectorNeedsScoping: function(selector, scopeSelector) { + if (Array.isArray(scopeSelector)) { + return true; + } + var re = this.makeScopeMatcher(scopeSelector); + return !selector.match(re); + }, + /** @this {ShadowCSS} */ + makeScopeMatcher: function(scopeSelector) { + scopeSelector = scopeSelector.replace(/\[/g, '\\[').replace(/\]/g, '\\]'); + return new RegExp('^(' + scopeSelector + ')' + selectorReSuffix, 'm'); + }, + /** @this {ShadowCSS} */ + applySelectorScope: function(selector, selectorScope) { + return Array.isArray(selectorScope) ? + this.applySelectorScopeList(selector, selectorScope) : + this.applySimpleSelectorScope(selector, selectorScope); + }, + // apply an array of selectors + /** @this {ShadowCSS} */ + applySelectorScopeList: function(selector, scopeSelectorList) { + var r = []; + for (var i=0, s; (s=scopeSelectorList[i]); i++) { + r.push(this.applySimpleSelectorScope(selector, s)); + } + return r.join(', '); + }, + // scope via name and [is=name] + /** @this {ShadowCSS} */ + applySimpleSelectorScope: function(selector, scopeSelector) { + if (selector.match(polyfillHostRe)) { + selector = selector.replace(polyfillHostNoCombinator, scopeSelector); + return selector.replace(polyfillHostRe, scopeSelector + ' '); + } else { + return scopeSelector + ' ' + selector; + } + }, + // return a selector with [name] suffix on each simple selector + // e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] + /** @this {ShadowCSS} */ + applyStrictSelectorScope: function(selector, scopeSelector) { + scopeSelector = scopeSelector.replace(/\[is=([^\]]*)\]/g, '$1'); + var splits = [' ', '>', '+', '~'], + scoped = selector, + attrName = '[' + scopeSelector + ']'; + splits.forEach(function(sep) { + var parts = scoped.split(sep); + scoped = parts.map(function(p) { + // remove :host since it should be unnecessary + var t = p.trim().replace(polyfillHostRe, ''); + if (t && (splits.indexOf(t) < 0) && (t.indexOf(attrName) < 0)) { + p = t.replace(/([^:]*)(:*)(.*)/, '$1' + attrName + '$2$3'); + } + return p; + }).join(sep); + }); + return scoped; + }, + /** @this {ShadowCSS} */ + propertiesFromRule: function(rule) { + var cssText = rule.style.cssText; + // TODO(sorvell): Safari cssom incorrectly removes quotes from the content + // property. (https://bugs.webkit.org/show_bug.cgi?id=118045) + // don't replace attr rules + if (rule.style.content && !rule.style.content.match(/['"]+|attr/)) { + cssText = cssText.replace(/content:[^;]*;/g, 'content: \'' + + rule.style.content + '\';'); + } + // TODO(sorvell): we can workaround this issue here, but we need a list + // of troublesome properties to fix https://github.com/Polymer/platform/issues/53 + // + // inherit rules can be omitted from cssText + // TODO(sorvell): remove when Blink bug is fixed: + // https://code.google.com/p/chromium/issues/detail?id=358273 + var style = rule.style; + for (var i in style) { + if (style[i] === 'initial') { + cssText += i + ': initial; '; + } + } + return cssText; + } +}; + +var selectorRe = /([^{]*)({[\s\S]*?})/gim, + cssCommentRe = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim, + // TODO(sorvell): remove either content or comment + cssCommentNextSelectorRe = /\/\*\s*@polyfill ([^*]*\*+([^/*][^*]*\*+)*\/)([^{]*?){/gim, + cssContentNextSelectorRe = /polyfill-next-selector[^}]*content\:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim, + // TODO(sorvell): remove either content or comment + cssCommentRuleRe = /\/\*\s@polyfill-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim, + cssContentRuleRe = /(polyfill-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim, + // TODO(sorvell): remove either content or comment + cssCommentUnscopedRuleRe = /\/\*\s@polyfill-unscoped-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim, + cssContentUnscopedRuleRe = /(polyfill-unscoped-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim, + cssPseudoRe = /::(x-[^\s{,(]*)/gim, + cssPartRe = /::part\(([^)]*)\)/gim, + // note: :host pre-processed to -shadowcsshost. + polyfillHost = '-shadowcsshost', + // note: :host-context pre-processed to -shadowcsshostcontext. + polyfillHostContext = '-shadowcsscontext', + parenSuffix = ')(?:\\((' + + '(?:\\([^)(]*\\)|[^)(]*)+?' + + ')\\))?([^,{]*)'; + var cssColonHostRe = new RegExp('(' + polyfillHost + parenSuffix, 'gim'), + cssColonHostContextRe = new RegExp('(' + polyfillHostContext + parenSuffix, 'gim'), + selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$', + colonHostRe = /\:host/gim, + colonHostContextRe = /\:host-context/gim, + /* host name without combinator */ + polyfillHostNoCombinator = polyfillHost + '-no-combinator', + polyfillHostRe = new RegExp(polyfillHost, 'gim'), + polyfillHostContextRe = new RegExp(polyfillHostContext, 'gim'), + shadowDOMSelectorsRe = [ + />>>/g, + /::shadow/g, + /::content/g, + // Deprecated selectors + /\/deep\//g, // former >>> + /\/shadow\//g, // former ::shadow + /\/shadow-deep\//g, // former /deep/ + /\^\^/g, // former /shadow/ + /\^(?!=)/g // former /shadow-deep/ + ]; diff --git a/tools/errortracker/app.yaml b/tools/errortracker/app.yaml index 5d8390c3df83..130d5b3002a4 100644 --- a/tools/errortracker/app.yaml +++ b/tools/errortracker/app.yaml @@ -1,5 +1,5 @@ application: amp-error-reporting -version: 8 +version: 13 runtime: go api_version: go1 diff --git a/tools/errortracker/errortracker.go b/tools/errortracker/errortracker.go index fd5e366be88f..833d275051cc 100644 --- a/tools/errortracker/errortracker.go +++ b/tools/errortracker/errortracker.go @@ -104,35 +104,63 @@ func handle(w http.ResponseWriter, r *http.Request) { // Fill query params into JSON struct. line, _ := strconv.Atoi(r.URL.Query().Get("l")) errorType := "default" + isUserError := false; if r.URL.Query().Get("a") == "1" { errorType = "assert" + isUserError = true } // By default we log as "INFO" severity, because reports are very spammy severity := "INFO" level := logging.Info // But if the request comes from the cache (and thus only from valid AMP // docs) we log as "ERROR". + isCdn := false if strings.HasPrefix(r.Referer(), "https://cdn.ampproject.org/") || + strings.Contains(r.Referer(), ".cdn.ampproject.org/") || strings.Contains(r.Referer(), ".ampproject.net/") { severity = "ERROR" level = logging.Error errorType += "-cdn" + isCdn = true } else { errorType += "-origin" } is3p := false - if r.URL.Query().Get("3p") == "1" { - is3p = true - errorType += "-3p" + runtime := r.URL.Query().Get("rt") + if runtime != "" { + errorType += "-" + runtime; + if runtime == "3p" { + is3p = true + } } else { - errorType += "-1p" + if r.URL.Query().Get("3p") == "1" { + is3p = true + errorType += "-3p" + } else { + errorType += "-1p" + } } isCanary := false; if r.URL.Query().Get("ca") == "1" { errorType += "-canary" isCanary = true; } - if !isCanary && !is3p && level != logging.Error && rand.Float32() > 0.01 { + sample := rand.Float64() + throttleRate := 0.01 + + if isCanary { + throttleRate = 1.0 // Explicitly log all canary errors. + } else if is3p { + throttleRate = 0.1 + } else if isCdn { + throttleRate = 0.1 + } + + if isUserError { + throttleRate = throttleRate / 10; + } + + if !(sample <= throttleRate) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "THROTTLED\n") @@ -146,7 +174,7 @@ func handle(w http.ResponseWriter, r *http.Request) { Environment: "prod", Application: errorType, AppID: appengine.AppID(c), - Filename: r.URL.Query().Get("f"), + Filename: r.URL.String(), Line: int32(line), Classname: r.URL.Query().Get("el"), Severity: severity, @@ -169,7 +197,7 @@ func handle(w http.ResponseWriter, r *http.Request) { URL: r.Referer(), } event.Request.Meta = &ErrorRequestMeta{ - HTTPReferrer: r.Referer(), + HTTPReferrer: r.URL.Query().Get("r"), HTTPUserAgent: r.UserAgent(), // Intentionally not logged. // RemoteIP: r.RemoteAddr, diff --git a/tools/experiments/README.md b/tools/experiments/README.md index c9d065f2faad..8e7d50fc80dc 100644 --- a/tools/experiments/README.md +++ b/tools/experiments/README.md @@ -1,15 +1,67 @@ # AMP Experiments -The features that are released but not yet ready for the wide use are protected -by the experiment. The developers and users can opt-in into these features -before they are fully released. +AMP experiments are features that are released but not yet ready for wide use, so they are protected by an **experimental** status. -The Experiments UI to enable or disable experiments for the content served from https://cdn.ampproject.org is available at: +Developers and users can opt-in into these features before they are fully released. However, experimental components should be used with caution, as they may contain bugs or have unexpected side effects. -[https://cdn.ampproject.org/experiments.html](https://cdn.ampproject.org/experiments.html) +## Enable an experiment for yourself -For content served from any other domain, experiments can be toggled in the devtools -console when development mode is enabled using: -``` +Experimental components can be served from [https://cdn.ampproject.org](https://cdn.ampproject.org) or any other domain. + +### Served from cdn.ampproject.org + +For content served from [https://cdn.ampproject.org](https://cdn.ampproject.org), go to the [AMP experiments page](https://cdn.ampproject.org/experiments.html) and enable (or disable) any experimental component by toggling them on (or off). Opting in will set a cookie on your browser that will enable the experiment on all AMP pages served through the Google AMP Cache. + +### Served from other domains + +For content served from any other domain, you can toggle experiments in the Chrome DevTools Console when development mode is enabled by using: + +```javascript AMP.toggleExperiment('experiment') ``` + +## Enable an experiment for a particular document +Document can choose to opt in a certain experiments. To do that, simply put a meta tag of name `amp-experiments-opt-in` in the head of the HTML document before your AMP script (`https://cdn.ampproject.org/v0.js`). Its `content` value is a comma separated string of experiment IDs to opt in. +```HTML + + ... + + ... +# AMP Validator Chrome Extension + +This is the source code to the publicly available [AMP Validator](https://chrome.google.com/webstore/detail/amp-validator/nmoffdblmcmgeicmolmhobpoocbbmknc) Chrome extension. + +## Building the extension + +The following tools are required to build the extension: + +* [Bower](https://bower.io/) +* [Polybuild](https://github.com/PolymerLabs/polybuild) + +Once these tools are installed, run `./build_extension.sh`. + +## Installing locally + +Once built this extension can be installed locally through Chrome's [Developer +Mode](https://developer.chrome.com/extensions/faq#faq-dev-01) diff --git a/validator/chromeextension/_locales/en/messages.json b/validator/chromeextension/_locales/en/messages.json new file mode 100644 index 000000000000..af843da9b2d6 --- /dev/null +++ b/validator/chromeextension/_locales/en/messages.json @@ -0,0 +1,52 @@ +{ + "extensionName": { + "message": "AMP Validator", + "description": "Extension name." + }, + "extensionDescription": { + "message": "Automatically checks each page for AMP validation.", + "description": "Extension description." + }, + "pageFailsValidationTitle": { + "message": "Invalid AMP", + "description": "Extension title stating that the page is not valid AMP HTML." + }, + "pageHasAmpAltTitle": { + "message": "AMP Available", + "description": "Extension title stating that the page is not an AMP HTML page, but one is available." + }, + "pageIsNotAmpTitle": { + "message": "Is Not AMP", + "description": "Extension title stating that the page is not an AMP HTML page." + }, + "pageFromAmpCacheTitle": { + "message": "From an AMP Cache", + "description": "Extension title stating that the page is on an AMP Cache." + }, + "pagePassesValidationTitle": { + "message": "Valid AMP", + "description": "Extension title stating that the page is valid AMP HTML." + }, + "emptyResultsText": { + "message": "None found.", + "description": "No errors or warnings were found." + }, + "errorsTabText": { + "message": "Errors: $count$", + "description": "Number of errors.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "warningsTabText": { + "message": "Warnings: $count$", + "description": "Number of warnings.", + "placeholders": { + "count": { + "content": "$1" + } + } + } +} diff --git a/validator/chromeextension/amp-link-128.png b/validator/chromeextension/amp-link-128.png new file mode 100644 index 000000000000..c85ce3a03000 Binary files /dev/null and b/validator/chromeextension/amp-link-128.png differ diff --git a/validator/chromeextension/amp-link-38.png b/validator/chromeextension/amp-link-38.png new file mode 100644 index 000000000000..919573089a45 Binary files /dev/null and b/validator/chromeextension/amp-link-38.png differ diff --git a/validator/chromeextension/amp-validator.html b/validator/chromeextension/amp-validator.html new file mode 100644 index 000000000000..e2290024f203 --- /dev/null +++ b/validator/chromeextension/amp-validator.html @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/validator/chromeextension/background.html b/validator/chromeextension/background.html new file mode 100644 index 000000000000..995513050bf3 --- /dev/null +++ b/validator/chromeextension/background.html @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/validator/chromeextension/background.js b/validator/chromeextension/background.js new file mode 100644 index 000000000000..b070e48154d8 --- /dev/null +++ b/validator/chromeextension/background.js @@ -0,0 +1,290 @@ +/** + * @license + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the license. + */ +var globals = {}; +globals.ampCacheBgcolor = "#ffffff"; +globals.ampCacheIconPrefix = "amp-link"; +globals.ampCacheTitle = chrome.i18n.getMessage("pageFromAmpCacheTitle"); +globals.ampPopup = "amp-validator.build.html"; +globals.invalidAmpBgcolor = "#8b0000"; +globals.invalidAmpIconPrefix = "invalid"; +globals.invalidAmpTitle = chrome.i18n.getMessage("pageFailsValidationTitle"); +globals.linkToAmpBgColor = "#ffffff"; +globals.linkToAmpIconPrefix = "amp-link"; +globals.linkToAmpTitle = chrome.i18n.getMessage("pageHasAmpAltTitle"); +globals.tabToUrl = {}; +globals.validAmpBgcolor = "#ffd700"; +globals.validAmpIconPrefix = "valid"; +globals.validAmpTitle = chrome.i18n.getMessage("pagePassesValidationTitle"); + +/** + * Format a hex value (HTML colors such as #ffffff) as an RGBA. + * + * @param {string} hex + * @return {string} rgba + */ +function hex2rgba(hex) { + // Remove the '#' char if necessary. + if (hex.charAt(0) === "#") { hex = hex.slice(1); } + hex = hex.toUpperCase(); + var hexAlpha = "0123456789ABCDEF", value = new Array(4), k = 0, int1, int2, i; + for (i = 0; i < 6; i += 2) { + int1 = hexAlpha.indexOf(hex.charAt(i)); + int2 = hexAlpha.indexOf(hex.charAt(i + 1)); + value[k] = (int1 * 16) + int2; + k += 1; + } + value[3] = 255; + return value; +} + +/** + * Returns a dictionary of the number of errors and warnings that occur + * in the set of ValidationErrors. + * + * @param {Array} errors Validation errors and/or warnings. + * @return {Object} + */ +function getErrorSeverityCounts(errors) { + numErrors = 0; + numWarnings = 0; + for (error in errors) { + if (errors[error].severity == 'ERROR') numErrors += 1; + if (errors[error].severity == 'WARNING') numWarnings += 1; + } + return {'ERROR': numErrors, 'WARNING': numWarnings}; +} + +/** + * Returns the number of errors that occur in the set of ValidationErrors. + * + * + * @param {Array} errors Validation errors and/or warnings. + * @return {number} + */ +function getNumberOfErrors(errors) { + return getErrorSeverityCounts(errors)['ERROR']; +} + +/** + * Returns the number of warnings that occur in the set of ValidationErrors. + * + * @param {Array} errors Validation errors and/or warnings. + * @return {number} + */ +function getNumberOfWarnings(errors) { + return getErrorSeverityCounts(errors)['WARNING']; +} + +/** + * Handles actions to be taken for pages that are on an AMP Cache. + * + * @param {integer} tabId ID of a tab. + * @param {string} ampHref The URL of the AMP page. + */ +function handleAmpCache(tabId, ampHref) { + updateTabStatus( + tabId, globals.ampCacheIconPrefix, globals.ampCacheTitle, + '' /*text*/, globals.ampCacheBgcolor); + chrome.browserAction.onClicked.addListener( + function loadAmpHref(tab) { + if (tab.id == tabId) { + chrome.browserAction.onClicked.removeListener(loadAmpHref); + chrome.tabs.sendMessage(tab.id, + {'loadAmp': true, 'ampHref': ampHref}); + } + } + ); +} + +/** + * Handles actions to be taken for AMP pages that fail validation. + * + * @param {integer} tabId ID of a tab. + * @param {!Object} validationResult + */ +function handleAmpFail(tabId, validationResult) { + numErrors = getNumberOfErrors(validationResult.errors); + updateTabStatus( + tabId, globals.invalidAmpIconPrefix, globals.invalidAmpTitle, + numErrors.toString(), globals.invalidAmpBgcolor); + updateTabPopup(tabId); +} + +/** + * Handles actions to be taken for pages that have an AMP page available. + * + * @param {integer} tabId ID of a tab. + * @param {string} ampHref The URL of the AMP page. + */ +function handleAmpLink(tabId, ampHref) { + updateTabStatus( + tabId, globals.linkToAmpIconPrefix, globals.linkToAmpTitle, + '' /*text*/, globals.linkToAmpBgColor); + chrome.browserAction.onClicked.addListener( + function loadAmpHref(tab) { + if (tab.id == tabId) { + chrome.browserAction.onClicked.removeListener(loadAmpHref); + chrome.tabs.sendMessage(tab.id, + {'loadAmp': true, 'ampHref': ampHref}); + } + } + ); +} + +/** + * Handles actions to be taken for AMP pages that pass validation. + * + * @param {integer} tabId ID of a tab. + * @param {!Object} validationResult + */ +function handleAmpPass(tabId, validationResult) { + var badgeTitle = ''; + var numWarnings = getNumberOfWarnings(validationResult.errors); + if (numWarnings > 0) badgeTitle = numWarnings.toString(); + updateTabStatus( + tabId, globals.validAmpIconPrefix, globals.validAmpTitle, + badgeTitle, globals.validAmpBgcolor); + if (numWarnings > 0) updateTabPopup(tabId); +} + +/** + * Returns whether the url is forbidden for the extension to act on. + * + * @param {string} url The URL of a tab. + * @return {boolean} + */ +function isForbiddenUrl(url) { + return (url.startsWith('chrome://') || url.startsWith('view-source')); +} + +/** + * Handles events for a specific tab and asks the tab's content_script to + * determine AMP details about the page's content. + * + * @param {Tab} tab The Tab which triggered the event. + */ +function updateTab(tab) { + if (!isForbiddenUrl(tab.url)) + chrome.tabs.sendMessage( + tab.id, {'getAmpDetails': true}, function(response) { + if (response && response.fromAmpCache && response.ampHref) { + handleAmpCache(tab.id, response.ampHref); + } else if (response && response.isAmp) { + validateUrlFromTab(tab); + } else if (response && !response.isAmp && response.ampHref) { + handleAmpLink(tab.id, response.ampHref); + } + } + ); +} + +/** + * Updates the tabId's extension popup. + * + * @param {number} tabId ID of a tab. + */ +function updateTabPopup(tabId) { + // Verify tab still exists + chrome.tabs.get(tabId, function(tab) { + if (!chrome.runtime.lastError) { + chrome.browserAction.setPopup({tabId: tabId, popup: globals.ampPopup}); + } + }); +} + +/** + * Updates the tabId's extension icon and badge. + * + * @param {number} tabId ID of a tab. + * @param {string} iconPrefix File name prefix of the icon to use. + * @param {string} title Title to display in extension icon hover. + * @param {string} text Text to display in badge. + * @param {string} color Background color for badge. + */ +function updateTabStatus(tabId, iconPrefix, title, text, color) { + // Verify tab still exists + chrome.tabs.get(tabId, function(tab) { + if (!chrome.runtime.lastError) { + chrome.browserAction.setIcon({path: {"19": iconPrefix + "-128.png", + "38": iconPrefix + "-38.png"}, + tabId: tabId}); + if (title !== undefined) + chrome.browserAction.setTitle({title: title, tabId: tabId}); + if (text !== undefined) + chrome.browserAction.setBadgeText({text: text, tabId: tabId}); + if (color !== undefined) + chrome.browserAction.setBadgeBackgroundColor( + {color: hex2rgba(color), tabId: tabId}); + } + }); +} + +/** + * Fetches the content of the tab's URL and validates it. Then updates the + * extension's icons with pass/fail. + * + * @param {Tab} tab The Tab which triggered the event. + */ +function validateUrlFromTab(tab) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', tab.url, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + const doc = xhr.responseText; + const validationResult = amp.validator.validateString(doc); + window.sessionStorage.setItem(tab.url, JSON.stringify(validationResult)); + if (validationResult.status == 'PASS') { + handleAmpPass(tab.id, validationResult); + } else { + handleAmpFail(tab.id, validationResult); + } + } + }; + xhr.send(); +} + +/** + * Listen for a new tab being created. + */ +chrome.tabs.onCreated.addListener(function(tab) { + updateTab(tab); +}); + +/** + * Listen for a tab being changed. + */ +chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { + globals.tabToUrl[tabId] = tab.url; + updateTab(tab); +}); + +/** + * Listen for a tab being removed. + */ +chrome.tabs.onRemoved.addListener(function(tabId, removeInfo) { + window.sessionStorage.removeItem(globals.tabToUrl[tabId]); +}); + +/** + * Listen for a tab being replaced (due to prerendering or instant). + */ +chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) { + window.sessionStorage.removeItem(globals.tabToUrl[removedTabId]); + chrome.tabs.get(addedTabId, function(tab) { + updateTab(tab); + }); +}); diff --git a/validator/chromeextension/bower.json b/validator/chromeextension/bower.json new file mode 100644 index 000000000000..06cf195f6ed8 --- /dev/null +++ b/validator/chromeextension/bower.json @@ -0,0 +1,11 @@ +{ + "name": "amp-validator-chrome-extension", + "version": "1.0.7", + "dependencies": { + "iron-pages": "PolymerElements/iron-pages#^1.0.0", + "paper-item": "PolymerElements/paper-item#^1.0.0", + "paper-spinner": "PolymerElements/paper-spinner#^1.0.0", + "paper-tabs": "PolymerElements/paper-tabs#^1.0.0", + "paper-toolbar": "PolymerElements/paper-toolbar#^1.0.0" + } +} diff --git a/validator/chromeextension/build_extension.sh b/validator/chromeextension/build_extension.sh new file mode 100755 index 000000000000..3e0ac4884bf6 --- /dev/null +++ b/validator/chromeextension/build_extension.sh @@ -0,0 +1,28 @@ +#!/bin/bash -ex +# +# Copyright 2016 The AMP HTML Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the license. +# +# A script to install dependencies and then build the extension. + +echo 'Installing web components' +bower install + +echo 'Polybuild AMP Validator' +polybuild amp-validator.html + +echo 'Removing web components' +rm -rf bower_components + +echo 'Done' diff --git a/validator/chromeextension/content_script.js b/validator/chromeextension/content_script.js new file mode 100644 index 000000000000..cbbc27b86249 --- /dev/null +++ b/validator/chromeextension/content_script.js @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the license. + */ +var globals = {}; +globals.amphtmlRegex = new RegExp('(^\s*)amphtml(\s*$)'); +globals.ampCaches = [ + { + 'getAmpHref': function() { + if (window.location.pathname.startsWith('/c/s')) { + return 'https://' + window.location.pathname.slice(5); + } else if (window.location.pathname.startsWith('/c')) { + return 'http://' + window.location.pathname.slice(3); + } else { + return ''; + } + }, + 'isAmpCache': function() { + return window.location.hostname === 'cdn.ampproject.org'; + }, + } +]; + +/** + * Returns the AMP page href from an AMP Cache. Otherwise returns empty string. + * + * @return {string} + * @private + */ +function getAmpCacheHref() { + for (var index in globals.ampCaches) { + var ampCache = globals.ampCaches[index]; + if (ampCache.isAmpCache()) { + return ampCache.getAmpHref(); + } + } + return ''; +} + +/** + * Determine if exists in the page and + * return the href's value if it does. Otherwise returns empty string. + * + * @return {string} + * @private + */ +function getAmpHtmlLinkHref() { + var ampHtmlLinkHref = ''; + var headLinks = document.head.getElementsByTagName('link'); + if (headLinks.length > 0) { + for (var index in headLinks) { + var link = headLinks[index]; + if (link instanceof HTMLLinkElement && + link.hasAttribute('rel') && + globals.amphtmlRegex.test(link.getAttribute('rel')) && + link.hasAttribute('href')) { + ampHtmlLinkHref = link.getAttribute('href'); + break; + } + } + } + return ampHtmlLinkHref; +} + +/** + * Determine if the page is from an AMP Cache. + * + * @return {boolean} + * @private + */ +function isAmpCache() { + for (var index in globals.ampCaches) { + var ampCache = globals.ampCaches[index]; + if (ampCache.isAmpCache()) { + return true; + } + } + return false; +} + +/** + * Determine if the page is an AMP page. + * + * @return {boolean} + * @private + */ +function isAmpDocument() { + return (document.documentElement.hasAttribute('amp') || + document.documentElement.hasAttribute('⚡')); +} + +/** + * Listener for requests from the extension. + * + * Requests for getAmpDetails. Return to the extension: + * - isAmp: Is the page marked as an AMP page. + * - fromAmpCache: Is the page from an AMP Cache. + * - ampHref: the href to an AMP page if the page is not an AMP page but there + * is an or if the page is from an AMP Cache. + * + * Requests for loadAmp and has ampHref, then redirects the browser to ampHref. + */ +chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { + if (request.getAmpDetails) { + const isAmp = isAmpDocument(); + const fromAmpCache = isAmpCache(); + var ampHref = ''; + if (!isAmp) ampHref = getAmpHtmlLinkHref(); + if (fromAmpCache) ampHref = getAmpCacheHref(); + sendResponse({ + 'isAmp': isAmp, 'fromAmpCache': fromAmpCache, 'ampHref': ampHref, + }); + } + if (request.loadAmp && request.ampHref) { + window.location = request.ampHref; + } +}); diff --git a/validator/chromeextension/icon-128.png b/validator/chromeextension/icon-128.png new file mode 100644 index 000000000000..e1ff7ce5a98a Binary files /dev/null and b/validator/chromeextension/icon-128.png differ diff --git a/validator/chromeextension/icon-16.png b/validator/chromeextension/icon-16.png new file mode 100644 index 000000000000..057a6162a128 Binary files /dev/null and b/validator/chromeextension/icon-16.png differ diff --git a/validator/chromeextension/icon-48.png b/validator/chromeextension/icon-48.png new file mode 100644 index 000000000000..c5e74ed521f7 Binary files /dev/null and b/validator/chromeextension/icon-48.png differ diff --git a/validator/chromeextension/icon-64.png b/validator/chromeextension/icon-64.png new file mode 100644 index 000000000000..8780fa50794e Binary files /dev/null and b/validator/chromeextension/icon-64.png differ diff --git a/validator/chromeextension/invalid-128.png b/validator/chromeextension/invalid-128.png new file mode 100644 index 000000000000..d9d484db57c8 Binary files /dev/null and b/validator/chromeextension/invalid-128.png differ diff --git a/validator/chromeextension/invalid-38.png b/validator/chromeextension/invalid-38.png new file mode 100644 index 000000000000..f8ea2b079cbb Binary files /dev/null and b/validator/chromeextension/invalid-38.png differ diff --git a/validator/chromeextension/manifest.json b/validator/chromeextension/manifest.json new file mode 100644 index 000000000000..ebb1934bf7ff --- /dev/null +++ b/validator/chromeextension/manifest.json @@ -0,0 +1,34 @@ +{ + "manifest_version": 2, + "name": "__MSG_extensionName__", + "version": "1.1.0", + "default_locale": "en", + "description": "__MSG_extensionDescription__", + "icons": { + "16": "icon-16.png", + "48": "icon-48.png", + "128": "icon-128.png" + }, + "browser_action": { + "default_icon": { + "19": "no-amp-128.png", + "38": "no-amp-38.png" + } + }, + "background": { + "page": "background.html" + }, + "content_scripts": [ + { + "js": [ "content_script.js" ], + "matches": [ "" ], + "run_at": "document_start" + } + ], + "content_security_policy": "script-src 'self' https://cdn.ampproject.org; object-src 'self'", + "homepage_url": "https://www.ampproject.org", + "permissions": [ + "tabs", + "" + ] +} diff --git a/validator/chromeextension/no-amp-128.png b/validator/chromeextension/no-amp-128.png new file mode 100644 index 000000000000..0c747f21c9b9 Binary files /dev/null and b/validator/chromeextension/no-amp-128.png differ diff --git a/validator/chromeextension/no-amp-38.png b/validator/chromeextension/no-amp-38.png new file mode 100644 index 000000000000..c63ab373b24a Binary files /dev/null and b/validator/chromeextension/no-amp-38.png differ diff --git a/validator/chromeextension/package_extension.sh b/validator/chromeextension/package_extension.sh new file mode 100755 index 000000000000..388206b514f3 --- /dev/null +++ b/validator/chromeextension/package_extension.sh @@ -0,0 +1,35 @@ +#!/bin/bash -ex +# +# Copyright 2016 The AMP HTML Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the license. +# +# A script to package only the necessary files of the extension. + +echo "Building chrome extension" + +./build_extension.sh + +echo "Packaging chrome extension" + +VERSION=$(egrep "\"version\":" manifest.json | cut -d\" -f4) +zip -r extension-"$VERSION".zip ./ -x amp-validator.html bower.json \ + build_extension.sh icon-64.png package_extension.sh polymer.html \ + promotional-440.png README.md screenshot-chrome-1.png \ + screenshot-chrome-2.png screenshot-opera-1.png screenshot-opera-2.png + +echo "Removing generated files" + +rm amp-validator.build.html amp-validator.build.js + +echo "Done" diff --git a/validator/chromeextension/polymer.html b/validator/chromeextension/polymer.html new file mode 100644 index 000000000000..03f3bbab4df5 --- /dev/null +++ b/validator/chromeextension/polymer.html @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/validator/chromeextension/promotional-440.png b/validator/chromeextension/promotional-440.png new file mode 100644 index 000000000000..30c499dd2346 Binary files /dev/null and b/validator/chromeextension/promotional-440.png differ diff --git a/validator/chromeextension/screenshot-chrome-1.png b/validator/chromeextension/screenshot-chrome-1.png new file mode 100644 index 000000000000..ae75254251ef Binary files /dev/null and b/validator/chromeextension/screenshot-chrome-1.png differ diff --git a/validator/chromeextension/screenshot-chrome-2.png b/validator/chromeextension/screenshot-chrome-2.png new file mode 100644 index 000000000000..0740a0ef427d Binary files /dev/null and b/validator/chromeextension/screenshot-chrome-2.png differ diff --git a/validator/chromeextension/screenshot-opera-1.png b/validator/chromeextension/screenshot-opera-1.png new file mode 100644 index 000000000000..7a42dc8d7c4d Binary files /dev/null and b/validator/chromeextension/screenshot-opera-1.png differ diff --git a/validator/chromeextension/screenshot-opera-2.png b/validator/chromeextension/screenshot-opera-2.png new file mode 100644 index 000000000000..b5724983fab4 Binary files /dev/null and b/validator/chromeextension/screenshot-opera-2.png differ diff --git a/validator/chromeextension/valid-128.png b/validator/chromeextension/valid-128.png new file mode 100644 index 000000000000..168b3c46d606 Binary files /dev/null and b/validator/chromeextension/valid-128.png differ diff --git a/validator/chromeextension/valid-38.png b/validator/chromeextension/valid-38.png new file mode 100644 index 000000000000..3128ff57f199 Binary files /dev/null and b/validator/chromeextension/valid-38.png differ diff --git a/validator/docs/building_a_command_line_amp_validator_for_mac_os_x.md b/validator/docs/building_a_command_line_amp_validator_for_mac_os_x.md deleted file mode 100644 index f10508e019db..000000000000 --- a/validator/docs/building_a_command_line_amp_validator_for_mac_os_x.md +++ /dev/null @@ -1,181 +0,0 @@ -#Building a command-line AMP Validator: Mac OS X - -## Purpose - -This documents how to build the official AMP-HTML command-line AMP Validator on Mac OS X. Once built in this fashion it can then be executed from a OS X terminal session to validate candidate HTML files over the local file system or the web as follows: - -No HTML file passed to the program: - -``` -$ node validate -usage: validate -``` - -Valid HTML file passed to the program: - -``` -$ node validate testdata/feature_tests/minimum_valid_amp.html -PASS -``` - -Invalid HTML file passed to the program: - -``` -$ node validate testdata/feature_tests/empty.html -FAIL -empty.html:1:0 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see [https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd]) -``` - -## References -- Development on AMP HTML - - [https://github.com/ampproject/amphtml/blob/master/DEVELOPING.md](https://github.com/ampproject/amphtml/blob/master/DEVELOPING.md) -- AMP HTML ⚡ Validator - - [https://github.com/ampproject/amphtml/blob/master/validator/README.md](https://github.com/ampproject/amphtml/blob/master/validator/README.md) -- Validating outside the browser - command line tool #937 - - [https://github.com/ampproject/amphtml/issues/937](https://github.com/ampproject/amphtml/issues/937) - -## Dependencies and configuration - -### Homebrew - -#### Installing Homebrew - -See: [http://brew.sh/](http://brew.sh/) - -``` -$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" -``` - -#### Maintaining your Homebrew environment - -See GitHub Gist: [pietergreyling/mybrew.sh](https://gist.github.com/pietergreyling/43b00966f0a775a84ac8) - -``` -#!/bin/bash -#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#-- desc: To clean up and verify your Homebrew setup. -#-- exec: In a terminal session run: $ ./mybrew.sh -clear -echo "-- Updating your Homebrew installation at: " && brew --prefix -sudo chown -R $(whoami):admin /usr/local -brew update && brew upgrade -echo "-- \$HOMEBREW_INSTALL: $HOMEBREW_INSTALL" -brew prune -brew doctor -#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Node JS - -``` -$ brew install node -``` - -### Edit /etc/hosts - -Not strictly initially necessary for the validator, but good not to forget: - -``` -127.0.0.1 ads.localhost iframe.localhost -``` - -### Google Protocol Buffers - -``` -$ brew tap homebrew/versions -$ brew install protobuf --c++11 -$ brew link protobuf -$ protoc --version -libprotoc 2.6.1 -$ python -Python 2.7.11 (default, Dec 5 2015, 14:44:53) ->> import google.protobuf -. . . exit if import worked . . . -``` - -### Getting a local working copy of the AMP repository source - -``` -$ cd my_amp_repo_work_directory -$ git clone [https://github.com/ampproject/amphtml.git](https://github.com/ampproject/amphtml.git) -$ cd amphtml -$ npm install -``` - -### Updating an existing AMP repository source copy - -``` -$ cd my_amp_repo_work_directory/amphtml -$ git pull origin master -$ npm install -``` - -## Building the validator - -``` -$ cd validator -$ npm install -$ sudo python ./build.py -[[build.py getNodeJsCmd]] - entering ... -[[build.py getNodeJsCmd]] - ... done -[[build.py CheckPrereqs]] - entering ... -[[build.py CheckPrereqs]] - ... done -[[build.py InstallNodeDependencies]] - entering ... -amp_validator@0.1.0 .../amphtml/validator -├── google-closure-compiler@20151015.0.0 -├── google-closure-library@20151015.0.0 -└─┬ jasmine@2.3.2 - ├── exit@0.1.2 - ├─┬ glob@3.2.11 - │ ├── inherits@2.0.1 - │ └─┬ minimatch@0.3.0 - │ ├── lru-cache@2.7.3 - │ └── sigmund@1.0.1 - └── jasmine-core@2.3.4 - -[[build.py InstallNodeDependencies]] - ... done -[[build.py SetupOutDir]] - entering ... -[[build.py SetupOutDir]] - ... done -[[build.py GenValidatorPb2Py]] - entering ... -[[build.py GenValidatorPb2Py]] - ... done -[[build.py GenValidatorGeneratedJs]] - entering ... -[[build.py GenValidatorGeneratedJs]] - ... done -[[build.py CompileValidatorMinified]] - entering ... -[[build.py CompileValidatorMinified]] - ... done -[[build.py GenerateValidateBin]] - entering ... -[[build.py GenerateValidateBin]] - ... done -[[build.py RunSmokeTest]] - entering ... -[[build.py RunSmokeTest]] - ... done -[[build.py CompileValidatorTestMinified]] - entering ... -[[build.py CompileValidatorTestMinified]] - ... success -[[build.py CompileHtmlparserTestMinified]] - entering ... -[[build.py CompileHtmlparserTestMinified]] - ... success -[[build.py CompileParseCssTestMinified]] - entering ... -[[build.py CompileParseCssTestMinified]] - ... success -[[build.py GenerateTestRunner]] - entering ... -[[build.py GenerateTestRunner]] - ... success -[[build.py RunTests]] - entering ... -Started -................................................................................... -83 specs, 0 failures -Finished in 0.394 seconds -[[build.py RunTests]] - ... success -``` - -## Running the validator (dist/validate) - -``` -$ ls -alG dist/validate --rwxr-x--- 1 245040 Jan 21 14:47 dist/validate - -$ node dist/validate -usage: validate ; - -$ node dist/validate testdata/feature_tests/minimum_valid_amp.html -PASS - -$ node dist/validate testdata/feature_tests/empty.html -FAIL -empty.html:1:0 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see [https://github.com/ampproject/amphtml/blob/master/spec/amp-html-format.md#ampd]) -. . . -``` diff --git a/validator/engine/amp4ads-parse-css.js b/validator/engine/amp4ads-parse-css.js new file mode 100644 index 000000000000..d8ba22e3a2b9 --- /dev/null +++ b/validator/engine/amp4ads-parse-css.js @@ -0,0 +1,218 @@ +/** + * @license + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the license. + */ + +goog.provide('parse_css.stripVendorPrefix'); +goog.provide('parse_css.validateAmp4AdsCss'); + +goog.require('parse_css.DelimToken'); +goog.require('parse_css.ErrorToken'); +goog.require('parse_css.IdentToken'); +goog.require('parse_css.RuleVisitor'); +goog.require('parse_css.Stylesheet'); + +/** + * Strips vendor prefixes from identifiers, e.g. property names or names + * of at rules. E.g., "-moz-keyframes" -> "keyframes". + * TODO(powdercloud): Revisit which vendor prefixes to cover. + * @param {string} identifier + * @return {string} + */ +parse_css.stripVendorPrefix = function(identifier) { + return identifier.replace(/^-[a-z]+-/, ''); +}; + +/** + * Fills an ErrorToken with the provided position, code, and params. + * @param {!parse_css.Token} positionToken + * @param {!amp.validator.ValidationError.Code} code + * @param {!Array} params + * @return {!parse_css.ErrorToken} + */ +function createParseErrorTokenAt(positionToken, code, params) { + const token = new parse_css.ErrorToken(code, params); + positionToken.copyPosTo(token); + return token; +} + +/** + * For a list of |tokens|, if the first non-whitespace token is an identifier, + * returns its string value. Otherwise, returns the empty string. + * @param {!Array} tokens + * @return {string} + */ +function firstIdent(tokens) { + if (tokens.length === 0) { + return ''; + } + if (tokens[0].tokenType === parse_css.TokenType.IDENT) { + return /** @type {!parse_css.StringValuedToken} */ (tokens[0]).value; + } + if (tokens.length >= 2 && + (tokens[0].tokenType === parse_css.TokenType.WHITESPACE) && + tokens[1].tokenType === parse_css.TokenType.IDENT) { + return /** @type {!parse_css.StringValuedToken} */ (tokens[1]).value; + } + return ''; +} + +/** + * For a qualified |rule|, determine whether its selector starts with + * '.amp-animate'. + * @param {!parse_css.QualifiedRule} rule + * @return {boolean} + */ +function hasAmpAnimate(rule) { + /** @type {!Array} */ + const prelude = rule.prelude; + if (prelude.length < 2) { + return false; + } + if (prelude[0].tokenType !== parse_css.TokenType.DELIM) { + return false; + } + const first = /** @type {!parse_css.DelimToken} */ (prelude[0]); + if (prelude[1].tokenType !== parse_css.TokenType.IDENT) { + return false; + } + const second = /** @type {!parse_css.IdentToken} */ (prelude[1]); + return first.value === '.' && second.value === 'amp-animate'; +} + +/** @private */ +class Amp4AdsVisitor extends parse_css.RuleVisitor { + /** + * @param {!Array} errors + */ + constructor(errors) { + super(); + + /** @type {!Array} */ + this.errors = errors; + + /** @type {parse_css.AtRule} */ + this.inKeyframes = null; + } + + /** @inheritDoc */ + visitDeclaration(declaration) { + // position:fixed and position:sticky are disallowed. + if (declaration.name !== 'position') { + return; + } + const ident = firstIdent(declaration.value); + if (ident === 'fixed' || ident === 'sticky') { + this.errors.push(createParseErrorTokenAt( + declaration, amp.validator.ValidationError.Code + .CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE, + ['style', 'position', ident])); + } + } + + /** @inheritDoc */ + visitQualifiedRule(qualifiedRule) { + // Precompute a determination whether transition or animation are + // present for the checks below this first loop. + /** @type {parse_css.Declaration} */ + let transitionOrAnimation = null; + for (const decl of qualifiedRule.declarations) { + const name = parse_css.stripVendorPrefix(decl.name); + if (name === 'transition' || name === 'animation') { + transitionOrAnimation = decl; + } + + // The name of the property may identify a transition. The only + // properties that may be transitioned are opacity and transform. + if (name === 'transition') { + const transitionedProperty = firstIdent(decl.value); + const transitionedPropertyStripped = + parse_css.stripVendorPrefix(transitionedProperty); + if (transitionedPropertyStripped !== 'opacity' && + transitionedPropertyStripped !== 'transform') { + this.errors.push(createParseErrorTokenAt( + decl, amp.validator.ValidationError.Code + .CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT, + [ + 'style', 'transition', transitionedProperty, + '[\'opacity\', \'transform\']' + ])); + } + } + // This is the @keyframes variant for identifying transitions; still, + // the only properties that may be transitioned are opacity and transform. + if (this.inKeyframes !== null && name !== 'transform' && + name !== 'opacity') { + this.errors.push(createParseErrorTokenAt( + decl, amp.validator.ValidationError.Code + .CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE, + [ + 'style', decl.name, this.inKeyframes.name, + '[\'opacity\', \'transform\']' + ])); + } + } + + // If transition or animation are present: + // (1) Only transition, animation, transform, visibility, opacity allowed. + // (2) Must be qualified with .amp_animate. + if (transitionOrAnimation === null) { + return; + } + for (const decl of qualifiedRule.declarations) { + // (1) Check that the declaration is in the allowed sorted (!) list. + const allowed = + ['animation', 'opacity', 'transform', 'transition', 'visibility']; + if (allowed.indexOf(parse_css.stripVendorPrefix(decl.name)) !== -1) { + continue; + } + this.errors.push(createParseErrorTokenAt( + decl, amp.validator.ValidationError.Code + .CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH, + [ + 'style', decl.name, transitionOrAnimation.name, + '[\'' + allowed.join('\', \'') + '\']' + ])); + } + // (2) Check that the rule is qualified with .amp-animate. + if (!hasAmpAnimate(qualifiedRule)) { + this.errors.push(createParseErrorTokenAt( + qualifiedRule, amp.validator.ValidationError.Code + .CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION, + ['style', transitionOrAnimation.name, '.amp-animate'])); + } + } + + /** @inheritDoc */ + visitAtRule(atRule) { + if (parse_css.stripVendorPrefix(atRule.name) === 'keyframes') { + this.inKeyframes = atRule; + } else { + this.inKeyframes = null; + } + } + + /** @inheritDoc */ + leaveAtRule(atRule) { this.inKeyframes = null; } +} + +/** + * @param {!parse_css.Stylesheet} styleSheet + * @param {!Array} errors + */ +parse_css.validateAmp4AdsCss = function(styleSheet, errors) { + const visitor = new Amp4AdsVisitor(errors); + styleSheet.accept(visitor); +}; diff --git a/validator/engine/amp4ads-parse-css_test.js b/validator/engine/amp4ads-parse-css_test.js new file mode 100644 index 000000000000..15c107860fd3 --- /dev/null +++ b/validator/engine/amp4ads-parse-css_test.js @@ -0,0 +1,390 @@ +/** + * @license + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the license. + */ +goog.provide('parse_css.Amp4AdsParseCssTest'); + +goog.require('json_testutil.makeJsonKeyCmpFn'); +goog.require('json_testutil.renderJSON'); +goog.require('parse_css.BlockType'); +goog.require('parse_css.parseAStylesheet'); +goog.require('parse_css.stripVendorPrefix'); +goog.require('parse_css.tokenize'); +goog.require('parse_css.validateAmp4AdsCss'); + +/** + * A strict comparison between two values that does not truncate the + * error messages and works well with the closure compiler. + * @param {*} expected + * @param {*} saw + */ +function assertStrictEqual(expected, saw) { + assert.ok(expected === saw, 'expected: ' + expected + ' saw: ' + saw); +} + +describe('stripVendorPrefix', () => { + it('removes typical vendor prefixes', () => { + assertStrictEqual('foo', parse_css.stripVendorPrefix('-moz-foo')); + assertStrictEqual('foo', parse_css.stripVendorPrefix('foo')); + assertStrictEqual('foo-foo', parse_css.stripVendorPrefix('foo-foo')); + assertStrictEqual('foo-foo', parse_css.stripVendorPrefix('-d-foo-foo')); + assertStrictEqual('-foo', parse_css.stripVendorPrefix('-foo')); + }); +}); + +/** + * For emitting json output with keys in logical order for ErrorToken. + * @param {string} a + * @param {string} b + * @return {number} + */ +const jsonKeyCmp = json_testutil.makeJsonKeyCmpFn( + ['line', 'col', 'tokenType', 'code', 'params']); + +/** + * @param {!Object} left + * @param {!Object} right + */ +function assertJSONEquals(left, right) { + assertStrictEqual( + json_testutil.renderJSON(left, jsonKeyCmp, /*offset=*/4), + json_testutil.renderJSON(right, jsonKeyCmp, /*offset=*/4)); +} + +/** @type {!Object} */ +const amp4AdsCssParsingSpec = { + 'font-face': parse_css.BlockType.PARSE_AS_DECLARATIONS, + 'media': parse_css.BlockType.PARSE_AS_RULES, + 'keyframes': parse_css.BlockType.PARSE_AS_RULES, + '-webkit-keyframes': parse_css.BlockType.PARSE_AS_RULES, + '-moz-keyframes': parse_css.BlockType.PARSE_AS_RULES, + '-o-keyframes': parse_css.BlockType.PARSE_AS_RULES, + '-ms-keyframes': parse_css.BlockType.PARSE_AS_RULES, +}; + +describe('validateAmp4AdsCss', () => { + it('validates good amp-animate example', () => { + const css = '.amp-animate .box { ' + + ' transform: rotate(180deg); transition: transform 2s; ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals([], errors); + }); + + it('validates good amp-animate example with vendor prefixes', () => { + const css = '.amp-animate .box { ' + + ' -moz-transform: rotate(180deg); ' + + ' -webkit-transition: -o-transform 2s; ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals([], errors); + }); + + it('reports that position fixed and position sticky are disallowed', () => { + const css = '.box { position: fixed; position:sticky; }'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals( + [ + { + 'line': 1, + 'col': 7, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE', + 'params': ['style', 'position', 'fixed'] + }, + { + 'line': 1, + 'col': 24, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE', + 'params': ['style', 'position', 'sticky'] + } + ], + errors); + }); + + it('reports non-animation properties in animation selectors', () => { + // The non-animation property (in this case color) is not allowed in an + // animation selector. + const css = '.amp-animate .box { ' + + ' color: red; ' + + ' transform: rotate(180deg);' + + ' transition: transform 2s;' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals( + [{ + 'line': 1, + 'col': 24, + 'tokenType': 'ERROR', + 'params': [ + 'style', 'color', 'transition', + '[\'animation\', \'opacity\', \'transform\', \'transition\', ' + + '\'visibility\']' + ], + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH' + }], + errors); + }); + + it('reports non-animation properties in animation selectors (vendor prefixed)', + () => { + // The non-animation property (in this case color) is not allowed in an + // animation selector. + const css = '.amp-animate .box { ' + + ' color: red; ' + + ' -o-transform: rotate(180deg);' + + ' -ms-transition: -webkit-transform 2s;' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals( + [{ + 'line': 1, + 'col': 24, + 'tokenType': 'ERROR', + 'params': [ + 'style', 'color', '-ms-transition', + '[\'animation\', \'opacity\', \'transform\', \'transition\', ' + + '\'visibility\']' + ], + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH' + }], + errors); + }); + + it('reports when .amp-animate is missing', () => { + const css = '.box { ' + + ' transform: rotate(180deg); ' + + ' transition: transform 2s; ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals( + [{ + 'line': 1, + 'col': 0, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION', + 'params': ['style', 'transition', '.amp-animate'] + }], + errors); + }); + + it('allows only opacity and transform to be transitioned', () => { + const css = '.amp-animate .box { ' + + ' transition: background-color 2s; ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals( + [{ + 'line': 1, + 'col': 24, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT', + 'params': [ + 'style', 'transition', 'background-color', + '[\'opacity\', \'transform\']' + ] + }], + errors); + }); + + it('allows keyframes as a mechanism for transitions', () => { + const css = '@keyframes turn { ' + + ' from { transform: rotate(180deg); } ' + + ' to { transform: rotate(90deg); } ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals([], errors); + }); + + it('allows keyframes as a mechanism for transitions (vendor prefixed)', + () => { + const css = '@-moz-keyframes turn { ' + + ' from { -webkit-transform: rotate(180deg); } ' + + ' to { -o-transform: rotate(90deg); } ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals([], errors); + }); + + it('allows only opacity, transform in keyframe transitions', () => { + const css = '@keyframes slidein { ' + + ' from { margin-left:100%; width:300%; } ' + + ' to { margin-left:0%; width:100%; } ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals( + [ + { + 'line': 1, + 'col': 30, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'margin-left', 'keyframes', + '[\'opacity\', \'transform\']' + ] + }, + { + 'line': 1, + 'col': 48, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'width', 'keyframes', '[\'opacity\', \'transform\']' + ] + }, + { + 'line': 1, + 'col': 69, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'margin-left', 'keyframes', + '[\'opacity\', \'transform\']' + ] + }, + { + 'line': 1, + 'col': 85, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'width', 'keyframes', '[\'opacity\', \'transform\']' + ] + } + ], + errors); + }); + + it('allows only opacity, transform in keyframe transitions (vendor prefixed)', + () => { + const css = '@-moz-keyframes slidein { ' + + ' from { margin-left:100%; width:300%; } ' + + ' to { margin-left:0%; width:100%; } ' + + '}'; + const errors = []; + const tokens = parse_css.tokenize(css, 1, 0, errors); + const sheet = parse_css.parseAStylesheet( + tokens, amp4AdsCssParsingSpec, parse_css.BlockType.PARSE_AS_IGNORE, + errors); + assertJSONEquals([], errors); + parse_css.validateAmp4AdsCss(sheet, errors); + assertJSONEquals( + [ + { + 'line': 1, + 'col': 35, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'margin-left', '-moz-keyframes', + '[\'opacity\', \'transform\']' + ] + }, + { + 'line': 1, + 'col': 53, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'width', '-moz-keyframes', + '[\'opacity\', \'transform\']' + ] + }, + { + 'line': 1, + 'col': 74, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'margin-left', '-moz-keyframes', + '[\'opacity\', \'transform\']' + ] + }, + { + 'line': 1, + 'col': 90, + 'tokenType': 'ERROR', + 'code': 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE', + 'params': [ + 'style', 'width', '-moz-keyframes', + '[\'opacity\', \'transform\']' + ] + } + ], + errors); + }); +}); diff --git a/validator/css-selectors.js b/validator/engine/css-selectors.js similarity index 99% rename from validator/css-selectors.js rename to validator/engine/css-selectors.js index 1a2b7873a994..5665fbc2f776 100644 --- a/validator/css-selectors.js +++ b/validator/engine/css-selectors.js @@ -262,7 +262,7 @@ parse_css.parseAnIdSelector = function(tokenStream) { */ parse_css.AttrSelector = class extends parse_css.Selector { /** - * @param {string?} namespacePrefix + * @param {?string} namespacePrefix * @param {string} attrName * @param {string} matchOperator is either the string * representation of the match operator (e.g., '=' or '~=') or '', @@ -274,13 +274,13 @@ parse_css.AttrSelector = class extends parse_css.Selector { */ constructor(namespacePrefix, attrName, matchOperator, value) { super(); - /** @type {string?} */ + /** @type {?string} */ this.namespacePrefix = namespacePrefix; /** @type {string} */ this.attrName = attrName; - /** @type {string?} */ + /** @type {string} */ this.matchOperator = matchOperator; - /** @type {string?} */ + /** @type {string} */ this.value = value; /** @type {parse_css.TokenType} */ this.tokenType = parse_css.TokenType.ATTR_SELECTOR; diff --git a/validator/dom-walker.js b/validator/engine/dom-walker.js similarity index 91% rename from validator/dom-walker.js rename to validator/engine/dom-walker.js index c5a7d2c1c556..617ef70a9c87 100644 --- a/validator/dom-walker.js +++ b/validator/engine/dom-walker.js @@ -26,6 +26,9 @@ * * See the {@code goog.string.html.HtmlParser}, this is simulating that * interface. + * + * TODO(powdercloud): We're not currently using this, but may use it. Ronsider + * removing it if not used after 2017-04-01. */ goog.provide('amp.domwalker'); @@ -90,7 +93,8 @@ amp.domwalker.NodeProcessingState_ = class { function attrList(namedNodeMap) { var ret = []; for (var i = 0; i < namedNodeMap.length; ++i) { - ret.push(namedNodeMap[i].name.toLowerCase()); + // The attribute name is always lower cased when returned by the browser. + ret.push(namedNodeMap[i].name); ret.push(namedNodeMap[i].value); } return ret; @@ -100,7 +104,7 @@ function attrList(namedNodeMap) { * Set of element names requiring cdata validation.  * @type {Object} */ -const CdataTagsToValidate = {'script': 0, 'style': 0}; +const CdataTagsToValidate = {'SCRIPT': 0, 'STYLE': 0}; /** * @enum {number} @@ -143,7 +147,7 @@ amp.domwalker.DomWalker = class { // Apparently the !doctype 'tag' is not considered an element in the DOM, // so we can't see it naively. Unsure if there is a better approach here. if (rootDoc.doctype !== null) { - handler.startTag('!doctype', [rootDoc.doctype.name, '']); + handler.startTag('!DOCTYPE', [rootDoc.doctype.name, '']); } // The approach here is to walk the DOM, generating handler calls which @@ -161,9 +165,8 @@ amp.domwalker.DomWalker = class { const curState = tagStack[tagStack.length - 1]; const nextChild = curState.nextChild(); if (nextChild !== undefined) { - // TODO(gregable): browser always returns upper case tag names, can we - // just make the validator use upper case tag names too to avoid this? - const tagName = nextChild.node().nodeName.toLowerCase(); + // The browser always returns upper case tag names. + const tagName = nextChild.node().nodeName; calls.push([ amp.domwalker.HandlerCalls.START_TAG, tagName, attrList(nextChild.node().attributes) @@ -179,7 +182,8 @@ amp.domwalker.DomWalker = class { if (tagStack.length > 1) { calls.push([ amp.domwalker.HandlerCalls.END_TAG, - curState.node().nodeName.toLowerCase() + // The browser always returns upper case tag names. + curState.node().nodeName ]); } tagStack.pop(); diff --git a/validator/htmlparser-interface.js b/validator/engine/htmlparser-interface.js similarity index 94% rename from validator/htmlparser-interface.js rename to validator/engine/htmlparser-interface.js index bd4d4ffa88de..1301b329fb4a 100644 --- a/validator/htmlparser-interface.js +++ b/validator/engine/htmlparser-interface.js @@ -71,6 +71,13 @@ amp.htmlparser.HtmlSaxHandler = class { * Handler called when the parsing is done. */ endDoc() {} + + /** + * Callback for informing that the parser is manufacturing a tag not + * actually found on the page. This will be followed by a startTag() with the + * actual body tag in question. + */ + markManufacturedBody() {} }; diff --git a/validator/htmlparser.js b/validator/engine/htmlparser.js similarity index 73% rename from validator/htmlparser.js rename to validator/engine/htmlparser.js index a45db203d326..9d07f668dc9e 100644 --- a/validator/htmlparser.js +++ b/validator/engine/htmlparser.js @@ -48,21 +48,21 @@ goog.require('amp.htmlparser.HtmlSaxHandlerWithLocation'); * @private */ const ElementsWithNoEndElements = { - 'base': 0, - 'link': 0, - 'meta': 0, - 'hr': 0, - 'br': 0, - 'wbr': 0, - 'img': 0, - 'embed': 0, - 'param': 0, - 'source': 0, - 'track': 0, - 'area': 0, - 'col': 0, - 'input': 0, - 'keygen': 0, + 'BASE': 0, + 'LINK': 0, + 'META': 0, + 'HR': 0, + 'BR': 0, + 'WBR': 0, + 'IMG': 0, + 'EMBED': 0, + 'PARAM': 0, + 'SOURCE': 0, + 'TRACK': 0, + 'AREA': 0, + 'COL': 0, + 'INPUT': 0, + 'KEYGEN': 0, }; /** @@ -73,29 +73,64 @@ const ElementsWithNoEndElements = { */ const HtmlStructureElements = { // See https://www.w3.org/TR/html5/document-metadata.html - '!doctype': 0, - 'html': 0, - 'head': 0, - 'body': 0, + '!DOCTYPE': 0, + 'HTML': 0, + 'HEAD': 0, + 'BODY': 0, }; /** -* The set of HTML tags which are legal in the HTML document and -* the 'head' tag itself. -* @type {Object} -* @private -*/ + * The set of HTML tags which are legal in the HTML document and + * the 'HEAD' tag itself. + * @type {Object} + * @private + */ const HeadElements = { - 'head': 0, + 'HEAD': 0, // See https://www.w3.org/TR/html5/document-metadata.html - 'title': 0, - 'base': 0, - 'link': 0, - 'meta': 0, - 'style': 0, + 'TITLE': 0, + 'BASE': 0, + 'LINK': 0, + 'META': 0, + 'STYLE': 0, // Also legal in the document , though not per spec. - 'noscript': 0, - 'script': 0, + 'NOSCRIPT': 0, + 'SCRIPT': 0, +}; + +/** + * The set of HTML tags whose presence will implicitly close a

    element. + * For example '

    foo

    bar

    ' should parse the same as + * '

    foo

    bar

    '. See https://www.w3.org/TR/html-markup/p.html + * @type {Object} + * @private + */ +const ElementsWhichClosePTag = { + 'ADDRESS': 0, + 'ARTICLE': 0, + 'ASIDE': 0, + 'BLOCKQUOTE': 0, + 'DIR': 0, + 'DL': 0, + 'FIELDSET': 0, + 'FOOTER': 0, + 'FORM': 0, + 'H1': 0, + 'H2': 0, + 'H3': 0, + 'H4': 0, + 'H5': 0, + 'H6': 0, + 'HEADER': 0, + 'HR': 0, + 'MENU': 0, + 'NAV': 0, + 'OL': 0, + 'P': 0, + 'PRE': 0, + 'SECTION': 0, + 'TABLE': 0, + 'UL': 0, }; /** @@ -178,36 +213,65 @@ class TagNameStack { // if the document has left them out or placed them in the wrong location. switch (this.region_) { case TagRegion.PRE_HEAD: - if (tagName === 'head') { + if (tagName === 'HEAD') { this.region_ = TagRegion.IN_HEAD; - } else if (tagName === 'body') { + } else if (tagName === 'BODY') { this.region_ = TagRegion.IN_BODY; } else if (!HtmlStructureElements.hasOwnProperty(tagName)) { if (HeadElements.hasOwnProperty(tagName)) { - this.startTag('head', []); + this.startTag('HEAD', []); } else { - this.startTag('body', []); + if (this.handler_.markManufacturedBody) + this.handler_.markManufacturedBody(); + this.startTag('BODY', []); } } break; case TagRegion.IN_HEAD: if (!HeadElements.hasOwnProperty(tagName)) { - this.endTag('head'); - if (tagName !== 'body') this.startTag('body', []); + this.endTag('HEAD'); + if (tagName !== 'BODY') { + if (this.handler_.markManufacturedBody) + this.handler_.markManufacturedBody(); + this.startTag('BODY', []); + } else { + this.region_ = TagRegion.IN_BODY; + } } break; case TagRegion.PRE_BODY: - if (tagName !== 'body') { - this.startTag('body', []); + if (tagName !== 'BODY') { + if (this.handler_.markManufacturedBody) + this.handler_.markManufacturedBody(); + this.startTag('BODY', []); } else { this.region_ = TagRegion.IN_BODY; } break; case TagRegion.IN_BODY: - if (tagName === 'body') { + if (tagName === 'BODY') { // If we've manufactured a body, then ignore the later body. return; } + // Check implicit tag closing due to opening tags. + if (this.stack_.length > 0) { + const parentTagName = this.stack_[this.stack_.length - 1]; + //

    tags can be implicitly closed by certain other start tags. + // See https://www.w3.org/TR/html-markup/p.html + if (parentTagName === 'P' && + ElementsWhichClosePTag.hasOwnProperty(tagName)) { + this.endTag('P'); + //

    and
    tags can be implicitly closed by other
    and
    + // tags. See https://www.w3.org/TR/html-markup/dd.html + } else if ((tagName == 'DD' || tagName == 'DT') && + (parentTagName == 'DD' || parentTagName == 'DT')) { + this.endTag(parentTagName); + //
  • tags can be implicitly closed by other
  • tags. + // See https://www.w3.org/TR/html-markup/li.html + } else if (tagName == 'LI' && parentTagName == 'LI') { + this.endTag('LI'); + } + } break; default: break; @@ -225,13 +289,40 @@ class TagNameStack { } } + /** + * Callback for pcdata. Some text nodes can trigger the start of the body + * region. + * @param {string} text + */ + pcdata(text) { + if (!amp.htmlparser.HtmlParser.SPACE_RE_.test(text)) { + switch (this.region_) { + case TagRegion.PRE_HEAD: + case TagRegion.PRE_BODY: + if (this.handler_.markManufacturedBody) + this.handler_.markManufacturedBody(); + this.startTag('BODY', []); + break; + case TagRegion.IN_HEAD: + this.endTag('HEAD'); + if (this.handler_.markManufacturedBody) + this.handler_.markManufacturedBody(); + this.startTag('BODY', []); + break; + default: + break; + } + } + this.handler_.pcdata(text); + } + /** * Upon exiting a tag, validation for the current matcher is triggered, * e.g. for checking that the tag had some specified number of children. * @param {!string} tagName */ endTag(tagName) { - if (this.region_ == TagRegion.IN_HEAD && tagName === 'head') + if (this.region_ == TagRegion.IN_HEAD && tagName === 'HEAD') this.region_ = TagRegion.PRE_BODY; // We ignore close body tags (, // (ex:
    ) which are only allowed in the , we will effectively // move them into the body section. - if (tagName === 'body') return; + if (tagName === 'BODY') return; // We look for tagName from the end. If we can find it, we pop // everything from thereon off the stack. If we can't find it, @@ -295,7 +386,7 @@ amp.htmlparser.HtmlParser = class { * @param {string} htmlText The html text. */ parse(handler, htmlText) { - let htmlLower = null; + let htmlUpper = null; let inTag = false; // True iff we're currently processing a tag. const attribs = []; // Accumulates attribute names and values. let tagName; // The name of the tag currently being processed. @@ -360,13 +451,13 @@ amp.htmlparser.HtmlParser = class { if (openTag && (eflags & (amp.htmlparser.HtmlParser.EFlags.CDATA | amp.htmlparser.HtmlParser.EFlags.RCDATA))) { - if (htmlLower === null) { - htmlLower = amp.htmlparser.toLowerCase(htmlText); + if (htmlUpper === null) { + htmlUpper = amp.htmlparser.toUpperCase(htmlText); } else { - htmlLower = - htmlLower.substring(htmlLower.length - htmlText.length); + htmlUpper = + htmlUpper.substring(htmlUpper.length - htmlText.length); } - let dataEnd = htmlLower.indexOf('': - handler.pcdata('>'); + tagStack.pcdata('>'); break; default: - handler.pcdata('&'); + tagStack.pcdata('&'); break; } } @@ -527,81 +618,81 @@ amp.htmlparser.HtmlParser.EFlags = { * @type {Object} */ amp.htmlparser.HtmlParser.Elements = { - 'a': 0, - 'abbr': 0, - 'acronym': 0, - 'address': 0, - 'applet': amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'area': amp.htmlparser.HtmlParser.EFlags.EMPTY, - 'b': 0, - 'base': amp.htmlparser.HtmlParser.EFlags.EMPTY | + 'A': 0, + 'ABBR': 0, + 'ACRONYM': 0, + 'ADDRESS': 0, + 'APPLET': amp.htmlparser.HtmlParser.EFlags.UNSAFE, + 'AREA': amp.htmlparser.HtmlParser.EFlags.EMPTY, + 'B': 0, + 'BASE': amp.htmlparser.HtmlParser.EFlags.EMPTY | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'basefont': amp.htmlparser.HtmlParser.EFlags.EMPTY | + 'BASEFONT': amp.htmlparser.HtmlParser.EFlags.EMPTY | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'bdo': 0, - 'big': 0, - 'blockquote': 0, - 'body': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG | + 'BDO': 0, + 'BIG': 0, + 'BLOCKQUOTE': 0, + 'BODY': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG | amp.htmlparser.HtmlParser.EFlags.UNSAFE | amp.htmlparser.HtmlParser.EFlags.FOLDABLE, - 'br': amp.htmlparser.HtmlParser.EFlags.EMPTY, - 'button': 0, - 'canvas': 0, - 'caption': 0, - 'center': 0, - 'cite': 0, - 'code': 0, - 'col': amp.htmlparser.HtmlParser.EFlags.EMPTY, - 'colgroup': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'dd': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'del': 0, - 'dfn': 0, - 'dir': 0, - 'div': 0, - 'dl': 0, - 'dt': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'em': 0, - 'fieldset': 0, - 'font': 0, - 'form': 0, - 'frame': amp.htmlparser.HtmlParser.EFlags.EMPTY | + 'BR': amp.htmlparser.HtmlParser.EFlags.EMPTY, + 'BUTTON': 0, + 'CANVAS': 0, + 'CAPTION': 0, + 'CENTER': 0, + 'CITE': 0, + 'CODE': 0, + 'COL': amp.htmlparser.HtmlParser.EFlags.EMPTY, + 'COLGROUP': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'DD': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'DEL': 0, + 'DFN': 0, + 'DIR': 0, + 'DIV': 0, + 'DL': 0, + 'DT': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'EM': 0, + 'FIELDSET': 0, + 'FONT': 0, + 'FORM': 0, + 'FRAME': amp.htmlparser.HtmlParser.EFlags.EMPTY | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'frameset': amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'h1': 0, - 'h2': 0, - 'h3': 0, - 'h4': 0, - 'h5': 0, - 'h6': 0, - 'head': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG | + 'FRAMESET': amp.htmlparser.HtmlParser.EFlags.UNSAFE, + 'H1': 0, + 'H2': 0, + 'H3': 0, + 'H4': 0, + 'H5': 0, + 'H6': 0, + 'HEAD': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG | amp.htmlparser.HtmlParser.EFlags.UNSAFE | amp.htmlparser.HtmlParser.EFlags.FOLDABLE, - 'hr': amp.htmlparser.HtmlParser.EFlags.EMPTY, - 'html': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG | + 'HR': amp.htmlparser.HtmlParser.EFlags.EMPTY, + 'HTML': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG | amp.htmlparser.HtmlParser.EFlags.UNSAFE | amp.htmlparser.HtmlParser.EFlags.FOLDABLE, - 'i': 0, - 'iframe': amp.htmlparser.HtmlParser.EFlags.UNSAFE | + 'I': 0, + 'IFRAME': amp.htmlparser.HtmlParser.EFlags.UNSAFE | amp.htmlparser.HtmlParser.EFlags.CDATA, - 'img': amp.htmlparser.HtmlParser.EFlags.EMPTY, - 'input': amp.htmlparser.HtmlParser.EFlags.EMPTY, - 'ins': 0, - 'isindex': amp.htmlparser.HtmlParser.EFlags.EMPTY | + 'IMG': amp.htmlparser.HtmlParser.EFlags.EMPTY, + 'INPUT': amp.htmlparser.HtmlParser.EFlags.EMPTY, + 'INS': 0, + 'ISINDEX': amp.htmlparser.HtmlParser.EFlags.EMPTY | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'kbd': 0, - 'label': 0, - 'legend': 0, - 'li': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'link': amp.htmlparser.HtmlParser.EFlags.EMPTY | + 'KBD': 0, + 'LABEL': 0, + 'LEGEND': 0, + 'LI': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'LINK': amp.htmlparser.HtmlParser.EFlags.EMPTY | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'map': 0, - 'menu': 0, - 'meta': amp.htmlparser.HtmlParser.EFlags.EMPTY | + 'MAP': 0, + 'MENU': 0, + 'META': amp.htmlparser.HtmlParser.EFlags.EMPTY | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'noframes': amp.htmlparser.HtmlParser.EFlags.UNSAFE | + 'NOFRAMES': amp.htmlparser.HtmlParser.EFlags.UNSAFE | amp.htmlparser.HtmlParser.EFlags.CDATA, // TODO(johannes): This used to read: - // 'noscript': amp.htmlparser.HtmlParser.EFlags.UNSAFE | + // 'NOSCRIPT': amp.htmlparser.HtmlParser.EFlags.UNSAFE | // amp.htmlparser.HtmlParser.EFlags.CDATA, // // It appears that the effect of that is that anything inside is @@ -612,43 +703,43 @@ amp.htmlparser.HtmlParser.Elements = { // On a broader note this also means we may be missing other start/end // tag events inside elements marked as CDATA which our parser // should better reject. Yikes. - 'noscript': amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'object': amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'ol': 0, - 'optgroup': 0, - 'option': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'p': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'param': amp.htmlparser.HtmlParser.EFlags.EMPTY | + 'NOSCRIPT': amp.htmlparser.HtmlParser.EFlags.UNSAFE, + 'OBJECT': amp.htmlparser.HtmlParser.EFlags.UNSAFE, + 'OL': 0, + 'OPTGROUP': 0, + 'OPTION': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'P': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'PARAM': amp.htmlparser.HtmlParser.EFlags.EMPTY | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'pre': 0, - 'q': 0, - 's': 0, - 'samp': 0, - 'script': amp.htmlparser.HtmlParser.EFlags.UNSAFE | + 'PRE': 0, + 'Q': 0, + 'S': 0, + 'SAMP': 0, + 'SCRIPT': amp.htmlparser.HtmlParser.EFlags.UNSAFE | amp.htmlparser.HtmlParser.EFlags.CDATA, - 'select': 0, - 'small': 0, - 'span': 0, - 'strike': 0, - 'strong': 0, - 'style': amp.htmlparser.HtmlParser.EFlags.UNSAFE | + 'SELECT': 0, + 'SMALL': 0, + 'SPAN': 0, + 'STRIKE': 0, + 'STRONG': 0, + 'STYLE': amp.htmlparser.HtmlParser.EFlags.UNSAFE | amp.htmlparser.HtmlParser.EFlags.CDATA, - 'sub': 0, - 'sup': 0, - 'table': 0, - 'tbody': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'td': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'textarea': amp.htmlparser.HtmlParser.EFlags.RCDATA, - 'tfoot': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'th': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'thead': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'title': amp.htmlparser.HtmlParser.EFlags.RCDATA | + 'SUB': 0, + 'SUP': 0, + 'TABLE': 0, + 'TBODY': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'TD': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'TEXTAREA': amp.htmlparser.HtmlParser.EFlags.RCDATA, + 'TFOOT': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'TH': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'THEAD': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'TITLE': amp.htmlparser.HtmlParser.EFlags.RCDATA | amp.htmlparser.HtmlParser.EFlags.UNSAFE, - 'tr': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, - 'tt': 0, - 'u': 0, - 'ul': 0, - 'var': 0 + 'TR': amp.htmlparser.HtmlParser.EFlags.OPTIONAL_ENDTAG, + 'TT': 0, + 'U': 0, + 'UL': 0, + 'VAR': 0 }; @@ -716,6 +807,14 @@ amp.htmlparser.HtmlParser.NULL_RE_ = /\0/g; */ amp.htmlparser.HtmlParser.ENTITY_RE_ = /&(#\d+|#x[0-9A-Fa-f]+|\w+);/g; +/** + * Regular expression that matches strings composed of all space characters: + * https://dev.w3.org/html5/spec-LC/common-microsyntaxes.html#space-character + * @type {RegExp} + * @private + */ +amp.htmlparser.HtmlParser.SPACE_RE_ = /^\s*$/; + /** * Regular expression that matches decimal numbers. @@ -739,8 +838,9 @@ amp.htmlparser.HtmlParser.HEX_ESCAPE_RE_ = /^#x([0-9A-Fa-f]+)$/; * @private */ amp.htmlparser.HtmlParser.INSIDE_TAG_TOKEN_ = new RegExp( - // Don't capture space. - '^\\s*(?:' + + // Don't capture space. In this case, we don't use \s because it includes a + // nonbreaking space which gets included as an attribute in our validation. + '^[ \\t\\n\\f\\r\\v]*(?:' + // Capture an attribute name in group 1, and value in group 3. // We capture the fact that there was an attribute in group 2, since // interpreters are inconsistent in whether a group that matches nothing @@ -767,7 +867,7 @@ amp.htmlparser.HtmlParser.INSIDE_TAG_TOKEN_ = new RegExp( // An unquoted value that is not an attribute name. // We know it is not an attribute name because the previous // zero-width match would've eliminated that possibility. - '|[^>\"\'\\s]*' + + '|[^>\\s]*' + ')') + ')') + '?' + @@ -790,9 +890,13 @@ amp.htmlparser.HtmlParser.OUTSIDE_TAG_TOKEN_ = new RegExp( // Entity captured in group 1. '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);' + // Comments not captured. - '|<[!]--[\\s\\S]*?-->' + + '|<[!]--[\\s\\S]*?(?:-->|$)' + // '/' captured in group 2 for close tags, and name captured in group 3. - '|<(/)?([a-z!\\?][a-z0-9_:-]*)' + + // The first character of a tag (after possibly '/') can be A-Z, a-z, + // '!' or '?'. The remaining characters are more easily expressed as a + // negative set of: '\0', ' ', '\n', '\r', '\t', '\f', '\v', '>', or + // '/'. + '|<(/)?([a-z!\\?][^\\0 \\n\\r\\t\\f\\v>/]*)' + // Text captured in group 4. '|([^<&>]+)' + // Cruft captured in group 5. @@ -874,7 +978,6 @@ amp.htmlparser.HtmlParser.DocLocatorImpl = getCol() { return this.col_; } }; - /** * @param {string} str The string to lower case. * @return {string} The str in lower case format. @@ -895,6 +998,26 @@ amp.htmlparser.toLowerCase = function(str) { }); }; +/** + * @param {string} str The string to upper case. + * @return {string} The str in upper case format. + */ +amp.htmlparser.toUpperCase = function(str) { + // htmlparser.js heavily relies on the length of the strings, and + // unfortunately some characters change their length when + // lowercased; for instance, the Turkish İ has a length of 1, but + // when lower-cased, it has a length of 2. So, as a workaround we + // check that the length be the same as before upper-casing, and if + // not, we only upper-case the letters A-Z. + const upperCased = str.toUpperCase(); + if (upperCased.length == str.length) { + return upperCased; + } + return str.replace(/[a-z]/g, function(ch) { + return String.fromCharCode(ch.charCodeAt(0) & 223); + }); +}; + /** * This function gets eliminated by closure compiler. It's purpose in life * is to work around a bug wherein the compiler renames the object keys @@ -905,4 +1028,5 @@ function unusedHtmlParser() { console./*OK*/log(ElementsWithNoEndElements['']); console./*OK*/log(HtmlStructureElements['']); console./*OK*/log(HeadElements['']); + console./*OK*/log(ElementsWhichClosePTag['']); } diff --git a/validator/htmlparser_test.js b/validator/engine/htmlparser_test.js similarity index 65% rename from validator/htmlparser_test.js rename to validator/engine/htmlparser_test.js index ad8082fc749e..5d26ded188da 100644 --- a/validator/htmlparser_test.js +++ b/validator/engine/htmlparser_test.js @@ -50,6 +50,9 @@ class LoggingHandler extends amp.htmlparser.HtmlSaxHandler { /** @override */ endDoc() { this.log.push('endDoc()'); } + /** @override */ + markManufacturedBody() { this.log.push('markManufacturedBody()'); } + /** @override */ startTag(tagName, attrs) { this.log.push('startTag(' + tagName + ',[' + attrs + '])'); @@ -65,7 +68,8 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, 'hello world'); expect(handler.log).toEqual([ - 'startDoc()', 'pcdata("hello world")', 'endDoc()' + 'startDoc()', 'markManufacturedBody()', 'startTag(BODY,[])', + 'pcdata("hello world")', 'endTag(BODY)', 'endDoc()' ]); }); @@ -74,8 +78,8 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, ''); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(body,[])', 'startTag(img,[src,hello.gif])', - 'endTag(img)', 'endTag(body)', 'endDoc()' + 'startDoc()', 'markManufacturedBody()', 'startTag(BODY,[])', + 'startTag(IMG,[src,hello.gif])', 'endTag(IMG)', 'endTag(BODY)', 'endDoc()' ]); }); @@ -84,9 +88,9 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, '
    hello world
    '); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(body,[])', 'startTag(div,[])', - 'startTag(span,[])', 'pcdata("hello world")', 'endTag(span)', - 'endTag(div)', 'endTag(body)', 'endDoc()' + 'startDoc()', 'markManufacturedBody()', 'startTag(BODY,[])', + 'startTag(DIV,[])', 'startTag(SPAN,[])', 'pcdata("hello world")', + 'endTag(SPAN)', 'endTag(DIV)', 'endTag(BODY)', 'endDoc()' ]); }); @@ -95,9 +99,9 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, ''); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(body,[])', - 'startTag(img,[src,hello.gif,width,400px])', 'endTag(img)', - 'endTag(body)', 'endDoc()' + 'startDoc()', 'markManufacturedBody()', 'startTag(BODY,[])', + 'startTag(IMG,[src,hello.gif,width,400px])', 'endTag(IMG)', + 'endTag(BODY)', 'endDoc()' ]); }); @@ -106,9 +110,9 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, ''); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(body,[])', - 'startTag(input,[type,checkbox,checked,])', 'endTag(input)', - 'endTag(body)', 'endDoc()' + 'startDoc()', 'markManufacturedBody()', 'startTag(BODY,[])', + 'startTag(INPUT,[type,checkbox,checked,])', 'endTag(INPUT)', + 'endTag(BODY)', 'endDoc()' ]); }); @@ -117,8 +121,8 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, ''); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(body,[])', 'startTag(span,[])', 'endTag(span)', - 'endTag(body)', 'endDoc()' + 'startDoc()', 'markManufacturedBody()', 'startTag(BODY,[])', + 'startTag(SPAN,[])', 'endTag(SPAN)', 'endTag(BODY)', 'endDoc()' ]); }); @@ -127,9 +131,9 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, ''); expect(handler.log).toEqual([ - 'startDoc()', 'startTag(body,[])', - 'startTag(span,[style,background-color: black;])', 'endTag(span)', - 'endTag(body)', 'endDoc()' + 'startDoc()', 'markManufacturedBody()', 'startTag(BODY,[])', + 'startTag(SPAN,[style,background-color: black;])', 'endTag(SPAN)', + 'endTag(BODY)', 'endDoc()' ]); }); @@ -138,8 +142,8 @@ describe('HtmlParser', () => { const parser = new amp.htmlparser.HtmlParser(); parser.parse(handler, ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/validator/testdata/amp4ads_feature_tests/extensions.out b/validator/testdata/amp4ads_feature_tests/extensions.out new file mode 100644 index 000000000000..5c99fa91aae6 --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/extensions.out @@ -0,0 +1,38 @@ +FAIL +amp4ads_feature_tests/extensions.html:35:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:39:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:55:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:57:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:69:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:75:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:93:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:103:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:107:2 The attribute 'custom-element' may not appear in tag 'amp4ads engine amp4ads-v0.js script'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-accordion extension .js script' was found on this page, but is unused (no 'amp-accordion' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-accordion.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-anim extension .js script' was found on this page, but is unused (no 'amp-anim' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-anim.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-brid-player extension .js script' was found on this page, but is unused (no 'amp-brid-player' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-brid-player.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-brightcove extension .js script' was found on this page, but is unused (no 'amp-brightcove' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-brightcove.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-carousel extension .js script' was found on this page, but is unused (no 'amp-carousel' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-dailymotion extension .js script' was found on this page, but is unused (no 'amp-dailymotion' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-dailymotion.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-facebook extension .js script' was found on this page, but is unused (no 'amp-facebook' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-facebook.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-fit-text extension .js script' was found on this page, but is unused (no 'amp-fit-text' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-fit-text.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-font extension .js script' was found on this page, but is unused (no 'amp-font' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-font.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-fx-flying-carpet extension .js script' was found on this page, but is unused (no 'amp-fx-flying-carpet' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-fx-flying-carpet.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-image-lightbox extension .js script' was found on this page, but is unused (no 'amp-image-lightbox' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-image-lightbox.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-instagram extension .js script' was found on this page, but is unused (no 'amp-instagram' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-instagram.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-jwplayer extension .js script' was found on this page, but is unused (no 'amp-jwplayer' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-jwplayer.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-kaltura-player extension .js script' was found on this page, but is unused (no 'amp-kaltura-player' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-kaltura-player.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-lightbox extension .js script' was found on this page, but is unused (no 'amp-lightbox' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-lightbox.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-list extension .js script' was found on this page, but is unused (no 'amp-list' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-list.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-live-list extension .js script' was found on this page, but is unused (no 'amp-live-list' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-live-list.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-mustache extension .js script' was found on this page, but is unused (no 'template' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-mustache.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-o2-player extension .js script' was found on this page, but is unused (no 'amp-o2-player' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-o2-player.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-pinterest extension .js script' was found on this page, but is unused (no 'amp-pinterest' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-pinterest.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-reach-player extension .js script' was found on this page, but is unused (no 'amp-reach-player' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-reach-player.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-social-share extension .js script' was found on this page, but is unused (no 'amp-social-share' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-social-share.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-soundcloud extension .js script' was found on this page, but is unused (no 'amp-soundcloud' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-soundcloud.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-springboard-player extension .js script' was found on this page, but is unused (no 'amp-springboard-player' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-springboard-player.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-twitter extension .js script' was found on this page, but is unused (no 'amp-twitter' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-twitter.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-vimeo extension .js script' was found on this page, but is unused (no 'amp-vimeo' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-vimeo.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-vine extension .js script' was found on this page, but is unused (no 'amp-vine' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-vine.html) [DEPRECATION] +amp4ads_feature_tests/extensions.html:119:20 The extension 'amp-youtube extension .js script' was found on this page, but is unused (no 'amp-youtube' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-youtube.html) [DEPRECATION] diff --git a/validator/testdata/amp4ads_feature_tests/min_valid_amp4ads.html b/validator/testdata/amp4ads_feature_tests/min_valid_amp4ads.html new file mode 100644 index 000000000000..20c5de026660 --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/min_valid_amp4ads.html @@ -0,0 +1,30 @@ + + + + + + + + + + +Hello, world. + diff --git a/validator/testdata/amp4ads_feature_tests/min_valid_amp4ads.out b/validator/testdata/amp4ads_feature_tests/min_valid_amp4ads.out new file mode 100644 index 000000000000..7ef22e9a431a --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/min_valid_amp4ads.out @@ -0,0 +1 @@ +PASS diff --git a/validator/testdata/amp4ads_feature_tests/noscript.html b/validator/testdata/amp4ads_feature_tests/noscript.html new file mode 100644 index 000000000000..3483d2104c12 --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/noscript.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/validator/testdata/amp4ads_feature_tests/noscript.out b/validator/testdata/amp4ads_feature_tests/noscript.out new file mode 100644 index 000000000000..8cbe32b1d3e0 --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/noscript.out @@ -0,0 +1,8 @@ +FAIL +amp4ads_feature_tests/noscript.html:30:2 The tag 'noscript' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/noscript.html:32:2 The tag 'img' is disallowed. [DISALLOWED_HTML_WITH_AMP_EQUIVALENT] +amp4ads_feature_tests/noscript.html:34:2 The tag 'noscript' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/noscript.html:35:4 The tag 'img' is disallowed. [DISALLOWED_HTML_WITH_AMP_EQUIVALENT] +amp4ads_feature_tests/noscript.html:37:2 The tag 'noscript' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/noscript.html:38:4 The tag 'video' is disallowed. [DISALLOWED_HTML_WITH_AMP_EQUIVALENT] +amp4ads_feature_tests/noscript.html:39:4 The tag 'audio' is disallowed. [DISALLOWED_HTML_WITH_AMP_EQUIVALENT] diff --git a/validator/testdata/amp4ads_feature_tests/obsolete_tags.html b/validator/testdata/amp4ads_feature_tests/obsolete_tags.html new file mode 100644 index 000000000000..3e2f9a0d1ae9 --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/obsolete_tags.html @@ -0,0 +1,48 @@ + + + + + + + + + + + +These tags are not allowed: + + +
    + +
    + + + + + + + + + + + + + diff --git a/validator/testdata/amp4ads_feature_tests/obsolete_tags.out b/validator/testdata/amp4ads_feature_tests/obsolete_tags.out new file mode 100644 index 000000000000..b8e9c505bbc9 --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/obsolete_tags.out @@ -0,0 +1,15 @@ +FAIL +amp4ads_feature_tests/obsolete_tags.html:31:0 The tag 'acronym' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:32:0 The tag 'big' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:33:0 The tag 'center' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:34:0 The tag 'dir' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:35:0 The tag 'hgroup' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:36:0 The tag 'listing' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:37:0 The tag 'multicol' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:38:0 The tag 'nextid' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:39:0 The tag 'nobr' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:40:0 The tag 'spacer' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:41:0 The tag 'strike' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:42:0 The tag 'tt' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:43:0 The tag 'xmp' is disallowed. [DISALLOWED_HTML] +amp4ads_feature_tests/obsolete_tags.html:44:0 The tag 'o:p' is disallowed. [DISALLOWED_HTML] diff --git a/validator/testdata/amp4ads_feature_tests/style-amp-custom.html b/validator/testdata/amp4ads_feature_tests/style-amp-custom.html new file mode 100644 index 000000000000..e497863b1964 --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/style-amp-custom.html @@ -0,0 +1,141 @@ + + + + + + + + + + + + +Hello, world. + diff --git a/validator/testdata/amp4ads_feature_tests/style-amp-custom.out b/validator/testdata/amp4ads_feature_tests/style-amp-custom.out new file mode 100644 index 000000000000..94c6ad00c33a --- /dev/null +++ b/validator/testdata/amp4ads_feature_tests/style-amp-custom.out @@ -0,0 +1,15 @@ +FAIL +amp4ads_feature_tests/style-amp-custom.html:37:6 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'color' is disallowed together with 'transition'. Allowed properties: ['animation', 'opacity', 'transform', 'transition', 'visibility']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:43:4 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'transition' is disallowed unless the enclosing rule is prefixed with the '.amp-animate' qualification. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:56:6 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'transition' is set to the disallowed value 'background-color'. Allowed values: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:73:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'margin-left' is disallowed within @keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:74:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'width' is disallowed within @keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:78:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'margin-left' is disallowed within @keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:79:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'width' is disallowed within @keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:93:6 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'color' is disallowed together with '-o-transition'. Allowed properties: ['animation', 'opacity', 'transform', 'transition', 'visibility']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:99:4 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property '-o-transition' is disallowed unless the enclosing rule is prefixed with the '.amp-animate' qualification. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:112:6 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'transition' is set to the disallowed value 'background-color'. Allowed values: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:129:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'margin-left' is disallowed within @-o-keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:130:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'width' is disallowed within @-o-keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:134:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'margin-left' is disallowed within @-o-keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] +amp4ads_feature_tests/style-amp-custom.html:135:8 CSS syntax error in tag 'style amp-custom (AMP4ADS)' - the property 'width' is disallowed within @-o-keyframes. Allowed properties: ['opacity', 'transform']. [AUTHOR_STYLESHEET_PROBLEM] diff --git a/validator/testdata/feature_tests/base_href.html b/validator/testdata/feature_tests/base_href.html new file mode 100644 index 000000000000..9159728d3a1c --- /dev/null +++ b/validator/testdata/feature_tests/base_href.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + +Hello, world. + + diff --git a/validator/testdata/feature_tests/base_href.out b/validator/testdata/feature_tests/base_href.out new file mode 100644 index 000000000000..d67c657a173c --- /dev/null +++ b/validator/testdata/feature_tests/base_href.out @@ -0,0 +1,2 @@ +FAIL +feature_tests/base_href.html:26:2 The attribute 'href' in tag 'base' is set to the invalid value 'https://example.com/'. [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/css_errors.html b/validator/testdata/feature_tests/css_errors.html index 4a8ac8e34e14..b6691583f2fe 100644 --- a/validator/testdata/feature_tests/css_errors.html +++ b/validator/testdata/feature_tests/css_errors.html @@ -26,6 +26,12 @@ + + + + +This tag is disallowed. + + diff --git a/validator/testdata/feature_tests/deprecation_warnings_and_errors.out b/validator/testdata/feature_tests/deprecation_warnings_and_errors.out new file mode 100644 index 000000000000..37d42fe640ee --- /dev/null +++ b/validator/testdata/feature_tests/deprecation_warnings_and_errors.out @@ -0,0 +1,4 @@ +FAIL +feature_tests/deprecation_warnings_and_errors.html:29:2 The tag 'head > style[amp-boilerplate] - old variant' is deprecated - use 'head > style[amp-boilerplate]' instead. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-boilerplate.md) [DEPRECATION] +feature_tests/deprecation_warnings_and_errors.html:30:12 The tag 'noscript > style[amp-boilerplate] - old variant' is deprecated - use 'noscript > style[amp-boilerplate]' instead. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-boilerplate.md) [DEPRECATION] +feature_tests/deprecation_warnings_and_errors.html:34:0 The tag 'disallowed-tag' is disallowed. [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/dog_doc_type.out b/validator/testdata/feature_tests/dog_doc_type.out index bf8c66edcfd5..8de31d498181 100644 --- a/validator/testdata/feature_tests/dog_doc_type.out +++ b/validator/testdata/feature_tests/dog_doc_type.out @@ -1,5 +1,5 @@ FAIL -feature_tests/dog_doc_type.html:23:0 The attribute '🐶' may not appear in tag 'html doctype'. [DISALLOWED_HTML] +feature_tests/dog_doc_type.html:23:0 The attribute '🐶' may not appear in tag 'html doctype'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] feature_tests/dog_doc_type.html:24:0 The mandatory attribute '⚡' is missing in tag 'html ⚡ for top-level html'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/dog_doc_type.html:35:7 The mandatory tag 'html doctype' is missing or incorrect. [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/dog_doc_type.html:35:7 The mandatory tag 'html doctype' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] feature_tests/dog_doc_type.html:35:7 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/duplicate_attribute.html b/validator/testdata/feature_tests/duplicate_attribute.html new file mode 100644 index 000000000000..7e48aa48b261 --- /dev/null +++ b/validator/testdata/feature_tests/duplicate_attribute.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/validator/testdata/feature_tests/duplicate_attribute.out b/validator/testdata/feature_tests/duplicate_attribute.out new file mode 100644 index 000000000000..d68e4a610de0 --- /dev/null +++ b/validator/testdata/feature_tests/duplicate_attribute.out @@ -0,0 +1,2 @@ +FAIL +feature_tests/duplicate_attribute.html:33:3 The attribute 'width' in tag 'amp-img' is set to the invalid value '100%'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_LAYOUT_PROBLEM] diff --git a/validator/testdata/feature_tests/empty.out b/validator/testdata/feature_tests/empty.out index 76770818ff0d..597582640ef9 100644 --- a/validator/testdata/feature_tests/empty.out +++ b/validator/testdata/feature_tests/empty.out @@ -1,5 +1,5 @@ FAIL -feature_tests/empty.html:1:0 The mandatory tag 'html doctype' is missing or incorrect. [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/empty.html:1:0 The mandatory tag 'html doctype' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] feature_tests/empty.html:1:0 The mandatory tag 'html ⚡ for top-level html' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] feature_tests/empty.html:1:0 The mandatory tag 'head' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] feature_tests/empty.html:1:0 The mandatory tag 'link rel=canonical' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/forms.html b/validator/testdata/feature_tests/forms.html new file mode 100644 index 000000000000..4f08711f59e7 --- /dev/null +++ b/validator/testdata/feature_tests/forms.html @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + +
    +
    + + + +
    +
    +
    +
    + +
    + + + + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + + + + +
    + + + + +
    + + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + + diff --git a/validator/testdata/feature_tests/forms.out b/validator/testdata/feature_tests/forms.out new file mode 100644 index 000000000000..496a4a9ea01a --- /dev/null +++ b/validator/testdata/feature_tests/forms.out @@ -0,0 +1,22 @@ +FAIL +feature_tests/forms.html:82:2 Invalid URL protocol 'http:' for attribute 'action-xhr' in tag 'FORM [method=POST]'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:86:2 The domain 'cdn.ampproject.org' for attribute 'action-xhr' in tag 'FORM [method=POST]' is disallowed. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:90:2 The domain 'example-com.cdn.ampproject.org' for attribute 'action-xhr' in tag 'FORM [method=POST]' is disallowed. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:94:2 The domain 'example-com.amp.cloudflare.com' for attribute 'action-xhr' in tag 'FORM [method=POST]' is disallowed. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:102:2 The attribute 'target' in tag 'FORM [method=POST]' is set to the invalid value '_new'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:107:2 The tag 'select' may only appear as a descendant of tag 'form'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:108:4 The tag 'option' may only appear as a descendant of tag 'form'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:110:2 The tag 'textarea' may only appear as a descendant of tag 'form'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:113:4 The attribute 'type' in tag 'input' is set to the invalid value 'button'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:114:4 The attribute 'type' in tag 'input' is set to the invalid value 'file'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:115:4 The attribute 'type' in tag 'input' is set to the invalid value 'image'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:116:4 The attribute 'type' in tag 'input' is set to the invalid value 'password'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:121:4 The attribute 'visible-when-invalid' in tag 'div' is missing or incorrect, but required by attribute 'validation-for'. [DISALLOWED_HTML] +feature_tests/forms.html:122:4 The attribute 'visibile-when-invalid' may not appear in tag 'div'. [DISALLOWED_HTML] +feature_tests/forms.html:128:4 The attribute 'submit-error' may not appear in tag 'input'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:129:4 Tag 'FORM > DIV [submit-success]' must have 1 child tags - saw 0 child tags. [DISALLOWED_HTML] +feature_tests/forms.html:130:4 Tag 'FORM > DIV [submit-error]' must have 1 child tags - saw 0 child tags. [DISALLOWED_HTML] +feature_tests/forms.html:135:4 The attribute 'submit-success' may not appear in tag 'span'. [DISALLOWED_HTML] +feature_tests/forms.html:138:2 The attribute 'action' may not appear in tag 'FORM [method=POST]'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:141:2 The mandatory attribute 'action' is missing in tag 'FORM [method=GET]'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/forms.html:144:2 The mandatory attribute 'action' is missing in tag 'FORM [method=GET]'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/incorrect_custom_style.out b/validator/testdata/feature_tests/incorrect_custom_style.out index e2bf75ab87e0..b99bcc281afd 100644 --- a/validator/testdata/feature_tests/incorrect_custom_style.out +++ b/validator/testdata/feature_tests/incorrect_custom_style.out @@ -5,3 +5,4 @@ feature_tests/incorrect_custom_style.html:50:22 CSS syntax error in tag 'style a feature_tests/incorrect_custom_style.html:29:4 CSS syntax error in tag 'style amp-custom' - saw invalid at rule '@import'. [AUTHOR_STYLESHEET_PROBLEM] feature_tests/incorrect_custom_style.html:33:4 CSS syntax error in tag 'style amp-custom' - saw invalid at rule '@viewport'. [AUTHOR_STYLESHEET_PROBLEM] feature_tests/incorrect_custom_style.html:34:24 CSS syntax error in tag 'style amp-custom' - saw invalid at rule '@notallowednested'. [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/incorrect_custom_style.html:27:2 The text (CDATA) inside tag 'style amp-custom' contains 'CSS !important', which is disallowed. (see https://www.ampproject.org/docs/reference/spec.html#stylesheets) [AUTHOR_STYLESHEET_PROBLEM] diff --git a/validator/testdata/feature_tests/javascript_xss.out b/validator/testdata/feature_tests/javascript_xss.out index 205d26041813..194cfb2be85a 100644 --- a/validator/testdata/feature_tests/javascript_xss.out +++ b/validator/testdata/feature_tests/javascript_xss.out @@ -1,5 +1,5 @@ FAIL -feature_tests/javascript_xss.html:27:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/javascript_xss.html:28:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/javascript_xss.html:29:2 Invalid URL protocol 'vbscript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/javascript_xss.html:30:2 Invalid URL protocol 'data:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] +feature_tests/javascript_xss.html:27:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/javascript_xss.html:28:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/javascript_xss.html:29:2 Invalid URL protocol 'vbscript:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/javascript_xss.html:30:2 Invalid URL protocol 'data:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/link_meta_values.html b/validator/testdata/feature_tests/link_meta_values.html index bd7b7f505da7..31204e1b2990 100644 --- a/validator/testdata/feature_tests/link_meta_values.html +++ b/validator/testdata/feature_tests/link_meta_values.html @@ -25,11 +25,14 @@ --> + + + @@ -44,5 +47,8 @@ Hello, world. + + OK + diff --git a/validator/testdata/feature_tests/link_meta_values.out b/validator/testdata/feature_tests/link_meta_values.out index c285f479ece1..189e98d15108 100644 --- a/validator/testdata/feature_tests/link_meta_values.out +++ b/validator/testdata/feature_tests/link_meta_values.out @@ -1,3 +1,6 @@ FAIL -feature_tests/link_meta_values.html:32:2 The attribute 'name' in tag 'meta name= and content=' is set to the invalid value 'content-disposition'. [DISALLOWED_HTML] -feature_tests/link_meta_values.html:43:2 The mandatory attribute 'rel' is missing in tag 'link rel='. [DISALLOWED_HTML] +feature_tests/link_meta_values.html:29:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'manifest foo'. (see https://www.ampproject.org/docs/reference/spec.html#html-tags) [DISALLOWED_HTML] +feature_tests/link_meta_values.html:34:2 The attribute 'name' in tag 'meta name= and content=' is set to the invalid value 'content-disposition'. [DISALLOWED_HTML] +feature_tests/link_meta_values.html:35:2 The property 'minimum-scale' is missing from attribute 'content' in tag 'meta name=viewport'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/link_meta_values.html:35:2 The property 'width' is missing from attribute 'content' in tag 'meta name=viewport'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/link_meta_values.html:46:2 The mandatory attribute 'rel' is missing in tag 'link rel='. (see https://www.ampproject.org/docs/reference/spec.html#html-tags) [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/mandatory_dimensions.html b/validator/testdata/feature_tests/mandatory_dimensions.html index d07b20f2f851..39795366d164 100644 --- a/validator/testdata/feature_tests/mandatory_dimensions.html +++ b/validator/testdata/feature_tests/mandatory_dimensions.html @@ -99,7 +99,7 @@ - + @@ -124,7 +124,7 @@ - + @@ -137,7 +137,7 @@ - + diff --git a/validator/testdata/feature_tests/mandatory_dimensions.out b/validator/testdata/feature_tests/mandatory_dimensions.out index 53f280891636..745e8c55b7f3 100644 --- a/validator/testdata/feature_tests/mandatory_dimensions.out +++ b/validator/testdata/feature_tests/mandatory_dimensions.out @@ -22,6 +22,7 @@ feature_tests/mandatory_dimensions.html:141:4 The attribute 'srcset' may not app feature_tests/mandatory_dimensions.html:142:4 The attribute 'srcset' may not appear in tag 'amp-instagram'. (see https://www.ampproject.org/docs/reference/extended/amp-instagram.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:143:4 The attribute 'src' may not appear in tag 'amp-lightbox'. (see https://www.ampproject.org/docs/reference/extended/amp-lightbox.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:146:4 The attribute 'foo' may not appear in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-ad extension .js script' is missing or incorrect, but required by 'amp-ad'. This will soon be an error. (see https://www.ampproject.org/docs/reference/amp-ad.html) [DEPRECATION] feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-anim extension .js script' is missing or incorrect, but required by 'amp-anim'. (see https://www.ampproject.org/docs/reference/extended/amp-anim.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-audio extension .js script' is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-carousel extension .js script' is missing or incorrect, but required by 'amp-carousel'. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [AMP_TAG_PROBLEM] @@ -30,4 +31,5 @@ feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-iframe extension .js feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-instagram extension .js script' is missing or incorrect, but required by 'amp-instagram'. (see https://www.ampproject.org/docs/reference/extended/amp-instagram.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-lightbox extension .js script' is missing or incorrect, but required by 'amp-lightbox'. (see https://www.ampproject.org/docs/reference/extended/amp-lightbox.html) [AMP_TAG_PROBLEM] feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-twitter extension .js script' is missing or incorrect, but required by 'amp-twitter'. (see https://www.ampproject.org/docs/reference/extended/amp-twitter.html) [AMP_TAG_PROBLEM] +feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-video extension .js script' is missing or incorrect, but required by 'amp-video'. This will soon be an error. (see https://www.ampproject.org/docs/reference/amp-video.html) [DEPRECATION] feature_tests/mandatory_dimensions.html:150:7 The tag 'amp-youtube extension .js script' is missing or incorrect, but required by 'amp-youtube'. (see https://www.ampproject.org/docs/reference/extended/amp-youtube.html) [AMP_TAG_PROBLEM] diff --git a/validator/testdata/feature_tests/manufactured_body.html b/validator/testdata/feature_tests/manufactured_body.html new file mode 100644 index 000000000000..295004ca6063 --- /dev/null +++ b/validator/testdata/feature_tests/manufactured_body.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/validator/testdata/feature_tests/manufactured_body.out b/validator/testdata/feature_tests/manufactured_body.out new file mode 100644 index 000000000000..951faa0fdd78 --- /dev/null +++ b/validator/testdata/feature_tests/manufactured_body.out @@ -0,0 +1,2 @@ +FAIL +feature_tests/manufactured_body.html:32:0 Tag or text which is only allowed inside the body section found outside of the body section. [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/parser.out b/validator/testdata/feature_tests/parser.out index ca613732d187..a2438d59d556 100644 --- a/validator/testdata/feature_tests/parser.out +++ b/validator/testdata/feature_tests/parser.out @@ -1,24 +1,24 @@ FAIL -feature_tests/parser.html:32:2 The attribute '"' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:33:2 The attribute ''' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:34:2 Missing URL for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:35:2 Missing URL for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:36:2 Missing URL for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:37:2 Missing URL for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:38:2 The attribute ''' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:39:2 The attribute '"' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:40:2 The attribute 'href"foo.html"' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:41:2 The attribute 'href'foo.html'' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:42:2 The attribute 'href"foo.html""' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:43:2 The attribute 'href'foo.html''' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:44:2 The attribute 'href""foo.html"' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:45:2 The attribute 'href''foo.html'' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:46:2 The attribute 'href""foo.html""' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:47:2 The attribute 'href''foo.html''' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:56:2 The attribute '<' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:57:2 The attribute '/' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:58:2 The attribute '/a' may not appear in tag 'a'. [DISALLOWED_HTML] -feature_tests/parser.html:59:2 The attribute '/a' may not appear in tag 'a'. [DISALLOWED_HTML] +feature_tests/parser.html:32:2 The attribute '"' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:33:2 The attribute ''' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:34:2 The attribute 'foo.html"' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:35:2 The attribute 'foo.html'' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:36:2 The attribute 'foo.html""' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:37:2 The attribute 'foo.html''' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:38:2 The attribute ''' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:39:2 The attribute '"' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:40:2 The attribute 'href"foo.html"' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:41:2 The attribute 'href'foo.html'' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:42:2 The attribute 'href"foo.html""' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:43:2 The attribute 'href'foo.html''' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:44:2 The attribute 'href""foo.html"' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:45:2 The attribute 'href''foo.html'' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:46:2 The attribute 'href""foo.html""' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:47:2 The attribute 'href''foo.html''' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:56:2 The attribute '<' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:57:2 The attribute '/' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:58:2 The attribute '/a' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/parser.html:59:2 The attribute '/a' may not appear in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] feature_tests/parser.html:62:2 The tag '?php' is disallowed. [DISALLOWED_HTML] feature_tests/parser.html:64:2 The tag '?php' is disallowed. [DISALLOWED_HTML] feature_tests/parser.html:65:2 The tag '?php' is disallowed. [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/partial_comment.html b/validator/testdata/feature_tests/partial_comment.html new file mode 100644 index 000000000000..c7f71b82bbbf --- /dev/null +++ b/validator/testdata/feature_tests/partial_comment.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + +Hello, world. + + + + + + + + + + + + + + +Hello, world. + + diff --git a/validator/testdata/feature_tests/property_parsing.out b/validator/testdata/feature_tests/property_parsing.out new file mode 100644 index 000000000000..7ef22e9a431a --- /dev/null +++ b/validator/testdata/feature_tests/property_parsing.out @@ -0,0 +1 @@ +PASS diff --git a/validator/testdata/feature_tests/rdfa.html b/validator/testdata/feature_tests/rdfa.html new file mode 100644 index 000000000000..1510f391a1b0 --- /dev/null +++ b/validator/testdata/feature_tests/rdfa.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + Buffy + Summers + + + + + diff --git a/validator/testdata/feature_tests/rdfa.out b/validator/testdata/feature_tests/rdfa.out new file mode 100644 index 000000000000..7ef22e9a431a --- /dev/null +++ b/validator/testdata/feature_tests/rdfa.out @@ -0,0 +1 @@ +PASS diff --git a/validator/testdata/feature_tests/regexps.html b/validator/testdata/feature_tests/regexps.html index 582a8a9231d2..085d6a0ab8b8 100644 --- a/validator/testdata/feature_tests/regexps.html +++ b/validator/testdata/feature_tests/regexps.html @@ -38,15 +38,26 @@ + + + + + @@ -65,6 +76,20 @@ + + + @@ -78,5 +103,31 @@ + +
    +
    +
    + + +
    +
    +
    + + + + + + + + diff --git a/validator/testdata/feature_tests/regexps.out b/validator/testdata/feature_tests/regexps.out index 50e5ef985b3f..3ded84b44261 100644 --- a/validator/testdata/feature_tests/regexps.out +++ b/validator/testdata/feature_tests/regexps.out @@ -3,14 +3,24 @@ feature_tests/regexps.html:27:2 The tag 'head > style[amp-boilerplate] - old var feature_tests/regexps.html:27:2 The mandatory text (CDATA) inside tag 'head > style[amp-boilerplate] - old variant' is missing or incorrect. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-boilerplate.md) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] feature_tests/regexps.html:36:2 The attribute 'src' in tag 'amp-vine extension .js script' is set to the invalid value 'https://cdn.ampproject.org/v0/amp-vine-0.1.js?foobar'. (see https://www.ampproject.org/docs/reference/extended/amp-vine.html) [CUSTOM_JAVASCRIPT_DISALLOWED] feature_tests/regexps.html:37:2 The attribute 'src' in tag 'amp-vine extension .js script' is set to the invalid value 'http://xss.com/https://cdn.ampproject.org/v0/amp-vine-0.1.js?foobar'. (see https://www.ampproject.org/docs/reference/extended/amp-vine.html) [CUSTOM_JAVASCRIPT_DISALLOWED] -feature_tests/regexps.html:45:2 The attribute 'href' in tag 'link rel=stylesheet for fonts' is set to the invalid value 'http://xss.com/https://fonts.googleapis.com/css?foobar'. [AUTHOR_STYLESHEET_PROBLEM] -feature_tests/regexps.html:55:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'import'. [DISALLOWED_HTML] -feature_tests/regexps.html:56:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'accessibility subresource'. [DISALLOWED_HTML] -feature_tests/regexps.html:57:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'manifest accessibility'. [DISALLOWED_HTML] -feature_tests/regexps.html:65:2 The attribute 'name' in tag 'meta name= and content=' is set to the invalid value 'content-disposition'. [DISALLOWED_HTML] -feature_tests/regexps.html:66:2 The attribute 'name' in tag 'meta name= and content=' is set to the invalid value 'invalid content-disposition'. [DISALLOWED_HTML] -feature_tests/regexps.html:77:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'invalid'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] -feature_tests/regexps.html:78:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'desktopfoo'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] -feature_tests/regexps.html:79:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'foodesktop'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] -feature_tests/regexps.html:82:7 The tag 'head > style[amp-boilerplate]' is missing or incorrect, but required by 'noscript > style[amp-boilerplate]'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-boilerplate.md) [GENERIC] -feature_tests/regexps.html:82:7 The tag 'amp-audio extension .js script' is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/regexps.html:47:2 The attribute 'href' in tag 'link rel=stylesheet for fonts' is set to the invalid value 'http://xss.com/https://fonts.googleapis.com/css?foobar'. (see https://www.ampproject.org/docs/reference/spec.html#custom-fonts) [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/regexps.html:56:2 The attribute 'href' in tag 'link rel=stylesheet for fonts' is set to the invalid value 'https://maxcdn.bootstrapcdn.com/font-awesome/../bootstrap/3.3.7/css/bootstrap.min.class'. (see https://www.ampproject.org/docs/reference/spec.html#custom-fonts) [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/regexps.html:66:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'import'. (see https://www.ampproject.org/docs/reference/spec.html#html-tags) [DISALLOWED_HTML] +feature_tests/regexps.html:67:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'accessibility subresource'. (see https://www.ampproject.org/docs/reference/spec.html#html-tags) [DISALLOWED_HTML] +feature_tests/regexps.html:68:2 The attribute 'rel' in tag 'link rel=' is set to the invalid value 'manifest accessibility'. (see https://www.ampproject.org/docs/reference/spec.html#html-tags) [DISALLOWED_HTML] +feature_tests/regexps.html:76:2 The attribute 'name' in tag 'meta name= and content=' is set to the invalid value 'content-disposition'. [DISALLOWED_HTML] +feature_tests/regexps.html:77:2 The attribute 'name' in tag 'meta name= and content=' is set to the invalid value 'invalid content-disposition'. [DISALLOWED_HTML] +feature_tests/regexps.html:83:2 The text (CDATA) inside tag 'style amp-custom' contains 'CSS i-amphtml- name prefix', which is disallowed. (see https://www.ampproject.org/docs/reference/spec.html#stylesheets) [AUTHOR_STYLESHEET_PROBLEM] +feature_tests/regexps.html:102:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'invalid'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/regexps.html:103:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'desktopfoo'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/regexps.html:104:2 The attribute 'autoplay' in tag 'amp-audio' is set to the invalid value 'foodesktop'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/regexps.html:111:2 The attribute 'class' in tag 'div' is set to the invalid value 'example-amp-font i-amphtml-hidden'. [DISALLOWED_HTML] +feature_tests/regexps.html:112:2 The attribute 'class' in tag 'div' is set to the invalid value 'i-amphtml-hidden example-amp-font'. [DISALLOWED_HTML] +feature_tests/regexps.html:119:2 The attribute 'id' in tag 'div' is set to the invalid value 'i-amphtml-abc'. [DISALLOWED_HTML] +feature_tests/regexps.html:120:2 The attribute 'id' in tag 'div' is set to the invalid value 'AMP'. [DISALLOWED_HTML] +feature_tests/regexps.html:128:2 The attribute 'name' in tag 'input' is set to the invalid value 'innerHTML'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/regexps.html:129:2 The attribute 'name' in tag 'input' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/regexps.html:130:2 The attribute 'name' in tag 'input' is set to the invalid value 'webkitRequestFullscreen'. (see https://www.ampproject.org/docs/reference/extended/amp-form.html) [DISALLOWED_HTML] +feature_tests/regexps.html:133:7 The tag 'head > style[amp-boilerplate]' is missing or incorrect, but required by 'noscript > style[amp-boilerplate]'. (see https://github.com/ampproject/amphtml/blob/master/spec/amp-boilerplate.md) [GENERIC] +feature_tests/regexps.html:133:7 The tag 'amp-audio extension .js script' is missing or incorrect, but required by 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/regexps.html:133:7 The extension 'amp-vine extension .js script' was found on this page, but is unused (no 'amp-vine' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-vine.html) [DEPRECATION] diff --git a/validator/testdata/feature_tests/several_errors.html b/validator/testdata/feature_tests/several_errors.html index bbd76ae5f5ea..ad4219f5a4b6 100644 --- a/validator/testdata/feature_tests/several_errors.html +++ b/validator/testdata/feature_tests/several_errors.html @@ -35,5 +35,9 @@ + + + diff --git a/validator/testdata/feature_tests/several_errors.out b/validator/testdata/feature_tests/several_errors.out index 46c328426a89..473487d1f113 100644 --- a/validator/testdata/feature_tests/several_errors.out +++ b/validator/testdata/feature_tests/several_errors.out @@ -4,6 +4,7 @@ feature_tests/several_errors.html:26:2 The tag 'script' is disallowed except in feature_tests/several_errors.html:32:2 The mandatory attribute 'height' is missing in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_LAYOUT_PROBLEM] feature_tests/several_errors.html:34:2 The attribute 'width' in tag 'amp-ad' is set to the invalid value '100%'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_LAYOUT_PROBLEM] feature_tests/several_errors.html:37:2 The attribute 'made_up_attribute' may not appear in tag 'amp-ad'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_TAG_PROBLEM] -feature_tests/several_errors.html:39:7 The mandatory tag 'meta charset=utf-8' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/several_errors.html:39:7 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] -feature_tests/several_errors.html:39:7 The mandatory tag 'amphtml engine v0.js script' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/several_errors.html:40:2 The attribute 'height' in tag 'amp-ad' is set to the invalid value 'foo bar baz'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_LAYOUT_PROBLEM] +feature_tests/several_errors.html:43:7 The mandatory tag 'meta charset=utf-8' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/several_errors.html:43:7 The mandatory tag 'meta name=viewport' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/several_errors.html:43:7 The mandatory tag 'amphtml engine v0.js script' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/testdata/feature_tests/spec_example.html b/validator/testdata/feature_tests/spec_example.html index 390c4b000906..540c7a75cfc7 100644 --- a/validator/testdata/feature_tests/spec_example.html +++ b/validator/testdata/feature_tests/spec_example.html @@ -41,7 +41,7 @@ - +

    Sample document

    diff --git a/validator/testdata/feature_tests/spec_example.out b/validator/testdata/feature_tests/spec_example.out index 7ef22e9a431a..c572bd265eba 100644 --- a/validator/testdata/feature_tests/spec_example.out +++ b/validator/testdata/feature_tests/spec_example.out @@ -1 +1,3 @@ PASS +feature_tests/spec_example.html:59:7 The tag 'amp-ad extension .js script' is missing or incorrect, but required by 'amp-ad'. This will soon be an error. (see https://www.ampproject.org/docs/reference/amp-ad.html) [DEPRECATION] +feature_tests/spec_example.html:59:7 The extension 'amp-carousel extension .js script' was found on this page, but is unused (no 'amp-carousel' tag seen). This may become an error in the future. (see https://www.ampproject.org/docs/reference/extended/amp-carousel.html) [DEPRECATION] diff --git a/validator/testdata/feature_tests/svg.html b/validator/testdata/feature_tests/svg.html index 8cb6373aeb04..875e67884ba4 100644 --- a/validator/testdata/feature_tests/svg.html +++ b/validator/testdata/feature_tests/svg.html @@ -68,6 +68,20 @@

    SVG

    + + + + + + + + + + diff --git a/validator/testdata/feature_tests/svg.out b/validator/testdata/feature_tests/svg.out index 7ef22e9a431a..e6792d4c8705 100644 --- a/validator/testdata/feature_tests/svg.out +++ b/validator/testdata/feature_tests/svg.out @@ -1 +1,2 @@ -PASS +FAIL +feature_tests/svg.html:75:2 The attribute 'xlink:href' in tag 'image' is set to the invalid value 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjE0cHgiIGhlaWdodD0iMTRweCIgdmlld0JveD0iMCAwIDE0IDE0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjYuMSAoMjYzMTMpIC0gaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoIC0tPgogICAgPHRpdGxlPkFNUCBMb2dvPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IkFNUC1Mb2dvIiBmaWxsPSIjMDM3OUM0Ij4KICAgICAgICAgICAgPHBhdGggZD0iTTguNjU3ODE3LDAuMjE2MDYyIEw3LjY0NzI5Nyw1LjU4OTk2MiBMMTAuMDg1MzI3LDUuNTg5OTYyIEMxMC4xODg3ODcsNS41ODk5NjIgMTAuMjU2NDc3LDUuNjk4MzIyIDEwLjIxMTExNyw1Ljc5MTI4MiBMNi4yMzAyMTcsMTMuOTU4NTMyIEM2LjQ4MDQ2NywxMy45ODU2MjIgNi43MzQ1NjcsMTMuOTk5OTcyIDYuOTkyMDk3LDEzLjk5OTk3MiBDMTAuODUzNzE3LDEzLjk5OTk3MiAxMy45ODQxMTcsMTAuODY5NTcyIDEzLjk4NDExNyw3LjAwNzk1MiBDMTMuOTg0MTE3LDMuNzIwNDAyIDExLjcxNTA2NywwLjk2MzMxMiA4LjY1NzgxNywwLjIxNjA2MiIgaWQ9IkZpbGwtNCI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNNi40MjUzOTgsOC41Mjk5NDggTDMuOTg3MTU4LDguNTI5OTQ4IEMzLjg4MzY5OCw4LjUyOTk0OCAzLjgxNTkzOCw4LjQyMTU4OCAzLjg2MTI5OCw4LjMyODYyOCBMNy44ODU4MDgsMC4wNzI0NzggQzcuNTkxODA4LDAuMDM0OTU4IDcuMjkyMTM4LDAuMDE1NzA4IDYuOTg3ODQ4LDAuMDE1ODQ4IEMzLjE2ODIyOCwwLjAxODA4OCAwLjAyODAyOCwzLjEzNjMwOCAwLjAwMDE2OCw2Ljk1NTg1OCBDLTAuMDI0MTkyLDEwLjMwMzMyOCAyLjMwNDA3OCwxMy4xMTExNjggNS40Mjk3MTgsMTMuODI0NjA4IEw2LjQyNTM5OCw4LjUyOTk0OCBaIiBpZD0iRmlsbC0xIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K'. (see https://www.ampproject.org/docs/reference/spec.html#svg) [DISALLOWED_HTML] diff --git a/validator/testdata/feature_tests/track_tag.out b/validator/testdata/feature_tests/track_tag.out index 3056a7c36c32..29f71e1391ee 100644 --- a/validator/testdata/feature_tests/track_tag.out +++ b/validator/testdata/feature_tests/track_tag.out @@ -4,3 +4,4 @@ feature_tests/track_tag.html:50:4 The mandatory attribute 'srclang' is missing i feature_tests/track_tag.html:55:11 The mandatory attribute 'srclang' is missing in tag 'audio > track[kind=subtitles]'. [DISALLOWED_HTML] feature_tests/track_tag.html:56:11 The mandatory attribute 'srclang' is missing in tag 'video > track[kind=subtitles]'. [DISALLOWED_HTML] feature_tests/track_tag.html:59:4 The mandatory attribute 'srclang' is missing in tag 'amp-video > track[kind=subtitles]'. [AMP_TAG_PROBLEM] +feature_tests/track_tag.html:63:7 The tag 'amp-video extension .js script' is missing or incorrect, but required by 'amp-video'. This will soon be an error. (see https://www.ampproject.org/docs/reference/amp-video.html) [DEPRECATION] diff --git a/validator/testdata/feature_tests/urls.html b/validator/testdata/feature_tests/urls.html index bae63c27120d..5a198074591c 100644 --- a/validator/testdata/feature_tests/urls.html +++ b/validator/testdata/feature_tests/urls.html @@ -42,7 +42,6 @@ Invalid protocol Invalid protocol Valid protocol - Malformed URL @@ -57,8 +56,6 @@ - - @@ -76,11 +73,25 @@ + + Invalid protocol + + + uhm, no + + + + + + + + diff --git a/validator/testdata/feature_tests/urls.out b/validator/testdata/feature_tests/urls.out index aa6e4e78cedf..0dd59fa80be0 100644 --- a/validator/testdata/feature_tests/urls.out +++ b/validator/testdata/feature_tests/urls.out @@ -1,29 +1,38 @@ FAIL feature_tests/urls.html:25:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'link rel=canonical'. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [DISALLOWED_HTML] -feature_tests/urls.html:39:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/urls.html:40:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/urls.html:41:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/urls.html:42:2 Invalid URL protocol 'vbscript:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/urls.html:43:2 Invalid URL protocol 'data:' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/urls.html:45:2 Malformed URL 'http://site:80808/%AFpage' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/urls.html:47:2 Invalid URL protocol 'javascript:' for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:49:2 Multiple image candidates with the same width or pixel density found in attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [DISALLOWED_HTML] -feature_tests/urls.html:57:2 Invalid URL protocol 'javascript:' for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:59:2 Invalid URL protocol 'javascript:' for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:61:2 Malformed URL 'http://site:80808/%AFimg' for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:63:2 Missing URL for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:65:2 Multiple image candidates with the same width or pixel density found in attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [DISALLOWED_HTML] -feature_tests/urls.html:67:2 The attribute 'srcset' in tag 'amp-img' is set to the invalid value 'image 1x.png 1x, image 2x.png 2x'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:72:2 Missing URL for attribute 'src' in tag 'amp-ad'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:73:2 Missing URL for attribute 'src' in tag 'amp-anim'. (see https://www.ampproject.org/docs/reference/extended/amp-anim.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:74:2 Missing URL for attribute 'src' in tag 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:75:2 Missing URL for attribute 'src' in tag 'amp-iframe'. (see https://www.ampproject.org/docs/reference/extended/amp-iframe.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:76:2 Missing URL for attribute 'src' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:77:2 Missing URL for attribute 'src' in tag 'amp-pixel'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:78:2 Missing URL for attribute 'src' in tag 'amp-video'. (see https://www.ampproject.org/docs/reference/amp-video.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:37:2 Malformed URL 'https://' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:39:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:40:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:41:2 Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:42:2 Invalid URL protocol 'vbscript:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:43:2 Invalid URL protocol 'data:' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:46:2 Invalid URL protocol 'javascript:' for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:48:2 Multiple image candidates with the same width or pixel density found in attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [DISALLOWED_HTML] +feature_tests/urls.html:56:2 Invalid URL protocol 'javascript:' for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:58:2 Invalid URL protocol 'javascript:' for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:60:2 Missing URL for attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:62:2 Multiple image candidates with the same width or pixel density found in attribute 'srcset' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [DISALLOWED_HTML] +feature_tests/urls.html:64:2 The attribute 'srcset' in tag 'amp-img' is set to the invalid value 'image 1x.png 1x, image 2x.png 2x'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:69:2 Missing URL for attribute 'src' in tag 'amp-ad'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:70:2 Missing URL for attribute 'src' in tag 'amp-anim'. (see https://www.ampproject.org/docs/reference/extended/amp-anim.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:71:2 Missing URL for attribute 'src' in tag 'amp-audio'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:72:2 Missing URL for attribute 'src' in tag 'amp-iframe'. (see https://www.ampproject.org/docs/reference/extended/amp-iframe.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:73:2 Missing URL for attribute 'src' in tag 'amp-img'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:74:2 Missing URL for attribute 'src' in tag 'amp-pixel'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:75:2 Missing URL for attribute 'src' in tag 'amp-video'. (see https://www.ampproject.org/docs/reference/amp-video.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:78:2 Missing URL for attribute 'src' in tag 'amp-ad'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_TAG_PROBLEM] feature_tests/urls.html:80:2 Invalid URL protocol 'http:' for attribute 'src' in tag 'amp-ad'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_TAG_PROBLEM] feature_tests/urls.html:81:2 Invalid URL protocol 'http:' for attribute 'src' in tag 'amp-iframe'. (see https://www.ampproject.org/docs/reference/extended/amp-iframe.html) [AMP_TAG_PROBLEM] feature_tests/urls.html:82:2 Invalid URL protocol 'http:' for attribute 'src' in tag 'amp-pixel'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_TAG_PROBLEM] feature_tests/urls.html:83:2 Invalid URL protocol 'http:' for attribute 'src' in tag 'amp-video'. (see https://www.ampproject.org/docs/reference/amp-video.html) [AMP_TAG_PROBLEM] -feature_tests/urls.html:84:2 Invalid URL protocol 'j a v a s c r i p t :' for attribute 'href' in tag 'a'. [DISALLOWED_HTML] -feature_tests/urls.html:86:7 The mandatory tag 'link rel=canonical' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] +feature_tests/urls.html:84:2 Invalid URL protocol 'j a v a s c r i p t :' for attribute 'href' in tag 'a'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:87:2 The attribute 'href' in tag 'a' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/spec.html#links) [DISALLOWED_HTML] +feature_tests/urls.html:88:2 The attribute 'src' in tag 'amp-ad' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/amp-ad.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:89:2 The attribute 'src' in tag 'amp-anim' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/extended/amp-anim.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:90:2 The attribute 'src' in tag 'amp-audio' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/extended/amp-audio.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:91:2 The attribute 'src' in tag 'amp-iframe' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/extended/amp-iframe.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:92:2 The attribute 'src' in tag 'amp-img' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/amp-img.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:93:2 The attribute 'src' in tag 'amp-pixel' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/amp-pixel.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:94:2 The attribute 'src' in tag 'amp-video' is set to the invalid value '__amp_source_origin'. (see https://www.ampproject.org/docs/reference/amp-video.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:95:2 The attribute 'src' in tag 'amp-video' is set to the invalid value '%5f_amp_source%5forigin'. (see https://www.ampproject.org/docs/reference/amp-video.html) [AMP_TAG_PROBLEM] +feature_tests/urls.html:97:7 The mandatory tag 'link rel=canonical' is missing or incorrect. (see https://www.ampproject.org/docs/reference/spec.html#required-markup) [MANDATORY_AMP_TAG_MISSING_OR_INCORRECT] diff --git a/validator/validator-full.js b/validator/validator-full.js deleted file mode 100644 index 1acf7d3cfd98..000000000000 --- a/validator/validator-full.js +++ /dev/null @@ -1,546 +0,0 @@ -/** - * @license - * Copyright 2016 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the license. - */ -goog.provide('amp.validator.Terminal'); -goog.provide('amp.validator.annotateWithErrorCategories'); -goog.provide('amp.validator.renderErrorMessage'); -goog.provide('amp.validator.renderValidationResult'); -goog.provide('amp.validator.validateString'); -goog.require('amp.htmlparser.HtmlParser'); -goog.require('amp.validator.ValidationHandler'); -goog.require('amp.validator.ValidationResult'); -goog.require('goog.asserts'); -goog.require('goog.string'); -goog.require('goog.uri.utils'); - -/** - * Validates a document input as a string. - * @param {string} inputDocContents - * @return {!amp.validator.ValidationResult} Validation Result (status and - * errors) - * @export - */ -amp.validator.validateString = function(inputDocContents) { - goog.asserts.assertString(inputDocContents, 'Input document is not a string'); - - const handler = new amp.validator.ValidationHandler(); - const parser = new amp.htmlparser.HtmlParser(); - parser.parse(handler, inputDocContents); - - return handler.Result(); -}; - -/** - * The terminal is an abstraction for the window.console object which - * accomodates differences between console implementations and provides - * a convenient way to capture what's being emitted to the terminal - * in a unittest. Pass the optional parameter to the constructor - * to observe the calls that would have gone to window.console otherwise. - * @constructor - */ -amp.validator.Terminal = class { - /** - * @param {!Array=} opt_out an array into which the terminal will - * emit one string per info / warn / error calls. - */ - constructor(opt_out) { this.out_ = opt_out || null; } - - /** @param {string} msg */ - info(msg) { - if (this.out_) { - this.out_.push('I: ' + msg); - } else { - (console.info || console.log).call(console, msg); - } - } - - /** @param {string} msg */ - warn(msg) { - if (this.out_) { - this.out_.push('W: ' + msg); - } else if (console.warn) { - console.warn(msg); - } else { - console.log('WARNING: ' + msg); - } - } - - /** @param {string} msg */ - error(msg) { - if (this.out_) { - this.out_.push('E: ' + msg); - } else if (console.error) { - console.error(msg); - } else { - console.log('ERROR: ' + msg); - } - } -}; - -/** - * Emits this validation result to the terminal, distinguishing warnings and - * errors. - * @param {string} url - * @param {!amp.validator.Terminal=} opt_terminal - * @param {string=} opt_errorCategoryFilter - */ -amp.validator.ValidationResult.prototype.outputToTerminal = function( - url, opt_terminal, opt_errorCategoryFilter) { - - const terminal = opt_terminal || new amp.validator.Terminal(); - const errorCategoryFilter = opt_errorCategoryFilter || null; - - const status = this.status; - if (status === amp.validator.ValidationResult.Status.PASS) { - terminal.info('AMP validation successful.'); - return; - } - if (status !== amp.validator.ValidationResult.Status.FAIL) { - terminal.error( - 'AMP validation had unknown results. This should not happen.'); - return; - } - let errors; - if (errorCategoryFilter === null) { - terminal.error('AMP validation had errors:'); - errors = this.errors; - } else { - errors = []; - for (const error of this.errors) { - if (amp.validator.categorizeError(error) === errorCategoryFilter) { - errors.push(error); - } - } - const urlWithoutFilter = - goog.uri.utils.removeFragment(url) + '#development=1'; - if (errors.length === 0) { - terminal.error( - 'AMP validation - no errors matching ' + - 'filter=' + errorCategoryFilter + ' found. ' + - 'To see all errors, visit ' + urlWithoutFilter); - } else { - terminal.error( - 'AMP validation - displaying errors matching ' + - 'filter=' + errorCategoryFilter + '. ' + - 'To see all errors, visit ' + urlWithoutFilter); - } - } - for (const error of errors) { - if (error.severity === amp.validator.ValidationError.Severity.ERROR) { - terminal.error(errorLine(url, error)); - } else { - terminal.warn(errorLine(url, error)); - } - } -}; - -/** - * Applies the format to render the params in the provided error. - * @param {string} format - * @param {!amp.validator.ValidationError} error - * @return {string} - */ -function applyFormat(format, error) { - let message = format; - for (let param = 1; param <= error.params.length; ++param) { - message = - message.replace(new RegExp('%' + param, 'g'), error.params[param - 1]); - } - return message.replace(new RegExp('%%', 'g'), '%'); -} - -/** - * Renders the error message for a single error. - * @param {!amp.validator.ValidationError} error - * @return {string} - * @export - */ -amp.validator.renderErrorMessage = function(error) { - goog.asserts.assert(error.code !== null); - goog.asserts.assert(error.params.length > 0); - const format = parsedValidatorRulesSingleton.getFormatByCode()[error.code]; - goog.asserts.assert(format !== undefined); - return applyFormat(format, error); -}; - -/** - * Renders one line of error output. - * @param {string} filenameOrUrl - * @param {!amp.validator.ValidationError} error - * @return {string} - */ -function errorLine(filenameOrUrl, error) { - const line = error.line || 1; - const col = error.col || 0; - - let errorLine = goog.uri.utils.removeFragment(filenameOrUrl) + ':' + line + - ':' + col + ' '; - errorLine += amp.validator.renderErrorMessage(error); - if (error.specUrl) { - errorLine += ' (see ' + error.specUrl + ')'; - } - if (error.category !== null) { - errorLine += ' [' + error.category + ']'; - } - return errorLine; -} - -/** - * Renders the validation results into an array of human readable strings. - * Careful when modifying this - it's called from - * https://github.com/ampproject/amphtml/blob/master/test/integration/test-example-validation.js. - * @param {!Object} validationResult - * @param {string} filename to use in rendering error messages. - * @return {!Array} - * @export - */ -amp.validator.renderValidationResult = function(validationResult, filename) { - const rendered = []; - rendered.push(validationResult.status); - for (const error of validationResult.errors) { - rendered.push(errorLine(filename, error)); - } - return rendered; -}; - - -/** - * Computes the validation category for this |error|. This is a higher - * level classification that distinguishes layout problems, problems - * with specific tags, etc. The category is determined with heuristics, - * just based on the information in |error|. We consider - * ValidationError::Code, ValidationError::params (including suffix / - * prefix matches. - * @param {!amp.validator.ValidationError} error - * @return {!amp.validator.ErrorCategory.Code} - * @export - */ -amp.validator.categorizeError = function(error) { - // This shouldn't happen in practice. We always set some params, and - // UNKNOWN_CODE would indicate that the field wasn't populated. - if (error.params.length === 0 || - error.code === amp.validator.ValidationError.Code.UNKNOWN_CODE || - error.code === null) { - return amp.validator.ErrorCategory.Code.UNKNOWN; - } - // E.g. "The tag 'img' may only appear as a descendant of tag - // 'noscript'. Did you mean 'amp-img'?" - if (error.code === amp.validator.ValidationError.Code.DISALLOWED_TAG) { - if (error.params[0] === 'img' || error.params[0] === 'video' || - error.params[0] === 'audio' || error.params[0] === 'iframe' || - error.params[0] === 'font') { - return amp.validator.ErrorCategory.Code - .DISALLOWED_HTML_WITH_AMP_EQUIVALENT; - } - // E.g. "The tag 'picture' is disallowed." - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // E.g. "tag 'img' may only appear as a descendant of tag - // 'noscript'. Did you mean 'amp-img'?" - if (error.code === - amp.validator.ValidationError.Code.MANDATORY_TAG_ANCESTOR_WITH_HINT) { - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML_WITH_AMP_EQUIVALENT; - } - // At the moment it's not possible to get this particular error since - // all mandatory tag ancestors have hints except for noscript, but - // usually when noscript fails then it reports an error for mandatory_parent - // (since there is such a TagSpec as well, for the head). - if (error.code === - amp.validator.ValidationError.Code.MANDATORY_TAG_ANCESTOR) { - if (goog.string./*OK*/ startsWith(error.params[0], 'amp-') || - goog.string./*OK*/ startsWith(error.params[1], 'amp-')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // E.g. "Tag 'amp-accordion > section' must have 2 child tags - saw - // 3 child tags." - if (error.code == - amp.validator.ValidationError.Code.INCORRECT_NUM_CHILD_TAGS) { - if (goog.string./*OK*/ startsWith(error.params[0], 'amp-')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // e.g. "Tag 'div' is disallowed as first child of tag - // 'amp-accordion > section'. Allowed first child tag names are - // ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']." - if (error.code == - amp.validator.ValidationError.Code.DISALLOWED_CHILD_TAG_NAME || - error.code == - amp.validator.ValidationError.Code.DISALLOWED_FIRST_CHILD_TAG_NAME) { - if (goog.string./*OK*/ startsWith(error.params[0], 'amp-') || - goog.string./*OK*/ startsWith(error.params[1], 'amp-')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // E.g. "The text (CDATA) inside tag 'style amp-custom' matches - // 'CSS !important', which is disallowed." - if (error.code === amp.validator.ValidationError.Code.STYLESHEET_TOO_LONG || - (error.code === - amp.validator.ValidationError.Code.CDATA_VIOLATES_BLACKLIST && - error.params[0] === 'style amp-custom')) { - return amp.validator.ErrorCategory.Code.AUTHOR_STYLESHEET_PROBLEM; - } - // E.g. "CSS syntax error in tag 'style amp-custom' - Invalid Declaration." - // TODO(powdercloud): Legacy generic css error code. Remove after 2016-06-01. - if (error.code === amp.validator.ValidationError.Code.CSS_SYNTAX && - error.params[0] === 'style amp-custom') { - return amp.validator.ErrorCategory.Code.AUTHOR_STYLESHEET_PROBLEM; - } - // E.g. "CSS syntax error in tag 'style amp-custom' - unterminated string." - if ((error.code === - amp.validator.ValidationError.Code - .CSS_SYNTAX_STRAY_TRAILING_BACKSLASH || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_UNTERMINATED_COMMENT || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_UNTERMINATED_STRING || - error.code === amp.validator.ValidationError.Code.CSS_SYNTAX_BAD_URL || - error.code === - amp.validator.ValidationError.Code - .CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_DECLARATION || - error.code === - amp.validator.ValidationError.Code - .CSS_SYNTAX_INCOMPLETE_DECLARATION || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_AT_RULE || - error.code === - amp.validator.ValidationError.Code - .CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_MISSING_SELECTOR || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_NOT_A_SELECTOR_START || - error.code === - amp.validator.ValidationError.Code - .CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_MISSING_URL || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_URL || - error.code === - amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_URL_PROTOCOL || - error.code === - amp.validator.ValidationError.Code - .CSS_SYNTAX_DISALLOWED_RELATIVE_URL) && - error.params[0] === 'style amp-custom') { - return amp.validator.ErrorCategory.Code.AUTHOR_STYLESHEET_PROBLEM; - } - // E.g. "The mandatory tag 'boilerplate (noscript)' is missing or - // incorrect." - if (error.code === amp.validator.ValidationError.Code.MANDATORY_TAG_MISSING || - (error.code === - amp.validator.ValidationError.Code.MANDATORY_ATTR_MISSING && - error.params[0] === '⚡') || - (error.code === - amp.validator.ValidationError.Code - .MANDATORY_CDATA_MISSING_OR_INCORRECT && - (goog.string./*OK*/ startsWith( - error.params[0], 'head > style[amp-boilerplate]') || - goog.string./*OK*/ startsWith( - error.params[0], 'noscript > style[amp-boilerplate]')))) { - return amp.validator.ErrorCategory.Code - .MANDATORY_AMP_TAG_MISSING_OR_INCORRECT; - } - // E.g. "The mandatory tag 'meta name=viewport' is missing or - // incorrect." - if ((error.code === - amp.validator.ValidationError.Code - .DISALLOWED_PROPERTY_IN_ATTR_VALUE || - error.code === - amp.validator.ValidationError.Code - .INVALID_PROPERTY_VALUE_IN_ATTR_VALUE || - error.code === - amp.validator.ValidationError.Code - .MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE) && - error.params[2] === 'meta name=viewport') { - return amp.validator.ErrorCategory.Code - .MANDATORY_AMP_TAG_MISSING_OR_INCORRECT; - } - // E.g. "The mandatory attribute 'height' is missing in tag 'amp-img'." - if (error.code === - amp.validator.ValidationError.Code.ATTR_VALUE_REQUIRED_BY_LAYOUT || - error.code === - amp.validator.ValidationError.Code.IMPLIED_LAYOUT_INVALID || - error.code === - amp.validator.ValidationError.Code.SPECIFIED_LAYOUT_INVALID || - (error.code === - amp.validator.ValidationError.Code - .INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT) || - ((error.code === amp.validator.ValidationError.Code.INVALID_ATTR_VALUE || - error.code === - amp.validator.ValidationError.Code.MANDATORY_ATTR_MISSING) && - (error.params[0] === 'width' || error.params[0] === 'height' || - error.params[0] === 'layout'))) { - return amp.validator.ErrorCategory.Code.AMP_LAYOUT_PROBLEM; - } - if (error.code === - amp.validator.ValidationError.Code - .ATTR_DISALLOWED_BY_IMPLIED_LAYOUT || - error.code === - amp.validator.ValidationError.Code - .ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT) { - return amp.validator.ErrorCategory.Code.AMP_LAYOUT_PROBLEM; - } - // E.g. "The attribute 'src' in tag 'amphtml engine v0.js script' - // is set to the invalid value - // '//static.breakingnews.com/ads/gptLoader.js'." - if (error.code === amp.validator.ValidationError.Code.INVALID_ATTR_VALUE && - error.params[0] === 'src' && - goog.string./*OK*/ endsWith(error.params[1], 'script')) { - return amp.validator.ErrorCategory.Code.CUSTOM_JAVASCRIPT_DISALLOWED; - } - // E.g. "The tag 'script' is disallowed except in specific forms." - if (error.code === - amp.validator.ValidationError.Code.GENERAL_DISALLOWED_TAG && - error.params[0] === 'script') { - return amp.validator.ErrorCategory.Code.CUSTOM_JAVASCRIPT_DISALLOWED; - } - // E.g.: "The attribute 'type' in tag 'script type=application/ld+json' - // is set to the invalid value 'text/javascript'." - if (error.code === amp.validator.ValidationError.Code.INVALID_ATTR_VALUE && - goog.string./*OK*/ startsWith(error.params[1], 'script') && - error.params[0] === 'type') { - return amp.validator.ErrorCategory.Code.CUSTOM_JAVASCRIPT_DISALLOWED; - } - // E.g. "The attribute 'srcset' may not appear in tag 'amp-audio > - // source'." - if ((error.code === amp.validator.ValidationError.Code.INVALID_ATTR_VALUE || - error.code === amp.validator.ValidationError.Code.DISALLOWED_ATTR || - error.code === - amp.validator.ValidationError.Code.MANDATORY_ATTR_MISSING)) { - if (goog.string./*OK*/ startsWith(error.params[1], 'amp-')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - if (goog.string./*OK*/ startsWith(error.params[1], 'on')) { - return amp.validator.ErrorCategory.Code.CUSTOM_JAVASCRIPT_DISALLOWED; - } - if (error.params[1] === 'style' || - error.params[1] === 'link rel=stylesheet for fonts') { - return amp.validator.ErrorCategory.Code.AUTHOR_STYLESHEET_PROBLEM; - } - // E.g. "The attribute 'async' may not appear in tag 'link - // rel=stylesheet for fonts'." - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // Like the previous example but the tag is params[0] here. This - // error should always be for AMP elements thus far, so we don't - // check for params[0]. - if (error.code === - amp.validator.ValidationError.Code.MANDATORY_ONEOF_ATTR_MISSING) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - // E.g. "The attribute 'shortcode' in tag 'amp-instagram' is deprecated - - // use 'data-shortcode' instead." - if (error.code === amp.validator.ValidationError.Code.DEPRECATED_ATTR || - error.code === amp.validator.ValidationError.Code.DEPRECATED_TAG) { - return amp.validator.ErrorCategory.Code.DEPRECATION; - } - // E.g. "The parent tag of tag 'source' is 'picture', but it can - // only be 'amp-audio'." - if (error.code === amp.validator.ValidationError.Code.WRONG_PARENT_TAG) { - if (goog.string./*OK*/ startsWith(error.params[0], 'amp-') || - goog.string./*OK*/ startsWith(error.params[1], 'amp-') || - goog.string./*OK*/ startsWith(error.params[2], 'amp-')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - // E.g. "The parent tag of tag 'script' is 'body', but it can only - // be 'head'". - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // E.g. "The 'amp-image-lightbox extension .js script' tag is - // missing or incorrect, but required by 'amp-image-lightbox'." - if (error.code === - amp.validator.ValidationError.Code.TAG_REQUIRED_BY_MISSING && - (goog.string./*OK*/ startsWith(error.params[1], 'amp-') || - error.params[1] === 'template')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - // E.g. "The attribute 'role' in tag 'amp-img' is missing or incorrect, - // but required by attribute 'on'." - if (error.code === - amp.validator.ValidationError.Code.ATTR_REQUIRED_BUT_MISSING) { - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // E.g. "Mutually exclusive attributes encountered in tag - // 'amp-youtube' - pick one of ['src', 'data-videoid']." - if (error.code === - amp.validator.ValidationError.Code.MUTUALLY_EXCLUSIVE_ATTRS && - goog.string./*OK*/ startsWith(error.params[0], 'amp-')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - // E.g. "The tag 'boilerplate (noscript) - old variant' appears - // more than once in the document." - if (error.code === amp.validator.ValidationError.Code.DUPLICATE_UNIQUE_TAG) { - return amp.validator.ErrorCategory.Code - .MANDATORY_AMP_TAG_MISSING_OR_INCORRECT; - } - // E.g. "Mustache template syntax in attribute name - // 'data-{{¬allowed}}' in tag 'p'." - if (error.code === - amp.validator.ValidationError.Code.UNESCAPED_TEMPLATE_IN_ATTR_VALUE || - error.code === - amp.validator.ValidationError.Code.TEMPLATE_PARTIAL_IN_ATTR_VALUE || - error.code === amp.validator.ValidationError.Code.TEMPLATE_IN_ATTR_NAME) { - return amp.validator.ErrorCategory.Code.AMP_HTML_TEMPLATE_PROBLEM; - } - // E.g. "The tag 'amp-ad' may not appear as a descendant of tag 'amp-sidebar'. - if (error.code === - amp.validator.ValidationError.Code.DISALLOWED_TAG_ANCESTOR && - (goog.string./*OK*/ startsWith(error.params[1], 'amp-'))) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - if (error.code === - amp.validator.ValidationError.Code.DISALLOWED_TAG_ANCESTOR && - (error.params[1] === 'template')) { - return amp.validator.ErrorCategory.Code.AMP_HTML_TEMPLATE_PROBLEM; - } - // E.g. "Missing URL for attribute 'href' in tag 'a'." - // E.g. "Invalid URL protocol 'http:' for attribute 'src' in tag - // 'amp-iframe'." Note: Parameters in the format strings appear out - // of order so that error.params(1) is the tag for all four of these. - if (error.code == amp.validator.ValidationError.Code.MISSING_URL || - error.code == amp.validator.ValidationError.Code.INVALID_URL || - error.code == amp.validator.ValidationError.Code.INVALID_URL_PROTOCOL || - error.code == - amp.validator.ValidationError.Code.DISALLOWED_RELATIVE_URL) { - if (goog.string./*OK*/ startsWith(error.params[1], 'amp-')) { - return amp.validator.ErrorCategory.Code.AMP_TAG_PROBLEM; - } - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - // E.g. "The dimension '1x' in attribute 'srcset' appears more than once." - if (error.code == amp.validator.ValidationError.Code.DUPLICATE_DIMENSION) { - return amp.validator.ErrorCategory.Code.DISALLOWED_HTML; - } - return amp.validator.ErrorCategory.Code.GENERIC; -}; - -/** - * Convenience function which calls |CategorizeError| for each error - * in |result| and sets its category field accordingly. - * @param {!amp.validator.ValidationResult} result - * @export - */ -amp.validator.annotateWithErrorCategories = function(result) { - for (const error of result.errors) { - error.category = amp.validator.categorizeError(error); - } -}; diff --git a/validator/validator-light.js b/validator/validator-light.js deleted file mode 100644 index c3eb4ea823b4..000000000000 --- a/validator/validator-light.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license - * Copyright 2016 The AMP HTML Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the license. - */ - -// The routines in this files are very simple equivalents to the -// routines in validator-full.js. They preserve whether or not a document -// will validate, but they won't produce error messages etc. -// Testing is done via validator-light_test.js. - -goog.provide('amp.validator.validateNode'); -goog.require('amp.domwalker.DomWalker'); -goog.require('amp.validator.ValidationHandler'); - -/** - * Validates a document stored below a DOM node. - * EXPERIMENTAL: Do not rely on this API for now, it is still a work in - * progress. - * - * @param {!Document} rootDoc - * @return {!amp.validator.ValidationResult} Validation Result (status and - * errors) - * @export - */ -amp.validator.validateNode = function(rootDoc) { - const handler = new amp.validator.ValidationHandler(); - const visitor = new amp.domwalker.DomWalker(); - visitor.walktree(handler, rootDoc); - - return handler.Result(); -}; diff --git a/validator/validator-main.protoascii b/validator/validator-main.protoascii index 07b28d646c15..bb96db1cfb2a 100644 --- a/validator/validator-main.protoascii +++ b/validator/validator-main.protoascii @@ -20,12 +20,12 @@ # in production from crashing. This id is not relevant to validator.js # because thus far, engine (validator.js) and spec file # (validator-main.protoascii) are always released together. -min_validator_revision_required: 135 +min_validator_revision_required: 210 # The spec file revision allows the validator engine to distinguish # newer versions of the spec file. This is currently a Google internal # mechanism, validator.js does not use this facility. However, any # change to this file (validator-main.js) requires updating this revision id. -spec_file_revision: 228 +spec_file_revision: 358 # Validator extensions. # ===================== @@ -38,7 +38,7 @@ spec_file_revision: 228 # (https://www.ampproject.org/docs/reference/spec.html) tags: { - tag_name: "!doctype" + tag_name: "!DOCTYPE" spec_name: "html doctype" mandatory_parent: "$ROOT" mandatory: true @@ -48,6 +48,7 @@ tags: { mandatory: true value: "" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } # Below, we list the allowed elements in the order in which they are appear @@ -56,11 +57,12 @@ tags: { # 4.1 The root element # 4.1.1 The html element -tags: { - tag_name: "html" +tags: { # AMP + html_format: AMP + tag_name: "HTML" spec_name: "html ⚡ for top-level html" mandatory: true - mandatory_parent: "!doctype" + mandatory_parent: "!DOCTYPE" unique: true attrs: { name: "⚡" @@ -70,37 +72,52 @@ tags: { } spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } +tags: { # AMP4ADS + html_format: AMP4ADS + tag_name: "HTML" + spec_name: "html ⚡4ads for top-level html" + mandatory: true + mandatory_parent: "!DOCTYPE" + unique: true + attrs: { + name: "⚡4ads" + alternative_names: "amp4ads" + mandatory: true + value: "" + } + spec_url: "https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md#a4a-format-rules" +} + # 4.2 Document metadata # 4.2.1 The head element tags: { - tag_name: "head" + tag_name: "HEAD" mandatory: true - mandatory_parent: "html" + mandatory_parent: "HTML" unique: true spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } # 4.2.2 The title element tags: { - tag_name: "title" + tag_name: "TITLE" spec_name: "title" } # 4.2.3 the base element tags: { - tag_name: "base" - # Currently the amp cache doesn't correctly rewrite URLs for documents - # with . When it does, we will add support for href attributes. - # attrs: { - # name: "href" - # value_url: { - # allow_relative: false - # allowed_protocol: "http" - # allowed_protocol: "https" - # } - # } + tag_name: "BASE" + unique: true + mandatory_parent: "HEAD" + # We only allow "/" right now because other value can cause havoc with PWA + # implementations. In the future, it may be possible to widen this to any + # absolute URL. + attrs: { + name: "href" + value: "/" + } attrs: { name: "target" - value_regex_casei: "(_blank|_self)" + value_regex_casei: "(_blank|_self|_top)" } } # Disallowed. @@ -112,13 +129,13 @@ attr_lists: { attrs: { name: "target" } attrs: { name: "type" } attrs: { name: "hreflang" } - attrs: { name: "charset" value: "utf-8" } + attrs: { name: "charset" value_casei: "utf-8" } attrs: { name: "color" } } tags: { - tag_name: "link" + tag_name: "LINK" spec_name: "link rel=" - disallowed_ancestor: "template" + disallowed_ancestor: "TEMPLATE" attrs: { name: "href" } attr_lists: "common-link-attrs" attrs: { @@ -139,13 +156,8 @@ tags: { blacklisted_value_regex: "(^|\\s)(" # Values are space separated. "canonical|" # Handled separately below, has specific requirements. "components|" - "dns-prefetch|" "import|" - "manifest|" - "preconnect|" - "prefetch|" - "preload|" - "prerender|" + "manifest|" # Handled separately below, has specific requirements. "serviceworker|" "stylesheet|" # Handled separately below, has specific requirements. "subresource|" @@ -153,11 +165,13 @@ tags: { } attrs: { name: "sizes" } attrs: { name: "type" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } tags: { - tag_name: "link" + html_format: AMP + tag_name: "LINK" spec_name: "link rel=canonical" - mandatory_parent: "head" + mandatory_parent: "HEAD" mandatory: true unique: true attrs: { @@ -168,21 +182,47 @@ tags: { allowed_protocol: "https" allow_relative: true } + blacklisted_value_regex: "__amp_source_origin" } attr_lists: "common-link-attrs" attrs: { name: "rel" - value: "canonical" + value_casei: "canonical" mandatory: true dispatch_key: true } spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } +# Allow but not +tags: { + tag_name: "LINK" + spec_name: "link rel=manifest" + mandatory_parent: "HEAD" + satisfies: "amp-app-banner data source" + attrs: { + name: "href" + mandatory: true + value_url: { + allowed_protocol: "https" + allow_relative: true + } + blacklisted_value_regex: "__amp_source_origin" + } + attr_lists: "common-link-attrs" + attrs: { + name: "rel" + value_casei: "manifest" + mandatory: true + dispatch_key: true + } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" +} # Whitelisted font providers tags: { - tag_name: "link" + tag_name: "LINK" spec_name: "link rel=stylesheet for fonts" - mandatory_parent: "head" + mandatory_parent: "HEAD" + attrs: { name: "async" } attrs: { name: "href" mandatory: true @@ -190,23 +230,30 @@ tags: { "https://fonts\\.googleapis\\.com/icon\\?.*|" "https://fonts\\.googleapis\\.com/earlyaccess/.*\\.css|" "https://fast\\.fonts\\.net/.*|" - "https://maxcdn\\.bootstrapcdn\\.com/font-awesome/.*" + "https://maxcdn\\.bootstrapcdn\\.com/font-awesome/" + "([0-9]+\\.?)+/css/font-awesome\\.min\\.css(\\?.*)?|" + "https://cloud\\.typography\\.com/[0-9]*/[0-9]*/css/fonts\\.css" } attrs: { name: "rel" mandatory: true - value: "stylesheet" + value_casei: "stylesheet" dispatch_key: true } attrs: { name: "type" - value: "text/css" + value_casei: "text/css" } attrs: { name: "media" } + # SRI attributes (https://www.w3.org/TR/SRI/) + attrs: { name: "crossorigin" } + attrs: { name: "integrity" } + + spec_url: "https://www.ampproject.org/docs/reference/spec.html#custom-fonts" } # itemprop=sameAs is allowed per schema.org, needs not be in head tags: { - tag_name: "link" + tag_name: "LINK" spec_name: "link itemprop=sameAs" attrs: { name: "href" @@ -215,14 +262,15 @@ tags: { attrs: { name: "itemprop" mandatory: true - value: "sameAs" + value_casei: "sameas" dispatch_key: true } attr_lists: "common-link-attrs" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # rel= isn't mandatory when itemprop= is present. tags: { - tag_name: "link" + tag_name: "LINK" spec_name: "link itemprop=" attrs: { name: "href" @@ -233,28 +281,44 @@ tags: { mandatory: true } attr_lists: "common-link-attrs" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" +} +# rel= isn't mandatory when property= is present. +tags: { + tag_name: "LINK" + spec_name: "link property=" + attrs: { + name: "href" + mandatory: true + } + attrs: { + name: "property" + mandatory: true + } + attr_lists: "common-link-attrs" + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # 4.2.5 the meta element # Charset must be utf8, and a specific viewport is required. tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta charset=utf-8" mandatory: true - mandatory_parent: "head" + mandatory_parent: "HEAD" unique: true attrs: { dispatch_key: true name: "charset" mandatory: true - value: "utf-8" + value_casei: "utf-8" } spec_url: "https://www.ampproject.org/docs/reference/spec.html#required-markup" } tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta name=viewport" mandatory: true - mandatory_parent: "head" + mandatory_parent: "HEAD" unique: true attrs: { name: "content" @@ -280,12 +344,12 @@ tags: { # This tag is a hack to tell IE 10 to use its modern rendering engine as opposed # to the IE8 engine. So it's explicitly allowed. tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=X-UA-Compatible" attrs: { name: "http-equiv" mandatory: true - value: "X-UA-Compatible" + value_casei: "x-ua-compatible" dispatch_key: true } attrs: { @@ -296,16 +360,36 @@ tags: { properties: { name: "chrome" value: "1" } } } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } +# Tag specific to apple-itunes-app installs, see also . tags: { - tag_name: "meta" + tag_name: "META" + spec_name: "meta name=apple-itunes-app" + mandatory_parent: "HEAD" + satisfies: "amp-app-banner data source" + attrs: { + name: "name" + value_casei: "apple-itunes-app" + mandatory: true + dispatch_key: true + } + attrs: { + name: "content" + mandatory: true + value_regex: ".*app-id=.*" + } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" +} +tags: { + tag_name: "META" spec_name: "meta name= and content=" # The validator accepts any name="..." attribute values except # for a few specific name values which have more specific rules above or # are altogether disallowed. attrs: { name: "name" - blacklisted_value_regex: "(^|\\s)(viewport|content-disposition|revisit-after)(\\s|$)" + blacklisted_value_regex: "(^|\\s)(viewport|content-disposition|revisit-after|apple-itunes-app)(\\s|$)" } attrs: { name: "content" } # property / content is non-standard, but commonly seen. @@ -315,141 +399,149 @@ tags: { # This is redundant with meta charset, but also harmless as long as it's # set to utf-8. tags: { - tag_name: "meta" - spec_name: "meta http-equiv=content-type" - attrs: { - name: "http-equiv" - mandatory: true - value: "content-type" - dispatch_key: true - } - attrs: { - name: "content" - mandatory: true - value: "text/html; charset=utf-8" - } -} -# Identical to the above, but a common case variant of content-type. -tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=Content-Type" attrs: { name: "http-equiv" mandatory: true - value: "Content-Type" + value_casei: "content-type" dispatch_key: true } attrs: { name: "content" mandatory: true - value: "text/html; charset=utf-8" + value_casei: "text/html; charset=utf-8" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # http-equiv content-language tag tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=content-language" attrs: { name: "http-equiv" mandatory: true - value: "content-language" + value_casei: "content-language" dispatch_key: true } attrs: { name: "content" mandatory: true } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # http-equiv pics-label tag # https://www.w3.org/PICS/ tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=pics-label" attrs: { name: "http-equiv" mandatory: true - value: "pics-label" + value_casei: "pics-label" dispatch_key: true } attrs: { name: "content" mandatory: true } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # http-equiv imagetoolbar tag # https://msdn.microsoft.com/en-us/library/ms532986(v=vs.85).aspx tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=imagetoolbar" attrs: { name: "http-equiv" mandatory: true - value: "imagetoolbar" + value_casei: "imagetoolbar" dispatch_key: true } attrs: { name: "content" mandatory: true } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # http-equiv content-style-type # https://www.w3.org/TR/REC-html40/present/styles.html#h-14.2.1 tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=Content-Style-Type" attrs: { name: "http-equiv" mandatory: true - value: "Content-Style-Type" + value_casei: "content-style-type" dispatch_key: true } attrs: { name: "content" mandatory: true - value: "text/css" + value_casei: "text/css" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # http-equiv content-script-type # https://www.w3.org/TR/html4/interact/scripts.html#h-18.2.2.1 tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=Content-Script-Type" attrs: { name: "http-equiv" mandatory: true - value: "Content-Script-Type" + value_casei: "content-script-type" + dispatch_key: true + } + attrs: { + name: "content" + mandatory: true + value_casei: "text/javascript" + } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" +} +# http-equiv origin-trial tag +tags: { + tag_name: "META" + spec_name: "meta http-equiv=origin-trial" + attrs: { + name: "http-equiv" + mandatory: true + value_casei: "origin-trial" dispatch_key: true } attrs: { name: "content" mandatory: true - value: "text/javascript" } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # http-equiv resource-type # http://www.metatags.info/meta_http_equiv_resource_type tags: { - tag_name: "meta" + tag_name: "META" spec_name: "meta http-equiv=resource-type" attrs: { name: "http-equiv" mandatory: true - value: "RESOURCE-TYPE" + value_casei: "resource-type" dispatch_key: true } attrs: { name: "content" mandatory: true } + spec_url: "https://www.ampproject.org/docs/reference/spec.html#html-tags" } # 4.2.6 The style # Text contents of the style tag will be validated seperately. -tags: { # Special custom 'author' spreadsheet. - tag_name: "style" +tags: { # Special custom 'author' spreadsheet for AMP + html_format: AMP + tag_name: "STYLE" spec_name: "style amp-custom" unique: true - mandatory_parent: "head" + mandatory_parent: "HEAD" attrs: { name: "amp-custom" mandatory: true @@ -460,9 +552,10 @@ tags: { # Special custom 'author' spreadsheet. # attribute and thus can't have a dispatch_key. # dispatch_key: true } + attrs: { name: "nonce" } attrs: { # This is a default, but it doesn't hurt. name: "type" - value: "text/css" + value_casei: "text/css" } cdata: { max_bytes: 50000 @@ -523,8 +616,8 @@ tags: { # Special custom 'author' spreadsheet. # These regex blacklists are temporary hacks to validate essential CSS # rules. They will be replaced later with more principled solutions. blacklisted_cdata_regex: { - regex: "\\.i?-amp-" - error_message: "CSS -amp- class name prefix" + regex: "(^|\\W)i-amphtml-" + error_message: "CSS i-amphtml- name prefix" } blacklisted_cdata_regex: { regex: "!important" @@ -533,38 +626,134 @@ tags: { # Special custom 'author' spreadsheet. } spec_url: "https://www.ampproject.org/docs/reference/spec.html#stylesheets" } +tags: { # Special custom 'author' spreadsheet for AMP4ADS + html_format: AMP4ADS + tag_name: "STYLE" + spec_name: "style amp-custom (AMP4ADS)" + unique: true + mandatory_parent: "HEAD" + attrs: { + name: "amp-custom" + mandatory: true + value: "" + # This is a fine dispatch key, but we would prefer that this tagspec + # is used for errors related to
    - + + + + + + + + AMP + AMP4ADS + + + Validate @@ -50,9 +76,16 @@ value: false } }, + htmlFormatChange: function() { + this.fire('webui-urlform-html-format-changes', + {'htmlFormat': this.$.htmlFormatSelector.selected}); + }, checkURL: function(e) { this.validURL = !this.$.urlToFetch.invalid; }, + displayToast: function() { + this.$.urlToast.open(); + }, keyPress: function(e) { if (this.validURL && 13 === e.charCode) { this.$.validateButton.click(); @@ -64,6 +97,12 @@ this.checkURL(); this.$.urlToFetch.validate(); this.$.validateButton.click(); + }, + getHtmlFormat: function() { + return this.$.htmlFormatSelector.selected; + }, + setHtmlFormat: function(htmlFormat) { + this.$.htmlFormatSelector.selected = htmlFormat; } }); diff --git a/validator/webui/README.md b/validator/webui/README.md new file mode 100644 index 000000000000..98e8f402bf70 --- /dev/null +++ b/validator/webui/README.md @@ -0,0 +1,34 @@ + +# Validator Web UI + +If you'd like to use the web UI, simply visit [validator.ampproject.org](https://validator.ampproject.org/). + +## Running your own Web UI + +In this directory, run + +``` +$ npm install +$ go build serve-standalone.go +$ ./serve-standalone +``` + +Then visit your own instance at http://127.0.0.1:8765/. + +If you'd like to run exactly the code that is running at +[validator.ampproject.org](https://validator.ampproject.org/), that's an +Appengine app - please refer to the instructions in serve.go. diff --git a/validator/webui/app.yaml b/validator/webui/app.yaml index cb0eb6e96bdb..cef3e4582283 100644 --- a/validator/webui/app.yaml +++ b/validator/webui/app.yaml @@ -5,11 +5,22 @@ api_version: go1 handlers: - url: /fetch script: _go_app + secure: always - url: /$ static_files: index.html upload: index.html + secure: always - url: /amp_favicon.png$ static_files: amp_favicon.png upload: amp_favicon\.png$ + secure: always + +- url: /logo-blue.svg$ + static_files: logo-blue.svg + upload: logo-blue\.svg$ + secure: always + +skip_files: + serve-standalone.go diff --git a/validator/webui/build.py b/validator/webui/build.py new file mode 100755 index 000000000000..929137035db4 --- /dev/null +++ b/validator/webui/build.py @@ -0,0 +1,169 @@ +#!/usr/bin/python2.7 +# +# Copyright 2016 The AMP HTML Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the license. +# +"""A build script which (thus far) works on Ubuntu 14.""" + +# TODO(powdercloud): Make a gulp file or similar for this. For now +# it's simply split off from the main build.py in the parent +# directory, but this is not an idiomatic use to build a Javascript or +# Polymer project, and unlike for the parent directory there's no +# particular benefit to using Python. + +import glob +import logging +import os +import platform +import re +import shutil +import subprocess +import sys +import tempfile + + +def Die(msg): + """Prints error and exits with status 1. + + Args: + msg: The error message to emit + """ + print >> sys.stderr, msg + sys.exit(1) + + +def GetNodeJsCmd(): + """Ensure Node.js is installed and return the proper command to run.""" + logging.info('entering ...') + + for cmd in ['node', 'nodejs']: + try: + output = subprocess.check_output([cmd, '--eval', 'console.log("42")']) + if output.strip() == '42': + logging.info('... done') + return cmd + except (subprocess.CalledProcessError, OSError): + continue + Die('Node.js not found. Try "apt-get install nodejs".') + + +def CheckPrereqs(): + """Checks that various prerequisites for this script are satisfied.""" + logging.info('entering ...') + + if platform.system() != 'Linux' and platform.system() != 'Darwin': + Die('Sorry, this script assumes Linux or Mac OS X thus far. ' + 'Please feel free to edit the source and fix it to your needs.') + + # Ensure source files are available. + for f in ['webui.js', 'index.html', + 'logo-blue.svg', 'package.json']: + if not os.path.exists(f): + Die('%s not found. Must run in amp_validator source directory.' % f) + + # Ensure that npm is installed. + try: + npm_version = subprocess.check_output(['npm', '--version']) + except (subprocess.CalledProcessError, OSError): + Die('npm package manager not found. Try "apt-get install npm".') + + # Ensure npm version '1.3.10' or newer. + m = re.search('^(\\d+)\\.(\\d+)\\.(\\d+)$', npm_version) + if (int(m.group(1)), int(m.group(2)), int(m.group(3))) < (1, 3, 10): + Die('Expected npm version 1.3.10 or newer, saw: %s' % npm_version) + + logging.info('... done') + + +def SetupOutDir(out_dir): + """Sets up a clean output directory. + + Args: + out_dir: directory name of the output directory. Must not have slashes, + dots, etc. + """ + logging.info('entering ...') + assert re.match(r'^[a-zA-Z_\-0-9]+$', out_dir), 'bad out_dir: %s' % out_dir + + if os.path.exists(out_dir): + subprocess.check_call(['rm', '-rf', out_dir]) + os.mkdir(out_dir) + logging.info('... done') + + +def InstallNodeDependencies(): + """Installs the dependencies using npm.""" + logging.info('entering ...') + # Install the project dependencies specified in package.json into + # node_modules. + logging.info('installing AMP Validator webui dependencies ...') + subprocess.check_call(['npm', 'install']) + logging.info('... done') + + +def CreateWebuiAppengineDist(out_dir): + """Creates the webui vulcanized directory to deploy to Appengine. + + Args: + out_dir: directory name of the output directory. Must not have slashes, + dots, etc. + """ + logging.info('entering ...') + try: + tempdir = tempfile.mkdtemp() + # Merge the contents of webui with the installed node_modules into a + # common root (a temp directory). This lets us use the vulcanize tool. + for entry in os.listdir('.'): + if entry != 'node_modules': + if os.path.isfile(entry): + shutil.copyfile(entry, os.path.join(tempdir, entry)) + else: + shutil.copytree(entry, os.path.join(tempdir, entry)) + for entry in os.listdir('node_modules'): + if entry == 'web-animations-js': + shutil.copytree(os.path.join('node_modules', entry), + os.path.join(tempdir, '@polymer', entry)) + elif entry != '@polymer': + shutil.copytree(os.path.join('node_modules', entry), + os.path.join(tempdir, entry)) + for entry in os.listdir('node_modules/@polymer'): + shutil.copytree(os.path.join('node_modules/@polymer', entry), + os.path.join(tempdir, '@polymer', entry)) + vulcanized_index_html = subprocess.check_output([ + 'node_modules/vulcanize/bin/vulcanize', + '--inline-scripts', '--inline-css', + '-p', tempdir, 'index.html']) + finally: + shutil.rmtree(tempdir) + webui_out = os.path.join(out_dir, 'webui_appengine') + shutil.copytree('.', webui_out, ignore=shutil.ignore_patterns('dist')) + f = open(os.path.join(webui_out, 'index.html'), 'w') + f.write(vulcanized_index_html) + f.close() + logging.info('... success') + + +def Main(): + """The main method, which executes all build steps and runs the tests.""" + logging.basicConfig(format='[[%(filename)s %(funcName)s]] - %(message)s', + level=logging.INFO) + nodejs_cmd = GetNodeJsCmd() + CheckPrereqs() + InstallNodeDependencies() + SetupOutDir(out_dir='dist') + CreateWebuiAppengineDist(out_dir='dist') + + +if __name__ == '__main__': + Main() diff --git a/validator/webui/index.html b/validator/webui/index.html index f7bbe79e4100..c587be3358cc 100644 --- a/validator/webui/index.html +++ b/validator/webui/index.html @@ -19,10 +19,11 @@ The AMP Validator - + +