diff --git a/.babelrc b/.babelrc deleted file mode 100644 index ccffea7eb508..000000000000 --- a/.babelrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "optional": ["utility.inlineEnvironmentVariables"], - "externalHelpers": true, - "whitelist": [ - "es3.memberExpressionLiterals", - "es3.propertyLiterals", - "es5.properties.mutators", - "es6.arrowFunctions", - "es6.blockScoping", - "es6.classes", - "es6.constants", - "es6.tailCall", - "es6.modules", - "es6.parameters", - "es6.properties.computed", - "es6.properties.shorthand", - "es6.templateLiterals", - "es6.spread" - ] -} diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000000..3861023e9dd0 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,32 @@ +# Settings for https://codecov.io integration + +# See https://docs.codecov.io/docs/pull-request-comments +comment: off + +# See https://docs.codecov.io/v4.3.0/docs/commit-status +coverage: + status: + project: + default: off + unit_tests: + flags: unit_tests + target: auto + threshold: 100 # Passing check no matter what the coverage is + base: auto + integration_tests: + flags: integration_tests + target: auto + threshold: 100 # Passing check no matter what the coverage is + base: auto + patch: + default: off + unit_tests: + flags: unit_tests + target: 100 # Target 100% coverage for diffs + threshold: 100 # Passing check no matter what the coverage is + base: auto + integration_tests: + flags: integration_tests + target: 100 # Target 100% coverage for diffs + threshold: 100 # Passing check no matter what the coverage is + base: auto diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..cdcc5959adb4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{*.md}] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000000..ec302f15cfd3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,25 @@ +node_modules +build +build-system/tasks/visual-diff/snippets/*.js +build-system/babel-plugins/**/fixtures/**/*.js +build-system/app-index/test/**/*.js +dist +dist.3p +dist.tools +out +examples +third_party +test/coverage +**/*.extern.js +validator/dist +validator/webui/dist +validator/node_modules +validator/nodejs/node_modules +validator/webui/node_modules +eslint-rules +karma.conf.js +testing/local-amp-chrome-extension +extensions/amp-a4a/0.1/test/testdata +extensions/amp-access/0.1/access-expr-impl.js +extensions/amp-animation/0.1/css-expr-impl.js +extensions/amp-bind/0.1/bind-expr-impl.js diff --git a/.eslintrc b/.eslintrc index eeb69918be7e..856b13961cbf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,39 +1,114 @@ { + "root": true, "parser": "babel-eslint", - "ecmaFeatures": { - "modules": true, - "arrowFunctions": true, - "blockBindings": true, - "forOf": false, - "destructuring": false, - "spread": false - }, + "plugins": [ + "chai-expect", + "eslint-plugin-amphtml-internal", + "eslint-plugin-google-camelcase", + "jsdoc", + "sort-imports-es6-autofix", + "sort-requires" + ], "env": { "es6": true, "browser": true }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "globals": { + "AMP": false, + "context": false, + "global": false + }, + "settings": { + "jsdoc": { + "tagNamePreference": { + "returns": "return", + "constant": "const", + "class": "constructor" + }, + "additionalTagNames": { + "customTags": [ + "deprecated", + "export", + "final", + "package", + "restricted", + "suppress", + "template", + "visibleForTesting" + ] + }, + "allowOverrideWithoutParam" : true + } + }, "rules": { + "amphtml-internal/closure-type-primitives": 2, + "amphtml-internal/dict-string-keys": 2, + "amphtml-internal/enforce-private-props": 2, + "amphtml-internal/html-template": 2, + "amphtml-internal/is-experiment-on": 2, + "amphtml-internal/no-array-destructuring": 2, + "amphtml-internal/no-deep-destructuring": 2, + "amphtml-internal/no-duplicate-import": 2, + "amphtml-internal/no-es2015-number-props": 2, + "amphtml-internal/no-export-side-effect": 2, + "amphtml-internal/no-for-of-statement": 2, + "amphtml-internal/no-global": 0, + "amphtml-internal/no-has-own-property-method": 1, + "amphtml-internal/no-import": 2, + "amphtml-internal/no-import-rename": 2, + "amphtml-internal/no-is-amp-alt": 2, + "amphtml-internal/no-mixed-operators": 2, + "amphtml-internal/no-non-string-log-args": 1, + "amphtml-internal/no-spread": 2, + "amphtml-internal/no-style-display": 2, + "amphtml-internal/no-style-property-setting": 2, + "amphtml-internal/no-swallow-return-from-allow-console-error": 2, + "amphtml-internal/prefer-deferred-promise": 1, + "amphtml-internal/prefer-destructuring": 2, + "amphtml-internal/query-selector": 2, + "amphtml-internal/todo-format": 0, + "amphtml-internal/unused-private-field": 1, + "amphtml-internal/vsync": 1, "array-bracket-spacing": [2, "never"], "arrow-parens": [2, "as-needed"], "arrow-spacing": 2, + "chai-expect/missing-assertion": 2, + "chai-expect/no-inner-compare": 2, + "chai-expect/terminating-properties": 2, + "comma-dangle": [2, "always-multiline"], "computed-property-spacing": [2, "never"], "curly": 2, "dot-location": [2, "property"], "eol-last": 2, "google-camelcase/google-camelcase": 2, - "indent": [2, 2, { "SwitchCase": 1 }], + "indent": [2, 2, { "SwitchCase": 1, "VariableDeclarator": 2, "MemberExpression": 2, "ObjectExpression": 1, "CallExpression": { "arguments": 2 } }], + "jsdoc/check-param-names": 2, + "jsdoc/check-tag-names": 2, + "jsdoc/check-types": 2, + "jsdoc/require-param": 2, + "jsdoc/require-param-name": 2, + "jsdoc/require-param-type": 2, + "jsdoc/require-returns-type": 2, "key-spacing": 2, "max-len": [2, 80, 4, { - "ignoreComments": true, - "ignoreUrls": true, - "ignorePattern": "" + "ignoreTrailingComments": true, + "ignoreRegExpLiterals": true, + "ignorePattern": "^} from.*;|= require\\(.*;$|@typedef|@param|@return|@private|@const|@type|@implements", + "ignoreUrls": true }], "no-alert": 2, + "no-cond-assign": 2, "no-debugger": 2, "no-div-regex": 2, + "no-dupe-keys": 2, "no-eval": 2, "no-extend-native": 2, "no-extra-bind": 2, + "no-extra-semi": 2, "no-implicit-coercion": [2, { "boolean": false }], "no-implied-eval": 2, "no-iterator": 2, @@ -49,28 +124,85 @@ "no-trailing-spaces": 2, "no-unused-expressions": 0, "no-unused-vars": [2, { - "argsIgnorePattern": "^var_|opt_|unused", - "varsIgnorePattern": "AmpElement|Def|Interface$" + "argsIgnorePattern": "^(var_args$|opt_|unused)", + "varsIgnorePattern": "(AmpElement|Def|Interface)$" }], "no-useless-call": 2, "no-useless-concat": 2, + "no-undef": 2, "no-var": 2, "no-warning-comments": [2, { "terms": ["do not submit"], "location": "anywhere" }], "object-curly-spacing": [2, "never", { "objectsInObjects": false, "arraysInObjects": false }], + "object-shorthand": [2, "properties", { "avoidQuotes": true }], "prefer-const": 2, + "quotes": [2, "single", "avoid-escape"], "radix": 2, + "require-jsdoc": [2, { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": false, + "ArrowFunctionExpression": false, + "FunctionExpression": false + } + }], "semi": 2, - "space-after-keywords": 2, + "keyword-spacing": [2, { "before": true, "after": true }], + "sort-imports-es6-autofix/sort-imports-es6": [2, { + "ignoreCase": false, + "ignoreMemberSort": false, + "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] + }], + "sort-requires/sort-requires": 2, "space-before-blocks": 2, "space-before-function-paren": [2, "never"], - "space-before-keywords": 2, "space-in-parens": 2, "space-infix-ops": 2, "space-unary-ops": [1, { "words": true, "nonwords": false }], - "space-return-throw-case": 2, "wrap-iife": [2, "any"] - } + }, + "overrides": [ + { + "files": ["test/**/*.js", "extensions/**/test/**/*.js", "ads/**/test/**/*.js", "testing/**/*.js"], + "rules": { + "require-jsdoc": 0, + "jsdoc/check-param-names": 0, + "jsdoc/check-tag-names": 0, + "jsdoc/check-types": 0, + "jsdoc/require-param": 0, + "jsdoc/require-param-name": 0, + "jsdoc/require-param-type": 0, + "jsdoc/require-returns-type": 0 + }, + "globals": { + "it": false, + "chai": false, + "expect": false, + "describe": false, + "beforeEach": false, + "afterEach": false, + "before": false, + "after": false, + "assert": false, + "sinon": true, + "sandbox": true, + "describes": true, + "allowConsoleError": false, + "expectAsyncConsoleError": false, + "restoreAsyncErrorThrows": false, + "stubAsyncErrorThrows": false + } + }, + { + "files": ["babel.config.js"], + "globals": { + "module": false, + "process": false, + "require": false + } + } + ] } diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000000..d144147618e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,28 @@ +**Please only file bugs/feature requests for AMP here.** + +- If you have questions about how to use AMP or other general questions about AMP please ask them on Stack Overflow under the AMP HTML tag instead of filing an issue here: http://stackoverflow.com/questions/tagged/amp-html +- If you have questions/issues related to Google Search please ask them in Google's AMP forum instead of filing an issue here: https://goo.gl/utQ1KZ + +If you have a bug or feature request for AMP please fill in the following template. Delete everything except the headers (including this text). + +## What's the issue? + +Briefly describe the bug/feature request. + +## How do we reproduce the issue? + +If this is a bug please provide a public URL and ideally a reduced test case (e.g. on jsbin.com) that exhibits only your issue and nothing else. Then provide step-by-step instructions for reproducing the issue: + +1. Step 1 to reproduce +2. Step 2 to reproduce +3. … + +If this is a feature request you can use this section to point to a prototype/mockup that will help us understand the request. + +## What browsers are affected? + +All browsers? Some specific browser? What device type? + +## Which AMP version is affected? + +Is this a new issue? Or was it always broken? Paste your AMP version. You can find it in the browser dev tools. diff --git a/.github/ISSUE_TEMPLATE/cherry_pick_template.md b/.github/ISSUE_TEMPLATE/cherry_pick_template.md new file mode 100644 index 000000000000..c0d2d8073346 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cherry_pick_template.md @@ -0,0 +1,33 @@ +**Replace *everything* in angle brackets in the title AND body of this issue. If you have any questions see the [cherry pick documentation](https://github.com/ampproject/amphtml/blob/master/contributing/release-schedule.md#cherry-picks).** + + +**GitHub issue your cherry pick is fixing:** + +Issue # + + +**PR that you are requesting a cherry pick for:** + +*(Put N/A if you do not yet have a PR with a fix; edit this issue to add it when the PR is ready.)* + +PR # + + +**Release(s) you requesting this cherry pick into:** + +*Release issues can be found at https://github.com/ampproject/amphtml/labels/Type%3A%20Release* + +*If you are requesting a cherry pick into a production release you will most likely need to cherry pick into canary as well, otherwise when the canary is pushed to production your fix will be lost. See the [cherry pick documentation](https://github.com/ampproject/amphtml/blob/master/contributing/release-schedule.md#cherry-picks).)* + +Production Release? If yes, Type: Release Issue # + +Canary release? If yes, Type: Release Issue #, otherwise + + +*Why does this issue meet the [cherry pick criteria](https://github.com/ampproject/amphtml/blob/master/contributing/release-schedule.md#cherry-pick-criteria)? Be specific.* + + + +*Assign this issue to the current TL (cramforce) if you have permission to, otherwise leave this cc line in.* + +/cc @cramforce diff --git a/.github/ISSUE_TEMPLATE/release_issue_template.md b/.github/ISSUE_TEMPLATE/release_issue_template.md new file mode 100644 index 000000000000..a8f000150425 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release_issue_template.md @@ -0,0 +1,38 @@ +# Release tracking issue + + + +- [x] Release `[](https://github.com/ampproject/amphtml/releases/tag/)` is cut as a new canary release +- [ ] Release pushed to dev channel () +- [ ] Release pushed to 1% () +- [ ] Release pushed to production () + + + +See the [release documentation](https://github.com/ampproject/amphtml/blob/master/contributing/release-schedule.md) for more information on the release process, including how to test changes in the Dev Channel. + +If you find a bug in this build, please file an [issue](https://github.com/ampproject/amphtml/issues/new). If you believe the bug should be fixed in this build, follow the instructions in the [cherry picks documentation](https://bit.ly/amp-cherry-pick). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..2e3b4570ab36 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,33 @@ +# Instructions: + +- Pick a meaningful title for your pull request. (Use sentence case.) + - Prefix the title with an emoji to identify what is being done. (Copy-paste the emoji itself (not the :code:) from the list below.) + - Do not overuse punctuation in the title (like `(chore):`). + - If it is helpful, use a simple prefix (like `ProjectX: Implement some feature`). +- Enter a succinct description that says why the PR is necessary, and what it does. + - Mention the GitHub issue that is being addressed by the pull request. + - The keywords `Fixes`, `Closes`, or `Resolves` followed the issue number will automatically close the issue. + +# Example of a good description: + +- Implement aspect X +- Leave out feature Y because of A +- Improve performance by B +- Improve accessibility by C + +# Emojis for categorizing pull requests: + +✨ New feature (`:sparkles:`) +🐛 Bug fix (`:bug:`) +🔥 P0 fix (`:fire:`) +✅ Tests (`:white_check_mark:`) +🚀 Performance improvements (`:rocket:`) +🖍 CSS / Styling (`:crayon:`) +♿ Accessibility (`:wheelchair:`) +🌐 Internationalization (`:globe_with_meridians:`) +📖 Documentation (`:book:`) +🏗 Infrastructure / Tooling / Builds / CI (`:building_construction:`) +⏪ Reverting a previous change (`:rewind:`) +♻️ Refactoring (like moving around code w/o any changes) (`:recycle:`) +🚮 Deleting code (`:put_litter_in_its_place:`) + diff --git a/.gitignore b/.gitignore index 44399320200d..fb03a0c023a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,35 @@ .DS_Store .g4ignore build +.amp-build c -dist +/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 +test/coverage +./package-lock.json +*.swp +*.swo +yarn-error.log +PERCY_BUILD_ID +chromedriver.log +sc-*-linux* +EXTENSIONS_CSS_MAP +deps.txt +flags-array.txt +out/ +firebase +.firebaserc +firebase.json +.firebase/ diff --git a/.lgtm.yml b/.lgtm.yml new file mode 100644 index 000000000000..fbd3c4f9fa12 --- /dev/null +++ b/.lgtm.yml @@ -0,0 +1,18 @@ +path_classifiers: + docs: + - "**/*.md" + test: + - "**/test/**/*.js" + - "**/testing/**/*.js" + third_party: + - "third_party/**/*.*" + externs: + - "**/*.extern.js" + build-system: + - "build-system/**/*.*" +extraction: + javascript: + index: + exclude: examples +queries: + exclude: js/unused-local-variable diff --git a/.travis.yml b/.travis.yml index ec43c1c73af7..9d85506ec6b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,47 +2,59 @@ language: node_js sudo: required # See http://docs.travis-ci.com/user/trusty-ci-environment/ dist: trusty node_js: - - "stable" + - "lts/*" python: - "2.7" notifications: + email: + recipients: + - amp-build-cop@grotations.appspotmail.com + on_success: change + on_failure: change webhooks: - http://savage.nonblocking.io:8080/savage/travis -addons: - sauce_connect: true - hosts: - - ads.localhost - - iframe.localhost - apt: - packages: - - protobuf-compiler - - python-protobuf before_install: - - export CHROME_BIN=chromium-browser + - export CHROME_BIN=google-chrome-stable - export DISPLAY=:99.0 + - unset _JAVA_OPTIONS # JVM heap sizes break closure compiler. #11203. - sh -e /etc/init.d/xvfb start + - curl -o- -L https://yarnpkg.com/install.sh | bash + - export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH" before_script: - - npm install -g gulp - pip install --user protobuf -script: - - gulp lint - - gulp build - - gulp dist - - gulp presubmit - # Unit tests with Travis' default chromium - - gulp test --compiled - # Integration tests with all saucelabs browsers - - gulp test --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. - # - gulp test --saucelabs --oldchrome - - gulp validator +script: node build-system/pr-check.js +after_script: + - build-system/sauce_connect/stop_sauce_connect.sh branches: only: - master - release - canary -env: - global: - - NPM_CONFIG_PROGRESS="false" + - /^amp-release-.*$/ +addons: + chrome: stable + hosts: + - ads.localhost + - iframe.localhost + # Requested by some tests because they need a valid font host, + # but should not resolve in tests. + - fonts.googleapis.com + apt: + packages: + - protobuf-compiler + - python-protobuf +matrix: + include: + - env: BUILD_SHARD="unit_tests" + - env: BUILD_SHARD="integration_tests" +cache: + yarn: true + directories: + - build-system/tasks/visual-diff/node_modules + - node_modules + - validator/node_modules + - validator/nodejs/node_modules + - validator/webui/node_modules + - sauce_connect + pip: true + bundler: true diff --git a/3p/.eslintrc b/3p/.eslintrc new file mode 100644 index 000000000000..03ca691f62fc --- /dev/null +++ b/3p/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "amphtml-internal/no-style-display": 0 + } +} diff --git a/3p/3d-gltf/animation-loop.js b/3p/3d-gltf/animation-loop.js new file mode 100644 index 000000000000..23ee0152a079 --- /dev/null +++ b/3p/3d-gltf/animation-loop.js @@ -0,0 +1,76 @@ +/** + * Copyright 2018 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. + */ + +export default class AnimationLoop { + /** + * Creates an instance of AnimationLoop. + * @param {!Function} task + */ + constructor(task) { + /** @private */ + this.task_ = task; + + /** @private {boolean} */ + this.isRunning_ = false; + + /** @private {number} */ + this.currentRAF_ = 0; + + /** @public {boolean} */ + this.needsUpdate = true; + + /** @private */ + this.loop_ = this.loop_.bind(this); + } + + /** + * Runs the task + * @return {boolean} + */ + run() { + if (this.isRunning_) { + return false; + } + this.isRunning_ = true; + this.loop_(); + return true; + } + + /** + * Stops the task execution. + */ + stop() { + this.isRunning_ = false; + if (this.currentRAF_ !== 0) { + cancelAnimationFrame(this.currentRAF_); + this.currentRAF_ = 0; + } + } + + /** @private */ + loop_() { + if (!this.isRunning_) { + return; + } + + if (this.needsUpdate) { + this.needsUpdate = false; + this.task_(); + } + + this.currentRAF_ = requestAnimationFrame(this.loop_); + } +} diff --git a/3p/3d-gltf/index.js b/3p/3d-gltf/index.js new file mode 100644 index 000000000000..a3e5ac241b9a --- /dev/null +++ b/3p/3d-gltf/index.js @@ -0,0 +1,89 @@ +/** + * Copyright 2018 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 {dict} from '../../src/utils/object'; +import {listenParent, nonSensitiveDataPostMessage} from '../messaging'; +import {loadScript} from '../3p'; +import {parseJson} from '../../src/json'; +import {user} from '../../src/log'; + +import GltfViewer from './viewer'; + +const seq = (taskA, taskB) => cb => taskA(() => taskB(cb)); +const parallel = (taskA, taskB) => cb => { + let n = 0; + const finish = () => { + n++; + if (n === 2) { + cb(); + } + }; + taskA(finish); + taskB(finish); +}; + +const loadThree = (global, cb) => { + const loadScriptCb = url => cb => loadScript(global, url, cb); + const loadThreeExample = examplePath => + loadScriptCb( + 'https://cdn.jsdelivr.net/npm/three@0.91/examples/js/' + examplePath); + + seq( + loadScriptCb( + 'https://cdnjs.cloudflare.com/ajax/libs/three.js/91/three.js'), + parallel( + loadThreeExample('loaders/GLTFLoader.js'), + loadThreeExample('controls/OrbitControls.js')) + )(cb); +}; + +/** + * @param {!Window} global + */ +export function gltfViewer(global) { + const dataReceived = parseJson(global.name)['attributes']._context; + + loadThree(global, () => { + const viewer = new GltfViewer(dataReceived, { + onload: () => { + /** @suppress {deprecated} */ + nonSensitiveDataPostMessage('loaded'); + }, + onprogress: e => { + if (!e.lengthComputable) { + return; + } + /** @suppress {deprecated} */ + nonSensitiveDataPostMessage('progress', dict({ + 'total': e.total, + 'loaded': e.loaded, + })); + }, + onerror: err => { + user().error('3DGLTF', err); + /** @suppress {deprecated} */ + nonSensitiveDataPostMessage('error', dict({ + 'error': (err || '').toString(), + })); + }, + }); + listenParent(global, 'action', msg => { + viewer.actions[msg['action']](msg['args']); + }); + /** @suppress {deprecated} */ + nonSensitiveDataPostMessage('ready'); + }); +} diff --git a/3p/3d-gltf/viewer.js b/3p/3d-gltf/viewer.js new file mode 100644 index 000000000000..438cfbad7e72 --- /dev/null +++ b/3p/3d-gltf/viewer.js @@ -0,0 +1,293 @@ +/** + * Copyright 2018 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. + */ + +/* global THREE */ + +import {setStyle} from '../../src/style'; +import AnimationLoop from './animation-loop'; + +const CAMERA_DISTANCE_FACTOR = 1; +const CAMERA_FAR_FACTOR = 50; +const CAMERA_NEAR_FACTOR = .1; + + +export default class GltfViewer { + /** + * Creates an instance of GltfViewer. + * @param {!JsonObject} options + * @param {*} handlers + */ + constructor(options, handlers) { + /** @private */ + this.options_ = options; + + /** @private */ + this.handlers_ = handlers; + + /** @private */ + this.renderer_ = new THREE.WebGLRenderer(options['renderer']); + + /** @private */ + this.camera_ = new THREE.PerspectiveCamera(); + + /** @private */ + this.controls_ = new THREE.OrbitControls( + this.camera_, + this.renderer_.domElement); + + /** @private */ + this.scene_ = new THREE.Scene(); + + /** @private */ + this.animationLoop_ = new AnimationLoop(() => this.step_()); + + /** @private */ + this.ampPlay_ = true; + + /** @private */ + this.ampInViewport_ = + options['initialIntersection']['intersectionRatio'] > 0; + + /** @private */ + this.setSize_ = this.setupSize_(); + + /** @private */ + this.model_ = new THREE.Group(); + + this.setupRenderer_(); + this.setupControls_(); + this.setupLight_(); + this.loadObject_(); + this.reconcileAnimationLoop_(); + + this.actions = { + 'setSize': this.setSize_, + 'toggleAmpPlay': this.toggleAmpPlay_.bind(this), + 'toggleAmpViewport': this.toggleAmpViewport_.bind(this), + 'setModelRotation': this.setModelRotation_.bind(this), + }; + } + + /** + * @param {JsonObject} args + * @private + */ + setModelRotation_(args) { + const xAngle = 'x' in args + ? this.getModelRotationOnAxis_(args, 'x') + : this.model_.rotation.x; + + const yAngle = 'y' in args + ? this.getModelRotationOnAxis_(args, 'y') + : this.model_.rotation.y; + + const zAngle = 'z' in args + ? this.getModelRotationOnAxis_(args, 'z') + : this.model_.rotation.z; + + this.model_.rotation.set(xAngle, yAngle, zAngle); + this.animationLoop_.needsUpdate = true; + } + + /** + * @param {JsonObject} args + * @param {string} axisName + * @return {number} + * @private + */ + getModelRotationOnAxis_(args, axisName) { + const { + [axisName]: value, + [axisName + 'Min']: min = 0, + [axisName + 'Max']: max = Math.PI * 2, + } = args; + return value * max + (1 - value) * min; + } + + /** @private */ + setupSize_() { + let oldW = null; + let oldH = null; + /** @param {JsonObject} box */ + const setSize = box => { + const w = box['width']; + const h = box['height']; + if (oldW === w && oldH === h) { + return; + } + this.camera_.aspect = w / h; + this.camera_.updateProjectionMatrix(); + this.renderer_.setSize(w, h); + this.animationLoop_.needsUpdate = true; + oldW = w; + oldH = h; + }; + + setSize(this.options_['initialLayoutRect']); + + return setSize; + } + + /** @private */ + setupControls_() { + Object.assign(this.controls_, this.options_['controls']); + this.controls_.addEventListener('change', () => { + this.animationLoop_.needsUpdate = true; + }); + } + + /** + * Sets lighting scheme. + * + * There are no formal reasoning behind lighting scheme. + * It just looks good. + * + * Two directional lights, from above and below. + * One ambient to avoid completely dark areas. + * All lights are white. + * + * @private */ + setupLight_() { + const amb = new THREE.AmbientLight(0xEDECD5, .5); + + const dir1 = new THREE.DirectionalLight(0xFFFFFF, .5); + dir1.position.set(0, 5, 3); + + const dir2 = new THREE.DirectionalLight(0xAECDD6, .4); + dir2.position.set(-1, -2, 4); + + const light = new THREE.Group(); + light.add(amb, dir1, dir2); + + this.scene_.add(light); + } + + /** @private */ + setupRenderer_() { + const el = this.renderer_.domElement; + setStyle(el, 'position', 'absolute'); + setStyle(el, 'top', 0); + setStyle(el, 'right', 0); + setStyle(el, 'bottom', 0); + setStyle(el, 'left', 0); + document.body.appendChild(this.renderer_.domElement); + + this.renderer_.gammaOutput = true; + this.renderer_.gammaFactor = 2.2; + this.renderer_.setPixelRatio( + Math.min( + this.options_['rendererSettings']['maxPixelRatio'], + devicePixelRatio)); + this.renderer_.setClearColor( + this.options_['rendererSettings']['clearColor'], + this.options_['rendererSettings']['clearAlpha'] + ); + } + + /** + * Camera is set to be not too far or near in terms of object's size. + * + * We are positioning camera on a ray coming from center (C) + * of bounding box to its corner with max coordinates (M), + * and outside bbox to the length of CM * CAMERA_DISTANCE_FACTOR. + * + * It may look weird for objects stretched along one axis, + * also there are objects meant to be observed from inside, + * it will incorrectly work for them too. + * + * @param {!THREE.Object3D} object + * @private */ + setupCameraForObject_(object) { + const center = new THREE.Vector3(); + const size = new THREE.Vector3(); + const bbox = new THREE.Box3(); + bbox.setFromObject(object); + bbox.getCenter(center); + bbox.getSize(size); + + const sizeLength = size.length(); + this.camera_.far = sizeLength * CAMERA_FAR_FACTOR; + this.camera_.near = sizeLength * CAMERA_NEAR_FACTOR; + this.camera_.position.lerpVectors( + center, + bbox.max, + 1 + CAMERA_DISTANCE_FACTOR + ); + this.camera_.lookAt(center); + + this.camera_.updateProjectionMatrix(); + this.camera_.updateMatrixWorld(); + + this.controls_.target.copy(center); + } + + /** @private */ + loadObject_() { + const loader = new THREE.GLTFLoader(); + loader.crossOrigin = true; + + loader.load( + this.options_['src'], + /** @param {{scene: !THREE.Scene}} gltfData */ + gltfData => { + this.setupCameraForObject_(gltfData.scene); + gltfData.scene.children + .slice() + .forEach(child => { + this.model_.add(child); + }); + + this.scene_.add(this.model_); + + this.animationLoop_.needsUpdate = true; + this.handlers_.onload(); + }, + this.handlers_.onprogress, + this.handlers_.onerror); + } + + /** @private */ + step_() { + this.controls_.update(); + this.renderer_.render(this.scene_, this.camera_); + } + + /** + * @param {boolean} value + * @private */ + toggleAmpPlay_(value) { + this.ampPlay_ = value; + this.reconcileAnimationLoop_(); + } + + /** + * @param {boolean} inVp + * @private */ + toggleAmpViewport_(inVp) { + this.ampInViewport_ = inVp; + this.reconcileAnimationLoop_(); + } + + /** @private */ + reconcileAnimationLoop_() { + if (this.ampInViewport_ && this.ampPlay_) { + this.animationLoop_.needsUpdate = true; + this.animationLoop_.run(); + } else { + this.animationLoop_.stop(); + } + } +} diff --git a/3p/3p.js b/3p/3p.js new file mode 100644 index 000000000000..c879905fc6d6 --- /dev/null +++ b/3p/3p.js @@ -0,0 +1,313 @@ +/** + * 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. + */ + +/** + * @fileoverview Utility functions for scripts running inside of a third + * party iframe. + */ + +// Note: loaded by 3p system. Cannot rely on babel polyfills. + + +import {dev, rethrowAsync, user} from '../src/log'; +import {hasOwn, map} from '../src/utils/object'; +import {isArray} from '../src/types'; + + +/** @typedef {function(!Window, !Object)} */ +let ThirdPartyFunctionDef; + + +/** + * @const {!Object} + * @visibleForTesting + */ +let registrations; + +/** @type {number} */ +let syncScriptLoads = 0; + +/** + * Returns the registration map + */ +export function getRegistrations() { + if (!registrations) { + registrations = map(); + } + return registrations; +} + +/** + * @param {string} id The specific 3p integration. + * @param {ThirdPartyFunctionDef} draw Function that draws the 3p integration. + */ +export function register(id, draw) { + const registrations = getRegistrations(); + dev().assert(!registrations[id], 'Double registration %s', id); + registrations[id] = draw; +} + +/** + * Execute the 3p integration with the given id. + * @param {string} id + * @param {!Window} win + * @param {!Object} data + */ +export function run(id, win, data) { + const fn = registrations[id]; + user().assert(fn, 'Unknown 3p: ' + id); + fn(win, data); +} + +/** + * Synchronously load the given script URL. Only use this if you need a sync + * load. Otherwise use {@link loadScript}. + * Supports taking a callback that will be called synchronously after the given + * script was executed. + * @param {!Window} win + * @param {string} url + * @param {function()=} opt_cb + */ +export function writeScript(win, url, opt_cb) { + /*eslint no-useless-concat: 0*/ + win.document + .write('<' + 'script src="' + encodeURI(url) + '"><' + '/script>'); + if (opt_cb) { + executeAfterWriteScript(win, opt_cb); + } +} + +/** + * Asynchronously load the given script URL. + * @param {!Window} win + * @param {string} url + * @param {function()=} opt_cb + * @param {function()=} opt_errorCb + */ +export function loadScript(win, url, opt_cb, opt_errorCb) { + /** @const {!Element} */ + const s = win.document.createElement('script'); + s.src = url; + if (opt_cb) { + s.onload = opt_cb; + } + if (opt_errorCb) { + s.onerror = opt_errorCb; + } + win.document.body.appendChild(s); +} + +/** + * Call function in micro task or timeout as a fallback. + * This is a lightweight helper, because we cannot guarantee that + * Promises are available inside the 3p frame. + * @param {!Window} win + * @param {function()} fn + */ +export function nextTick(win, fn) { + const P = win.Promise; + if (P) { + P.resolve().then/*OK*/(fn); + } else { + win.setTimeout(fn, 0); + } +} + +/** + * Run the function after all currently waiting sync scripts have been + * executed. + * @param {!Window} win + * @param {function()} fn + */ +function executeAfterWriteScript(win, fn) { + const index = syncScriptLoads++; + win['__runScript' + index] = fn; + win.document.write('<' + 'script>__runScript' + index + '()<' + '/script>'); +} + +/** + * Throws if the given src doesn't start with prefix(es). + * @param {!Array|string} prefix + * @param {string} src + */ +export function validateSrcPrefix(prefix, src) { + if (!isArray(prefix)) { + prefix = [prefix]; + } + if (src !== undefined) { + for (let p = 0; p < prefix.length; p++) { + const protocolIndex = src.indexOf(prefix[p]); + if (protocolIndex == 0) { + return; + } + } + } + throw new Error('Invalid src ' + src); +} + +/** + * Throws if the given src doesn't contain the string + * @param {string} string + * @param {string} src + */ +export function validateSrcContains(string, src) { + if (src.indexOf(string) === -1) { + throw new Error('Invalid src ' + src); + } +} + +/** + * Utility function to perform a potentially asynchronous task + * exactly once for all frames of a given type and the provide the respective + * value to all frames. + * @param {!Window} global Your window + * @param {string} taskId Must be not conflict with any other global variable + * you use. Must be the same for all callers from all frames that want + * the same result. + * @param {function(function(*))} work Function implementing the work that + * is to be done. Receives a second function that should be called with + * the result when the work is done. + * @param {function(*)} cb Callback function that is called when the work is + * done. The first argument is the result. + */ +export function computeInMasterFrame(global, taskId, work, cb) { + const {master} = global.context; + let tasks = master.__ampMasterTasks; + if (!tasks) { + tasks = master.__ampMasterTasks = {}; + } + let cbs = tasks[taskId]; + if (!tasks[taskId]) { + cbs = tasks[taskId] = []; + } + cbs.push(cb); + if (!global.context.isMaster) { + return; // Only do work in master. + } + work(result => { + for (let i = 0; i < cbs.length; i++) { + cbs[i].call(null, result); + } + tasks[taskId] = { + push(cb) { + cb(result); + }, + }; + }); +} + +/** + * 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=} opt_optionalFields + */ +export function validateData(data, mandatoryFields, opt_optionalFields) { + let allowedFields = opt_optionalFields || []; + for (let i = 0; i < mandatoryFields.length; i++) { + const field = mandatoryFields[i]; + 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); + } +} + +/** + * Throws an exception if data does not contains exactly one field + * mentioned in the alternativeFields array. + * @param {!Object} data + * @param {!Array} alternativeFields + */ +function validateExactlyOne(data, alternativeFields) { + user().assert(alternativeFields.filter(field => data[field]).length === 1, + '%s must contain exactly one of attributes: %s.', + data.type, + alternativeFields.join(', ')); +} + +/** + * Throws a non-interrupting exception if data contains a field not supported + * by this embed type. + * @param {!Object} data + * @param {!Array} allowedFields + */ +function validateAllowedFields(data, allowedFields) { + const defaultAvailableFields = { + width: true, + height: true, + type: true, + referrer: true, + canonicalUrl: true, + pageViewId: true, + location: true, + mode: true, + consentNotificationId: true, + blockOnConsent: true, + ampSlotIndex: true, + adHolderText: true, + loadingStrategy: true, + htmlAccessAllowed: true, + adContainerId: true, + }; + + for (const field in data) { + if (!hasOwn(data, field) || field in defaultAvailableFields) { + continue; + } + 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 && !!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-integration.js b/3p/ampcontext-integration.js new file mode 100644 index 000000000000..78a888f6e9a2 --- /dev/null +++ b/3p/ampcontext-integration.js @@ -0,0 +1,156 @@ +/** + * 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 {AbstractAmpContext} from './ampcontext'; +import {adConfig} from '../ads/_config'; +import {computeInMasterFrame} from './3p'; +import {dev, user} from '../src/log'; +import {dict} from '../src/utils/object'; + + +/** + * Returns the "master frame" for all widgets of a given type. + * This frame should be used to e.g. fetch scripts that can + * be reused across frames. + * once experiment is removed. + * @param {!Window} win + * @param {string} type + * @return {!Window} + */ +export function masterSelection(win, type) { + type = type.toLowerCase(); + const configType = adConfig[type] && + adConfig[type]['masterFrameAccessibleType']; + // The master has a special name. + const masterName = 'frame_' + (configType || type) + '_master'; + let master; + try { + // Try to get the master from the parent. If it does not + // exist yet we get a security exception that we catch + // and ignore. + master = win.parent.frames[masterName]; + } catch (expected) { + /* ignore */ + } + if (!master) { + // No master yet, rename ourselves to be master. Yaihh. + win.name = masterName; + master = win; + } + return master; +} + + +export class IntegrationAmpContext extends AbstractAmpContext { + + /** @override */ + isAbstractImplementation_() { + return false; + } + + /** + * @return {boolean} + * @protected + */ + updateDimensionsEnabled_() { + // Only make this available to selected embeds until the generic solution is + // available. + return (this.embedType_ === 'facebook' + || this.embedType_ === 'twitter' + || this.embedType_ === 'github' + || this.embedType_ === 'mathml' + || this.embedType_ === 'reddit' + || this.embedType_ === 'yotpo' + || this.embedType_ === 'embedly' + ); + } + + /** @return {!Window} */ + get master() { + return this.master_(); + } + + /** @return {!Window} */ + master_() { + return masterSelection(this.win_, dev().assertString(this.embedType_)); + } + + /** @return {boolean} */ + get isMaster() { + return this.isMaster_(); + } + + /** @return {boolean} */ + isMaster_() { + return this.master == this.win_; + } + + /** + * @param {number} width + * @param {number} height + */ + updateDimensions(width, height) { + user().assert(this.updateDimensionsEnabled_(), 'Not available.'); + this.requestResize(width, height); + } + + /** + * Sends bootstrap loaded message. + */ + bootstrapLoaded() { + this.client_.sendMessage('bootstrap-loaded'); + } + + /** + * @param {!JsonObject=} opt_data Fields: width, height + */ + renderStart(opt_data) { + this.client_.sendMessage('render-start', opt_data); + } + + /** + * Reports the "entity" that was rendered to this frame to the parent for + * reporting purposes. + * The entityId MUST NOT contain user data or personal identifiable + * information. One example for an acceptable data item would be the + * creative id of an ad, while the user's location would not be + * acceptable. + * TODO(alanorozco): Remove duplicate in 3p/integration.js once this + * implementation becomes canonical. + * @param {string} entityId See comment above for content. + */ + reportRenderedEntityIdentifier(entityId) { + this.client_.sendMessage('entity-id', dict({ + 'id': user().assertString(entityId), + })); + } + + /** + * Performs a potentially asynchronous task exactly once for all frames of a + * given type and the provide the respective value to all frames. + * @param {!Window} global Your window + * @param {string} taskId Must be not conflict with any other global variable + * you use. Must be the same for all callers from all frames that want + * the same result. + * @param {function(function(*))} work Function implementing the work that + * is to be done. Receives a second function that should be called with + * the result when the work is done. + * @param {function(*)} cb Callback function that is called when the work is + * done. The first argument is the result. + */ + computeInMasterFrame(global, taskId, work, cb) { + computeInMasterFrame(global, taskId, work, cb); + } +} diff --git a/3p/ampcontext-lib.js b/3p/ampcontext-lib.js new file mode 100644 index 000000000000..ff691c82028f --- /dev/null +++ b/3p/ampcontext-lib.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. + */ + +// src/polyfills.js must be the first import. +import './polyfills'; // eslint-disable-line sort-imports-es6-autofix/sort-imports-es6 + +import {AmpContext} from './ampcontext.js'; +import {initLogConstructor, setReportError} from '../src/log'; + + +initLogConstructor(); + + +// TODO(alanorozco): Refactor src/error.reportError so it does not contain big +// transitive dependencies and can be included here. +setReportError(() => {}); + + +/** + * 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..9a67d3fadb3d --- /dev/null +++ b/3p/ampcontext.js @@ -0,0 +1,371 @@ +/** + * 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 {AmpEvents} from '../src/amp-events'; +import {IframeMessagingClient} from './iframe-messaging-client'; +import {MessageType} from '../src/3p-frame-messaging'; +import {dev} from '../src/log'; +import {dict} from '../src/utils/object'; +import {isExperimentOn, nextTick} from './3p'; +import {isObject} from '../src/types'; +import {parseUrlDeprecated} from '../src/url'; +import {tryParseJson} from '../src/json'; + +export class AbstractAmpContext { + + /** + * @param {!Window} win The window that the instance is built inside. + */ + constructor(win) { + dev().assert(!this.isAbstractImplementation_(), + 'Should not construct AbstractAmpContext instances directly'); + + /** @protected {!Window} */ + this.win_ = win; + + // This value is cached since it could be overwritten by the master frame + // check using a value of a different type. + /** @private {?string} */ + this.cachedFrameName_ = this.win_.name || null; + + /** @protected {?string} */ + this.embedType_ = null; + + // ---------------------------------------------------- + // Please keep public attributes alphabetically sorted. + // ---------------------------------------------------- + + /** @public {?string|undefined} */ + this.canary = null; + + /** @type {?string} */ + this.canonicalUrl = null; + + /** @type {?string} */ + this.clientId = null; + + /** @type {?string|undefined} */ + this.container = null; + + /** @type {?Object} */ + this.consentSharedData = null; + + /** @type {?Object} */ + this.data = null; + + /** @type {?string} */ + this.domFingerprint = null; + + /** @type {?boolean} */ + this.hidden = null; + + /** @type {?number} */ + this.initialConsentState = null; + + /** @type {?Object} */ + this.initialLayoutRect = null; + + /** @type {?Object} */ + this.initialIntersection = null; + + /** @type {?Location} */ + this.location = null; + + /** @type {?Object} */ + this.mode = null; + + /** @type {?string} */ + this.pageViewId = null; + + /** @type {?string} */ + this.referrer = null; + + /** @type {?string} */ + this.sentinel = null; + + /** @type {?string} */ + this.sourceUrl = null; + + /** @type {?number} */ + this.startTime = null; + + /** @type {?string} */ + this.tagName = null; + + this.findAndSetMetadata_(); + + /** @protected {!IframeMessagingClient} */ + this.client_ = new IframeMessagingClient(win); + this.client_.setHostWindow(this.getHostWindow_()); + this.client_.setSentinel(dev().assertString(this.sentinel)); + + this.listenForPageVisibility_(); + } + + /** + * @return {boolean} + * @protected + */ + isAbstractImplementation_() { + return true; + } + + /** Registers an general handler for page visibility. */ + listenForPageVisibility_() { + this.client_.makeRequest( + MessageType.SEND_EMBED_STATE, + MessageType.EMBED_STATE, + data => { + this.hidden = data['pageHidden']; + this.dispatchVisibilityChangeEvent_(); + }); + } + + /** + * TODO(alanorozco): Deprecate native event mechanism. + * @private + */ + dispatchVisibilityChangeEvent_() { + const event = this.win_.document.createEvent('Event'); + event.data = {hidden: this.hidden}; + event.initEvent(AmpEvents.VISIBILITY_CHANGE, true, true); + this.win_.dispatchEvent(event); + } + + /** + * Listen to page visibility changes. + * @param {function({hidden: boolean})} callback Function to call every time + * we receive a page visibility message. + * @return {function()} that when called stops triggering the callback + * every time we receive a page visibility message. + */ + onPageVisibilityChange(callback) { + return this.client_.registerCallback(MessageType.EMBED_STATE, data => { + callback({hidden: data['pageHidden']}); + }); + } + + /** + * Send message to runtime to start sending intersection messages. + * @param {function(Array)} callback Function to call every time we + * receive an intersection message. + * @return {function()} that when called stops triggering the callback + * every time we receive an intersection message. + */ + observeIntersection(callback) { + const unlisten = this.client_.makeRequest( + MessageType.SEND_INTERSECTIONS, + MessageType.INTERSECTION, + intersection => { + callback(intersection['changes']); + }); + + if (!isExperimentOn('no-initial-intersection')) { // eslint-disable-line + // Call the callback with the value that was transmitted when the + // iframe was drawn. Called in nextTick, so that callers don't + // have to specially handle the sync case. + // TODO(lannka, #8562): Deprecate this behavior + nextTick(this.win_, () => { + callback([this.initialIntersection]); + }); + } + + return unlisten; + } + + /** + * Requests HTML snippet from the parent window. + * @param {string} selector CSS selector + * @param {!Array} attributes whitelisted attributes to be kept + * in the returned HTML string + * @param {function(*)} callback to be invoked with the HTML string + */ + getHtml(selector, attributes, callback) { + this.client_.getData(MessageType.GET_HTML, dict({ + 'selector': selector, + 'attributes': attributes, + }), callback); + } + + /** + * Requests consent state from the parent window. + * + * @param {function(*)} callback + */ + getConsentState(callback) { + this.client_.getData( + MessageType.GET_CONSENT_STATE, null, 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 {number} width The new width for the ad we are requesting. + * @param {number} height The new height for the ad we are requesting. + * @param {boolean=} hasOverflow Whether the ad handles its own overflow ele + */ + requestResize(width, height, hasOverflow) { + this.client_.sendMessage(MessageType.EMBED_SIZE, dict({ + 'width': width, + 'height': height, + 'hasOverflow': hasOverflow, + })); + } + + /** + * 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(number, number)} 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(number, number)} 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) { + // TODO(alanorozco): consider the AMP_CONTEXT_DATA case + iframe.name = dev().assertString(this.cachedFrameName_); + } + + /** + * Notifies the parent document of no content available inside embed. + */ + noContentAvailable() { + this.client_.sendMessage(MessageType.NO_CONTENT); + } + + /** + * Parse the metadata attributes from the name and add them to + * the class instance. + * @param {!Object|string} data + * @private + */ + setupMetadata_(data) { + // TODO(alanorozco): Use metadata utils in 3p/frame-metadata + const dataObject = dev().assert( + typeof data === 'string' ? tryParseJson(data) : data, + 'Could not setup metadata.'); + + const context = dataObject._context || dataObject.attributes._context; + + this.data = dataObject.attributes || dataObject; + + // TODO(alanorozco, #10576): This is really ugly. Find a better structure + // than passing context values via data. + if ('_context' in this.data) { + delete this.data['_context']; + } + + this.canary = context.canary; + this.canonicalUrl = context.canonicalUrl; + this.clientId = context.clientId; + this.consentSharedData = context.consentSharedData; + this.container = context.container; + this.domFingerprint = context.domFingerprint; + this.hidden = context.hidden; + this.initialConsentState = context.initialConsentState; + this.initialLayoutRect = context.initialLayoutRect; + this.initialIntersection = context.initialIntersection; + this.location = parseUrlDeprecated(context.location.href); + this.mode = context.mode; + this.pageViewId = context.pageViewId; + this.referrer = context.referrer; + this.sentinel = context.sentinel; + this.sourceUrl = context.sourceUrl; + this.startTime = context.startTime; + this.tagName = context.tagName; + + this.embedType_ = dataObject.type || null; + } + + /** + * 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. + // TODO(alanorozco): why the heck could AMP_CONTEXT_DATA be two different + // types? FIX THIS. + if (isObject(this.win_.sf_) && this.win_.sf_.cfg) { + this.setupMetadata_(/** @type {string}*/(this.win_.sf_.cfg)); + } else if (this.win_.AMP_CONTEXT_DATA) { + if (typeof this.win_.AMP_CONTEXT_DATA == 'string') { + this.sentinel = this.win_.AMP_CONTEXT_DATA; + } else if (isObject(this.win_.AMP_CONTEXT_DATA)) { + this.setupMetadata_(this.win_.AMP_CONTEXT_DATA); + } + } else { + this.setupMetadata_(this.win_.name); + } + } + + /** + * Send 3p error to parent iframe + * @param {!Error} e + */ + report3pError(e) { + if (!e.message) { + return; + } + this.client_.sendMessage(MessageType.USER_ERROR_IN_IFRAME, dict({ + 'message': e.message, + })); + } +} + +export class AmpContext extends AbstractAmpContext { + /** @override */ + isAbstractImplementation_() { + return false; + } +} diff --git a/3p/beopinion.js b/3p/beopinion.js new file mode 100644 index 000000000000..c26bf1bd5509 --- /dev/null +++ b/3p/beopinion.js @@ -0,0 +1,123 @@ +/** + * Copyright 2018 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'; +import {setStyles} from '../src/style'; + +/** + * Produces the Twitter API object for the passed in callback. If the current + * frame is the master frame it makes a new one by injecting the respective + * script, otherwise it schedules the callback for the script from the master + * window. + * @param {!Window} global + */ +function getBeOpinion(global) { + loadScript(global, 'https://widget.beopinion.com/sdk.js', function() {}); +} + +/** + * @param {!Window} global + * @description Make canonicalUrl available from iframe + */ +function addCanonicalLinkTag(global) { + const {canonicalUrl} = global.context; + if (canonicalUrl) { + const link = global.document.createElement('link'); + link.setAttribute('rel', 'canonical'); + link.setAttribute('href', canonicalUrl); + global.document.head.appendChild(link); + } +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function createContainer(global, data) { + // add canonical link tag + addCanonicalLinkTag(global); + + // create div + const container = global.document.createElement('container'); + container.className = 'BeOpinionWidget'; + + // get content id + if (data['content'] !== null) { + container.setAttribute('data-content', data['content']); + } + + // get my-content value, forcing it to '1' if it is not an amp-ad + if (global.context.tagName === 'AMP-BEOPINION') { + container.setAttribute('data-my-content', '1'); + } else if (data['my-content'] !== null) { + container.setAttribute('data-my-content', data['my-content']); + } + + // get slot name + if (data['name'] !== null) { + container.setAttribute('data-name', data['name']); + } + + setStyles(container, { + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }); + + return container; +} + +/** + * @param {*} global + * @param {*} accountId + * @return {!Function} + */ +function getBeOpinionAsyncInit(global, accountId) { + const {context} = global; + return function() { + global.BeOpinionSDK.init({ + account: accountId, + onContentReceive: function(hasContent) { + if (hasContent) { + context.renderStart(); + } else { + context.noContentAvailable(); + } + }, + onHeightChange: function(newHeight) { + const c = global.document.getElementById('c'); + const boundingClientRect = c./*REVIEW*/getBoundingClientRect(); + context.onResizeDenied(context.requestResize.bind(context)); + context.requestResize(boundingClientRect.width, newHeight); + }, + }); + global.BeOpinionSDK['watch'](); // global.BeOpinionSDK.watch() fails 'gulp check-types' validation + }; +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function beopinion(global, data) { + const container = createContainer(global, data); + const c = global.document.getElementById('c'); + c.appendChild(container); + + global.beOpinionAsyncInit = getBeOpinionAsyncInit(global, data.account); + getBeOpinion(global); +} diff --git a/3p/bodymovinanimation.js b/3p/bodymovinanimation.js new file mode 100644 index 000000000000..da0029214407 --- /dev/null +++ b/3p/bodymovinanimation.js @@ -0,0 +1,99 @@ +/** + * Copyright 2018 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 {dict} from '../src/utils/object'; +import {getData} from '../src/event-helper'; +import {loadScript} from './3p'; +import {parseJson} from '../src/json'; +import {setStyles} from '../src/style'; + +/** + * Produces the AirBnB Bodymovin Player SDK object for the passed in callback. + * @param {!Window} global + * @param {function(!Object)} cb + */ + +let animationHandler; + +/** + * @param {!Window} global + * @param {string} renderer + * @param {!Function} cb + */ +function getBodymovinAnimationSdk(global, renderer, cb) { + const scriptToLoad = renderer === 'svg' ? + 'https://cdnjs.cloudflare.com/ajax/libs/bodymovin/4.13.0/bodymovin_light.min.js' : + 'https://cdnjs.cloudflare.com/ajax/libs/bodymovin/4.13.0/bodymovin.min.js'; + loadScript(global, scriptToLoad, function() { + cb(global.bodymovin); + }); +} + +/** + * @param {!Event} event + */ +function parseMessage(event) { + const eventMessage = parseJson(getData(event)); + const action = eventMessage['action']; + if (action == 'play') { + animationHandler.play(); + } else if (action == 'pause') { + animationHandler.pause(); + } else if (action == 'stop') { + animationHandler.stop(); + } else if (action == 'seekTo') { + if (eventMessage['valueType'] === 'time') { + animationHandler.goToAndStop(eventMessage['value']); + } else { + const frameNumber = + Math.round(eventMessage['value'] * animationHandler.totalFrames); + animationHandler.goToAndStop(frameNumber, true); + } + } +} + +/** + * @param {!Window} global + */ +export function bodymovinanimation(global) { + const dataReceived = parseJson(global.name)['attributes']._context; + const dataLoop = dataReceived['loop']; + const animatingContainer = global.document.createElement('div'); + setStyles(animatingContainer, { + width: '100%', + height: '100%', + }); + + global.document.getElementById('c').appendChild(animatingContainer); + const shouldLoop = dataLoop != 'false'; + const loop = !isNaN(dataLoop) ? dataLoop : shouldLoop; + const renderer = dataReceived['renderer']; + getBodymovinAnimationSdk(global, renderer, function(bodymovin) { + animationHandler = bodymovin.loadAnimation({ + container: animatingContainer, + renderer, + loop, + autoplay: dataReceived['autoplay'], + animationData: dataReceived['animationData'], + }); + const message = JSON.stringify(dict({ + 'action': 'ready', + })); + global.addEventListener('message', parseMessage, false); + global.parent. /*OK*/postMessage(message, '*'); + }); +} + diff --git a/3p/embedly.js b/3p/embedly.js new file mode 100644 index 000000000000..863933768c8e --- /dev/null +++ b/3p/embedly.js @@ -0,0 +1,119 @@ +/** + * Copyright 2018 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 {hasOwn} from '../src/utils/object'; +import {loadScript} from './3p'; +import {setStyle} from '../src/style'; + +/** + * Embedly platform library url to create cards. + * @const {string} + */ +const EMBEDLY_SDK_URL = 'https://cdn.embedly.com/widgets/platform.js'; + +/** + * Event name emitted by embedly's SDK. + * @type {string} + */ +const RESIZE_EVENT_NAME = 'card.resize'; + +/** + * Css class expected by embedly library to style card. + * @const {string} + */ +const CARD_CSS_CLASS = 'embedly-card'; + +/** + * Whitelisted card options. + * + * - Key is in camel case as received in "data". + * - The value is in the format expected by embedly. + * + * @see {@link http://docs.embed.ly/docs/cards#customize} + * @const @enum {string} + */ +export const CardOptions = { + cardVia: 'card-via', + cardTheme: 'card-theme', + cardImage: 'card-image', + cardControls: 'card-controls', + cardAlign: 'card-align', + cardRecommend: 'card-recommend', + cardEmbed: 'card-embed', + cardKey: 'card-key', +}; + +/** + * Loads embedly card SDK that is consumed by this 3p integration. + * + * @param {!Window} global + * @param {function(!Object)} callback + * @visibleForTesting + */ +function getEmbedly(global, callback) { + loadScript(global, EMBEDLY_SDK_URL, function() { + callback(global); + }); +} + +/** + * Creates embedly card using sdk. + * + * @param {!Window} global + * @param {!Object} data + */ +export function embedly(global, data) { + const card = global.document.createElement('a'); + + card.href = data.url; + card.classList.add(CARD_CSS_CLASS); + + // Add whitelisted data attributes and values to card + // when these are provided by component. + for (const key in CardOptions) { + if ( + hasOwn(CardOptions, key) && + typeof data[key] !== 'undefined' + ) { + card.setAttribute(`data-${CardOptions[key]}`, data[key]); + } + } + + const container = global.document.getElementById('c'); + + // Adds support to embedly dark theme not set by the sdk + if (data['cardTheme'] === 'dark') { + setStyle(container, 'background', 'rgba(51, 51, 51)'); + } + + container.appendChild(card); + + getEmbedly(global, function() { + // Given by the parent frame. + delete data.width; + delete data.height; + + global.window['embedly']('card', card); + + // Use embedly SDK to listen to resize event from loaded card + global.window['embedly']('on', RESIZE_EVENT_NAME, function(iframe) { + context.requestResize( + iframe./*OK*/width, + parseInt(iframe./*OK*/height, 10) + /* margin */ 5 + ); + }); + }); +} diff --git a/3p/environment.js b/3p/environment.js index 0302dd46a997..a1213667a97d 100644 --- a/3p/environment.js +++ b/3p/environment.js @@ -29,8 +29,10 @@ export function setInViewportForTesting(inV) { inViewport = inV; } -let rafId = 0; -let rafQueue = {}; +// Active intervals. Must be global, because people clear intervals +// with clearInterval from a different window. +const intervals = {}; +let intervalId = 0; /** * Add instrumentation to a window and all child iframes. @@ -60,6 +62,7 @@ function manageWin_(win) { installObserver(win); // Existing iframes. maybeInstrumentsNodes(win, win.document.querySelectorAll('iframe')); + blockSyncPopups(win); } @@ -70,13 +73,15 @@ function manageWin_(win) { */ function instrumentDocWrite(parent, win) { const doc = win.document; - const close = doc.close; + const {close} = doc; doc.close = function() { parent.ampManageWin = function(win) { manageWin(win); }; - doc.write(''); - // .call does not work in Safari with document.write. + if (!parent.ampSeen) { + // .call does not work in Safari with document.write. + doc.write(''); + } doc._close = close; return doc._close(); }; @@ -99,7 +104,7 @@ function instrumentSrcdoc(parent, iframe) { /** * Instrument added nodes if they are instrumentable iframes. * @param {!Window} win - * @param {!Array} addedNodes + * @param {!Array|NodeList|NodeList|null} addedNodes */ function maybeInstrumentsNodes(win, addedNodes) { for (let n = 0; n < addedNodes.length; n++) { @@ -173,75 +178,81 @@ function installObserver(win) { */ function instrumentEntryPoints(win) { // Change setTimeout to respect a minimum timeout. - const setTimeout = win.setTimeout; + const {setTimeout} = win; win.setTimeout = function(fn, time) { time = minTime(time); - return setTimeout(fn, time); + arguments[1] = time; + return setTimeout.apply(this, arguments); }; // Implement setInterval in terms of setTimeout to make // it respect the same rules - const intervals = {}; - let intervalId = 0; - win.setInterval = function(fn, time) { + win.setInterval = function(fn) { const id = intervalId++; - function next() { - intervals[id] = win.setTimeout(function() { - next(); + const args = Array.prototype.slice.call(arguments); + /** + * @return {*} + */ + function wrapper() { + next(); + if (typeof fn == 'string') { + // Handle rare and dangerous string arg case. + return (0, win.eval/*NOT OK but whatcha gonna do.*/).call(win, fn); // lgtm [js/useless-expression] + } else { return fn.apply(this, arguments); - }, time); + } + } + args[0] = wrapper; + /** + * + */ + function next() { + intervals[id] = win.setTimeout.apply(win, args); } next(); return id; }; + const {clearInterval} = win; win.clearInterval = function(id) { + clearInterval(id); win.clearTimeout(intervals[id]); delete intervals[id]; }; - // Throttle requestAnimationFrame. - const requestAnimationFrame = win.requestAnimationFrame || - win.webkitRequestAnimationFrame; - win.requestAnimationFrame = function(cb) { - if (!inViewport) { - // If the doc is not visible, queue up the frames until we become - // visible again. - const id = rafId++; - rafQueue[id] = [win, cb]; - // Only queue 20 frame requests to avoid mem leaks. - delete rafQueue[id - 20]; - return id; - } - return requestAnimationFrame.call(this, cb); - }; - const cancelAnimationFrame = win.cancelAnimationFrame; - win.cancelAnimationFrame = function(id) { - cancelAnimationFrame.call(this, id); - delete rafQueue[id]; - }; - if (win.webkitRequestAnimationFrame) { - win.webkitRequestAnimationFrame = win.requestAnimationFrame; - win.webkitCancelAnimationFrame = win.webkitCancelRequestAnimationFrame = - win.cancelAnimationFrame; - } } /** - * Run when we just became visible again. Runs all the queued up rafs. - * @visibleForTesting + * Blackhole the legacy popups since they should never be used for anything. + * @param {!Window} win */ -export function becomeVisible() { - for (const id in rafQueue) { - if (rafQueue.hasOwnProperty(id)) { - const f = rafQueue[id]; - f[0].requestAnimationFrame(f[1]); +function blockSyncPopups(win) { + let count = 0; + /** + * Checks for security error. + */ + function maybeThrow() { + // Prevent deep recursion. + if (count++ > 2) { + throw new Error('security error'); } } - rafQueue = {}; + try { + win.alert = maybeThrow; + win.prompt = function() { + maybeThrow(); + return ''; + }; + win.confirm = function() { + maybeThrow(); + return false; + }; + } catch (e) { + console./*OK*/error(e.message, e.stack); + } } /** * Calculates the minimum time that a timeout should have right now. - * @param {number} time - * @return {number} + * @param {number|undefined} time + * @return {number|undefined} */ function minTime(time) { if (!inViewport) { @@ -255,9 +266,12 @@ function minTime(time) { return time; } -listenParent('embed-state', function(data) { - inViewport = data.inViewport; - if (inViewport) { - becomeVisible(); - } -}); +/** + * Installs embed state listener. + */ +export function installEmbedStateListener() { + /** @suppress {deprecated} */ + listenParent(window, 'embed-state', function(data) { + inViewport = data.inViewport; + }); +} diff --git a/3p/facebook.js b/3p/facebook.js index 154906154857..2ac48f410da6 100644 --- a/3p/facebook.js +++ b/3p/facebook.js @@ -14,54 +14,227 @@ * limitations under the License. */ -import {loadScript} from '../src/3p'; -import {assert} from '../src/asserts'; - +import {dashToUnderline} from '../src/string'; +import {dict} from '../src/utils/object'; +import {loadScript} from './3p'; +import {setStyle} from '../src/style'; +import {user} from '../src/log'; /** * Produces the Facebook SDK object for the passed in callback. * - * Note: Facebook SDK fails to render multiple posts when the SDK is only loaded - * in one frame. To Allow the SDK to render them correctly we load the script - * per iframe. + * Note: Facebook SDK fails to render multiple plugins when the SDK is only + * loaded in one frame. To Allow the SDK to render them correctly we load the + * script per iframe. * * @param {!Window} global * @param {function(!Object)} cb + * @param {string} locale */ -function getFacebookSdk(global, cb) { - loadScript(global, 'https://connect.facebook.net/en_US/sdk.js', () => { +function getFacebookSdk(global, cb, locale) { + loadScript(global, 'https://connect.facebook.net/' + locale + '/sdk.js', () => { cb(global.FB); }); } +/** + * Create DOM element for all Facebook embeds. + * @param {!Window} global + * @param {string} classNameSuffix The suffix for the `fb-` class. + * @param {string} href + * @return {!Element} div + */ +function createContainer(global, classNameSuffix, href) { + const container = global.document.createElement('div'); + container.className = 'fb-' + classNameSuffix; + container.setAttribute('data-href', href); + return container; +} + +/** + * Create DOM element for the Facebook embedded content plugin for posts. + * @see https://developers.facebook.com/docs/plugins/embedded-posts + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getPostContainer(global, data) { + if (data.alignCenter) { + const c = global.document.getElementById('c'); + setStyle(c, 'text-align', 'center'); + } + return createContainer(global, 'post', data.href); +} + +/** + * Create DOM element for the Facebook embedded content plugin for videos. + * @see https://developers.facebook.com/docs/plugins/embedded-video-player + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getVideoContainer(global, data) { + const container = createContainer(global, 'video', data.href); + // If the user hasn't set the `data-embed-as` attribute and the provided href + // is a video, Force the `data-embed-as` attribute to 'video' and make sure + // to show the post's text. + if (!data.embedAs) { + container.setAttribute('data-embed-as', 'video'); + // Since 'data-embed-as="video"' disables post text, setting the + // 'data-show-text' to 'true' enables the ability to see the text (changed + // from the default 'false') + container.setAttribute('data-show-text', 'true'); + } + return container; +} + +/** + * Create DOM element for the Facebook embedded content plugin for comments or + * comment replies. + * @see https://developers.facebook.com/docs/plugins/embedded-comments + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getCommentContainer(global, data) { + const c = global.document.getElementById('c'); + const container = createContainer(global, 'comment-embed', data.href); + container.setAttribute( + 'data-include-parent', data.includeCommentParent || 'false'); + container.setAttribute('data-width', c./*OK*/offsetWidth); + return container; +} + +/** + * Gets the default type to embed as, if not specified. + * @param {string} href + * @return {string} + */ +function getDefaultEmbedAs(href) { + return href.match(/\/videos\/\d+\/?$/) ? 'video' : 'post'; +} + +/** + * Create DOM element for the Facebook embedded content plugin. + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getEmbedContainer(global, data) { + const embedAs = data.embedAs || getDefaultEmbedAs(data.href); + + user().assert(['post', 'video', 'comment'].indexOf(embedAs) !== -1, + 'Attribute data-embed-as for value is wrong, should be' + + ' "post", "video" or "comment" but was: %s', embedAs); + + switch (embedAs) { + case 'comment': + return getCommentContainer(global, data); + case 'video': + return getVideoContainer(global, data); + default: + return getPostContainer(global, data); + } +} + +/** + * Create DOM element for the Facebook embedded page plugin. + * Reference: https://developers.facebook.com/docs/plugins/page-plugin + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getPageContainer(global, data) { + const container = createContainer(global, 'page', data.href); + container.setAttribute('data-tabs', data.tabs); + container.setAttribute('data-hide-cover', data.hideCover); + container.setAttribute('data-show-facepile', data.showFacepile); + container.setAttribute('data-hide-cta', data.hideCta); + container.setAttribute('data-small-header', data.smallHeader); + container.setAttribute('data-adapt-container-width', true); + + const c = global.document.getElementById('c'); + // Note: The facebook embed allows a maximum width of 500px. + // If the container's width exceeds that, the embed's width will + // be clipped to 500px. + container.setAttribute('data-width', c./*OK*/offsetWidth); + return container; +} + +/** + * Create DOM element for the Facebook comments plugin: + * Reference: https://developers.facebook.com/docs/plugins/comments + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getCommentsContainer(global, data) { + const container = createContainer(global, 'comments', data.href); + container.setAttribute('data-numposts', data.numposts || 10); + container.setAttribute('data-colorscheme', data.colorscheme || 'light'); + container.setAttribute('data-order-by', data.orderBy || 'social'); + container.setAttribute('data-width', '100%'); + return container; +} + +/** + * Create DOM element for the Facebook like-button plugin: + * Reference: https://developers.facebook.com/docs/plugins/like-button + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getLikeContainer(global, data) { + const container = createContainer(global, 'like', data.href); + container.setAttribute('data-action', data.action || 'like'); + container.setAttribute('data-colorscheme', data.colorscheme || 'light'); + container.setAttribute('data-kd_site', data.kd_site || 'false'); + container.setAttribute('data-layout', data.layout || 'standard'); + container.setAttribute('data-ref', data.ref || ''); + container.setAttribute('data-share', data.share || 'false'); + container.setAttribute('data-show_faces', data.show_faces || 'false'); + container.setAttribute('data-size', data.size || 'small'); + return container; +} + /** * @param {!Window} global * @param {!Object} data */ export function facebook(global, data) { - const embedAs = data.embedAs || 'post'; - assert(['post', 'video'].indexOf(embedAs) !== -1, - 'Attribute data-embed-as for value is wrong, should be' + - ' "post" or "video" was: %s', embedAs); - const fbPost = document.createElement('div'); - fbPost.className = 'fb-' + embedAs; - fbPost.setAttribute('data-href', data.href); - global.document.getElementById('c').appendChild(fbPost); + const extension = global.context.tagName; + let container; + + if (extension === 'AMP-FACEBOOK-PAGE') { + container = getPageContainer(global, data); + } else if (extension === 'AMP-FACEBOOK-LIKE') { + container = getLikeContainer(global, data); + } else if (extension === 'AMP-FACEBOOK-COMMENTS') { + container = getCommentsContainer(global, data); + } else /*AMP-FACEBOOK */ { + container = getEmbedContainer(global, data); + } + + global.document.getElementById('c').appendChild(container); + getFacebookSdk(global, FB => { // Dimensions are given by the parent frame. delete data.width; delete data.height; - // Only need to listen to post resizing as FB videos have a fixed ratio - // and can automatically resize correctly given the initial width/height. - if (embedAs === 'post') { - FB.Event.subscribe('xfbml.resize', event => { - context.updateDimensions( - parseInt(event.width, 10), - parseInt(event.height, 10) + /* margins */ 20); - }); - } + FB.Event.subscribe('xfbml.resize', event => { + context.updateDimensions( + parseInt(event.width, 10), + parseInt(event.height, 10) + /* margins */ 20); + }); + FB.init({xfbml: true, version: 'v2.5'}); - }); + // Report to parent that the SDK has loaded and is ready to paint + const message = JSON.stringify(dict({ + 'action': 'ready', + })); + global.parent. /*OK*/postMessage(message, '*'); + + }, data.locale ? data.locale : dashToUnderline(window.navigator.language)); } diff --git a/3p/frame-metadata.js b/3p/frame-metadata.js new file mode 100644 index 000000000000..3d101052e9e9 --- /dev/null +++ b/3p/frame-metadata.js @@ -0,0 +1,167 @@ +/** + * 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 {dev} from '../src/log'; +import {dict} from '../src/utils/object.js'; +import {getMode} from '../src/mode'; +import {once} from '../src/utils/function.js'; +import {parseJson} from '../src/json'; +import {parseUrlDeprecated} from '../src/url'; + + +/** + * @typedef {{ + * ampcontextFilepath: ?string, + * ampcontextVersion: ?string, + * canary: ?boolean, + * canonicalUrl: ?string, + * clientId: ?string, + * container: ?string, + * domFingerprint: ?string, + * hidden: ?boolean, + * initialIntersection: ?IntersectionObserverEntry, + * initialLayoutRect: + * ?{left: number, top: number, width: number, height: number}, + * mode: ?../src/mode.ModeDef, + * pageViewId: ?string, + * referrer: ?string, + * sentinel: ?string, + * sourceUrl: ?string, + * startTime: ?number, + * tagName: ?string, + * }} + */ +export let ContextStateDef; + + +/** @const {!JsonObject} */ +const FALLBACK = dict({ + 'attributes': dict({ + '_context': dict(), + }), +}); + + +/** + * Gets metadata encoded in iframe name attribute. + * @return {!JsonObject} + */ +const allMetadata = once(() => { + const iframeName = window.name; + + try { + // TODO(bradfrizzell@): Change the data structure of the attributes + // to make it less terrible. + return parseJson(iframeName); + } catch (err) { + if (!getMode().test) { + dev().info( + 'INTEGRATION', 'Could not parse context from:', iframeName); + } + return FALLBACK; + } +}); + + +/** + * @return {{mode: !Object, experimentToggles: !Object}} + */ +export function getAmpConfig() { + const metadata = allMetadata(); + + return { + mode: metadata['attributes']['_context'].mode, + experimentToggles: metadata['attributes']['_context'].experimentToggles, + }; +} + + +/** + * @return {!JsonObject} + */ +const getAttributeDataImpl_ = once(() => { + const data = Object.assign(dict({}), allMetadata()['attributes']); + + // TODO(alanorozco): don't delete _context. refactor data object structure. + if ('_context' in data) { + delete data['_context']; + } + + return data; +}); + + +/** + * @return {!JsonObject} + */ +export function getAttributeData() { + // using indirect invocation to prevent no-export-side-effect issue + return getAttributeDataImpl_(); +} + + +/** + * @return {!Location} + */ +const getLocationImpl_ = once(() => { + const href = allMetadata()['attributes']['_context']['location']['href']; + return parseUrlDeprecated(href); +}); + + +/** + * @return {!Location} + */ +export function getLocation() { + // using indirect invocation to prevent no-export-side-effect issue + return getLocationImpl_(); +} + + +/** + * @return {!ContextStateDef} + */ +export function getContextState() { + const rawContext = allMetadata()['attributes']['_context']; + + return { + ampcontextFilepath: rawContext['ampcontextFilepath'], + ampcontextVersion: rawContext['ampcontextVersion'], + canary: rawContext['canary'], + canonicalUrl: rawContext['canonicalUrl'], + clientId: rawContext['clientId'], + container: rawContext['container'], + domFingerprint: rawContext['domFingerprint'], + hidden: rawContext['hidden'], + initialIntersection: rawContext['initialIntersection'], + initialLayoutRect: rawContext['initialLayoutRect'], + mode: rawContext['mode'], + pageViewId: rawContext['pageViewId'], + referrer: rawContext['referrer'], + sentinel: rawContext['sentinel'], + sourceUrl: rawContext['sourceUrl'], + startTime: rawContext['startTime'], + tagName: rawContext['tagName'], + }; +} + + +/** + * @return {string} + */ +export function getEmbedType() { + return getAttributeData()['type']; +} diff --git a/3p/github.js b/3p/github.js new file mode 100644 index 000000000000..5a47e255caef --- /dev/null +++ b/3p/github.js @@ -0,0 +1,72 @@ +/** + * 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 {user} from '../src/log'; +import {writeScript} from './3p'; + +/** + * Get the correct script for the gist. + * + * Use writeScript: Failed to execute 'write' on 'Document': It isn't possible + * to write into a document from an asynchronously-loaded external script unless + * it is explicitly opened. + * + * @param {!Window} global + * @param {string} scriptSource The source of the script, different for post and comment embeds. + * @param {function(*)} cb + */ +function getGistJs(global, scriptSource, cb) { + writeScript(global, scriptSource, function() { + cb(global.gist); + }); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function github(global, data) { + user().assert( + data.gistid, + 'The data-gistid attribute is required for %s', + data.element); + + let gistUrl = + 'https://gist.github.com/' + encodeURIComponent(data.gistid) + '.js'; + + if (data.file) { + gistUrl += '?file=' + encodeURIComponent(data.file); + } + + getGistJs(global, gistUrl, function() { + // Dimensions are given by the parent frame. + delete data.width; + delete data.height; + const gistContainer = global.document.querySelector('#c .gist'); + + // get all links in the embed + const gistLinks = global.document.querySelectorAll('.gist-meta a'); + for (let i = 0; i < gistLinks.length; i++) { + // have the links open in a new tab #8587 + gistLinks[i].target = '_BLANK'; + } + + context.updateDimensions( + gistContainer./*OK*/offsetWidth, + gistContainer./*OK*/offsetHeight + ); + }); +} diff --git a/3p/iframe-messaging-client.js b/3p/iframe-messaging-client.js new file mode 100644 index 000000000000..9fc838a24a5f --- /dev/null +++ b/3p/iframe-messaging-client.js @@ -0,0 +1,197 @@ +/** + * 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 { + CONSTANTS, + deserializeMessage, + listen, + serializeMessage, +} from '../src/3p-frame-messaging'; +import {Observable} from '../src/observable'; +import {dev} from '../src/log'; +import {dict, map} from '../src/utils/object'; +import {getData} from '../src/event-helper'; +import {getMode} from '../src/mode'; + +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; + /** @type {number} */ + this.nextMessageId_ = 1; + /** + * Map messageType keys to observables to be fired when messages of that + * type are received. + * @private {!Object} + */ + this.observableFor_ = map(); + this.setupEventListener_(); + } + + /** + * Retrieves data from host. + * + * @param {string} requestType + * @param {?Object} payload + * @param {function(*)} callback + */ + getData(requestType, payload, callback) { + const responseType = requestType + CONSTANTS.responseTypeSuffix; + const messageId = this.nextMessageId_++; + const unlisten = this.registerCallback(responseType, result => { + if (result[CONSTANTS.messageIdFieldName] === messageId) { + unlisten(); + callback(result[CONSTANTS.contentFieldName]); + } + }); + const data = dict(); + data[CONSTANTS.payloadFieldName] = payload; + data[CONSTANTS.messageIdFieldName] = messageId; + this.sendMessage(requestType, data); + } + + /** + * 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(JsonObject)} 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; + } + + /** + * Make a one time event listening request to the host window. + * Will unlisten after response is received + * + * @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. + */ + requestOnce(requestType, responseType, callback) { + const unlisten = this.registerCallback(responseType, event => { + unlisten(); + callback(event); + }); + 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(?JsonObject)} 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 + return this.getOrCreateObservableFor_(messageType).add(callback); + } + + /** + * Send a postMessage to Host Window + * @param {string} type The type of message to send. + * @param {JsonObject=} 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(getData(event)); + if (!message || message['sentinel'] != this.sentinel_) { + return; + } + + message['origin'] = event.origin; + + this.fireObservable_(message['type'], message); + }); + } + + /** + * @param {!Window} win + */ + setHostWindow(win) { + this.hostWindow_ = win; + } + + /** + * @param {string} sentinel + */ + setSentinel(sentinel) { + this.sentinel_ = sentinel; + } + + /** + * @param {string} messageType + * @return {!Observable} + */ + getOrCreateObservableFor_(messageType) { + if (!(messageType in this.observableFor_)) { + this.observableFor_[messageType] = new Observable(); + } + return this.observableFor_[messageType]; + } + + /** + * @param {string} messageType + * @param {Object} message + */ + fireObservable_(messageType, message) { + if (messageType in this.observableFor_) { + this.observableFor_[messageType].fire(message); + } + } +} diff --git a/3p/iframe-transport-client-lib.js b/3p/iframe-transport-client-lib.js new file mode 100644 index 000000000000..7a635e4a717b --- /dev/null +++ b/3p/iframe-transport-client-lib.js @@ -0,0 +1,36 @@ +/** + * 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. + */ + +// src/polyfills.js must be the first import. +import './polyfills'; // eslint-disable-line sort-imports-es6-autofix/sort-imports-es6 + +import {IframeTransportClient} from './iframe-transport-client.js'; +import {initLogConstructor, setReportError} from '../src/log'; + +initLogConstructor(); +// TODO(alanorozco): Refactor src/error.reportError so it does not contain big +// transitive dependencies and can be included here. +setReportError(() => {}); + +/** + * Instantiate IframeTransportClient, to provide the creative with + * all the required functionality. + */ +try { + new IframeTransportClient(window); +} catch (err) { + // do nothing with error +} diff --git a/3p/iframe-transport-client.js b/3p/iframe-transport-client.js new file mode 100644 index 000000000000..88cf5905ce84 --- /dev/null +++ b/3p/iframe-transport-client.js @@ -0,0 +1,166 @@ +/** + * 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 {IframeMessagingClient} from './iframe-messaging-client'; +import {MessageType} from '../src/3p-frame-messaging'; +import {dev, user} from '../src/log'; +import {tryParseJson} from '../src/json'; + +/** @private @const {string} */ +const TAG_ = 'iframe-transport-client'; + +/** + * Receives event messages bound for this cross-domain iframe, from all + * creatives. + */ +export class IframeTransportClient { + + /** @param {!Window} win */ + constructor(win) { + /** @private {!Window} */ + this.win_ = win; + + /** @private {!Object} */ + this.creativeIdToContext_ = {}; + + const parsedFrameName = tryParseJson(this.win_.name); + + /** @private {string} */ + this.vendor_ = dev().assertString(parsedFrameName['type'], + 'Parent frame must supply vendor name as type in ' + + this.win_.location.href); + // Note: amp-ad-exit will validate the vendor name before performing + // variable substitution, so if the vendor name is not a valid one from + // vendors.js, then its response messages will have no effect. + dev().assert(this.vendor_.length, 'Vendor name cannot be empty in ' + + this.win_.location.href); + + /** @protected {!IframeMessagingClient} */ + this.iframeMessagingClient_ = new IframeMessagingClient(win); + this.iframeMessagingClient_.setHostWindow(this.win_.parent); + this.iframeMessagingClient_.setSentinel(dev().assertString( + parsedFrameName['sentinel'], + 'Invalid/missing sentinel on iframe name attribute' + this.win_.name)); + this.iframeMessagingClient_.makeRequest( + MessageType.SEND_IFRAME_TRANSPORT_EVENTS, + MessageType.IFRAME_TRANSPORT_EVENTS, + eventData => { + const events = + /** + * @type + * {!Array<../src/3p-frame-messaging.IframeTransportEvent>} + */ + (eventData['events']); + dev().assert(events, + 'Received malformed events list in ' + this.win_.location.href); + dev().assert(events.length, + 'Received empty events list in ' + this.win_.location.href); + events.forEach(event => { + try { + dev().assert(event.creativeId, + 'Received malformed event in ' + this.win_.location.href); + this.contextFor_(event.creativeId).dispatch(event.message); + } catch (e) { + user().error(TAG_, + 'Exception in callback passed to onAnalyticsEvent', + e); + } + }); + }); + } + + /** + * Retrieves/creates a context object to pass events pertaining to a + * particular creative. + * @param {string} creativeId The ID of the creative + * @return {!IframeTransportContext} + * @private + */ + contextFor_(creativeId) { + return this.creativeIdToContext_[creativeId] || + (this.creativeIdToContext_[creativeId] = + new IframeTransportContext(this.win_, this.iframeMessagingClient_, + creativeId, this.vendor_)); + } + + /** + * Gets the IframeMessagingClient. + * @return {!IframeMessagingClient} + * @visibleForTesting + */ + getIframeMessagingClient() { + return this.iframeMessagingClient_; + } +} + +/** + * A context object to be passed along with event data. + */ +export class IframeTransportContext { + /** + * @param {!Window} win + * @param {!IframeMessagingClient} iframeMessagingClient + * @param {string} creativeId The ID of the creative that the event + * pertains to. + * @param {string} vendor The 3p vendor name + */ + constructor(win, iframeMessagingClient, creativeId, vendor) { + /** @private {!IframeMessagingClient} */ + this.iframeMessagingClient_ = iframeMessagingClient; + + /** @private @const {!Object} */ + this.baseMessage_ = {creativeId, vendor}; + + /** @private {?function(string)} */ + this.listener_ = null; + + user().assert(win['onNewContextInstance'] && + typeof win['onNewContextInstance'] == 'function', + 'Must implement onNewContextInstance in ' + win.location.href); + win['onNewContextInstance'](this); + } + + /** + * Registers a callback function to be called when an AMP analytics event + * is received. + * Note that calling this a second time will result in the first listener + * being removed - the events will not be sent to both callbacks. + * @param {function(string)} listener + */ + onAnalyticsEvent(listener) { + this.listener_ = listener; + } + + /** + * Receives an event from IframeTransportClient, and passes it along to + * the creative that this context represents. + * @param {string} event + */ + dispatch(event) { + this.listener_ && this.listener_(event); + } + + /** + * Sends a response message back to the creative. + * @param {!Object} data + */ + sendResponseToCreative(data) { + this.iframeMessagingClient_./*OK*/sendMessage( + MessageType.IFRAME_TRANSPORT_RESPONSE, + /** @type {!JsonObject} */ + (Object.assign({message: data}, this.baseMessage_))); + } +} diff --git a/3p/integration.js b/3p/integration.js index 70a6dd951861..a88d9aaa327a 100644 --- a/3p/integration.js +++ b/3p/integration.js @@ -22,44 +22,493 @@ * https://3p.ampproject.net/$version/f.js */ -import './polyfills'; +// src/polyfills.js must be the first import. +import './polyfills'; // eslint-disable-line sort-imports-es6-autofix/sort-imports-es6 + +import { + IntegrationAmpContext, +} from './ampcontext-integration'; +import {dict} from '../src/utils/object.js'; +import {endsWith} from '../src/string'; +import { + getAmpConfig, + getEmbedType, + getLocation, +} from './frame-metadata'; +import {getMode} from '../src/mode'; +import {getSourceUrl, isProxyOrigin, parseUrlDeprecated} from '../src/url'; +import { + initLogConstructor, + isUserErrorMessage, + setReportError, + user, +} from '../src/log'; +import {installEmbedStateListener, manageWin} from './environment'; +import {parseJson} from '../src/json'; +import { + register, + run, + setExperimentToggles, +} from './3p'; +import {startsWith} from '../src/string.js'; +import {urls} from '../src/config'; + +// Disable auto-sorting of imports from here on. +/* eslint-disable sort-imports-es6-autofix/sort-imports-es6 */ + +// 3P - please keep in alphabetic order +import {beopinion} from './beopinion'; +import {bodymovinanimation} from './bodymovinanimation'; +import {embedly} from './embedly'; +import {facebook} from './facebook'; +import {github} from './github'; +import {gltfViewer} from './3d-gltf/index'; +import {mathml} from './mathml'; +import {reddit} from './reddit'; +import {twitter} from './twitter'; +import {viqeoplayer} from './viqeoplayer'; +import {yotpo} from './yotpo'; + +import {_ping_} from '../ads/_ping_'; + +// 3P Ad Networks - please keep in alphabetic order +import {_24smi} from '../ads/24smi'; +import {a8} from '../ads/a8'; import {a9} from '../ads/a9'; +import {accesstrade} from '../ads/accesstrade'; +import {adagio} from '../ads/adagio'; +import {adblade, industrybrains} from '../ads/adblade'; +import {adbutler} from '../ads/adbutler'; +import {adform} from '../ads/adform'; +import {adfox} from '../ads/adfox'; +import {adgeneration} from '../ads/adgeneration'; +import {adhese} from '../ads/adhese'; +import {adincube} from '../ads/adincube'; +import {adition} from '../ads/adition'; +import {adman} from '../ads/adman'; +import {admanmedia} from '../ads/admanmedia'; +import {admixer} from '../ads/admixer'; +import {adocean} from '../ads/adocean'; +import {adpicker} from '../ads/adpicker'; +import {adplugg} from '../ads/adplugg'; import {adreactor} from '../ads/adreactor'; -import {adsense} from '../ads/adsense'; +import {adsense} from '../ads/google/adsense'; +import {adsnative} from '../ads/adsnative'; +import {adspeed} from '../ads/adspeed'; +import {adspirit} from '../ads/adspirit'; +import {adstir} from '../ads/adstir'; import {adtech} from '../ads/adtech'; -import {plista} from '../ads/plista'; -import {doubleclick} from '../ads/doubleclick'; +import {adthrive} from '../ads/adthrive'; +import {adunity} from '../ads/adunity'; +import {aduptech} from '../ads/aduptech'; +import {adventive} from '../ads/adventive'; +import {adverline} from '../ads/adverline'; +import {adverticum} from '../ads/adverticum'; +import {advertserve} from '../ads/advertserve'; +import {adyoulike} from '../ads/adyoulike'; +import {affiliateb} from '../ads/affiliateb'; +import {aja} from '../ads/aja'; +import {amoad} from '../ads/amoad'; +import {appnexus} from '../ads/appnexus'; +import {appvador} from '../ads/appvador'; +import {atomx} from '../ads/atomx'; +import {bidtellect} from '../ads/bidtellect'; +import {brainy} from '../ads/brainy'; +import {bringhub} from '../ads/bringhub'; +import {broadstreetads} from '../ads/broadstreetads'; +import {caajainfeed} from '../ads/caajainfeed'; +import {capirs} from '../ads/capirs'; +import {caprofitx} from '../ads/caprofitx'; +import {cedato} from '../ads/cedato'; +import {chargeads} from '../ads/chargeads'; +import {colombia} from '../ads/colombia'; +import {connatix} from '../ads/connatix'; +import {contentad} from '../ads/contentad'; +import {criteo} from '../ads/criteo'; +import {csa} from '../ads/google/csa'; +import {dable} from '../ads/dable'; +import {directadvert} from '../ads/directadvert'; +import {distroscale} from '../ads/distroscale'; import {dotandads} from '../ads/dotandads'; -import {facebook} from './facebook'; -import {manageWin} from './environment'; -import {nonSensitiveDataPostMessage, listenParent} from './messaging'; -import {twitter} from './twitter'; -import {register, run} from '../src/3p'; -import {parseUrl} from '../src/url'; -import {assert} from '../src/asserts'; +import {eadv} from '../ads/eadv'; +import {eas} from '../ads/eas'; +import {engageya} from '../ads/engageya'; +import {epeex} from '../ads/epeex'; +import {eplanning} from '../ads/eplanning'; +import {ezoic} from '../ads/ezoic'; +import {f1e} from '../ads/f1e'; +import {f1h} from '../ads/f1h'; +import {felmat} from '../ads/felmat'; +import {flite} from '../ads/flite'; +import {fluct} from '../ads/fluct'; +import {fusion} from '../ads/fusion'; +import {genieessp} from '../ads/genieessp'; +import {giraff} from '../ads/giraff'; +import {gmossp} from '../ads/gmossp'; +import {gumgum} from '../ads/gumgum'; +import {holder} from '../ads/holder'; +import {ibillboard} from '../ads/ibillboard'; +import {imaVideo} from '../ads/google/imaVideo'; +import {imedia} from '../ads/imedia'; +import {imobile} from '../ads/imobile'; +import {imonomy} from '../ads/imonomy'; +import {improvedigital} from '../ads/improvedigital'; +import {inmobi} from '../ads/inmobi'; +import {innity} from '../ads/innity'; +import {ix} from '../ads/ix'; +import {kargo} from '../ads/kargo'; +import {kiosked} from '../ads/kiosked'; +import {kixer} from '../ads/kixer'; +import {kuadio} from '../ads/kuadio'; +import {ligatus} from '../ads/ligatus'; +import {lockerdome} from '../ads/lockerdome'; +import {loka} from '../ads/loka'; +import {mads} from '../ads/mads'; +import {mantisDisplay, mantisRecommend} from '../ads/mantis'; +import {mediaimpact} from '../ads/mediaimpact'; +import {medianet} from '../ads/medianet'; +import {mediavine} from '../ads/mediavine'; +import {medyanet} from '../ads/medyanet'; +import {meg} from '../ads/meg'; +import {microad} from '../ads/microad'; +import {miximedia} from '../ads/miximedia'; +import {mixpo} from '../ads/mixpo'; +import {monetizer101} from '../ads/monetizer101'; +import {mytarget} from '../ads/mytarget'; +import {mywidget} from '../ads/mywidget'; +import {nativo} from '../ads/nativo'; +import {navegg} from '../ads/navegg'; +import {nend} from '../ads/nend'; +import {netletix} from '../ads/netletix'; +import {noddus} from '../ads/noddus'; +import {nokta} from '../ads/nokta'; +import {openadstream} from '../ads/openadstream'; +import {openx} from '../ads/openx'; +import {outbrain} from '../ads/outbrain'; +import {pixels} from '../ads/pixels'; +import {plista} from '../ads/plista'; +import {polymorphicads} from '../ads/polymorphicads'; +import {popin} from '../ads/popin'; +import {postquare} from '../ads/postquare'; +import {pressboard} from '../ads/pressboard'; +import {pubexchange} from '../ads/pubexchange'; +import {pubguru} from '../ads/pubguru'; +import {pubmatic} from '../ads/pubmatic'; +import {pubmine} from '../ads/pubmine'; +import {pulsepoint} from '../ads/pulsepoint'; +import {purch} from '../ads/purch'; +import {quoraad} from '../ads/quoraad'; +import {realclick} from '../ads/realclick'; +import {recomad} from '../ads/recomad'; +import {relap} from '../ads/relap'; +import {revcontent} from '../ads/revcontent'; +import {revjet} from '../ads/revjet'; +import {rfp} from '../ads/rfp'; +import {rubicon} from '../ads/rubicon'; +import {runative} from '../ads/runative'; +import {sekindo} from '../ads/sekindo'; +import {sharethrough} from '../ads/sharethrough'; +import {sklik} from '../ads/sklik'; +import {slimcutmedia} from '../ads/slimcutmedia'; +import {smartadserver} from '../ads/smartadserver'; +import {smartclip} from '../ads/smartclip'; +import {smi2} from '../ads/smi2'; +import {sogouad} from '../ads/sogouad'; +import {sortable} from '../ads/sortable'; +import {sovrn} from '../ads/sovrn'; +import {spotx} from '../ads/spotx'; +import {sunmedia} from '../ads/sunmedia'; +import {swoop} from '../ads/swoop'; import {taboola} from '../ads/taboola'; +import {teads} from '../ads/teads'; +import {triplelift} from '../ads/triplelift'; +import {trugaze} from '../ads/trugaze'; +import {uas} from '../ads/uas'; +import {unruly} from '../ads/unruly'; +import {uzou} from '../ads/uzou'; +import {valuecommerce} from '../ads/valuecommerce'; +import {videointelligence} from '../ads/videointelligence'; +import {videonow} from '../ads/videonow'; +import {viralize} from '../ads/viralize'; +import {vmfive} from '../ads/vmfive'; +import {webediads} from '../ads/webediads'; +import {weboramaDisplay} from '../ads/weborama'; +import {widespace} from '../ads/widespace'; +import {wisteria} from '../ads/wisteria'; +import {wpmedia} from '../ads/wpmedia'; +import {xlift} from '../ads/xlift'; +import {yahoo} from '../ads/yahoo'; +import {yahoojp} from '../ads/yahoojp'; +import {yandex} from '../ads/yandex'; +import {yengo} from '../ads/yengo'; +import {yieldbot} from '../ads/yieldbot'; +import {yieldmo} from '../ads/yieldmo'; +import {yieldone} from '../ads/yieldone'; +import {yieldpro} from '../ads/yieldpro'; +import {zedo} from '../ads/zedo'; +import {zen} from '../ads/zen'; +import {zergnet} from '../ads/zergnet'; +import {zucks} from '../ads/zucks'; + /** * Whether the embed type may be used with amp-embed tag. - * @const {!Object} + * @const {!Object} */ const AMP_EMBED_ALLOWED = { - taboola: true + aja: true, + _ping_: true, + '24smi': true, + bringhub: true, + dable: true, + engageya: true, + epeex: true, + kuadio: true, + 'mantis-recommend': true, + miximedia: true, + mywidget: true, + outbrain: true, + plista: true, + postquare: true, + pubexchange: true, + smartclip: true, + smi2: true, + taboola: true, + zen: true, + zergnet: true, + runative: true, }; +init(window); + + +if (getMode().test || getMode().localDev) { + register('_ping_', _ping_); +} + +// Keep the list in alphabetic order +register('24smi', _24smi); +register('3d-gltf', gltfViewer); +register('a8', a8); register('a9', a9); +register('accesstrade', accesstrade); +register('adagio', adagio); +register('adblade', adblade); +register('adbutler', adbutler); +register('adform', adform); +register('adfox', adfox); +register('adgeneration', adgeneration); +register('adhese', adhese); +register('adincube', adincube); +register('adition', adition); +register('adman', adman); +register('admanmedia', admanmedia); +register('admixer', admixer); +register('adocean', adocean); +register('adpicker', adpicker); +register('adplugg', adplugg); register('adreactor', adreactor); register('adsense', adsense); +register('adsnative', adsnative); +register('adspeed', adspeed); +register('adspirit', adspirit); +register('adstir', adstir); register('adtech', adtech); +register('adthrive', adthrive); +register('adunity', adunity); +register('aduptech', aduptech); +register('adventive', adventive); +register('adverline', adverline); +register('adverticum', adverticum); +register('advertserve', advertserve); +register('adyoulike', adyoulike); +register('affiliateb', affiliateb); +register('aja', aja); +register('amoad', amoad); +register('appnexus', appnexus); +register('appvador', appvador); +register('atomx', atomx); +register('beopinion', beopinion); +register('bidtellect', bidtellect); +register('bodymovinanimation', bodymovinanimation); +register('brainy', brainy); +register('bringhub', bringhub); +register('broadstreetads', broadstreetads); +register('caajainfeed', caajainfeed); +register('capirs', capirs); +register('caprofitx', caprofitx); +register('cedato', cedato); +register('chargeads', chargeads); +register('colombia', colombia); +register('connatix',connatix); +register('contentad', contentad); +register('criteo', criteo); +register('csa', csa); +register('dable', dable); +register('directadvert', directadvert); +register('distroscale', distroscale); +register('dotandads', dotandads); +register('eadv', eadv); +register('eas', eas); +register('embedly', embedly); +register('engageya', engageya); +register('epeex', epeex); +register('eplanning', eplanning); +register('ezoic', ezoic); +register('f1e', f1e); +register('f1h', f1h); +register('facebook', facebook); +register('felmat', felmat); +register('flite', flite); +register('fluct', fluct); +register('fusion', fusion); +register('genieessp', genieessp); +register('giraff', giraff); +register('github', github); +register('gmossp', gmossp); +register('gumgum', gumgum); +register('holder', holder); +register('ibillboard', ibillboard); +register('ima-video', imaVideo); +register('imedia', imedia); +register('imobile', imobile); +register('imonomy', imonomy); +register('improvedigital', improvedigital); +register('industrybrains', industrybrains); +register('inmobi', inmobi); +register('innity', innity); +register('ix', ix); +register('kargo', kargo); +register('kiosked', kiosked); +register('kixer', kixer); +register('kuadio', kuadio); +register('ligatus', ligatus); +register('lockerdome', lockerdome); +register('loka', loka); +register('mads', mads); +register('mantis-display', mantisDisplay); +register('mantis-recommend', mantisRecommend); +register('mathml', mathml); +register('mediaimpact', mediaimpact); +register('medianet', medianet); +register('mediavine', mediavine); +register('medyanet', medyanet); +register('meg', meg); +register('microad', microad); +register('miximedia', miximedia); +register('mixpo', mixpo); +register('monetizer101', monetizer101); +register('mytarget', mytarget); +register('mywidget', mywidget); +register('nativo', nativo); +register('navegg', navegg); +register('nend', nend); +register('netletix', netletix); +register('noddus', noddus); +register('nokta', nokta); +register('openadstream', openadstream); +register('openx', openx); +register('outbrain', outbrain); +register('pixels', pixels); register('plista', plista); -register('doubleclick', doubleclick); +register('polymorphicads', polymorphicads); +register('popin', popin); +register('postquare', postquare); +register('pressboard', pressboard); +register('pubexchange', pubexchange); +register('pubguru', pubguru); +register('pubmatic', pubmatic); +register('pubmine', pubmine); +register('pulsepoint', pulsepoint); +register('purch', purch); +register('quoraad', quoraad); +register('realclick', realclick); +register('reddit', reddit); +register('recomad', recomad); +register('relap', relap); +register('revcontent', revcontent); +register('revjet', revjet); +register('rfp', rfp); +register('rubicon', rubicon); +register('runative', runative); +register('sekindo', sekindo); +register('sharethrough', sharethrough); +register('sklik', sklik); +register('slimcutmedia', slimcutmedia); +register('smartadserver', smartadserver); +register('smartclip', smartclip); +register('smi2', smi2); +register('sogouad', sogouad); +register('sortable', sortable); +register('sovrn', sovrn); +register('spotx', spotx); +register('sunmedia', sunmedia); +register('swoop', swoop); register('taboola', taboola); -register('dotandads', dotandads); -register('_ping_', function(win, data) { - win.document.getElementById('c').textContent = data.ping; -}); +register('teads', teads); +register('triplelift', triplelift); +register('trugaze', trugaze); register('twitter', twitter); -register('facebook', facebook); +register('uas', uas); +register('unruly', unruly); +register('uzou', uzou); +register('valuecommerce', valuecommerce); +register('videointelligence', videointelligence); +register('videonow', videonow); +register('viqeoplayer', viqeoplayer); +register('viralize', viralize); +register('vmfive', vmfive); +register('webediads', webediads); +register('weborama-display', weboramaDisplay); +register('widespace', widespace); +register('wisteria', wisteria); +register('wpmedia', wpmedia); +register('xlift' , xlift); +register('yahoo', yahoo); +register('yahoojp', yahoojp); +register('yandex', yandex); +register('yengo', yengo); +register('yieldbot', yieldbot); +register('yieldmo', yieldmo); +register('yieldone', yieldone); +register('yieldpro', yieldpro); +register('yotpo', yotpo); +register('zedo', zedo); +register('zen', zen); +register('zergnet', zergnet); +register('zucks', zucks); + +// For backward compat, we always allow these types without the iframe +// opting in. +const defaultAllowedTypesInCustomFrame = [ + // Entries must be reasonably safe and not allow overriding the injected + // JS URL. + // Each custom iframe can override this through the second argument to + // draw3p. See amp-ad docs. + 'facebook', + 'twitter', + 'doubleclick', + 'yieldbot', + '_ping_', +]; + + +/** + * Initialize 3p frame. + * @param {!Window} win + */ +function init(win) { + const config = getAmpConfig(); + + // Overriding to short-circuit src/mode#getMode() + win.AMP_MODE = config.mode; + + initLogConstructor(); + setReportError(console.error.bind(console)); + + setExperimentToggles(config.experimentToggles); +} + /** * Visible for testing. @@ -73,47 +522,19 @@ register('facebook', facebook); * on this. */ export function draw3p(win, data, configCallback) { - const type = data.type; - assert(win.context.location.originValidated != null, - 'Origin should have been validated'); + const type = data['type']; - assert(isTagNameAllowed(data.type, win.context.tagName), - 'Embed type %s not allowed with tag %s', data.type, win.context.tagName); + user().assert(isTagNameAllowed(type, win.context.tagName), + 'Embed type %s not allowed with tag %s', type, win.context.tagName); if (configCallback) { configCallback(data, data => { - assert(data, 'Expected configuration to be passed as first argument'); + user().assert(data, + 'Expected configuration to be passed as first argument'); run(type, win, data); }); } else { run(type, win, data); } -}; - -/** - * Returns the "master frame" for all widgets of a given type. - * This frame should be used to e.g. fetch scripts that can - * be reused across frames. - * @param {string} type - * @return {!Window} - */ -function masterSelection(type) { - // The master has a special name. - const masterName = 'frame_' + type + '_master'; - let master; - try { - // Try to get the master from the parent. If it does not - // exist yet we get a security exception that we catch - // and ignore. - master = window.parent.frames[masterName]; - } catch (expected) { - /* ignore */ - } - if (!master) { - // No master yet, rename ourselves to be master. Yaihh. - window.name = masterName; - master = window; - } - return master; } /** @@ -123,156 +544,166 @@ function masterSelection(type) { * 1. The configuration parameters supplied to this embed. * 2. A callback that MUST be called for rendering to proceed. It takes * no arguments. Configuration is expected to be modified in-place. + * @param {!Array=} opt_allowed3pTypes List of advertising network + * types you expect. + * @param {!Array=} opt_allowedEmbeddingOrigins List of domain suffixes + * that are allowed to embed this frame. */ -window.draw3p = function(opt_configCallback) { - const data = parseFragment(location.hash); - window.context = data._context; - window.context.location = parseUrl(data._context.location.href); - validateParentOrigin(window, window.context.location); - window.context.master = masterSelection(data.type); - window.context.isMaster = window.context.master == window; - window.context.data = data; - window.context.noContentAvailable = triggerNoContentAvailable; - window.context.requestResize = triggerResizeRequest; - - if (data.type === 'facebook' || data.type === 'twitter') { - // Only make this available to selected embeds until the generic solution is - // available. - window.context.updateDimensions = triggerDimensions; - } +window.draw3p = function(opt_configCallback, opt_allowed3pTypes, + opt_allowedEmbeddingOrigins) { + try { + const location = getLocation(); - // This only actually works for ads. - window.context.observeIntersection = observeIntersection; - window.context.onResizeSuccess = onResizeSuccess; - window.context.onResizeDenied = onResizeDenied; - window.context.reportRenderedEntityIdentifier = - reportRenderedEntityIdentifier; - delete data._context; - // Run this only in canary and local dev for the time being. - if (location.pathname.indexOf('-canary') || - location.pathname.indexOf('current')) { + ensureFramed(window); + validateParentOrigin(window, location); + validateAllowedTypes(window, getEmbedType(), opt_allowed3pTypes); + if (opt_allowedEmbeddingOrigins) { + validateAllowedEmbeddingOrigins(window, opt_allowedEmbeddingOrigins); + } + window.context = new IntegrationAmpContext(window); manageWin(window); - } - draw3p(window, data, opt_configCallback); - nonSensitiveDataPostMessage('render-start'); -}; + installEmbedStateListener(); -function triggerNoContentAvailable() { - nonSensitiveDataPostMessage('no-content'); -} + // Ugly type annotation is due to Event.prototype.data being blacklisted + // and the compiler not being able to discern otherwise + // TODO(alanorozco): Do this more elegantly once old impl is cleaned up. + draw3p( + window, + (/** @type {!IntegrationAmpContext} */ (window.context)).data || {}, + opt_configCallback); -function triggerDimensions(width, height) { - nonSensitiveDataPostMessage('embed-size', { - width: width, - height: height, - }); -} + window.context.bootstrapLoaded(); + } catch (e) { + if (window.context && window.context.report3pError) { + // window.context has initiated yet + if (e.message && isUserErrorMessage(e.message)) { + // report user error to parent window + window.context.report3pError(e); + } + } -function triggerResizeRequest(width, height) { - nonSensitiveDataPostMessage('embed-size', { - width: width, - height: height - }); -} + const c = window.context || {mode: {test: false}}; + if (!c.mode.test) { + lightweightErrorReport(e, c.canary); + throw e; + } + } +}; /** - * Registers a callback for intersections of this iframe with the current - * viewport. - * The passed in array has entries that aim to be compatible with - * the IntersectionObserver spec callback. - * http://rawgit.com/slightlyoff/IntersectionObserver/master/index.html#callbackdef-intersectionobservercallback - * @param {function(!Array)} observerCallback - * @returns {!function} A function which removes the event listener that - * observes for intersection messages. + * Throws if the current frame's parent origin is not equal to + * the claimed origin. + * Only check for browsers that support ancestorOrigins + * @param {!Window} window + * @param {!Location} parentLocation + * @visibleForTesting */ -function observeIntersection(observerCallback) { - // Send request to received records. - nonSensitiveDataPostMessage('send-intersections'); - return listenParent('intersection', data => { - observerCallback(data.changes); - }); +export function validateParentOrigin(window, parentLocation) { + const ancestors = window.location.ancestorOrigins; + // Currently only webkit and blink based browsers support + // ancestorOrigins. In that case we proceed but mark the origin + // as non-validated. + if (!ancestors || !ancestors.length) { + return; + } + user().assert(ancestors[0] == parentLocation.origin, + 'Parent origin mismatch: %s, %s', + ancestors[0], parentLocation.origin); } /** - * Registers a callback for communicating when a resize request succeeds. - * @param {function(number)} observerCallback - * @returns {!function} A function which removes the event listener that - * observes for resize status messages. + * Check that this iframe intended this particular ad type to run. + * @param {!Window} window + * @param {string} type 3p type + * @param {!Array|undefined} allowedTypes May be undefined. + * @visibleForTesting */ -function onResizeSuccess(observerCallback) { - return listenParent('embed-resize-changed', data => { - observerCallback(data.requestedHeight); - }); -} +export function validateAllowedTypes(window, type, allowedTypes) { + const thirdPartyHost = parseUrlDeprecated(urls.thirdParty).hostname; -/** - * Registers a callback for communicating when a resize request is denied. - * @param {function(number)} observerCallback - * @returns {!function} A function which removes the event listener that - * observes for resize status messages. - */ -function onResizeDenied(observerCallback) { - return listenParent('embed-resize-denied', data => { - observerCallback(data.requestedHeight); - }); + // Everything allowed in default iframe. + if (window.location.hostname == thirdPartyHost) { + return; + } + if (urls.thirdPartyFrameRegex.test(window.location.hostname)) { + return; + } + if (window.location.hostname == 'ads.localhost') { + return; + } + if (defaultAllowedTypesInCustomFrame.indexOf(type) != -1) { + return; + } + user().assert(allowedTypes && allowedTypes.indexOf(type) != -1, + 'Non-whitelisted 3p type for custom iframe: %s', type); } /** - * Reports the "entity" that was rendered to this frame to the parent for - * reporting purposes. - * The entityId MUST NOT contain user data or personal identifiable - * information. One example for an acceptable data item would be the - * creative id of an ad, while the user's location would not be - * acceptable. - * @param {string} entityId See comment above for content. + * Check that parent host name was whitelisted. + * @param {!Window} window + * @param {!Array} allowedHostnames Suffixes of allowed host names. + * @visibleForTesting */ -function reportRenderedEntityIdentifier(entityId) { - assert(typeof entityId == 'string', - 'entityId should be a string %s', entityId); - nonSensitiveDataPostMessage('entity-id', { - id: entityId - }); +export function validateAllowedEmbeddingOrigins(window, allowedHostnames) { + if (!window.document.referrer) { + throw new Error('Referrer expected: ' + window.location.href); + } + const ancestors = window.location.ancestorOrigins; + // We prefer the unforgable ancestorOrigins, but referrer is better than + // nothing. + const ancestor = ancestors ? ancestors[0] : window.document.referrer; + let {hostname} = parseUrlDeprecated(ancestor); + 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. + hostname = parseUrlDeprecated(getSourceUrl(window.document.referrer)) + .hostname; + } + for (let i = 0; i < allowedHostnames.length; i++) { + // Either the hostname is exactly as whitelisted… + if (allowedHostnames[i] == hostname) { + return; + } + // Or it ends in .$hostname (aka is a sub domain of the whitelisted domain. + if (endsWith(hostname, '.' + allowedHostnames[i])) { + return; + } + } + throw new Error('Invalid embedding hostname: ' + hostname + ' not in ' + + allowedHostnames); } /** - * 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. + * Throws if this window is a top level window. * @param {!Window} window - * @param {!Location} parentLocation * @visibleForTesting */ -export function validateParentOrigin(window, parentLocation) { - const ancestors = window.location.ancestorOrigins; - // Currently only webkit and blink based browsers support - // ancestorOrigins. In that case we proceed but mark the origin - // as non-validated. - if (!ancestors || !ancestors.length) { - parentLocation.originValidated = false; - return; +export function ensureFramed(window) { + if (window == window.parent) { + throw new Error('Must be framed: ' + window.location.href); } - assert(ancestors[0] == parentLocation.origin, - 'Parent origin mismatch: %s, %s', - ancestors[0], parentLocation.origin); - parentLocation.originValidated = true; } /** * Expects the fragment to contain JSON. * @param {string} fragment Value of location.fragment - * @return {!JSONObject} + * @return {?JsonObject} * @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 (startsWith(json, '{%22')) { + json = decodeURIComponent(json); + } + return /** @type {!JsonObject} */ (json ? parseJson(json) : dict()); + } catch (err) { + return null; } - return json ? JSON.parse(json) : {}; } /** @@ -289,3 +720,22 @@ export function isTagNameAllowed(type, tagName) { } return true; } + +/** + * Reports an error to the server. Must only be called once per page. + * Not for use in event handlers. + * + * We don't use the default error in error.js handler because it has + * too many deps for this small JS binary. + * + * @param {!Error} e + * @param {boolean} isCanary + */ +function lightweightErrorReport(e, isCanary) { + new Image().src = urls.errorReporting + + '?3p=1&v=' + encodeURIComponent('$internalRuntimeVersion$') + + '&m=' + encodeURIComponent(e.message) + + '&ca=' + (isCanary ? 1 : 0) + + '&r=' + encodeURIComponent(document.referrer) + + '&s=' + encodeURIComponent(e.stack || ''); +} diff --git a/3p/mathml.js b/3p/mathml.js new file mode 100644 index 000000000000..a4b76e072671 --- /dev/null +++ b/3p/mathml.js @@ -0,0 +1,77 @@ +/** + * Copyright 2018 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 {setStyle} from '../src/style'; +import {user} from '../src/log'; +import {writeScript} from './3p'; + +/** + * Get the correct script for the mathml formula. + * + * Use writeScript: Failed to execute 'write' on 'Document': It isn't possible + * to write into a document from an asynchronously-loaded external script unless + * it is explicitly opened. + * + * @param {!Window} global + * @param {string} scriptSource The source of the script, different for post and comment embeds. + * @param {function(*)} cb + */ +function getMathmlJs(global, scriptSource, cb) { + writeScript(global, scriptSource, function() { + cb(global.MathJax); + }); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function mathml(global, data) { + user().assert( + data.formula, + 'The formula attribute is required for %s', + data.element); + + getMathmlJs( + global, + 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML', + mathjax => { + // Dimensions are given by the parent frame. + delete data.width; + delete data.height; + const div = document.createElement('div'); + div.setAttribute('id','mathmlformula'); + div.textContent = data.formula; + setStyle(div, 'visibility', 'hidden'); + global.document.body.appendChild(div); + mathjax.Hub.Config({ + showMathMenu: false, + }); + mathjax.Hub.Queue(function() { + const rendered = document.getElementById('MathJax-Element-1-Frame'); + // Remove built in mathjax margins. + const display = document.getElementsByClassName('MJXc-display'); + if (display[0]) { + display[0].setAttribute('style','margin-top:0;margin-bottom:0'); + context.requestResize( + rendered./*OK*/offsetWidth, + rendered./*OK*/offsetHeight + ); + setStyle(div, 'visibility', 'visible'); + } + }); + } + ); +} diff --git a/3p/messaging.js b/3p/messaging.js index 5995d365513e..fc72a450ec94 100644 --- a/3p/messaging.js +++ b/3p/messaging.js @@ -14,41 +14,98 @@ * limitations under the License. */ +import {getData} from '../src/event-helper'; +import {parseJson} from '../src/json'; + /** * Send messages to parent frame. These should not contain user data. * @param {string} type Type of messages - * @param {*=} opt_object Data for the message. + * @param {!JsonObject=} opt_object Data for the message. + * @deprecated Use iframe-messaging-client.js */ export function nonSensitiveDataPostMessage(type, opt_object) { if (window.parent == window) { - return; // Nothing to do. + return; // Nothing to do. } - const object = opt_object || {}; - object.type = type; - object.sentinel = 'amp-3p'; + const object = opt_object || /** @type {JsonObject} */ ({}); + object['type'] = type; + object['sentinel'] = window.context.sentinel; window.parent./*OK*/postMessage(object, window.context.location.origin); } +/** + * Message event listeners. + * @const {!Array<{type: string, cb: function(!JsonObject)}>} + */ +const listeners = []; + /** * Listen to message events from document frame. + * @param {!Window} win * @param {string} type Type of messages - * @param {function(*)} callback Called with data payload of message. + * @param {function(!JsonObject)} callback Called with data payload of message. * @return {function()} function to unlisten for messages. + * @deprecated Use iframe-messaging-client.js */ -export function listenParent(type, callback) { - const listener = function(event) { - if (event.source != window.parent || - event.origin != window.context.location.origin || - !event.data || - event.data.sentinel != 'amp-3p' || - event.data.type != type) { - return; - } - callback(event.data); +export function listenParent(win, type, callback) { + const listener = { + type, + cb: callback, }; - window.addEventListener('message', listener); + listeners.push(listener); + startListening(win); return function() { - window.removeEventListener('message', listener); + const index = listeners.indexOf(listener); + if (index > -1) { + listeners.splice(index, 1); + } }; } + +/** + * Listens for message events and dispatches to listeners registered + * via listenParent. + * @param {!Window} win + */ +function startListening(win) { + if (win.AMP_LISTENING) { + return; + } + win.AMP_LISTENING = true; + win.addEventListener('message', function(event) { + // Cheap operations first, so we don't parse JSON unless we have to. + const eventData = getData(event); + if (event.source != win.parent || + event.origin != win.context.location.origin || + typeof eventData != 'string' || + eventData.indexOf('amp-') != 0) { + return; + } + // Parse JSON only once per message. + const data = /** @type {!JsonObject} */ ( + parseJson(/**@type {string} */ (getData(event)).substr(4))); + if (win.context.sentinel && data['sentinel'] != win.context.sentinel) { + return; + } + // Don't let other message handlers interpret our events. + if (event.stopImmediatePropagation) { + event.stopImmediatePropagation(); + } + // Find all the listeners for this type. + for (let i = 0; i < listeners.length; i++) { + if (listeners[i].type != data['type']) { + continue; + } + const {cb} = listeners[i]; + try { + cb(data); + } catch (e) { + // Do not interrupt execution. + setTimeout(() => { + throw e; + }); + } + } + }); +} diff --git a/3p/nameframe.max.html b/3p/nameframe.max.html new file mode 100644 index 000000000000..a422732520aa --- /dev/null +++ b/3p/nameframe.max.html @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/3p/polyfills.js b/3p/polyfills.js index deec963d37f0..45b4c9de75a8 100644 --- a/3p/polyfills.js +++ b/3p/polyfills.js @@ -20,4 +20,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/recaptcha.js b/3p/recaptcha.js new file mode 100644 index 000000000000..e190e1960529 --- /dev/null +++ b/3p/recaptcha.js @@ -0,0 +1,217 @@ +/** + * Copyright 2018 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. + */ + +// src/polyfills.js must be the first import. +import './polyfills'; // eslint-disable-line sort-imports-es6-autofix/sort-imports-es6 + +import ampToolboxCacheUrl from + '../third_party/amp-toolbox-cache-url/dist/amp-toolbox-cache-url.esm'; + +import {IframeMessagingClient} from './iframe-messaging-client'; +import {dev, initLogConstructor, setReportError, user} from '../src/log'; +import {dict} from '../src/utils/object'; +import {isProxyOrigin, parseUrlDeprecated} from '../src/url'; +import {loadScript} from './3p'; +import {parseJson} from '../src/json'; + +/** + * @fileoverview + * Boostrap Iframe for communicating with the recaptcha API. + * + * Here are the following iframe messages using .postMessage() + * used between the iframe and recaptcha service: + * amp-recaptcha-ready / Service <- Iframe : + * Iframe and Recaptcha API are ready. + * amp-recaptcha-action / Service -> Iframe : + * Execute and action using supplied data + * amp-recaptcha-token / Service <- Iframe : + * Response to 'amp-recaptcha-action'. The token + * returned by the recaptcha API. + * amp-recaptcha-error / Service <- Iframe : + * Response to 'amp-recaptcha-action'. Error + * From attempting to get a token from action. + */ + +/** @const {string} */ +const TAG = 'RECAPTCHA'; + +/** @const {string} */ +const RECAPTCHA_API_URL = 'https://www.google.com/recaptcha/api.js?render='; + +/** {?IframeMessaginClient} **/ +let iframeMessagingClient = null; + +/** {?string} **/ +let sitekey = null; + +/** + * Initialize 3p frame. + */ +function init() { + initLogConstructor(); + setReportError(console.error.bind(console)); +} + +// Immediately call init +init(); + +/** + * Main function called by the recaptcha bootstrap frame + */ +export function initRecaptcha() { + + const win = window; + + /** + * Get the data from our name attribute + * sitekey {string} - reCAPTCHA sitekey used to identify the site + * sentinel {string} - string used to psuedo-confirm that we are + * receiving messages from the recaptcha frame + */ + let dataObject; + try { + dataObject = parseJson(win.name); + } catch (e) { + throw new Error(TAG + ' Could not parse the window name.'); + } + + // Get our sitekey from the iframe name attribute + dev().assert( + dataObject.sitekey, + 'The sitekey is required for the iframe' + ); + sitekey = dataObject.sitekey; + const recaptchaApiUrl = RECAPTCHA_API_URL + sitekey; + + loadScript(win, recaptchaApiUrl, function() { + const {grecaptcha} = win; + + grecaptcha.ready(function() { + initializeIframeMessagingClient(win, grecaptcha, dataObject); + iframeMessagingClient./*OK*/sendMessage('amp-recaptcha-ready'); + }); + }, function() { + dev().error(TAG + ' Failed to load recaptcha api script'); + }); +} +window.initRecaptcha = initRecaptcha; + +/** + * Function to initialize our IframeMessagingClient + * @param {Window} win + * @param {*} grecaptcha + * @param {!JsonObject} dataObject + */ +function initializeIframeMessagingClient(win, grecaptcha, dataObject) { + iframeMessagingClient = new IframeMessagingClient(win); + iframeMessagingClient.setSentinel(dataObject.sentinel); + iframeMessagingClient.registerCallback( + 'amp-recaptcha-action', + actionTypeHandler.bind(this, win, grecaptcha) + ); +} + +/** + * Function to handle executing actions using the grecaptcha Object, + * and sending the token back to the parent amp-recaptcha component + * + * Data Object will have the following fields + * action {string} - action to be dispatched with grecaptcha.execute + * id {number} - id given to us by a counter in the recaptcha service + * + * @param {Window} win + * @param {*} grecaptcha + * @param {Object} data + */ +function actionTypeHandler(win, grecaptcha, data) { + doesOriginDomainMatchIframeSrc(win, data).then(() => { + const executePromise = grecaptcha.execute(sitekey, { + action: data.action, + }); + + // .then() promise pollyfilled by recaptcha api script + executePromise./*OK*/then(function(token) { + iframeMessagingClient./*OK*/sendMessage('amp-recaptcha-token', dict({ + 'id': data.id, + 'token': token, + })); + }, function(err) { + user().error(TAG, '%s', err.message); + iframeMessagingClient./*OK*/sendMessage('amp-recaptcha-error', dict({ + 'id': data.id, + 'error': err.message, + })); + }); + }).catch(error => { + dev().error(TAG, '%s', error.message); + }); +} + +/** + * Function to verify our origin domain from the + * parent window. + * @param {Window} win + * @param {Object} data + * @return {!Promise} + */ +export function doesOriginDomainMatchIframeSrc(win, data) { + + if (!data.origin) { + return Promise.reject( + new Error('Could not retreive the origin domain') + ); + } + + // Using the deprecated parseUrl here, as we don't have access + // to the URL service in a 3p frame. + const originLocation = parseUrlDeprecated(data.origin); + + if (isProxyOrigin(data.origin)) { + const curlsSubdomain = originLocation.hostname.split('.')[0]; + return compareCurlsDomain(win, curlsSubdomain, data.origin); + } + + return ampToolboxCacheUrl.createCurlsSubdomain(data.origin) + .then(curlsSubdomain => { + return compareCurlsDomain(win, curlsSubdomain, data.origin); + }); +} + +/** + * Function to compare curls domains with the passed subdomain + * and window + * @param {Window} win + * @param {string} curlsSubdomain + * @param {string} origin + * @return {!Promise} + */ +function compareCurlsDomain(win, curlsSubdomain, origin) { + + // Get the hostname after the culrs subdomain of the current iframe window + const locationWithoutCurlsSubdomain = + win.location.hostname.split('.').slice(1).join('.'); + const curlsHostname = + curlsSubdomain + '.' + locationWithoutCurlsSubdomain; + + if (curlsHostname === win.location.hostname) { + return Promise.resolve(); + } + + return Promise.reject( + new Error('Origin domain does not match Iframe src: ' + origin) + ); +} + diff --git a/3p/recaptcha.max.html b/3p/recaptcha.max.html new file mode 100644 index 000000000000..9b23e05f37c5 --- /dev/null +++ b/3p/recaptcha.max.html @@ -0,0 +1,13 @@ + + + + + + + + +
+ +
+ + diff --git a/3p/reddit.js b/3p/reddit.js new file mode 100644 index 000000000000..2743e0aa1258 --- /dev/null +++ b/3p/reddit.js @@ -0,0 +1,91 @@ +/** + * 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); + + global.addEventListener('resize', event => { + global.context.updateDimensions( + event.target.outerWidth, + event.target.outerHeight); + }); +} diff --git a/3p/remote.html b/3p/remote.html index c7d206bdc0ee..c160c339f13f 100644 --- a/3p/remote.html +++ b/3p/remote.html @@ -13,7 +13,15 @@
- +
diff --git a/3p/twitter.js b/3p/twitter.js index 47a6f6d24c0c..16f0640243e5 100644 --- a/3p/twitter.js +++ b/3p/twitter.js @@ -16,7 +16,9 @@ // TODO(malteubl) Move somewhere else since this is not an ad. -import {loadScript} from '../src/3p'; +import {loadScript} from './3p'; +import {setStyles} from '../src/style'; +import {startsWith} from '../src/string'; /** * Produces the Twitter API object for the passed in callback. If the current @@ -27,21 +29,20 @@ import {loadScript} from '../src/3p'; * @param {function(!Object)} cb */ function getTwttr(global, cb) { - if (context.isMaster) { - global.twttrCbs = [cb]; - loadScript(global, 'https://platform.twitter.com/widgets.js', () => { - for (let i = 0; i < global.twttrCbs.length; i++) { - global.twttrCbs[i](global.twttr); - } - global.twttrCbs.push = function(cb) { - cb(global.twttr); - }; - }); - } else { - // Because we rely on this global existing it is important that - // this array is created synchronously after master selection. - context.master.twttrCbs.push(cb); - } + loadScript(global, 'https://platform.twitter.com/widgets.js', () => { + cb(global.twttr); + }); + // Temporarily disabled the code sharing between frames. + // The iframe throttling implemented in modern browsers can break with this, + // because things may execute in frames that are currently throttled, even + // though they are needed in the main frame. + // See https://github.com/ampproject/amphtml/issues/3220 + // + // computeInMasterFrame(global, 'twttrCbs', done => { + // loadScript(global, 'https://platform.twitter.com/widgets.js', () => { + // done(global.twttr); + // }); + //}, cb); } /** @@ -49,32 +50,106 @@ function getTwttr(global, cb) { * @param {!Object} data */ export function twitter(global, data) { - const tweet = document.createElement('div'); + const tweet = global.document.createElement('div'); tweet.id = 'tweet'; - tweet.style.width = '100%'; + 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. delete data.width; delete data.height; - twttr.widgets.createTweet(data.tweetid, tweet, data)./*OK*/then(() => { - const iframe = global.document.querySelector('#c iframe'); - // Unfortunately the tweet isn't really done at this time. - // We listen for resize to learn when things are - // really done. - iframe.contentWindow.addEventListener('resize', function() { - render(); - }, true); - render(); - }); + + if (data.tweetid) { + twttr.widgets.createTweet(cleanupTweetId_(data.tweetid), tweet, data) + ./*OK*/then(el => tweetCreated(twttr, el)); + } else if (data.momentid) { + twttr.widgets.createMoment(data.momentid, tweet, data) + ./*OK*/then(el => tweetCreated(twttr, el)); + } else if (data.timelineSourceType) { + // Extract properties starting with 'timeline'. + const timelineData = Object.keys(data) + .filter(prop => startsWith(prop, 'timeline')) + .reduce((newData, prop) => { + newData[stripPrefixCamelCase(prop, 'timeline')] = data[prop]; + return newData; + }, {}); + twttr.widgets.createTimeline(timelineData, tweet, data) + ./*OK*/then(el => tweetCreated(twttr, el)); + } }); + /** + * Handles a tweet or moment being created, resizing as necessary. + * @param {!Object} twttr + * @param {?Element} el + */ + function tweetCreated(twttr, el) { + if (!el) { + global.context.noContentAvailable(); + return; + } + + resize(el); + twttr.events.bind('resize', event => { + // To be safe, make sure the resize event was triggered for the widget we + // created below. + if (el === event.target) { + resize(el); + } + }); + } - function render() { - const iframe = global.document.querySelector('#c iframe'); - const body = iframe.contentWindow.document.body; + /** + * @param {!Element} container + */ + function resize(container) { + const height = container./*OK*/offsetHeight; + // 0 height is always wrong and we should get another resize request + // later. + if (height == 0) { + return; + } context.updateDimensions( - body./*OK*/offsetWidth, - body./*OK*/offsetHeight + /* margins */ 20); + container./*OK*/offsetWidth, + height + /* margins */ 20); } + + /** + * @param {string} input + * @param {string} prefix + */ + function stripPrefixCamelCase(input, prefix) { + const stripped = input.replace(new RegExp('^' + prefix), ''); + return stripped.charAt(0).toLowerCase() + stripped.substr(1); + } +} + +/** + * @param {string} tweetid + * @visibleForTesting + */ +export function cleanupTweetId_(tweetid) { + // 1) + // Handle malformed ids such as + // https://twitter.com/abc123/status/585110598171631616 + tweetid = tweetid.toLowerCase(); + let match = tweetid.match(/https:\/\/twitter.com\/[^\/]+\/status\/(\d+)/); + if (match) { + return match[1]; + } + + // 2) + // Handle malformed ids such as + // 585110598171631616?ref_src + match = tweetid.match(/^(\d+)\?ref.*/); + if (match) { + return match[1]; + } + + return tweetid; } diff --git a/3p/viqeoplayer.js b/3p/viqeoplayer.js new file mode 100644 index 000000000000..d2bba7f9d09b --- /dev/null +++ b/3p/viqeoplayer.js @@ -0,0 +1,196 @@ +/** + * Copyright 2018 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 {assertAbsoluteHttpOrHttpsUrl, tryDecodeUriComponent} from '../src/url'; +import {getData} from '../src/event-helper'; +import {loadScript} from './3p'; +import {parseJson} from '../src/json'; +import {setStyles} from '../src/style'; + +/** + * @param {Window} global + * @param {boolean} autoplay + * @param {Object} VIQEO + * @param {function(Object)} VIQEO.getPlayers - returns viqeo player + * @param {function(function(Object), Object)} VIQEO.subscribeTracking - subscriber + * @private + */ +function viqeoPlayerInitLoaded(global, autoplay, VIQEO) { + let viqeoPlayerInstance; + global.addEventListener('message', parseMessage, false); + + subscribe('added', 'ready', () => { + const players = VIQEO['getPlayers']({container: 'stdPlayer'}); + viqeoPlayerInstance = players && players[0]; + }); + subscribe('started', 'started', () => { + // if autoplay is off then player will be paused as it just has been started + !autoplay && viqeoPlayerInstance && viqeoPlayerInstance.pause(); + }); + subscribe('paused', 'pause'); + subscribe('played', 'play'); + subscribe('replayed', 'play'); + subscribeTracking({ + Mute: 'mute', + Unmute: 'unmute', + }); + + /** + * Subscribe on viqeo's events + * @param {string} playerEventName + * @param {string} targetEventName + * @param {function()|undefined|null} extraHandler + * @private + */ + function subscribe(playerEventName, targetEventName, extraHandler = null) { + VIQEO['subscribeTracking']( + () => { + sendMessage(targetEventName); + if (extraHandler) { + extraHandler(); + } + }, + {eventName: `Player:${playerEventName}`, container: 'stdPlayer'} + ); + } + + /** + * Subscribe viqeo's tracking + * @param {Object.} eventsDescription + * @private + */ + function subscribeTracking(eventsDescription) { + VIQEO['subscribeTracking'](params => { + const name = params && params['trackingParams'] && + params['trackingParams'].name; + const targetEventName = eventsDescription[name]; + sendMessage(targetEventName); + }, 'Player:userAction'); + } + + const sendMessage = (eventName, value = null) => { + const {parent} = global; + const message = /** @type {JsonObject} */({ + source: 'ViqeoPlayer', + action: eventName, + value, + }); + parent./*OK*/postMessage(message, '*'); + }; + + /** + * Parse events data for viqeo + * @param {!Event|{data: !JsonObject}} event + */ + function parseMessage(event) { + const eventData = getData(event); + const action = eventData['action']; + if (!action) { + return; + } + if (action === 'play') { + viqeoPlayerInstance.play(); + } else if (action === 'pause') { + viqeoPlayerInstance.pause(); + } else if (action === 'stop') { + viqeoPlayerInstance.stop(); + } else if (action === 'mute') { + viqeoPlayerInstance.setVolume(0); + } else if (action === 'unmute') { + viqeoPlayerInstance.setVolume(1); + } + } +} + +/** + * Prepare and return viqeo instance + * @param {!Window} global + */ +export function viqeoplayer(global) { + const data = getData(global.context); + let autoplay = false; + try { + autoplay = parseJson(global.name)['attributes']._context['autoplay']; + } catch (e) { + // do nothing + } + const videoId = data['videoid']; + const profileId = data['profileid']; + + const markTagsAdvancedParams = data['tag-settings']; + + const kindIsProd = data['data-kind'] !== 'stage'; + + let scriptPlayerInit = data['script-url']; + scriptPlayerInit = + (scriptPlayerInit + && tryDecodeUriComponent(scriptPlayerInit) + ) + || + (kindIsProd + ? 'https://cdn.viqeo.tv/js/vq_player_init.js?amp=true' + : 'https://static.viqeo.tv/js/vq_player_init.js?branch=dev1&=true' + ); + // embed preview url + let previewUrl = data['player-url']; + previewUrl = + (previewUrl + && previewUrl.length && decodeURI(previewUrl) + ) + || (kindIsProd ? 'https://cdn.viqeo.tv/embed' : 'https://stage.embed.viqeo.tv'); + + // Create preview iframe source path + previewUrl = assertAbsoluteHttpOrHttpsUrl( + `${previewUrl}/?vid=${videoId}&=true`); + + const doc = global.document; + const mark = doc.createElement('div'); + + const markTagsStyle = Object.assign({ + position: 'relative', + width: '100%', + height: '0', + paddingBottom: '100%', + }, markTagsAdvancedParams); + + setStyles(mark, markTagsStyle); + + mark.setAttribute('data-vnd', videoId); + mark.setAttribute('data-profile', profileId); + mark.classList.add('viqeo-embed'); + + const iframe = doc.createElement('iframe'); + + iframe.setAttribute('width', '100%'); + iframe.setAttribute('height', '100%'); + iframe.setAttribute('style', 'position: absolute'); + iframe.setAttribute('frameBorder', '0'); + iframe.setAttribute('allowFullScreen', ''); + iframe.src = previewUrl; + + mark.appendChild(iframe); + + doc.getElementById('c').appendChild(mark); + + loadScript(global, scriptPlayerInit, () => { + if (!global['VIQEO']) { + global['onViqeoLoad'] = + viqeoPlayerInitLoaded.bind(null, global, autoplay); + } else { + viqeoPlayerInitLoaded(global, autoplay, global['VIQEO']); + } + }); +} diff --git a/3p/yotpo.js b/3p/yotpo.js new file mode 100644 index 000000000000..a6fe7b7927fc --- /dev/null +++ b/3p/yotpo.js @@ -0,0 +1,318 @@ +/** + * Copyright 2018 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. + * @param {function(!Object, string)} cb + */ +function getContainerScript(global, scriptSource, cb) { + loadScript(global, scriptSource, () => { + global.Yotpo = global.Yotpo || {}; + delete global.Yotpo.widgets['testimonials']; + const yotpoWidget = + (typeof global.yotpo === 'undefined') ? undefined : global.yotpo; + yotpoWidget.on('CssReady', function() { + cb(yotpoWidget, 'cssLoaded'); + }); + yotpoWidget.on('BatchReady', function() { + cb(yotpoWidget, 'batchLoaded'); + }); + }); +} + +/** + * Create DOM element for the Yotpo bottom line plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getBottomLineContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'preview-only-full-height'; + + const childDiv = global.document.createElement('div'); + childDiv.className = 'preview-only-flex-center preview-only-full-height'; + container.appendChild(childDiv); + + const bottomLine = global.document.createElement('div'); + bottomLine.className = 'yotpo bottomLine'; + bottomLine.setAttribute('data-product-id', data.productId); + + childDiv.appendChild(bottomLine); + + return container; +} + +/** + * Create DOM element for the Yotpo main widget plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getMainWidgetContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-main-widget'; + container.setAttribute('data-product-id', data.productId); + container.setAttribute('data-name', data.name); + container.setAttribute('data-url', data.url); + container.setAttribute('data-image-url', data.imageUrl); + container.setAttribute('data-description', data.description); + container.setAttribute('data-yotpo-element-id', data.yotpoElementId); + return container; +} + +/** + * Create DOM element for the Yotpo reviews carousel plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getReviewsCarouselContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-reviews-carousel yotpo-size-7'; + container.setAttribute('data-background-color', data.backgroudColor); + container.setAttribute('data-mode', data.mode); + container.setAttribute('data-review-ids', data.reviewIds); + container.setAttribute('data-show-bottomline', data.showBottomLine); + container.setAttribute('data-autoplay-enabled', data.autoplayEnabled); + container.setAttribute('data-autoplay-speed', data.autoplaySpeed); + container.setAttribute('data-show-navigation', data.showNavigation); + container.setAttribute('data-yotpo-element-id', data.yotpoElementId); + container.setAttribute('style', 'max-width: 1250px;'); + return container; +} + +/** + * Create DOM element for the Yotpo UGC Gallery plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getUgcGalleryContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-pictures-gallery'; + container.setAttribute('data-layout', data.layout); + container.setAttribute('data-layout-scroll', data.layoutScroll); + container.setAttribute('data-spacing', data.spacing); + container.setAttribute('data-source', data.source); + container.setAttribute('data-title', data.title); + container.setAttribute('data-hover-color', data.hoverColor); + container.setAttribute('data-hover-opacity', data.hoverOpacity); + container.setAttribute('data-hover-icon', data.hoverIcon); + container.setAttribute('data-cta-text', data.ctaText); + container.setAttribute('data-cta-color', data.ctaColor); + return container; +} + +/** + * Create DOM element for the Yotpo Badges plugin: + * @param {!Window} global + * @return {!Element} div + */ +function getBadgetsContainer(global) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-badge badge-init'; + container.setAttribute('id', 'y-badges'); + return container; +} + +/** + * Create DOM element for the Yotpo Reviews Tab plugin: + * @param {!Window} global + * @return {!Element} div + */ +function getReviewsTabContainer(global) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-modal'; + return container; +} + +/** + * Create DOM element for the Yotpo Product Gallery plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getProductGalleryContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-pictures-gallery yotpo-product-gallery ' + + 'yotpo-size-6'; + container.setAttribute('data-product-id', data.productId); + container.setAttribute('data-demo', data.demo); + container.setAttribute('data-layout-rows', data.layoutRows); + container.setAttribute('data-layout-scroll', data.layoutScroll); + container.setAttribute('data-spacing', data.spacing); + container.setAttribute('data-source', data.source); + container.setAttribute('data-title', data.title); + container.setAttribute('data-hover-color', data.hoverColor); + container.setAttribute('data-hover-opacity', data.hoverOpacity); + container.setAttribute('data-hover-icon', data.hoverIcon); + container.setAttribute('data-upload-button', data.uploadButton); + container.setAttribute('data-preview', data.preview); + container.setAttribute('data-yotpo-element-id', data.yotpoElementId); + return container; +} + + +/** + * Create DOM element for the Yotpo Visual UGC Gallery plugin: + * @param {!Window} global + * @return {!Element} div + */ +function getVisualUgcGalleryContainer(global) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-preview-pictures-gallery'; + + const childDiv = global.document.createElement('div'); + childDiv.className = 'yotpo yotpo-pictures-gallery'; + container.appendChild(childDiv); + + return container; +} + +/** + * Create DOM element for the Yotpo Embedded Widget plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getEmbeddedWidgetContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'preview-only-table'; + + const cellCentered = global.document.createElement('div'); + cellCentered.className = 'preview-only-table-cell-centered'; + container.appendChild(cellCentered); + + const embeddedWidget = global.document.createElement('div'); + embeddedWidget.className = 'yotpo embedded-widget'; + cellCentered.appendChild(embeddedWidget); + + embeddedWidget.setAttribute('data-product-id', data.productId); + embeddedWidget.setAttribute('data-demo', data.demo); + embeddedWidget.setAttribute('data-layout', data.layout); + embeddedWidget.setAttribute('data-width', data.width); + embeddedWidget.setAttribute('data-reviews', data.reviews); + embeddedWidget.setAttribute('data-header-text', data.headerText); + embeddedWidget.setAttribute('data-header-background-color', + data.headerBackgroundColor); + embeddedWidget.setAttribute('data-body-background-color', + data.bodyBackgroundColor); + embeddedWidget.setAttribute('data-font-size', data.fontSize); + embeddedWidget.setAttribute('data-font-color', data.fontColor); + embeddedWidget.setAttribute('data-yotpo-element-id', data.yotpoElementId); + + return container; +} + +/** + * Create DOM element for the Yotpo Embedded Widget plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getPhotosCarouselContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-preview-slider'; + + const photosCarousel = global.document.createElement('div'); + photosCarousel.className = 'yotpo yotpo-slider'; + container.appendChild(photosCarousel); + + photosCarousel.setAttribute('data-product-id', data.productId); + photosCarousel.setAttribute('data-demo', data.demo); + + return container; +} + +/** + * Create DOM element for the Yotpo Promoted Products plugin: + * @param {!Window} global + * @param {!Object} data The element data + * @return {!Element} div + */ +function getPromotedProductsContainer(global, data) { + const container = global.document.createElement('div'); + container.className = 'yotpo yotpo-main-widget yotpo-promoted-product ' + + 'yotpo-medium promoted-products-box'; + + container.setAttribute('id', 'widget-div-id'); + container.setAttribute('data-demo', data.demo); + container.setAttribute('data-product-id', data.productId); + + return container; +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function yotpo(global, data) { + const {widgetType} = data; + let container; + if (widgetType == 'BottomLine') { + container = getBottomLineContainer(global, data); + } else if (widgetType == 'ReviewsCarousel') { + container = getReviewsCarouselContainer(global, data); + } else if (widgetType == 'PicturesGallery') { + container = getUgcGalleryContainer(global, data); + } else if (widgetType == 'Badge') { + container = getBadgetsContainer(global); + } else if (widgetType == 'ReviewsTab') { + container = getReviewsTabContainer(global); + } else if (widgetType == 'ProductGallery') { + container = getProductGalleryContainer(global, data); + } else if (widgetType == 'VisualUgcGallery') { + container = getVisualUgcGalleryContainer(global); + } else if (widgetType == 'EmbeddedWidget') { + container = getEmbeddedWidgetContainer(global, data); + } else if (widgetType == 'PhotosCarousel') { + container = getPhotosCarouselContainer(global, data); + } else if (widgetType == 'PromotedProducts') { + container = getPromotedProductsContainer(global, data); + } else { + container = getMainWidgetContainer(global, data); + } + + global.document.getElementById('c').appendChild(container); + + + let cssLoaded = false; + let batchLoaded = false; + const scriptSource = 'https://staticw2.yotpo.com/' + data.appKey + '/widget.js'; + getContainerScript(global, scriptSource, (yotpoWidget, eventType) => { + if (eventType === 'cssLoaded') { + cssLoaded = true; + } + if (eventType === 'batchLoaded') { + batchLoaded = true; + } + + if (batchLoaded && cssLoaded) { + setTimeout(() => { + if (yotpoWidget.widgets[0]) { + context.updateDimensions( + yotpoWidget.widgets[0].element./*OK*/offsetWidth, + yotpoWidget.widgets[0].element./*OK*/offsetHeight); + } + }, 100); + } + }); +} diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index ccc1dd378b9f..000000000000 --- a/AUTHORS +++ /dev/null @@ -1,6 +0,0 @@ -# This is a list of contributors to AMP HTML. - -# Names should be added to this file like so: -# Name or Organization - -Google Inc. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 606a195bad1e..8318bba7a4ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,68 +1,182 @@ - +> :grey_question: If you have questions about using AMP or are encountering problems using AMP on your site please visit our [support page](SUPPORT.md) for help. -## Contributing to AMP HTML +#### Contents -### Filing Issues +- [Reporting issues with AMP](#reporting-issues-with-amp) + * [Bugs](#bugs) + * [Suggestions and feature requests](#suggestions-and-feature-requests) +- [Contributing code](#contributing-code) + * [Tips for new open source contributors](#tips-for-new-open-source-contributors) + * [Contributing extended components](#contributing-extended-components) + * [How to contribute code](#how-to-contribute-code) + + [Contributing a new feature (concept & design phase)](#phase-concept-design) + + [Contributing code for a feature (coding phase)](#phase-coding) +- [Contributor License Agreement](#contributor-license-agreement) +- [Ongoing participation](#ongoing-participation) + * [Discussion channels](#discussion-channels) + * [Status updates](#status-updates) + * [Weekly design reviews](#weekly-design-reviews) + * [Working groups](#working-groups) + * [See Also](#see-also) +## Reporting issues with AMP -**Suggestions** +### Bugs -The AMP HTML project is meant to evolve with feedback - the project and its users greatly appreciate any thoughts on ways to improve the design or features. Please use the `enhancement` tag to specifically denote issues that are suggestions - this helps us triage and respond appropriately. +If you find a bug in AMP, please [file a GitHub issue](https://github.com/ampproject/amphtml/issues/new). Members of the community are regularly monitoring issues and will try to fix open bugs quickly according to our [prioritization guidelines](./contributing/issue-priorities.md). -**Bugs** +The best bug reports provide a detailed description of the issue (including screenshots if possible), step-by-step instructions for predictably reproducing the issue, and possibly even a working example that demonstrates the issue. -As with all pieces of software, you may end up running into bugs. Please submit bugs as regular issues on GitHub - AMP HTML developers are regularly monitoring issues and will try to fix open bugs quickly. +### Suggestions and feature requests -The best bug reports include a detailed way to predictably reproduce the issue, and possibly even a working example that demonstrates the issue. +The AMP Project is meant to evolve with feedback. The project and its users appreciate your thoughts on ways to improve the design or features. -### Contributing Code +To make a suggestion or feature request [file a GitHub issue](https://github.com/ampproject/amphtml/issues/new) describing your idea. -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. +If you are suggesting a feature that you are intending to implement, please see the [Contributing a new feature](#phase-concept-design) section below for next steps. -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. +## Contributing code -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. +The AMP Project accepts and greatly appreciates code contributions! -**Google Individual Contributor License** +### Tips for new open source contributors -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. +If you are new to contributing to an open source project, Git/GitHub, etc. welcome! We are glad you're interested in contributing to the AMP Project and we want to help make your open source experience a success. -### Contributing Features +The [Getting Started End-to-End Guide](./contributing/getting-started-e2e.md) provides step-by-step instructions for everything from creating a GitHub account to getting your code reviewed and merged. Even if you've never contributed to an open source project before you'll soon be building AMP, making improvements and seeing your code live across the web. -All pull requests for new features must go through the following process: -* Intent-to-implement GitHub issue started for discussion -* 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. -* 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. +The community has created a list of [Good First Issues](https://github.com/ampproject/amphtml/labels/good%20first%20issue) specifically for new contributors to the project. Feel free to find one of the [unclaimed Good First Issues](https://github.com/ampproject/amphtml/issues?utf8=%E2%9C%93&q=is%3Aopen%20label%3A%22good%20first%20issue%22%20-label%3A%22GFI%20Claimed!%22) that interests you, claim it by adding a comment to it and jump in! -#### Contributing Extended Components +If you're interested in helping out but can't find a Good First Issue that matches your skills/interests, [sign up for our Slack](https://docs.google.com/forms/d/e/1FAIpQLSd83J2IZA6cdR6jPwABGsJE8YL4pkypAbKMGgUZZriU7Qu6Tg/viewform?fbzx=4406980310789882877) and then reach out in the [#welcome-contributors channel](https://amphtml.slack.com/messages/welcome-contributors/) or send a Direct Message to [mrjoro](https://amphtml.slack.com/team/mrjoro/). -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. +If you run into any problems we have plenty of people who are willing to help; see the [How to get help](./contributing/getting-started-e2e.md#how-to-get-help) section of the Getting Started guide. -Because Extended Components may have significant impact on AMP HTML performance, security, and usage, Extended Component contributions will be very carefully analyzed and scrutinized. +> :bookmark: You might have noticed that we use GitHub emojis in our pull requests. To learn what these emojis mean and which ones to use, see :sparkles:[our list of emojis](./.github/PULL_REQUEST_TEMPLATE.md#emojis-for-categorizing-pull-requests) :sparkles:. -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. +### Contributing extended components -For further detail on integrating third party services, fonts, embeds, etc. see our [3p contribution guidelines](https://github.com/ampproject/amphtml/tree/master/3p). +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. -### See Also +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. + +We have a few additional resources that provide an introduction to contributing extended components: +* ["Building an AMP Extension"](./contributing/building-an-amp-extension.md) has a detailed description of how to build an AMP component. +* ["Creating your first AMP Component" codelab](https://codelabs.developers.google.com/codelabs/creating-your-first-amp-component/#0) provides a quick overview of the steps you need to go through to create a component with examples you can modify for your component. +* The ["Building a new AMP component" talk at AMP Conf 2017](https://youtu.be/FJEhQFNKeaQ?list=PLXTOW_XMsIDTDXYO-NAi2OpEH0zyguvqX) provides an introduction to contributing AMP components. + +For further detail on integrating third-party services (e.g., fonts, embeds, etc.), see our [3p contribution guidelines](https://github.com/ampproject/amphtml/tree/master/3p). + +### How to contribute code + +Contributing to AMP involves two phases: + +1. [Concept & Design](#phase-concept-design) +2. [Coding & Implementation](#phase-coding) + +#### Contributing a new feature (concept & design phase) + +1. Familiarize yourself with our [Design Principles](./contributing/DESIGN_PRINCIPLES.md). +1. [Create an Intent to Implement (I2I) GH issue](https://github.com/ampproject/amphtml/issues/new) to discuss your new feature. In your I2I, include the following: + - A high-level description of the feature. + - A description of the API you plan to create. + - If you are integrating a third-party service, provide a link to the third-party's site and product. + - Details on any data collection or tracking that the feature might perform. + - A prototype or mockup (for example, an image, a GIF, or a link to a demo). +3. Before starting on the code, get approval for your feature from an [OWNER](https://github.com/ampproject/amphtml/search?utf8=%E2%9C%93&q=filename%3AOWNERS.yaml&type=Code) of your feature's area and a [core committer](GOVERNANCE.md#core-committers). In most cases the people who can give this approval and are most familiar with your feature's area will get involved proactively or someone else in the community will add them. As part of the design review, you might be required to discuss your design in the [weekly design review](#weekly-design-reviews) meeting. +5. [Start coding](#phase-coding). + +#### Contributing code for a feature (coding & implementation phase) + +1. If you haven't already, consider [joining the AMP Project](https://goo.gl/forms/T65peVtfQfEoDWeD3). This is entirely *optional* but by joining the project you become part of the AMP contributor community, and it allows for the ability to assign issues to you in GitHub. +1. [Perform the one-time setup](./contributing/getting-started-quick.md#one-time-setup): Set up your GitHub account, install Node, Yarn, Gulp CLI, fork repo, track repo, etc. +1. [Create a working branch](./contributing/getting-started-e2e.md#create-a-git-branch). +1. [Build AMP](./contributing/getting-started-e2e.md#building-amp-and-starting-a-local-server). +1. Write code and consult these resources for guidance and guidelines: + - **Design**: [AMP Design Principles](./contributing/DESIGN_PRINCIPLES.md) + - **JavaScript**: [Google JavaScript Code Style Guide](https://google.github.io/styleguide/jsguide.html) + - **CSS**: [Writing CSS For AMP Runtime](./contributing/writing-css.md) + - **Creating new components**: + - [Instructions and Guidelines for building an AMP component](./contributing/building-an-amp-extension.md) + - Learn to create your first component in this [codelab](https://codelabs.developers.google.com/codelabs/creating-your-first-amp-component/#0) + - Watch this [YouTube video](https://youtu.be/FJEhQFNKeaQ?list=PLXTOW_XMsIDTDXYO-NAi2OpEH0zyguvqX) to learn about "Building a new AMP component" + - **Integrating third-party software, embeds, services**: [Guidelines](./3p/README.md) +1. [Commit your files](./contributing/getting-started-e2e.md#edit-files-and-commit-them). +1. [Test your changes](./contributing/getting-started-e2e.md#testing-your-changes): A key feature of AMP is performance. All changes will be analyzed for any performance impact; we particularly appreciate changes that make things even faster. Please include any measured performance impact with substantial pull requests. +1. [Put your new component behind an experiment flag](./contributing/building-an-amp-extension.md#experiments). +1. [Pull the latest changes from the AMPHTML repo](./contributing/getting-started-e2e.md#pull-the-latest-changes-from-the-amphtml-repository) and resolve any conflicts. +1. [Sign the CLA](CONTRIBUTING.md#contributor-license-agreement): This is a one-time task. +1. Run the **pre push** check, which is a tool that helps catch any issues before you submit your code. To enable the git pre-push hook, see [`enable-git-pre-push.sh`](./build-system/enable-git-pre-push.sh#L17-L20). +1. Prepare for your code review: + - [ ] [Correct test coverage](./contributing/TESTING.md) + - [ ] [Code follows style and design guidelines](./contributing/DEVELOPING.md#guidelines--style) + - [ ] [Documentation for your feature](./contributing/building-an-amp-extension.md#documenting-your-element) + - [ ] [Presubmit passes (no lint and type check errors, tests are passing)](./build-system/enable-git-pre-push.sh#L17-L20) + - [ ] [Validation rules and validation tests provided](./contributing/building-an-amp-extension.md#allowing-proper-validations) + - [ ] [Feature is behind an experiment flag](./contributing/building-an-amp-extension.md#experiments) + - [ ] [Example provided](./contributing/building-an-amp-extension.md#example-of-using-your-extension) +1. [Push your changes](./contributing/getting-started-e2e.md#push-your-changes-to-your-github-fork) +1. [Send a Pull Request (PR) to review your code](./contributing/getting-started-e2e.md#send-a-pull-request-ie-request-a-code-review). Your PR needs to include: + - A descriptive title + - A link to your GitHub Intent To Implement # or Issue # + - A visual demonstration of your change (e.g., a screenshot, GIF, or links to a published demo (we can link to our Heroku setup which allows developers to publish work-in-progress code to the cloud) + - @mention the core committer or someone who's already worked with you on the feature +1. Make sure your PR presubmit passes (no lint and type check errors, tests are passing). +1. [Respond to feedback](./contributing/getting-started-e2e.md#respond-to-pull-request-comments). +1. After your PR is approved, it's merged by a core committer. To check on your changes and find out when they get into production, read [See your changes in production](./contributing/getting-started-quick.md#see-your-changes-in-production). +1. [Clean up](./contributing/getting-started-quick.md#delete-your-branch-after-your-changes-are-merged-optional): After your changes are merged, you can delete your working branch. + +## Contributor License Agreement + +The AMP Project hosted at GitHub requires all contributors to either sign an individual Contributor License Agreement or be covered by a corporate Contributor License Agreement in order to protect contributors and users in issues of intellectual property. + +We recommend you handle signing/being covered by a CLA *before* you send a pull request to avoid problems, though this is not absolutely necessary until your code is ready to be merged in. + +**Make sure that the email you associate with your CLA is the same email address you associate with your commits (likely via the `user.email` Git config as described on GitHub's [Set up Git](https://help.github.com/articles/set-up-git/) page).** + +* **If you are contributing code on your own behalf** you can sign the [individual CLA](https://developers.google.com/open-source/cla/individual) instantly online. +* **If you are planning on contributing code on behalf of your company:** + * Your company will need to agree to a [corporate CLA](https://developers.google.com/open-source/cla/corporate) if it has not already done so. Although this is a relatively straightforward process, it requires approval from an authorized signer at your company and a manual verification process that may take a couple of days. To ensure you can get your code reviewed and merged quickly please start this process as soon as possible. The signer of your corporate CLA will associate a Google Group to the corporate CLA, and any email address added to this Google Group will be considered to be covered by this corporate CLA. + * To be covered by your company's corporate CLA the owner of the Google Group associated with the corporate CLA (someone at your company) will need to add your address to this Google Group. + +## Ongoing participation + +We actively encourage ongoing participation by community members. + +### Discussion channels + +Technical issues, designs, etc. are discussed using several different channels: + +- [GitHub issues](https://github.com/ampproject/amphtml/issues) and [pull requests](https://github.com/ampproject/amphtml/pulls) +- [Slack](https://amphtml.slack.com) ([signup](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877)) + - the [#contributing](https://amphtml.slack.com/messages/C9HRJ1GPN/details/) channel is the main channel for you to discuss/ask questions about *contributing* to the open source project + - if you're *new to contributing* to AMP stop by [#welcome-contributors](https://amphtml.slack.com/messages/C432AFMFE/details/) to say hi! + - **NOTE: if you have a question about *using AMP on your site*, use [Stack Overflow](https://stackoverflow.com/questions/tagged/amp-html) rather than Slack** as Stack Overflow is more actively monitored for these types of questions + - there are many other Slack channels for more specific topics; after you join our Slack click on the "Channels" header to find other channels you want to participate in +- the [amphtml-discuss Google Group](https://groups.google.com/forum/#!forum/amphtml-discuss) + +### Status updates + +Status updates from members of the community are tracked using approximately bi-weekly [Status Update GitHub issues](https://github.com/ampproject/amphtml/issues?q=label%3A%22Type%3A+Status+Update%22). + +We encourage everyone who is actively contributing to AMP to add a comment to the relevant Status Update issue. + +### Weekly design reviews + +The community holds weekly engineering [design reviews](./contributing/design-reviews.md) via video conference. We encourage everyone in the community to participate in these discussions and to bring their designs for new features and significant bug fixes to these reviews. + +### Working groups + +AMP Project [working groups](./contributing/working-groups.md) bring together parties with related interests to discuss ideas for how AMP can evolve and to receive updates on new features and changes in AMP that are relevant to the group. + +## See also * [Code of conduct](CODE_OF_CONDUCT.md) -* [DEVELOPING](DEVELOPING.md) resources * [3p contribution guidelines](https://github.com/ampproject/amphtml/tree/master/3p) * The [GOVERNANCE](GOVERNANCE.md) model diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index ee8288df3b02..000000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,11 +0,0 @@ -# Names should be added to this file as: -# Name - -Dima Voytenko -Erwin Mombay -Jake Moening -Jordan Adler -Kent Brewster -Malte Ubl -Niall Kennedy -Taylor Savage diff --git a/DEVELOPING.md b/DEVELOPING.md deleted file mode 100644 index 4609644648d8..000000000000 --- a/DEVELOPING.md +++ /dev/null @@ -1,138 +0,0 @@ - - -## Development on AMP HTML - -### Slack and mailing list - -We discuss implementation issues on [amphtml-discuss@googlegroups.com](https://groups.google.com/forum/#!forum/amphtml-discuss). - -For more immediate feedback, [sign up for our Slack](https://docs.google.com/forms/d/1wAE8w3K5preZnBkRk-MD1QkX8FmlRDxd_vs4bFSeJlQ/viewform?fbzx=4406980310789882877). - -### Starter 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. - -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)! - -### Installation - -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`. -
-  127.0.0.1               ads.localhost iframe.localhost
-
- -### Build & Test - -| Command | Description | -| ----------------------------- | --------------------------------------------------------------------- | -| `gulp` | Runs "watch" and "serve". | -| `gulp dist` | Builds production binaries. | -| `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 serve` | Serves content in repo root dir over http://localhost:8000/. | -|-------------------------------|-----------------------------------------------------------------------| - -To fix issues with Safari test runner launching multiple instances of the test, run: -
-  defaults write com.apple.Safari ApplePersistenceIgnoreState YES
-
- -#### Saucelabs - -Running tests on Sauce Labs requires an account. You can get one by signing up for [Open Sauce](https://saucelabs.com/opensauce/). This will provide you with a user name and access code that you need to add to your `.bashrc` or equivalent like this: - -``` -export SAUCE_USERNAME=sauce-labs-user-name -export SAUCE_ACCESS_KEY=access-key -``` - -Also for local testing, download [saucelabs connect](https://docs.saucelabs.com/reference/sauce-connect/) (If you are having trouble, downgrade to 4.3.10) and establish a tunnel by running the `sc` before running tests. - -Because of the user name and password requirement pull requests do not directly run on Travis. If your pull request contains JS or CSS changes and it does not change the build system, it will be automatically build by our bot [@ampsauce](https://github.com/ampsauce/amphtml). Builds can be seen on [@ampsauce's Travis](https://travis-ci.org/ampsauce/amphtml/builds) and after they finished their state will be logged to your PR. - -If a test flaked on a pull request you can ask for a retry by sending the comment `@ampsauce retry`. This will only be accepted if you are a member of the "ampproject" org. Ping us if you'd like to be added. You may also need to publicly reveal your membership. - -### Manual testing - -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). - -## Repository Layout -
-  3p/             - Implementation of third party sandbox iframes.
-  ads/            - Modules implementing specific ad networks used in 
-  build/          - (generated) intermediate generated files
-  build-system/   - build infrastructure
-  builtins/       - tags built into the core AMP runtime
-      *.md        - documentation for use of the builtin
-      *.js        - source code for builtin tag
-  css/            - default css
-  dist/           - (generated) main JS binaries are created here. This is what
-                    gets deployed to cdn.ampproject.org.
-  dist.3p/        - (generated) JS binaries and HTML files for 3p embeds and ads.
-                    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
-  test/           - tests for the AMP runtime and builtins
-  testing/        - testing infrastructure
-
- -## Supported browsers - -In general we support the 2 latest versions of major browsers like Chrome, Firefox, Edge, Safari and Opera. We support desktop, phone, tablet and the web view version of these respective browsers. - -Beyond that the core AMP library and builtin elements should aim for very wide browser support and we accept fixes for all browsers with market share greater than 1 percent. - -In particular, we try to maintain "it might not be perfect but isn't broken"-support for the Android 4.0 system browser and Chrome 28+ on phones. - -## Eng docs - -- [Life of an AMP *](https://docs.google.com/document/d/1WdNj3qNFDmtI--c2PqyRYrPrxSg2a-93z5iX0SzoQS0/edit#) -- [AMP Layout system](spec/amp-html-layout.md) - -We also recommend scanning the [spec](spec/). The non-element part should help understand some of the design aspects. - -## AMP Dev Channel (Experimental) - -AMP Dev Channel is a way to opt a browser into using a newer version of the AMP JS libraries. - -This release may be less stable and it may contain features not available to all users. Opt into this option if you'd like to help test new versions of AMP, report bugs or build documents that require a new feature that is not yet available to everyone. - -To opt your browser into the AMP Dev Channel, go to [the AMP experiments page](https://cdn.ampproject.org/experiments.html) and activate the "AMP Dev Channel" experiment. - - -## [Code of conduct](CODE_OF_CONDUCT.md) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 43c34c3d0045..334b83f30b7d 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,14 +1,32 @@ -The AMP Project's chief and primary concern is with the development of AMP HTML, an open-source runtime shared by many producers and consumers of (mostly) static web content. Its governance model thus reflects only the need to steer engineering direction, and not any other activities, which would be out of scope. +# AMP Project open source governance + +**Note:** An [update to this policy is being discussed](https://amphtml.wordpress.com/2018/09/18/governance/). + +This document describes the governance model for the AMP open source project, and in particular the [AMP HTML GitHub project](https://github.com/ampproject/amphtml). Our governance model is as follows: -* There is a single Tech Lead, who will have the final say on all decisions regarding technical direction. -* The Tech Lead directs the Core Committers, whose members include the Tech Lead and those who have been appointed by the Tech Lead as Core Committers. -* In the event the Tech Lead is unable to perform his or her duties, or abdicates, the Core Committers can select a new Tech Lead from amongst themselves. -* In the event there are no Core Committers, Google Inc. will appoint one. +* There is a single [Tech Lead](#list-of-core-committers), who will have the final say on all decisions regarding technical direction. +* The Tech Lead directs the [Core Committers](#list-of-core-committers), whose members include the Tech Lead and those who have been appointed by the Tech Lead as Core Committers. +* In the event the Tech Lead is unable to perform their duty, or abdicates, the Core Committers can select a new Tech Lead from amongst themselves. +* In the unlikely event that there are no more Core Committers, Google Inc. will appoint a new Tech Lead. +* Significant feature development and changes to AMP require following the ["Intent to implement"](./CONTRIBUTING.md#contributing-features) process including approval from the Tech Lead and one Core Committer. +* Before contributions can be merged into the AMP Project, approval must be given by an [Owner and a Core Committer](./contributing/owners-and-committers.md) -Core Committers: -* Tech Lead: Malte Ubl -* Erwin Mombay -* Dima Voytenko +### List of Core Committers: +* **Tech Lead: Malte Ubl (@cramforce)** +* Alan Orozco (@alanorozco) +* Ali Ghassemi (@aghassemi) +* Cathy Zhu (@cathyxz) +* Chen Shay (@chenshay) +* David Sedano (@honeybadgerdontcare). Specialty: Validator +* Dima Voytenko (@dvoytenko) +* Erwin Mombay (@erwinmombay) +* Greg Grothaus (@Gregable). Specialty: Validator +* Hongfei Ding (@lannka) +* Johannes Henkel (@powdercloud). Specialty: Validator +* Justin Ridgewell (@jridgewell) +* Wassim Gharbi (@wassgha) +* William Chou (@choumx) +* Yuxuan Zhou (@zhouyx) diff --git a/LICENSE b/LICENSE index 92e0bfd85e3e..e64cf95b7fc1 100644 --- a/LICENSE +++ b/LICENSE @@ -51,12 +51,12 @@ submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communiampion sent + means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to - communiampion on electronic mailing lists, source code control systems, + communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but - excluding communiampion that is conspicuously marked or otherwise + excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a diff --git a/OWNERS.yaml b/OWNERS.yaml new file mode 100644 index 000000000000..fd3fc6019086 --- /dev/null +++ b/OWNERS.yaml @@ -0,0 +1,5 @@ +- cramforce +- dvoytenko +- jridgewell +- ampproject/validator: + - "*.protoascii" diff --git a/Procfile b/Procfile new file mode 100644 index 000000000000..48d242edbd33 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gulp serve --host=0.0.0.0 diff --git a/README.md b/README.md index d2ce54bda2be..bdcfa9eade6e 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,45 @@ - - -[![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 ⚡ -[AMP HTML](https://www.ampproject.org/docs/get_started/about-amp.html) is a way to build web pages for static content that render with reliable, fast performance. It is our attempt at fixing what many perceive as painfully slow page load times – especially when reading content on the mobile web. - -AMP HTML is entirely built on existing web technologies. It achieves reliable performance by restricting some parts of HTML, CSS and JavaScript. These restrictions are enforced with a validator that ships with AMP HTML. To make up for those limitations AMP HTML defines a set of [custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) for rich content beyond basic HTML. Learn more about [how AMP speeds up performance](https://www.ampproject.org/docs/get_started/technical_overview.html). - -# How does AMP HTML work? - -AMP HTML works by including the AMP JS library and adding a bit of boilerplate to a web page, so that it meets the AMP HTML Specification. The simplest AMP HTML file looks like this: - -```html - - - - - - - - - - Hello World! - -``` +[![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovateapp.com/) -This allows the AMP library to include: -* The AMP JS library, that manages the loading of external resources to ensure a - fast rendering of the page. -* An AMP validator that provides a way for web developers to easily validate - that their code meets the AMP HTML specification. -* Some custom elements, called AMP HTML components, which make common patterns - easy to implement in a performant way. +[AMP HTML](https://www.ampproject.org/docs/get_started/about-amp.html) is a way to build web pages that render with reliable and fast performance. It is our attempt at fixing what many perceive as painfully slow page load times – especially when reading content on the mobile web. AMP HTML is built on existing web technologies; an AMP page will load (quickly) in any modern browser. -Get started [creating your first AMP page](https://www.ampproject.org/docs/get_started/create_page.html). +You can learn more at [ampproject.org](https://www.ampproject.org/) including [what AMP is](https://www.ampproject.org/learn/about-amp/), [how it works](https://www.ampproject.org/learn/how-amp-works/) and the importance of [validation in AMP](https://www.ampproject.org/docs/guides/validate). You can also walk through [creating an AMP page](https://www.ampproject.org/docs/get_started/create) and read through the [reference docs](https://www.ampproject.org/docs/reference/components). -## The AMP JS library +## We'd love your help making AMP better! -The AMP JS library provides [builtin](builtins/README.md) AMP Components, manages the loading of external resources, and ensures a reliably fast time-to-paint. +There are a lot of ways you can [contribute](CONTRIBUTING.md) to making AMP better! You can [report bugs and feature requests](CONTRIBUTING.md#reporting-issues-with-amp) or ideally become an [ongoing participant](CONTRIBUTING.md#ongoing-participation) in the AMP Project community and [contribute code to the open source project](CONTRIBUTING.md#contributing-code). -## The AMP Validator +We enthusiastically welcome new contributors to the AMP Project **_even if you have no experience being part of an open source project_**. We've got some [tips for new contributors](https://github.com/ampproject/amphtml/blob/master/CONTRIBUTING.md#tips-for-new-open-source-contributors) and guides to getting started (both a [detailed version](contributing/getting-started-e2e.md) and a [TL;DR](contributing/getting-started-quick.md)). -The AMP Validator allows a web developer to easily identify if the web page -doesn't meet the [AMP HTML specification](spec/amp-html-format.md). +If you're new to **contributing to the AMP open source project**, sign up for our [Slack](https://docs.google.com/forms/d/e/1FAIpQLSd83J2IZA6cdR6jPwABGsJE8YL4pkypAbKMGgUZZriU7Qu6Tg/viewform?fbzx=4406980310789882877) and say "Hi!" in the appropriately named [#welcome-contributors](https://amphtml.slack.com/messages/welcome-contributors/) channel ❤️️. -Adding "#development=1" to the URL of the page instructs the AMP Runtime to run -a series of assertions confirming the page's markup meets the AMP HTML -Specification. Validation errors are logged to the browser's console when the -page is rendered, allowing web developers to easily see how complex changes in -web code might impact performance and user experience. +## Using AMP on your site -It also allows apps that integrate web content to validate the web page against -the specification. This allows an app to make sure the page is fast and -mobile-friendly, as pages adhering to the AMP HTML specification are reliably -fast. +If you are using AMP on your site, +check out the docs on [ampproject.org](https://www.ampproject.org/), samples on [ampbyexample.com](https://ampbyexample.com/) and templates on [ampstart.com](https://ampstart.com). -Learn more about [validating your AMP pages](https://www.ampproject.org/docs/guides/validate.html). +The best place to get help with **questions about using AMP** on your site is [Stack Overflow](https://stackoverflow.com/questions/tagged/amp-html). You'll find answers to many common questions there. In the event your question hasn't already been answered you can post a new one, and one of the many people who are knowledgeable about AMP and who monitor Stack Overflow will likely answer it before too long. -## AMP HTML Components +## Further reading -AMP HTML Components are a series of extended custom elements that supplement -or replace functionality of core HTML5 elements to allow the runtime to ensure -it is solely responsible for loading external assets and to provide for shared -best practices in implementation. +* [Component reference](https://www.ampproject.org/docs/reference/components) +* [Release schedule](contributing/release-schedule.md) +* [Format specification](spec/amp-html-format.md) +* [Custom element specification](spec/amp-html-components.md) -These components can: -* Replace HTML5 elements that are not directly permitted in the specification - such as [amp-img](builtins/amp-img.md) and [amp-video](builtins/amp-video.md). -* Implement embedded third-party content, such as -[amp-ad](builtins/amp-ad.md), -[amp-pinterest](extensions/amp-pinterest/amp-pinterest.md), -[amp-twitter](extensions/amp-twitter/amp-twitter.md), -and [amp-youtube](extensions/amp-youtube/amp-youtube.md). -* Provide for common patterns in web pages, -such as [amp-lightbox](extensions/amp-lightbox/amp-lightbox.md) -and [amp-carousel](extensions/amp-carousel/amp-carousel.md). -* Make advanced performance techniques easy, -such as [amp-anim](extensions/amp-anim/amp-anim.md), -which allows web developers to dynamically serve animated images -as either image files (GIF) or video files (WebM or MP4) based on browser compatibility. +## Who makes AMP HTML? -# Further Reading +AMP HTML is made by the [AMP Project](https://www.ampproject.org/). If you're a [contributor to the open source community](https://github.com/ampproject/amphtml/graphs/contributors) this includes you! -If you are creating AMP pages, -check out the docs on [ampproject.org](https://www.ampproject.org/). +## Security disclosures -These docs are public and open-source: [https://github.com/ampproject/docs/](https://github.com/ampproject/docs/). -See something that's missing from the docs, or that could be worded better? -[Create an issue](https://github.com/ampproject/docs/issues) and -we will do our best to respond quickly. +The AMP Project accepts responsible security disclosures through the [Google Application Security program](https://www.google.com/about/appsecurity/). -Resources: -* [AMP HTML samples](examples/) -* [AMP-HTML on StackOverflow](https://stackoverflow.com/questions/tagged/amp-html) +## Code of conduct - +The AMP Project strives for a positive and growing project community that provides a safe environment for everyone. All members, committers and volunteers in the community are required to act according to the [code of conduct](CODE_OF_CONDUCT.md). -Reference: -* [AMP HTML core built-in elements](builtins/README.md) -* [AMP HTML optional extended elements](extensions/README.md) +## License -Technical Specifications: -* [AMP HTML format specification](spec/amp-html-format.md) -* [AMP HTML custom element specification](spec/amp-html-components.md) - -# Who makes AMP HTML? - -AMP HTML is made by the [AMP Project](https://www.ampproject.org/), and is licensed -under the [Apache License, Version 2.0](LICENSE). - -## Contributing - -Please see [the CONTRIBUTING file](CONTRIBUTING.md) for information on contributing to the AMP Project, and [the DEVELOPING file](DEVELOPING.md) for documentation on the AMP library internals and [hints how to get started](DEVELOPING.md#starter-issues). - -### [Code of conduct](CODE_OF_CONDUCT.md) +AMP HTML is licensed under the [Apache License, Version 2.0](LICENSE). diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000000..67592a1a08c4 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,22 @@ +# Getting Support + +There are many ways to get help for questions and issues related to AMP: + +**Need help with AMP?** + +If you are looking for help to get started using AMP on your site or you are having issues using AMP, consult these resources: + +* [ampproject.org](https://www.ampproject.org/docs) provides guides and tutorials to help you learn about AMP. +* [AMP by Example](https://ampbyexample.com/) provides hands-on samples and demos for using AMP components. +* [AMP Start](https://ampstart.com/) provides pre-styled templates and components that you can use to create styled AMP sites from scratch. +* [Stack Overflow](http://stackoverflow.com/questions/tagged/amp-html) is our recommended way to find answers to questions about AMP; since members of the AMP Project community regularly monitor Stack Overflow you will probably receive the fastest response to your questions there. +* For AMP on Google Search questions or issues, please use [Google's AMP forum](https://goo.gl/utQ1KZ). +* To check the status of AMP serving and its related services, see the [AMP Status](https://status.ampproject.org/) page. + +**Found a bug? Suggest a feature?** + +If you encounter a bug in AMP or have a feature request for AMP, see [Reporting issues with AMP](https://github.com/ampproject/amphtml/blob/master/CONTRIBUTING.md#reporting-issues-with-amp) for information on filing an issue or requesting features. + +**Contributing to AMP?** + +[Contributing to AMP HTML](https://github.com/ampproject/amphtml/blob/master/CONTRIBUTING.md#ongoing-participation) is a great place to find out how you can make contributions to the AMP open source project and how you can get help if you run into questions when contributing to AMP. diff --git a/TICKEVENTS.md b/TICKEVENTS.md deleted file mode 100644 index 2d5de9262035..000000000000 --- a/TICKEVENTS.md +++ /dev/null @@ -1,32 +0,0 @@ - - -### Tick Events - -We use very short string names as tick labels, so the table below -further describes these labels. -Every start label has an assumed e_`label` for its "end" counterpart label. -As an example if we executed `perf.tick('label')` we assume we have a counterpart -`perf.tick('e_label')`. - -| Name | id | Description | -----------------------|-------------------|------------------------------------| -| Install Styles | `is` | Set when the styles are installed. | -| Window load event | `ol` | Window load even 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. | diff --git a/ads/.eslintrc b/ads/.eslintrc new file mode 100644 index 000000000000..30cbe7749784 --- /dev/null +++ b/ads/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "amphtml-internal/no-style-display": 0, + "amphtml-internal/query-selector": 0 + } +} diff --git a/ads/24smi.js b/ads/24smi.js new file mode 100644 index 000000000000..66be097312ca --- /dev/null +++ b/ads/24smi.js @@ -0,0 +1,51 @@ +/** + * 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, validateSrcPrefix} from '../3p/3p'; + +const jsnPrefix = 'https://jsn.24smi.net/'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function _24smi(global, data) { + validateData(data, ['src']); + const {src} = data; + validateSrcPrefix(jsnPrefix, src); + + createContainer(global, getBlockId(src)); + loadScript(global, src); +} + +/** + * @param {!Window} global + * @param {string} blockId + */ +function createContainer(global, blockId) { + const d = global.document.createElement('div'); + d.id = `smi_teaser_${blockId}`; + global.document.getElementById('c').appendChild(d); +} + +/** + * @param {string} src + * @return {string} + */ +function getBlockId(src) { + const parts = src.split('/'); + return parts[parts.length - 1].split('.')[0]; +} diff --git a/ads/24smi.md b/ads/24smi.md new file mode 100644 index 000000000000..e84158acf221 --- /dev/null +++ b/ads/24smi.md @@ -0,0 +1,36 @@ + + +# 24smi + +Provides support for [24smi](https://partner.24smi.info/) widgets. + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact [24smi](https://partner.24smi.info). + +### Required parameters + +- `src` diff --git a/ads/OWNERS.yaml b/ads/OWNERS.yaml new file mode 100644 index 000000000000..dfaedfc4035a --- /dev/null +++ b/ads/OWNERS.yaml @@ -0,0 +1,2 @@ +- lannka +- zhouyx diff --git a/ads/README.md b/ads/README.md index 7d9d03c09f9e..78e64617c558 100644 --- a/ads/README.md +++ b/ads/README.md @@ -1,161 +1,361 @@ # Integrating ad networks into AMP -See also our [ad integration guidelines](../3p/README.md#ads). +This guide provides details for ad networks to create an `amp-ad` integration for their network. + +**Table of contents** + +- [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) +- [Developer announcements for ads related API changes ](#developer-announcements-for-ads-related-API-changes) ## 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. The AMP Project aims to support a large subset of existing ads with little or no changes to how the integrations work. AMP Project's 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: -- Because AMPs are served on HTTPS and ads cannot be proxied, ads must be served over HTTPS. -- The size of an ad unit must be static. It must be knowable without fetching the ad and it cannot change at runtime except through iframe resizing https://github.com/ampproject/amphtml/issues/728. -- If placing the ad requires running JavaScript (assumed to be true for 100% of ads served through networks), the ad must be placed on an origin different from the AMP document itself. -Reasons include: +Below is a summary of constraints placed on external resources, such as ads in AMP HTML: + +- Because AMP pages are served on HTTPS and ads cannot be proxied, ads must be served over HTTPS. +- The size of an ad unit must be static. It must be knowable without fetching the ad and it cannot change at runtime except through [iframe resizing](#ad-resizing). +- If placing the ad requires running JavaScript (assumed to be true for 100% of ads served through networks), the ad must be placed on an origin different from the AMP document itself. Reasons include: - Improved security. - Takes synchronous HTTP requests made by the ad out of the critical rendering path of the primary page. - Allows browsers to run the ad in a different process from the primary page (even better security and prevents JS inside the ad to block the main page UI thread). - Prevents ads doing less than optimal things to measure user behavior and other interference with the primary page. -- The AMP runtime may at any moment decide that there are too many iframes on a page and that memory is low. In that case it would unload ads that were previously loaded and are no longer visible. It may later load new ads in the same slot if the user scrolls them back into view. - +- The AMP Runtime may at any moment decide that there are too many iframes on a page and that memory is low. In that case, the AMP Runtime unloads ads that were previously loaded and are no longer visible. It may later load new ads in the same slot if the user scrolls them back into view. +- The AMP Runtime may decide to set an ad that is currently not visible to `display: none` to reduce browser layout and compositing cost. ## 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. By default, the iframe 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`). + +### Available information to the ad + +The AMP runtime provides the following information to the ad: + +
+
ad viewability
+
For details, see the ad viewability section below.
+
window.context.canonicalUrl
+
Contains the canonical URL of the primary document as defined by its link rel=canonical tag.
+
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 clientIdScope variable _config.js. Use clientIdCookieName to provide a cookie name for non-proxy case, otherwise value of clientIdScope is used.
  • +
  • Only available on pages that load amp-analytics. The clientId is null if amp-analytics isn't loaded on the given page.
  • +
+
+
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.
+
window.context.domFingerprint
+
Contains a string key based on where in the page the ad slot appears. Its purpose is to identify the same ad slot across many page views. It is formed by listing the ancestor tags and their ordinal position, up to 25 levels. For example, if its value is amp-ad.0,td.1,tr.0,table.0,div/id2.0,div/id1.0 this would mean the first amp-ad child of the second td child of the first tr child of... etc.
+
window.context.location
+
Contains the sanitized Location object of the primary document. This object contains keys like href, origin and other keys common for Location objects. 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.pageViewId
+
Contains a relatively low entropy id that is the same for all ads shown on a page.
+
window.context.referrer
+
Contains the origin of the referrer value of the primary document if available. +
    +
  • document.referrer typically contains the URL of the primary document. This may change in the future (See window.context.location for a more reliable method).
  • +
+
+
window.context.sourceUrl
+
Contains the source URL of the original AMP document. See details here.
+
window.context.startTime
+
Contains the time at which processing of the amp-ad element started.
+
-### Information available to the ad -We will provide the following information to the ad: +More information can be provided in a similar fashion if needed (Please file an issue). -- `window.context.referrer` contains the origin of the referrer value of the primary document if available. -- `document.referrer` will typically contain the URL of the primary document. This may change in the future (See next value for a more reliable method). -- `window.context.location` contains the sanitized `Location` object of the primary document. - This object contains keys like `href`, `origin` and other keys common for [Location](https://developer.mozilla.org/en-US/docs/Web/API/Location) objects. - 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.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) +### Available APIs + +
+
window.context.getHtml (selector, attrs, callback)
+
Retrieves the 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.

This API is by default disabled. To enable it, the `amp-ad` needs to put attribute data-html-access-allowed to explicitly opt-in.

+
window.context.noContentAvailable()
+
Informs the AMP runtime that the ad slot cannot be filled. The ad slot will then display the fallback content if provided, otherwise tries to collapse the ad slot.
+
window.context.renderStart(opt_data)
+
Informs the 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 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.
+
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.
+
-More information can be provided in a similar fashion if needed (Please file an issue). -### Methods available to the ad. +### Exceptions to available APIs and information -- `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 placeholder 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. +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. The AMP Project is 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 -Ads can call the special API `window.context.observeIntersection(changesCallback)` to receive IntersectionObserver style [change records](http://rawgit.com/slightlyoff/IntersectionObserver/master/index.html#intersectionobserverentry) of the ad's intersection with the parent viewport. +#### Position in viewport -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. +Ads can call the special `window.context.observeIntersection(changesCallback)`API to receive IntersectionObserver style [change records](https://github.com/w3c/IntersectionObserver/blob/master/explainer.md) of the ad's intersection with the parent viewport. -Example usage: +The API allows you to specify 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. + +*Example usage*: ```javascript - window.context.observeIntersection(function(changes) { - changes.forEach(function(c) { - console.info('Height of intersection', c.intersectionRect.height); - }); +window.context.observeIntersection(function(changes) { + changes.forEach(function(c) { + console.info('Height of intersection', c.intersectionRect.height); }); +}); ``` `window.context.observeIntersection` returns a function which when called will stop listening for intersection messages. -Example usage: +*Example usage* ```javascript - var unlisten = window.context.observeIntersection(function(changes) { - changes.forEach(function(c) { - console.info('Height of intersection', c.intersectionRect.height); - }); +var unlisten = window.context.observeIntersection(function(changes) { + changes.forEach(function(c) { + console.info('Height of intersection', c.intersectionRect.height); }); +}); - // condition to stop listening to intersection messages. - unlisten(); +// condition to stop listening to intersection messages. +unlisten(); ``` +##### Initial layout rect + +The value `window.context.initialLayoutRect` contains the initial rect of the ad's position in the page. + +##### Initial viewport intersection + +The value `window.context.initialIntersection` contains the initial viewport intersection record at the time the iframe was created. + +#### Page visibility + +AMP documents may be practically invisible without the visibility being reflected by the [page visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API). This is primarily the case when a document is swiped away or being prerendered. + +Whether a document is actually being visible can be queried using: + +`window.context.hidden` which is true if the page is not visible as per page visibility API or because the AMP viewer currently does not show it. + +Additionally, one can observe the `amp:visibilitychange` on the `window` object to be notified about changes in visibility. + ### Ad resizing Ads can call the special API -`window.context.requestResize(width, height)` to send a resize request. +`window.context.requestResize(width, height, opt_hasOverflow)` to send a resize request. Once the request is processed the AMP runtime will try to accommodate this request as soon as possible, but it will take into account where the reader is currently reading, whether the scrolling is ongoing and any other UX or performance factors. -Ads can observe wehther resize request were successful using the `window.context.onResizeSuccess` and `window.context.onResizeDenied` methods. +Ads can observe whether resize request were successful using the `window.context.onResizeSuccess` and `window.context.onResizeDenied` methods. + +The `opt_hasOverflow` is an optional boolean value, ads can specify `opt_hasOverflow` to `true` to let AMP runtime know that the ad context can handle overflow when attempt to resize is denied, and not to throw warning in such cases. + +*Example:* -Example ```javascript -var unlisten = window.context.onResizeSuccess(function(requestedHeight) { +var unlisten = window.context.onResizeSuccess(function(requestedHeight, requestedWidth) { // Hide any overflow elements that were shown. - // The requestedHeight argument may be used to check which height change the request corresponds to. + // The requestedHeight and requestedWidth arguments may be used to + // check which size change the request corresponds to. }); -var unlisten = window.context.onResizeDenied(function(requestedHeight) { - // Show the overflow element and send a window.context.requestResize(width, height) when the overflow element is clicked. - // You may use the requestedHeight to check which height change the request corresponds to. +var unlisten = window.context.onResizeDenied(function(requestedHeight, requestedWidth) { + // Show the overflow element and send a window.context.requestResize(width, height) + // when the overflow element is clicked. + // You may use the requestedHeight and requestedWidth to check which + // size change the request corresponds to. }); ``` - -Here are some factors that affect how fast the resize will be executed: +Here are some factors that affect whether the resize will be executed: - Whether the resize is triggered by the user action; - 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. +#### Specifying an overflow element + +You can specify an `overflow` element that is only shown when a resize request is declined. When the user clicks the overflow element, the resize passes the "interaction" rule and will resize. + +*Example: Using an `overflow` element* + +```html + +
Click to resize
+ +
+``` + +### 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. + +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, opt_hasOverflow)` API. Details in [Ad Resizing](#ad-resizing). + +### amp-consent integration +If [amp-consent](https://github.com/ampproject/amphtml/blob/master/extensions/amp-consent/amp-consent.md) extension is used on the page, `data-block-on-consent` attribute +can be added to `amp-ad` element to respect the corresponding `amp-consent` policy. +In that case, the `amp-ad` element will be blocked from loading until the consent accepted. +Individual ad network can override this default consent handling by putting a `consentHandlingOverride: true` in `ads/_config.js`. +Doing so will unblock the ad loading once the consent is responded. It will be then the ad network's responsibility +to respect user's consent choice, for example to serve non-personalized ads on consent rejection. +AMP runtime provides the following `window.context` APIs for ad network to access the consent state. + +
+
window.context.initialConsentState
+
+ Provides the initial consent state when the ad is unblocked. + The states are integers defined here + (code). +
+
window.context.getConsentState(callback)
+
+ Queries the current consent state asynchronously. The `callback` function + will be invoked with the current consent state. +
+
window.context.consentSharedData
+
+ Provides additional user privacy related data retrieved from publishers. + See here for details. +
+
+ +After overriding the default consent handling behavior, don't forget to update your publisher facing + documentation with the new behaviors on user's consent choices. You can refer to our documentation example [here](https://github.com/ampproject/amphtml/blob/master/ads/_ping_.md#user-consent-integration). ### Optimizing ad performance #### JS reuse across iframes -To allow ads to bundle HTTP requests across multiple ad units on the same page the object `window.context.master` will contain the window object of the iframe being elected master iframe for the current page. The `window.context.isMaster` property is `true` when the current frame is the master frame. + +To allow ads to bundle HTTP requests across multiple ad units on the same page the object `window.context.master` will contain the window object of the iframe being elected master iframe for the current page. The `window.context.isMaster` property is `true` when the current frame is the master frame. + +The `computeInMasterFrame` function is designed to make it easy to perform a task only in the master frame and provide the result to all frames. It is also available to custom ad iframes as `window.context.computeInMasterFrame`. See [3p.js](https://github.com/ampproject/amphtml/blob/master/3p/3p.js) for function signature. #### Preconnect and prefetch + Add the JS URLs that an ad **always** fetches or always connects to (if you know the origin but not the path) to [_config.js](_config.js). This triggers prefetch/preconnect when the ad is first seen, so that loads are faster when they come into view. - ### Ad markup -Ads are loaded using a the tag given the type of the ad network and name value pairs of configuration. This is an example for the A9 network: +Ads are loaded using the `` tag containing the specified `type` for the ad netowkr, and name value pairs of configuration. + +This is an example for the A9 network: ```html - - + + ``` and another for DoubleClick: ```html - - - ```` + + +``` For ad networks that support loading via a single script tag, this form is supported: ```html - - + + ``` -Note, that the network still needs to be whitelisted and provide a prefix to valid URLs. We may add similar support for ad networks that support loading via an iframe tag. +Note, that the network still needs to be white-listed and provide a prefix to valid URLs. The AMP Project may add similar support for ad networks that support loading via an iframe tag. -Technically the `` tag loads an iframe to a generic bootstrap URL that knows how to render the ad given the parameters to the tag. +Technically, the `` tag loads an iframe to a generic bootstrap URL that knows how to render the ad given the parameters to the tag. ### 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 publisher's 1st party cookies may be achieved through a custom ad bootstrap file. See ["Running ads from a custom domain"](https://www.ampproject.org/docs/reference/components/amp-ad#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. For example, 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 ability to read or write cookies for nytimes.com. + +## Developer guidelines for a pull request + +Please read through [DEVELOPING.md](../contributing/DEVELOPING.md) before contributing to this code repository. + +### Files to change + +If you're adding support for a new third-party ad service, changes to the following files are expected: + +- `/ads/yournetwork.js`: Implement the main logic here. This is the code that's invoked in the third-party iframe once loaded. +- `/ads/yournetwork.md`: Documentation detailing yourr ad service for 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 a 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`: + +1. Start a local gulp web server by running command `gulp`. +2. Visit `http://localhost:8000/examples/ads.amp.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 will it be a more confident example for publishers to follow, but also allows the AMP team to catch any regression bug during AMP releases. + +It's encouraged that you 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](../CONTRIBUTING.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. Using a different email address in the git commit. + 2. Not providing the exact company name in the PR thread. + +## Developer announcements for ads related API changes + +For any major Ads API related changes that introduce new functionality or cause backwards compatible changes, the AMP Project will notify the [amp-ads-announce@googlegroups.com](https://groups.google.com/d/forum/amp-ads-announce) at least 2 weeks in advance to make sure you have enough time to absorb those changes. diff --git a/ads/_a4a-config.js b/ads/_a4a-config.js new file mode 100644 index 000000000000..dc89c85da5dc --- /dev/null +++ b/ads/_a4a-config.js @@ -0,0 +1,83 @@ +/** + * 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 { + cloudflareIsA4AEnabled, +} from + '../extensions/amp-ad-network-cloudflare-impl/0.1/cloudflare-a4a-config'; +import { + gmosspIsA4AEnabled, +} from + '../extensions/amp-ad-network-gmossp-impl/0.1/gmossp-a4a-config'; +import {map} from '../src/utils/object'; +import { + tripleliftIsA4AEnabled, +} from + '../extensions/amp-ad-network-triplelift-impl/0.1/triplelift-a4a-config'; + +/** + * 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} + */ +let a4aRegistry; + +/** + * Returns the a4a registry map + * @return {Object} + */ +export function getA4ARegistry() { + if (!a4aRegistry) { + a4aRegistry = map({ + 'adsense': adsenseIsA4AEnabled, + 'adzerk': () => true, + 'doubleclick': () => true, + 'triplelift': tripleliftIsA4AEnabled, + 'cloudflare': cloudflareIsA4AEnabled, + 'gmossp': gmosspIsA4AEnabled, + 'fake': () => true, + // 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. + }); + } + + return a4aRegistry; +} + +/** + * 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 1fccdace1726..9ce9bbe16eb4 100644 --- a/ads/_config.js +++ b/ads/_config.js @@ -1,5 +1,5 @@ /** - * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * 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. @@ -15,46 +15,1052 @@ */ /** - * URLs to prefetch for a given ad type. - * - * This MUST be kept in sync with actual implementation. - * - * @const {!Object)>} + * @typedef {{ + * prefetch: (string|undefined), + * preconnect: (string|undefined), + * renderStartImplemented: (boolean|undefined), + * clientIdScope: (string|undefined), + * clientIdCookieName: (string|undefined), + * consentHandlingOverride: (boolean|undefined), + * remoteHTMLDisabled: (boolean|undefined), + * fullWidthHeightRatio: (number|undefined), + * mcFullWidthHeightRatio: (number|undefined), + * }} */ -export const adPrefetch = { - doubleclick: 'https://www.googletagservices.com/tag/js/gpt.js', - a9: 'https://c.amazon-adsystem.com/aax2/assoc.js', - adsense: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js', - dotandads: 'https://amp.ad.dotandad.com/dotandadsAmp.js', -}; +let AdNetworkConfigDef; /** - * URLs to connect to 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 adPreconnect = { - adreactor: 'https://adserver.adreactor.com', - adsense: 'https://googleads.g.doubleclick.net', - taboola: 'https://cdn.taboola.com', - doubleclick: [ - 'https://partner.googleadservices.com', - 'https://securepubads.g.doubleclick.net', - 'https://tpc.googlesyndication.com', - ], - dotandads: 'https://bal.ad.dotandad.com', -}; - -/** - * The externalCidScope used to provide CIDs to ads of the given type. + * // List of URLs for prefetch + * prefetch: string|array + * + * // List of hosts for preconnect + * preconnect: string|array + * + * // The scope used to provide CIDs to ads + * clientIdScope: string + * + * // The cookie name to store the CID. In absence, `clientIdScope` is used. + * clientIdCookieName: string + * + * // If the ad network is willing to override the consent handling, which + * // by default is blocking ad load until the consent is accepted. + * consentHandlingOverride: boolean + * + * // Whether render-start API has been implemented + * // We highly recommend all networks to implement the API, + * // see details in the README.md + * renderStartImplemented: boolean + * + * // The width / height ratio for full width ad units. + * // If absent, it means the network does not support full width ad units. + * // Example value: 1.2 + * fullWidthHeightRatio: number * - * @const {!Object} + * // The width / height ratio for matched content full width ad units. + * // If absent, it means the network does not support matched content full + * // width ad unit. + * // Example value: 0.27 + * mcFullWidthHeightRatio: number + * } + * + * @const {!Object}} */ -export const clientIdScope = { - // Add a mapping like - // adNetworkType: 'cidScope' here. - adsense: 'AMP_ECID_GOOGLE', - doubleclick: 'AMP_ECID_GOOGLE', +export const adConfig = { + '_ping_': { + renderStartImplemented: true, + clientIdScope: '_PING_', + consentHandlingOverride: true, + }, + + '24smi': { + prefetch: 'https://jsn.24smi.net/smi.js', + preconnect: 'https://data.24smi.net', + }, + + 'a8': { + prefetch: 'https://statics.a8.net/amp/ad.js', + renderStartImplemented: true, + }, + + 'a9': { + prefetch: 'https://z-na.amazon-adsystem.com/widgets/onejs?MarketPlace=US', + }, + + 'accesstrade': { + prefetch: 'https://h.accesstrade.net/js/amp/amp.js', + }, + + 'adagio': { + prefetch: 'https://js-ssl.neodatagroup.com/adagio_amp.js', + preconnect: [ + 'https://ad-aws-it.neodatagroup.com', + 'https://tracker.neodatagroup.com', + ], + renderStartImplemented: true, + }, + + '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': {}, + + 'adfox': { + prefetch: 'https://yastatic.net/pcode/adfox/loader.js', + renderStartImplemented: true, + }, + + 'adgeneration': { + prefetch: 'https://i.socdm.com/sdk/js/adg-script-loader.js', + }, + + 'adhese': { + renderStartImplemented: true, + }, + + 'adincube': { + renderStartImplemented: true, + }, + + 'adition': {}, + + 'adman': {}, + + 'admanmedia': { + renderStartImplemented: true, + }, + + 'admixer': { + renderStartImplemented: true, + preconnect: [ + 'https://inv-nets.admixer.net', + 'https://cdn.admixer.net', + ], + }, + + 'adocean': {}, + + 'adpicker': { + renderStartImplemented: true, + }, + + 'adplugg': { + prefetch: 'https://www.adplugg.com/serve/js/ad.js', + renderStartImplemented: true, + }, + + 'adreactor': {}, + + 'adsense': { + prefetch: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js', + preconnect: 'https://googleads.g.doubleclick.net', + clientIdScope: 'AMP_ECID_GOOGLE', + clientIdCookieName: '_ga', + remoteHTMLDisabled: true, + masterFrameAccessibleType: 'google_network', + fullWidthHeightRatio: 1.2, + mcFullWidthHeightRatio: 0.27, + consentHandlingOverride: true, + }, + + 'adsnative': { + prefetch: 'https://static.adsnative.com/static/js/render.v1.js', + preconnect: 'https://api.adsnative.com', + }, + + 'adspeed': { + preconnect: 'https://g.adspeed.net', + renderStartImplemented: true, + }, + + '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', + ], + }, + + 'adthrive': { + 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, + }, + + 'adunity': { + preconnect: [ + 'https://content.adunity.com', + ], + renderStartImplemented: true, + }, + + 'aduptech': { + prefetch: 'https://s.d.adup-tech.com/jsapi', + preconnect: [ + 'https://d.adup-tech.com', + 'https://m.adup-tech.com', + ], + renderStartImplemented: true, + }, + + 'adventive': { + preconnect: [ + 'https://ads.adventive.com', + 'https://amp.adventivedev.com', + ], + renderStartImplemented: true, + }, + + 'adverline': { + prefetch: 'https://ads.adverline.com/richmedias/amp.js', + preconnect: [ + 'https://adnext.fr', + ], + renderStartImplemented: true, + }, + + 'adverticum': {}, + + 'advertserve': { + renderStartImplemented: true, + }, + + 'adyoulike': { + consentHandlingOverride: true, + prefetch: 'https://pixels.omnitagjs.com/amp.js', + renderStartImplemented: true, + }, + + 'adzerk': {}, + + 'affiliateb': { + prefetch: 'https://track.affiliate-b.com/amp/a.js', + renderStartImplemented: true, + }, + + 'aja': { + renderStartImplemented: true, + prefetch: 'https://cdn.as.amanad.adtdp.com/sdk/asot-v2.js', + }, + + 'appvador': { + prefetch: [ + 'https://cdn.apvdr.com/js/VastAdUnit.min.js', + 'https://cdn.apvdr.com/js/VideoAd.min.js', + 'https://cdn.apvdr.com/js/VideoAd3PAS.min.js', + 'https://cdn.apvdr.com/js/VideoAdAutoPlay.min.js', + 'https://cdn.apvdr.com/js/VideoAdNative.min.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', + renderStartImplemented: true, + }, + + 'atomx': { + prefetch: 'https://s.ato.mx/p.js', + }, + + 'beopinion': { + prefetch: 'https://widget.beopinion.com/sdk.js', + preconnect: [ + 'https://t.beopinion.com', + 'https://s.beopinion.com', + 'https://data.beopinion.com', + ], + renderStartImplemented: true, + }, + + 'bidtellect': {}, + + 'brainy': {}, + + 'bringhub': { + renderStartImplemented: true, + preconnect: [ + 'https://static.bh-cdn.com', + 'https://core-api.bringhub.io', + ], + }, + + 'broadstreetads': { + prefetch: 'https://cdn.broadstreetads.com/init-2.min.js', + }, + + 'caajainfeed': { + prefetch: [ + 'https://cdn.amanad.adtdp.com/sdk/ajaamp.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', + }, + + 'cedato': { + renderStartImplemented: true, + }, + + 'chargeads': {}, + + 'colombia': { + prefetch: 'https://static.clmbtech.com/ad/commons/js/colombia-amp.js', + }, + + 'connatix': { + renderStartImplemented: true, + }, + + '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', + }, + + 'dable': { + preconnect: [ + 'https://static.dable.io', + 'https://api.dable.io', + 'https://images.dable.io', + ], + renderStartImplemented: true, + }, + + 'directadvert': { + renderStartImplemented: true, + }, + + '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', + }, + + 'eadv': { + renderStartImplemented: true, + clientIdScope: 'AMP_ECID_EADV', + prefetch: [ + 'https://www.eadv.it/track/esr.min.js', + 'https://www.eadv.it/track/ead.min.js', + ], + }, + + 'eas': { + prefetch: 'https://amp.emediate.eu/amp.v0.js', + renderStartImplemented: true, + }, + + 'engageya': {}, + + 'epeex': {}, + + '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', + ], + clientIdScope: 'AMP_ECID_EZOIC', + consentHandlingOverride: true, + }, + + 'f1e': { + prefetch: 'https://img.ak.impact-ad.jp/util/f1e_amp.min.js', + }, + + 'f1h': { + preconnect: 'https://img.ak.impact-ad.jp', + renderStartImplemented: true, + }, + + 'fake': {}, + + 'felmat': { + prefetch: 'https://t.felmat.net/js/fmamp.js', + renderStartImplemented: true, + }, + + 'flite': {}, + + 'fluct': { + preconnect: [ + 'https://cdn-fluct.sh.adingo.jp', + 'https://s.sh.adingo.jp', + 'https://i.adingo.jp', + ], + }, + + 'fusion': { + prefetch: 'https://assets.adtomafusion.net/fusion/latest/fusion-amp.min.js', + }, + + 'genieessp': { + prefetch: 'https://js.gsspcln.jp/l/amp.js', + }, + + 'giraff': { + renderStartImplemented: true, + }, + + 'gmossp': { + prefetch: 'https://cdn.gmossp-sp.jp/ads/amp.js', + }, + + 'gumgum': { + prefetch: 'https://g2.gumgum.com/javascripts/ad.js', + renderStartImplemented: true, + }, + + 'holder': { + prefetch: 'https://i.holder.com.ua/js2/holder/ajax/ampv1.js', + preconnect: 'https://h.holder.com.ua', + renderStartImplemented: true, + }, + + 'ibillboard': {}, + + 'imedia': { + prefetch: 'https://i.imedia.cz/js/im3.js', + renderStartImplemented: true, + }, + + 'imobile': { + prefetch: 'https://spamp.i-mobile.co.jp/script/amp.js', + preconnect: 'https://spad.i-mobile.co.jp', + }, + 'imonomy': { + renderStartImplemented: true, + }, + '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, + }, + + 'innity': { + prefetch: 'https://cdn.innity.net/admanager.js', + preconnect: 'https://as.innity.com', + renderStartImplemented: true, + }, + + 'ix': { + prefetch: [ + 'https://js-sec.indexww.com/apl/amp.js', + ], + preconnect: 'https://as-sec.casalemedia.com', + renderStartImplemented: true, + }, + + 'kargo': {}, + + 'kiosked': { + renderStartImplemented: true, + }, + + 'kixer': { + prefetch: 'https://cdn.kixer.com/ad/load.js', + renderStartImplemented: true, + }, + + 'kuadio': {}, + + 'ligatus': { + prefetch: 'https://ssl.ligatus.com/render/ligrend.js', + renderStartImplemented: true, + }, + + 'lockerdome': { + prefetch: 'https://cdn2.lockerdomecdn.com/_js/amp.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://amp.mediavine.com/wrapper.min.js', + preconnect: [ + 'https://partner.googleadservices.com', + 'https://securepubads.g.doubleclick.net', + 'https://tpc.googlesyndication.com', + ], + renderStartImplemented: true, + consentHandlingOverride: true, + }, + + 'medyanet': { + 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', + ], + }, + + 'miximedia': { + renderStartImplemented: true, + }, + + 'mixpo': { + prefetch: 'https://cdn.mixpo.com/js/loader.js', + preconnect: [ + 'https://player1.mixpo.com', + 'https://player2.mixpo.com', + ], + }, + + 'monetizer101': { + renderStartImplemented: true, + }, + + 'mytarget': { + prefetch: 'https://ad.mail.ru/static/ads-async.js', + renderStartImplemented: true, + }, + + 'mywidget': { + preconnect: 'https://likemore-fe.go.mail.ru', + prefetch: 'https://likemore-go.imgsmail.ru/widget_amp.js', + renderStartImplemented: true, + }, + + 'nativo': { + prefetch: 'https://s.ntv.io/serve/load.js', + }, + + 'navegg': { + renderStartImplemented: true, + }, + + 'nend': { + prefetch: 'https://js1.nend.net/js/amp.js', + preconnect: [ + 'https://output.nend.net', + 'https://img1.nend.net', + ], + }, + + 'netletix': { + preconnect: [ + 'https://call.netzathleten-media.de', + ], + renderStartImplemented: true, + }, + + 'noddus': { + prefetch: 'https://noddus.com/amp_loader.js', + renderStartImplemented: true, + }, + + '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, + }, + + 'outbrain': { + renderStartImplemented: true, + prefetch: 'https://widgets.outbrain.com/widgetAMP/outbrainAMP.min.js', + preconnect: [ + 'https://odb.outbrain.com', + ], + consentHandlingOverride: true, + }, + + 'pixels': { + prefetch: 'https://cdn.adsfactor.net/amp/pixels-amp.min.js', + clientIdCookieName: '__AF', + renderStartImplemented: true, + }, + + 'plista': {}, + + 'polymorphicads': { + prefetch: 'https://www.polymorphicads.jp/js/amp.js', + preconnect: [ + 'https://img.polymorphicads.jp', + 'https://ad.polymorphicads.jp', + ], + renderStartImplemented: true, + }, + + 'popin': { + renderStartImplemented: true, + }, + + 'postquare': {}, + + 'pressboard': { + renderStartImplemented: true, + }, + + 'pubexchange': {}, + + 'pubguru': { + 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, + }, + + 'quoraad': { + prefetch: 'https://a.quora.com/amp_ad.js', + preconnect: 'https://ampad.quora.com', + renderStartImplemented: true, + }, + + 'realclick': { + renderStartImplemented: true, + }, + + 'recomad': { + 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, + }, + + 'revjet': { + prefetch: 'https://cdn.revjet.com/~cdn/JS/03/amp.js', + renderStartImplemented: true, + }, + + 'rfp': { + prefetch: 'https://js.rfp.fout.jp/rfp-amp.js', + preconnect: 'https://ad.rfp.fout.jp', + renderStartImplemented: true, + }, + + 'rubicon': {}, + + 'runative': { + prefetch: 'https://cdn.run-syndicate.com/sdk/v1/n.js', + renderStartImplemented: true, + }, + + 'sekindo': { + renderStartImplemented: true, + }, + + 'sharethrough': { + renderStartImplemented: true, + }, + + 'sklik': { + prefetch: 'https://c.imedia.cz/js/amp.js', + }, + + 'slimcutmedia': { + preconnect: [ + 'https://sb.freeskreen.com', + 'https://static.freeskreen.com', + 'https://video.freeskreen.com', + ], + renderStartImplemented: true, + }, + + '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, + }, + + 'smi2': { + renderStartImplemented: true, + }, + + 'sogouad': { + prefetch: 'https://theta.sogoucdn.com/wap/js/aw.js', + 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', + ], + renderStartImplemented: true, + }, + + 'sovrn': { + prefetch: 'https://ap.lijit.com/www/sovrn_amp/sovrn_ads.js', + }, + + 'spotx': { + preconnect: 'https://js.spotx.tv', + renderStartImplemented: true, + }, + + 'sunmedia': { + prefetch: 'https://vod.addevweb.com/sunmedia/amp/ads/sunmedia.js', + preconnect: 'https://static.addevweb.com', + renderStartImplemented: true, + }, + + 'swoop': { + prefetch: 'https://www.swoop-amp.com/amp.js', + preconnect: [ + 'https://www.swpsvc.com', + 'https://client.swpcld.com', + ], + renderStartImplemented: true, + }, + + 'taboola': {}, + + 'teads': { + prefetch: 'https://a.teads.tv/media/format/v3/teads-format.min.js', + preconnect: [ + 'https://cdn2.teads.tv', + 'https://t.teads.tv', + 'https://r.teads.tv', + ], + consentHandlingOverride: true, + }, + + 'triplelift': {}, + + 'trugaze': { + clientIdScope: '__tg_amp', + renderStartImplemented: true, + }, + + 'uas': { + prefetch: 'https://ads.pubmatic.com/AdServer/js/phoenix.js', + }, + + 'uzou': { + preconnect: [ + 'https://speee-ad.akamaized.net', + ], + renderStartImplemented: true, + }, + + 'unruly': { + prefetch: 'https://video.unrulymedia.com/native/native-loader.js', + renderStartImplemented: true, + }, + + 'valuecommerce': { + prefetch: 'https://amp.valuecommerce.com/amp_bridge.js', + preconnect: [ + 'https://ad.jp.ap.valuecommerce.com', + 'https://ad.omks.valuecommerce.com', + ], + renderStartImplemented: true, + }, + + 'videointelligence': { + preconnect: 'https://s.vi-serve.com', + renderStartImplemented: true, + }, + + 'videonow': { + renderStartImplemented: true, + }, + + 'viralize': { + renderStartImplemented: true, + }, + + 'vmfive': { + prefetch: 'https://man.vm5apis.com/dist/adn-web-sdk.js', + preconnect: [ + 'https://vawpro.vm5apis.com', + 'https://vahfront.vm5apis.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': {}, + + 'wisteria': { + renderStartImplemented: true, + }, + + 'wpmedia': { + prefetch: 'https://std.wpcdn.pl/wpjslib/wpjslib-amp.js', + preconnect: [ + 'https://www.wp.pl', + 'https://v.wpimg.pl', + ], + renderStartImplemented: true, + }, + + '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', + }, + + 'yandex': { + prefetch: 'https://yastatic.net/partner-code/loaders/context_amp.js', + renderStartImplemented: true, + }, + + 'yengo': { + renderStartImplemented: true, + }, + + '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.1.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', + }, + + 'yieldpro': { + preconnect: 'https://creatives.yieldpro.eu', + renderStartImplemented: true, + }, + + 'zedo': { + prefetch: 'https://ss3.zedo.com/gecko/tag/Gecko.amp.min.js', + renderStartImplemented: true, + }, + + 'zen': { + prefetch: 'https://zen.yandex.ru/widget-loader', + preconnect: [ + 'https://yastatic.net/', + ], + 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 new file mode 100644 index 000000000000..172609e6f2a8 --- /dev/null +++ b/ads/_integration-guide.md @@ -0,0 +1,94 @@ +# Guidelines for Integrating with AMP + +If you are an ad technology provider looking to integrate with AMP HTML, please see the guidelines below. +To ensure minimum latency and quality, please follow the instructions listed [here](../3p/README.md#ads) before submitting a pull request to the AMP open-source project. For general guidance on how to get started with contributing to the AMP project, please see [here](../CONTRIBUTING.md). + +## Ad Server + +*Examples : DFP, A9* + +As an ad server, publishers you support include a JavaScript library provided by you and place various "ad snippets" that rely on the JavaScript library to fetch ads and render them on the publisher’s website. + +Because AMP doesn’t allow publishers to execute arbitrary JavaScript, you will need to contribute to the AMP open-source code to allow the `amp-ad` tag to request ads from your ad server. + +For example : Amazon A9 server can be invoked by using following syntax: + +```html + + +``` + +Note that each of the attributes that follow `type` are dependent on the parameters that the Amazon’s A9 server expects in order to deliver an ad. The [a9.js](./a9.js) file shows you how the parameters are mapped to making a JavaScript call which invokes the A9 server via the `https://c.amazon-adsystem.com/aax2/assoc.js` URL. The corresponding parameters passed by the AMP ad tag are appended to the URL to return an ad. + +For details on how to integrate your ad network with AMP, see [Integrating ad networks into AMP](https://github.com/ampproject/amphtml/blob/master/ads/README.md). + +## Supply Side Platform (SSP) or an Ad Exchange + +*Examples : Rubicon, Criteo OR Appnexus, Ad-Exchange* + +If you are a sell-side platform that wants to get called directly from a publisher’s webpage, you will need to follow the same directions as listed above for integrating with an Ad Server. Adding your own `type` value to the amp-ad tag allows you to distribute your tag directly to the publisher, so they can insert your tags directly into their AMP pages. + +More commonly, SSPs work with the publisher to traffick the SSP’s ad tags in their ad server. In this case, ensure that all assets being loaded by your script in the ad server’s creative are being made over HTTPS. There are some restrictions around some ad formats like expandables, so we recommend that you test out the most commonly delivered creative formats with your publishers. + +## Ad Agency +*Examples : Essence, Omnicom* + +Work with your publisher to ensure that the creatives you develop are AMP-compliant. Since all creatives are served into iframes whose size is determined when the ad is called, ensure that your creative doesn't try to modify the size of the iframe. + +Ensure that all assets that are part of the creative are requested using HTTPS. +Some ad formats are not fully supported at the moment and we recommend testing the creatives in an AMP environment. Some examples are : Rich Media Expandables, Interstitials, Page Level Ads. + +## Video Player + +*Examples : Brightcove, Ooyala* + +A video player that works in regular HTML pages will not work in AMP and therefore a specific tag must be created that allows the AMP Runtime to load your player. +Brightcove has created a custom [amp-brightcove](https://github.com/ampproject/amphtml/blob/master/extensions/amp-brightcove/amp-brightcove.md) tag that allows media and ads to be played in AMP pages. + +A Brightcove player can be invoked by the following: + +```html + + +``` +For instructions on how to develop an amp tag like Brightcove, see [this pull request](https://github.com/ampproject/amphtml/pull/1052). + +## Video Ad Network + +*Examples : Tremor, Brightroll* + +If you are a video ad network, please work with your publisher to ensure that: + +- All video assets are served over HTTPS +- The publisher’s video player has AMP support + +## Data Management Platform (DMP) +*Examples : KRUX, Bluekai* + +See [how to enhance custom ad configuration](https://www.ampproject.org/docs/reference/components/amp-ad#enhance-incoming-ad-configuration). + +You can use a similar approach to enrich the ad call by passing in audience segments that you get from the user cookie into the ad call. + +## Viewability Provider + +*Examples : MOAT, Integral Ad Science* + +Viewability providers typically integrate with publishers via the ad server’s creative wrappers. If that is the case, ensure that the creative wrapper loads all assets over HTTPS. + +For e.g. for MOAT, make sure `http://js.moatads.com` is switched to `https://z.moatads.com` + +Also, see the approach to using the [intersection observer pattern](https://github.com/ampproject/amphtml/blob/master/ads/README.md#ad-viewability). + +## Content-Recommendation Platform + +*Examples : Taboola, Outbrain* + +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..a785f5486234 --- /dev/null +++ b/ads/_ping_.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 {dev, user} from '../src/log'; +import {validateData} from '../3p/3p'; + +/** + * A fake ad network integration that is mainly used for testing + * and demo purposes. This implementation gets stripped out in compiled + * production code. + * @param {!Window} global + * @param {!Object} data + */ +export function _ping_(global, data) { + // for testing only. see #10628 + global.networkIntegrationDataParamForTesting = data; + + validateData(data, ['url'], + ['valid', 'adHeight', 'adWidth', 'enableIo']); + user().assert(!data['error'], 'Fake user error!'); + 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]; + }); + } + global.context.getHtml('a', ['href'], function(html) { + dev().info('GET-HTML', html); + }); + global.context.getConsentState(function(consentState) { + dev().info('GET-CONSENT-STATE', consentState); + }); + if (global.context.consentSharedData) { + const TAG = 'consentSharedData'; + dev().info(TAG, global.context.consentSharedData); + } + } else { + global.setTimeout(() => { + global.context.noContentAvailable(); + }, 1000); + } +} diff --git a/ads/_ping_.md b/ads/_ping_.md new file mode 100644 index 000000000000..695e87294a94 --- /dev/null +++ b/ads/_ping_.md @@ -0,0 +1,54 @@ + + +# \_PING_ + +A fake ad type that is only used for local development. + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the [ad network](#configuration) or refer to their [documentation](#ping). + +### Required parameters + +- `data-url` : Image ad with the image. + +### Optional parameters + +- `data-valid` : Set to false to return a no fill ad. +- `data-ad-height` : Ad image size. +- `data-ad-width` : Ad image width. +- `data-enable-io` : Enable logging IntersectionObserver entry. + +## User Consent Integration + +When [user consent](https://github.com/ampproject/amphtml/blob/master/extensions/amp-consent/amp-consent.md#blocking-behaviors) is required. \_Ping_ ad approaches user consent in the following ways: + +- `CONSENT_POLICY_STATE.SUFFICIENT`: Serve a personalized ad to the user. +- `CONSENT_POLICY_STATE.INSUFFICIENT`: Serve a non-personalized ad to the user. +- `CONSENT_POLICY_STATE.UNKNOWN_NOT_REQUIRED`: Serve a personalized ad to the user. +- `CONSENT_POLICY_STATE.UNKNOWN`: Will not serve an ad to the user. diff --git a/ads/a8.js b/ads/a8.js new file mode 100644 index 000000000000..6c9ac98ddd98 --- /dev/null +++ b/ads/a8.js @@ -0,0 +1,27 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function a8(global, data) { + validateData(data, ['aid'], ['wid', 'eno', 'mid', 'mat', 'type']); + global.a8Param = data; + writeScript(global, 'https://statics.a8.net/amp/ad.js'); +} diff --git a/ads/a8.md b/ads/a8.md new file mode 100644 index 000000000000..89a571ee4aeb --- /dev/null +++ b/ads/a8.md @@ -0,0 +1,51 @@ + +# A8net + +Serves ads from [a8.net](https://www.a8.net/). + +## Example + +### Normal ad + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://support.a8.net/as/contact/form/conentry.php. + +### Required parameters + +- `data-aid` + +### Optional parameters + +- `data-wid` +- `data-eno` +- `data-mid` +- `data-mat` +- `data-type` + diff --git a/ads/a9.js b/ads/a9.js index 8be7698014e0..b36900ca7377 100644 --- a/ads/a9.js +++ b/ads/a9.js @@ -14,17 +14,147 @@ * limitations under the License. */ -import {writeScript, checkData} from '../src/3p'; +import {hasOwn} from '../src/utils/object'; +import {loadScript, validateData, writeScript} from '../3p/3p'; +import {parseJson} from '../src/json'; + +const mandatoryParams = [], + optionalParams = [ + 'ad_mode', 'placement', 'tracking_id', 'ad_type', + 'marketplace', 'region', 'title', + 'default_search_phrase', 'default_category', 'linkid', + 'search_bar', 'search_bar_position', 'rows', + 'design', 'asins', 'debug', 'aax_src_id', + 'header_style', 'link_style', 'link_hover_style', + 'text_style', 'random_permute', 'render_full_page', + 'axf_exp_name', 'axf_treatment', 'disable_borders', + 'attributes', 'carousel', 'feedback_enable', + 'max_ads_in_a_row', 'list_price', 'prime', + 'prime_position', 'widget_padding', + 'strike_text_style', 'brand_text_link', 'brand_position', + 'large_rating', 'rating_position', 'max_title_height', + 'enable_swipe_on_mobile', 'overrides', + 'ead', 'force_win_bid', 'fallback_mode', 'url', + 'regionurl', 'divid', 'recomtype', 'adinstanceid', + ]; +const prefix = 'amzn_assoc_'; /** * @param {!Window} global * @param {!Object} data */ export function a9(global, data) { - checkData(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; - global.aax_src = data.aax_src; - writeScript(global, 'https://c.amazon-adsystem.com/aax2/assoc.js'); + let i; + for (i = 0; i < 46; i++) { + optionalParams[i] = prefix + optionalParams[i]; + } + + validateData(data, mandatoryParams, optionalParams); + + if (data.amzn_assoc_ad_mode) { + if (data.amzn_assoc_ad_mode === 'auto') { + if (data.adinstanceid && + (data.adinstanceid !== '')) { + loadRecTag(global, data); + } + else { + loadSearTag(global, data); + } + } + else if ((data.amzn_assoc_ad_mode === 'search') + || (data.amzn_assoc_ad_mode === 'manual')) { + loadSearTag(global, data); + } + } + else { + loadSearTag(global, data); + } +} + +/** + * @param {!Object} data + */ +function getURL(data) { + let url = 'https://z-na.amazon-adsystem.com/widgets/onejs?MarketPlace=US'; + if (data.regionurl && (data.regionurl !== '')) { + url = data.regionurl; + } + + return url; +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function loadRecTag(global, data) { + let url = getURL(data); + url += '&adInstanceId=' + data['adinstanceid']; + if (data['recomtype'] === 'sync') { + writeScript(global, url); + } + else if (data['recomtype'] === 'async') { + const d = global.document.createElement('div'); + d.setAttribute('id', data['divid']); + global.document.getElementById('c').appendChild(d); + loadScript(global, url); + } +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function loadSearTag(global, data) { + /** + * Sets macro type. + * @param {string} type + */ + function setMacro(type) { + if (!type) { + return; + } + + if (hasOwn(data, type)) { + global[type] = data[type]; + } + } + + /** + * Sets the params. + */ + function setParams() { + const url = getURL(data); + let i; + + for (i = 0; i < 44; i++) { + setMacro(optionalParams[i]); + } + + if (data.amzn_assoc_fallback_mode) { + const str = (data.amzn_assoc_fallback_mode).split(','); + let types = str[0].split(':'); + let typev = str[1].split(':'); + types = parseJson(types[1]); + typev = parseJson(typev[1]); + global['amzn_assoc_fallback_mode'] = { + 'type': types, + 'value': typev, + }; + } + if (data.amzn_assoc_url) { + global['amzn_assoc_URL'] = data['amzn_assoc_url']; + } + + writeScript(global, url); + } + + /** + * Initializer. + */ + function init() { + setParams(); + } + + init(); } diff --git a/ads/a9.md b/ads/a9.md index 544423099222..c0699622a01c 100644 --- a/ads/a9.md +++ b/ads/a9.md @@ -1,38 +1,104 @@ - + # A9 - -## Example - + +The A9 amp ad unit supports the Search, Recommendation and Custom ad widgets of the Native Shopping Ads program. These widgets would need to be created here first `https://affiliate-program.amazon.com/home/ads` + +# Examples + + +## Recommendation Ad + +### Sync ```html - + ``` -## Configuration +### Async +```html +

A9 Recom Ads Async

+ + +``` -For semantics of configuration, please see ad network documentation. +## Search Ad + +```html + + +``` + +## Custom Ad + +```html + + +``` + +# Configuration + +## Required parameters + +- `data-amzn_assoc_ad_mode` - specify the ad type as search, manual or auto (for recommendation ads) + +## Optional parameters + +We have default ```regionurl``` as ```//z-na.amazon-adsystem.com/widgets/onejs?MarketPlace=US``` but this can be custom provided. The parameters ```amzn_assoc_width, amzn_assoc_height, amzn_assoc_size``` have been replaced as AMP provide custom setting of height and width (as specified above in the amp-ad node). For recommendation, we have ```recomtype``` specifying it is async or sync. -Supported parameters: +All other configurations supported by the NSA ads would be supported here and would depend on the ad mode. The ad parameters need to be prefixed with "data-" -- data-aax_size -- data-aax_pubname -- data-aax_src +# Support + +For further queries, please feel free to reach out to your contact at Amazon Affiliates. + +Otherwise you can write to our support team: Email: nsasupport@amazon.com diff --git a/ads/accesstrade.js b/ads/accesstrade.js new file mode 100644 index 000000000000..8d4bf0239fdc --- /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 {validateData, writeScript} 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..68f42b120508 --- /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/adagio.js b/ads/adagio.js new file mode 100755 index 000000000000..5360b3b4b84c --- /dev/null +++ b/ads/adagio.js @@ -0,0 +1,33 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adagio(global, data) { + + validateData(data, ['sid', 'loc']); + + const $neodata = global; + + $neodata._adagio = {}; + $neodata._adagio.amp = data; + + loadScript($neodata, 'https://js-ssl.neodatagroup.com/adagio_amp.js'); +} diff --git a/ads/adagio.md b/ads/adagio.md new file mode 100755 index 000000000000..84e46ee9679d --- /dev/null +++ b/ads/adagio.md @@ -0,0 +1,41 @@ + + +# Ad.Agio - Neodata Group + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +Supported parameters: + +- `data-sid` +- `data-loc` +- `data-keywords` +- `data-uservars` + diff --git a/ads/adblade.js b/ads/adblade.js new file mode 100644 index 000000000000..859c8223f89e --- /dev/null +++ b/ads/adblade.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 {validateData, writeScript} from '../3p/3p'; + +const adbladeFields = ['width', 'height', 'cid']; +const adbladeHostname = 'web.adblade.com'; +const industrybrainsHostname = 'web.industrybrains.com'; + +/** + * @param {string} hostname + * @param {!Window} global + * @param {!Object} data + */ +function addAdiantUnit(hostname, global, data) { + validateData(data, adbladeFields, []); + + // create a data element so our script knows what to do + const ins = global.document.createElement('ins'); + ins.setAttribute('class', 'adbladeads'); + ins.setAttribute('data-width', data.width); + ins.setAttribute('data-height', data.height); + ins.setAttribute('data-cid', data.cid); + ins.setAttribute('data-host', hostname); + ins.setAttribute('data-protocol', 'https'); + 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'); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adblade(global, data) { + addAdiantUnit(adbladeHostname, global, data); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function industrybrains(global, data) { + addAdiantUnit(industrybrainsHostname, global, data); +} diff --git a/ads/adblade.md b/ads/adblade.md new file mode 100644 index 000000000000..34de979121b5 --- /dev/null +++ b/ads/adblade.md @@ -0,0 +1,38 @@ + + +# Adblade + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, see [Adblade's documentation](https://www.adblade.com/doc/publisher-solutions). + +Supported parameters: + +- `data-cid` +- `data-width` +- `data-height` diff --git a/ads/adbutler.js b/ads/adbutler.js new file mode 100644 index 000000000000..11c89fcdc3fc --- /dev/null +++ b/ads/adbutler.js @@ -0,0 +1,61 @@ +/** + * 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) { + validateData( + data, + ['account', 'zone', 'width', 'height'], + ['keyword', 'place'] + ); + + data['place'] = data['place'] || 0; + + const 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(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..b4bffd33cf5f --- /dev/null +++ b/ads/adbutler.md @@ -0,0 +1,44 @@ + + +# AdButler + +Serves ads from [AdButler](https://www.adbutler.com/). + +## Example + +```html + + +``` +## Configuration + +For details on the configuration semantics, please see [AdButler's documentation](http://www.adbutlerhelp.com/amp-configuration). + +### Required parameters + +- `width` +- `height` +- `data-account` +- `data-zone` + +### Optional parameters + +- `data-place` +- `data-keyword` diff --git a/ads/adform.js b/ads/adform.js new file mode 100644 index 000000000000..f73939f270ba --- /dev/null +++ b/ads/adform.js @@ -0,0 +1,54 @@ +/** + * 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, validateSrcPrefix, writeScript} from '../3p/3p'; + +// Valid adform ad source hosts +const hosts = { + track: 'https://track.adform.net', + adx: 'https://adx.adform.net', + a2: 'https://a2.adform.net', + adx2: 'https://adx2.adform.net', + asia: 'https://asia.adform.net', + adx3: 'https://adx3.adform.net', +}; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adform(global, data) { + validateData(data, [['src', 'bn', 'mid']]); + global.Adform = {ampData: data}; + const {src, bn, mid} = data; + let url; + + // Custom ad url using "data-src" attribute + if (src) { + validateSrcPrefix(Object.keys(hosts).map(type => hosts[type]), src); + url = src; + } + // Ad tag using "data-bn" attribute + else if (bn) { + url = hosts.track + '/adfscript/?bn=' + encodeURIComponent(bn) + ';msrc=1'; + } + // Ad placement using "data-mid" attribute + else if (mid) { + url = hosts.adx + '/adx/?mid=' + encodeURIComponent(mid); + } + + writeScript(global, url); +} diff --git a/ads/adform.md b/ads/adform.md new file mode 100644 index 000000000000..bca5b8a80e72 --- /dev/null +++ b/ads/adform.md @@ -0,0 +1,65 @@ + + +# Adform + +## Examples + +### Simple ad tag with `data-bn` + +```html + + +``` + +### Ad placement with `data-mid` + +```html + + +``` + +### Ad tag or placement with `src` + +```html + + +``` + +## Configuration + +Please refer to [Adform Help Center](https://www.adform.com) for more +information on how to get required ad tag or placement IDs. + +### Supported parameters + +Only one of the mentioned parameters should be used at the same time. + +- `data-bn` +- `data-mid` +- `src`: must use https protocol and must be from one of the +allowed Adform hosts. + + + + + diff --git a/ads/adfox.js b/ads/adfox.js new file mode 100644 index 000000000000..260cccc2fff3 --- /dev/null +++ b/ads/adfox.js @@ -0,0 +1,82 @@ +/** + * 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} from '../3p/3p'; +import {yandex} from './yandex'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adfox(global, data) { + validateData(data, ['adfoxParams', 'ownerId']); + loadScript(global, 'https://yastatic.net/pcode/adfox/loader.js', + () => initAdFox(global, data)); +} + +/** + * @param {!Window} global + * @param {Object} data + */ +function initAdFox(global, data) { + const params = JSON.parse(data.adfoxParams); + + createContainer(global, 'adfox_container'); + + global.Ya.adfoxCode.create({ + ownerId: data.ownerId, + containerId: 'adfox_container', + params, + onLoad: (data, onRender, onError) => { + checkLoading(global, data, onRender, onError); + }, + onRender: () => global.context.renderStart(), + onError: () => global.context.noContentAvailable(), + onStub: () => global.context.noContentAvailable(), + }); +} + +/** + * @param {!Window} global + * @param {!Object} data + * @param {!Object} onRender + * @param {!Object} onError + * @return {boolean} + */ +function checkLoading(global, data, onRender, onError) { + if (data.bundleName === 'banner.direct') { + const dblParams = { + blockId: data.bundleParams.blockId, + data: data.bundleParams.data, + onRender, + onError, + }; + + yandex(global, dblParams); + return false; + } + return true; +} + +/** + * @param {!Window} global + * @param {string} id + */ +function createContainer(global, id) { + const container = global.document.createElement('div'); + container.setAttribute('id', id); + global.document.getElementById('c').appendChild(container); +} diff --git a/ads/adfox.md b/ads/adfox.md new file mode 100644 index 000000000000..aa15c05a3fc5 --- /dev/null +++ b/ads/adfox.md @@ -0,0 +1,36 @@ + + +# AdFox + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please see [AdFox's documentation](https://specs.adfox.ru/page/254/). + +### Required parameters + +- `data-owner-id` +- `data-adfox-params` diff --git a/ads/adgeneration.js b/ads/adgeneration.js new file mode 100644 index 000000000000..9ffbfd53d215 --- /dev/null +++ b/ads/adgeneration.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. + */ + +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adgeneration(global, data) { + validateData(data, ['id'], + ['targetid', 'displayid', 'adtype', '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) + + '&async=true' + + '&adType=' + + (validateAdType(data.adType)) + + '&displayid=' + + (data.displayid ? encodeURIComponent(data.displayid) : '1') + + '&tagver=2.0.0' + + (data.targetid ? '&targetID=' + encodeURIComponent(data.targetid) : '') + + (option ? '&' + option : ''); + loadScript(global, url); +} + +/** + * URL encoding of query string + * @param {string} str + * @return {string} + */ +function encodeQueryValue(str) { + return str.split('&').map(v => { + const key = v.split('=')[0], + val = v.split('=')[1]; + return encodeURIComponent(key) + '=' + encodeURIComponent(val); + }).join('&'); +} +/** + * If adtype is "RECTANGLE", replace it with "RECT" + * @param {string} str + * @return {string} + */ +function validateAdType(str) { + if (str != null) { + const upperStr = encodeURIComponent(str.toUpperCase()); + if (upperStr === 'RECTANGLE') { + return 'RECT'; + } else { + return upperStr; + } + } + return 'FREE'; +} diff --git a/ads/adgeneration.md b/ads/adgeneration.md new file mode 100644 index 000000000000..1737f98d306d --- /dev/null +++ b/ads/adgeneration.md @@ -0,0 +1,38 @@ + + +# Ad Generation + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please see [Ad Generation's documentation](https://github.com/AdGeneration/sdk/wiki). + +Supported parameters: + +- `data-id` +- `data-targetid` +- `data-displayid` +- `data-adtype` +- `data-option` diff --git a/ads/adhese.js b/ads/adhese.js new file mode 100644 index 000000000000..9e2ac8d99b06 --- /dev/null +++ b/ads/adhese.js @@ -0,0 +1,65 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adhese(global, data) { + validateData(data, ['location','format', 'account', 'requestType']); + let targetParam = ''; + if (data['targeting']) { + const targetList = data['targeting']; + for (const category in targetList) { + targetParam += encodeURIComponent(category); + const targets = targetList[category]; + for (let x = 0; x < targets.length; x++) { + targetParam += encodeURIComponent(targets[x]) + + (targets.length - 1 > x ? ';' : ''); + } + targetParam += '/'; + } + } + targetParam += '?t=' + Date.now(); + writeScript(window, 'https://ads-' + encodeURIComponent(data['account']) + + '.adhese.com/' + encodeURIComponent(data['requestType']) + '/sl' + + encodeURIComponent(data['location']) + + encodeURIComponent(data['position']) + '-' + + encodeURIComponent(data['format']) + '/' + targetParam); + const co = global.document.querySelector('#c'); + co.width = data['width']; + co.height = data['height']; + co.addEventListener('adhLoaded', getAdInfo.bind(null, global), false); +} + +/** + * @param {!Window} global + * @param {!Object} e + */ +function getAdInfo(global, e) { + if (e.detail.isReady && e.detail.width == e.target.width && + e.detail.height == e.target.height) { + global.context.renderStart(); + } else if (e.detail.isReady && (e.detail.width != e.target.width || + e.detail.height != e.target.height)) { + global.context.renderStart({width: e.detail.width, + height: e.detail.height}); + } else { + global.context.noContentAvailable(); + } +} diff --git a/ads/adhese.md b/ads/adhese.md new file mode 100644 index 000000000000..ec90f44d411c --- /dev/null +++ b/ads/adhese.md @@ -0,0 +1,67 @@ + + +# Adhese + +Serves ads from [Adhese](https://www.adhese.com). + +## Example + +### Basic setup + +```html + + +``` + +### With additional parameters + +```html + + +``` + + +## Configuration + +For details on the configuration semantics, see the [Adhese website](https://www.adhese.com) or contact Adhese support. + +### Required parameters + +- `data-account` +- `data-request_type` +- `data-location` +- `data-position` +- `data-format` + +### Optional parameter + +The following optional parameter is supported via the 'json' attribute: + +- `targeting` diff --git a/ads/adincube.js b/ads/adincube.js new file mode 100644 index 000000000000..a82b0d135736 --- /dev/null +++ b/ads/adincube.js @@ -0,0 +1,62 @@ +/** + * Copyright 2018 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 {hasOwn} from '../src/utils/object'; +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adincube(global, data) { + validateData(data, ['adType', 'siteKey'], ['params']); + + let url = global.context.location.protocol; + url += '//tag.adincube.com/tag/1.0/next?'; + url += 'ad_type=' + encodeURIComponent(String(data['adType']).toUpperCase()); + url += '&ad_subtype=' + encodeURIComponent(data['width']); + url += 'x' + encodeURIComponent(data['height']); + url += '&site_key=' + encodeURIComponent(data['siteKey']); + url += '&r=' + encodeURIComponent(global.context.referrer); + url += '&h=' + encodeURIComponent(global.context.location.href); + url += '&c=amp'; + url += '&t=' + Date.now(); + + if (data['params']) { + url += parseParams(data['params']); + } + + loadScript(global, url); +} + +/** + * @param {string} data + * @return {string} + */ +function parseParams(data) { + try { + const params = JSON.parse(data); + let queryParams = ''; + for (const p in params) { + if (hasOwn(params, p)) { + queryParams += '&' + p + '=' + encodeURIComponent(params[p]); + } + } + return queryParams; + } catch (e) { + return ''; + } +} diff --git a/ads/adincube.md b/ads/adincube.md new file mode 100644 index 000000000000..82c8653784a8 --- /dev/null +++ b/ads/adincube.md @@ -0,0 +1,63 @@ + + +# AdinCube + +Visit [dashboard.adincube.com](https://dashboard.adincube.com/dashboard) to create a publisher account and get access to our AMP ads. + +## Examples + +### In content + +Uses fixed size by the given `width` and `height`. + +```html + + +``` + +### Sticky banner +Uses fixed size by the given `width` and `height`. + +```html + + + + +``` + +Refer to the [amp-sticky-ad](https://www.ampproject.org/docs/reference/components/amp-sticky-ad) documentation to see how to implement this ad. + + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + + +### Required parameters + +* `data-ad-type` - type of the ad +* `data-site-key` - unique key attached to a website + +### Optional parameters + +* `data-params` - additional config parameters diff --git a/ads/adition.js b/ads/adition.js new file mode 100644 index 000000000000..475898336e26 --- /dev/null +++ b/ads/adition.js @@ -0,0 +1,28 @@ +/** + * 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, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adition(global, data) { + validateData(data, ['version']); + global.data = data; + writeScript(global, 'https://imagesrv.adition.com/js/amp/v' + + encodeURIComponent(data['version']) + '.js'); +} diff --git a/ads/adition.md b/ads/adition.md new file mode 100644 index 000000000000..dd9905652013 --- /dev/null +++ b/ads/adition.md @@ -0,0 +1,40 @@ + + +# ADITION technologies AG + +ADITION technologies AG specializes in the development and distribution of its own ad server technology, which is called ADITION ad serving. The ADITION ad server now supports AMP. + +For more information, visit [www.adition.com](https://www.adition.com). + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration and examples, sign-in and see the [ADITION documentation](https://wiki.adition.com/index.php?title=ADITION_technologies_AG:Manual/AMP) or [contact ADITION](https://www.adition.com/en/contact/). + +Supported parameters: + +- `data-version` +- `data-wp_id` diff --git a/ads/adman.js b/ads/adman.js new file mode 100644 index 000000000000..d52e3a40738c --- /dev/null +++ b/ads/adman.js @@ -0,0 +1,35 @@ +/** + * 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'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adman(global, data) { + 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); + script.setAttribute('data-tech', 'amp'); + + script.src = 'https://static.adman.gr/adman.js'; + + global.document.body.appendChild(script); +} diff --git a/ads/adman.md b/ads/adman.md new file mode 100644 index 000000000000..47876f3aa8b2 --- /dev/null +++ b/ads/adman.md @@ -0,0 +1,38 @@ + + +# Adman + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please see [Adman documentation](http://www.adman.gr/docs). + +### Required parameters + +- `data-ws` - Ad unit unique id +- `data-s` - Ad unit size +- `data-host` - SSL enabled Adman service domain diff --git a/ads/admanmedia.js b/ads/admanmedia.js new file mode 100644 index 000000000000..2e67e0dcc35c --- /dev/null +++ b/ads/admanmedia.js @@ -0,0 +1,35 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function admanmedia(global, data) { + validateData(data, ['id']); + + const encodedId = encodeURIComponent(data.id); + loadScript(global, `https://mona.admanmedia.com/go?id=${encodedId}`, () => { + const pattern = `script[src$="id=${encodedId}"]`; + const scriptTag = global.document.querySelector(pattern); + scriptTag.setAttribute('id', `hybs-${encodedId}`); + global.context.renderStart(); + }, () => { + global.context.noContentAvailable(); + }); +} diff --git a/ads/admanmedia.md b/ads/admanmedia.md new file mode 100644 index 000000000000..7b2acb79b7d2 --- /dev/null +++ b/ads/admanmedia.md @@ -0,0 +1,36 @@ + + +# Admanmedia + +Please visit [www.admanmedia.com](http://www.admanmedia.com) for more details. + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, see [Admanmedia documentation](http://www.admanmedia.com). + +### Required parameters + +- `data-id` - Ad unit unique id diff --git a/ads/admixer.js b/ads/admixer.js new file mode 100644 index 000000000000..34a47bc6e20e --- /dev/null +++ b/ads/admixer.js @@ -0,0 +1,44 @@ +/** + * Copyright 2018 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 {tryParseJson} from '../src/json'; +import {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function admixer(global, data) { + validateData(data, ['zone'], ['sizes']); + /** + * @type {Object} + */ + const payload = { + imps: [], + referrer: window.context.referrer, + }; + const imp = { + params: { + zone: data.zone, + }, + }; + if (data.sizes) { + imp.sizes = tryParseJson(data.sizes); + } + payload.imps.push(imp); + const json = JSON.stringify(/** @type {JsonObject} */ (payload)); + writeScript(global, 'https://inv-nets.admixer.net/ampsrc.js?data=' + json); +} diff --git a/ads/admixer.md b/ads/admixer.md new file mode 100644 index 000000000000..ae56487f1435 --- /dev/null +++ b/ads/admixer.md @@ -0,0 +1,40 @@ + + +# Admixer + +## Example + +```html + + +``` + +## Configuration + +For configuration semantics, see [Admixer documentation](http://docs.admixer.net/3/en/topic/amp-configuration). + +### Required parameters + +- `data-zone` + +### Optional parameters + +- `data-sizes` + diff --git a/ads/adocean.js b/ads/adocean.js new file mode 100644 index 000000000000..5e3e4ec6808f --- /dev/null +++ b/ads/adocean.js @@ -0,0 +1,192 @@ +/** + * 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 {parseJson} from '../src/json'; +import {validateData, writeScript} from '../3p/3p'; + +/** + * @const {Object} + */ +const ADO_JS_PATHS = { + 'sync': '/files/js/ado.js', + 'buffered': '/files/js/ado.FIF.0.99.3.js', +}; + +/** + * @param {string} str + * @return {boolean} + */ +function isFalseString(str) { + return /^(?:false|off)?$/i.test(str); +} + +/** + * @param {string} mode + * @param {!Window} global + */ +function setupAdoConfig(mode, global) { + if (global['ado']) { + const config = { + mode: (mode == 'sync') ? 'old' : 'new', + protocol: 'https:', + fif: { + enabled: mode != 'sync', + }, + }; + + global['ado']['config'](config); + } +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function setupPreview(global, data) { + if (global.ado && data.aoPreview && !isFalseString(data.aoPreview)) { + global.ado.preview({ + enabled: true, + emiter: data['aoEmitter'], + id: data['aoPreview'], + }); + } +} + +/** + * @param {string} str + * @return {Object|undefined} + * @throws {SyntaxError} + */ +function parseJSONObj(str) { + return str.match(/^\s*{/) ? parseJson(str) : undefined; +} + +/** + * @param {string} keys + * @return {string|undefined} + */ +function buildKeys(keys) { + return keys || undefined; +} + +/** + * @param {string} vars + * @return {Object|string|undefined} + */ +function buildVars(vars) { + try { + return parseJSONObj(vars); + } catch (e) { + return vars || undefined; + } +} + +/** + * @param {string} clusters + * @return {Object|undefined} + */ +function buildClusters(clusters) { + try { + return parseJSONObj(clusters); + } catch (e) { + // return undefined + } +} + +/** @type {number} */ +let runSyncCount = 0; + +/** + * @param {!Window} global + * @param {function()} cb + */ +function runSync(global, cb) { + global['__aoPrivFnct' + ++runSyncCount] = cb; + /*eslint no-useless-concat: 0*/ + global.document + .write('<' + 'script>__aoPrivFnct' + runSyncCount + '();<' + '/script>'); +} + +/** + * @param {string} mode + * @param {!Window} global + * @param {!Object} data + */ +function appendPlacement(mode, global, data) { + const doc = global.document; + const placement = doc.createElement('div'); + placement.id = data['aoId']; + + const dom = doc.getElementById('c'); + dom.appendChild(placement); + + const config = { + id: data['aoId'], + server: data['aoEmitter'], + keys: buildKeys(data['aoKeys']), + vars: buildVars(data['aoVars']), + clusters: buildClusters(data['aoClusters']), + }; + + if (global.ado) { + if (mode == 'sync') { + runSync(global, function() { + global['ado']['placement'](config); + }); + + runSync(global, function() { + if (!config['_hasAd']) { + window.context.noContentAvailable(); + } + }); + } else { + global['ado']['onAd'](data['aoId'], function(isAd) { + if (!isAd) { + window.context.noContentAvailable(); + } + }); + global['ado']['placement'](config); + } + } +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adocean(global, data) { + validateData(data, [ + 'aoEmitter', + 'aoId', + ], [ + 'aoMode', + 'aoPreview', + 'aoKeys', + 'aoVars', + 'aoClusters', + ]); + + const mode = (data['aoMode'] != 'sync') ? 'buffered' : 'sync'; + const adoUrl = 'https://' + data['aoEmitter'] + ADO_JS_PATHS[mode]; + + writeScript(global, adoUrl, () => { + setupAdoConfig(mode, global); + setupPreview(global, data); + + appendPlacement(mode, global, data); + }); +} diff --git a/ads/adocean.md b/ads/adocean.md new file mode 100644 index 000000000000..8c372dac8a65 --- /dev/null +++ b/ads/adocean.md @@ -0,0 +1,44 @@ + + +# AdOcean + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, see [AdOcean documentation](http://www.adocean-global.com). + +### Required parameters + +- `data-ao-id` - Ad unit unique id +- `data-ao-emitter` - Ad server hostname + +### Optional parameters + +- `data-ao-mode` - sync|buffered - processing mode +- `data-ao-preview` - livepreview configuration id +- `data-ao-keys` - additional configuration, see adserver documentation +- `data-ao-vars` - additional configuration, see adserver documentation +- `data-ao-clusters` - additional configuration,see adserver documentation diff --git a/ads/adpicker.js b/ads/adpicker.js new file mode 100644 index 000000000000..c54662fc497f --- /dev/null +++ b/ads/adpicker.js @@ -0,0 +1,31 @@ +/** + * Copyright 2018 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 adpicker(global, data) { + validateData(data, ['ph']); + const url = 'https://cdn.adpicker.net' + + '/ads/main.js?et=amp' + + '&ph=' + encodeURIComponent(data.ph) + + '&cb=' + Math.floor(89999999 * Math.random() + 10000000); + writeScript(global, url); +} + diff --git a/ads/adpicker.md b/ads/adpicker.md new file mode 100644 index 000000000000..7fef3f918d2d --- /dev/null +++ b/ads/adpicker.md @@ -0,0 +1,35 @@ + + +# AdPicker + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, visit [adpicker.net](https://adpicker.net). + +### Required parameters + +- `data-ph`: the placement id diff --git a/ads/adplugg.js b/ads/adplugg.js new file mode 100644 index 000000000000..e0dafd854009 --- /dev/null +++ b/ads/adplugg.js @@ -0,0 +1,84 @@ +/** + * Copyright 2018 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 {hasOwn} from '../src/utils/object'; +import {loadScript, validateData} from '../3p/3p'; + +/** + * Make an AdPlugg iframe. + * @param {!Window} global + * @param {!Object} data + */ +export function adplugg(global,data) { + // Load ad.js + loadScript(global, 'https://www.adplugg.com/serve/js/ad.js'); + + // Validate the amp-ad attributes. + validateData( + data, + ['accessCode', 'width', 'height'], //required + ['zone'] //optional + ); + + // Get the amp wrapper element. + const ampwrapper = global.document.getElementById('c'); + + // Build and append the ad tag. + const adTag = global.document.createElement('div'); + adTag.setAttribute('class', 'adplugg-tag'); + adTag.setAttribute('data-adplugg-access-code', data['accessCode']); + if (data['zone']) { + adTag.setAttribute('data-adplugg-zone', data['zone']); + } + ampwrapper.appendChild(adTag); + + // Get a handle on the AdPlugg SDK. + global.AdPlugg = (global.AdPlugg || []); + const {AdPlugg} = global; + + // Register event listeners (via async wrapper). + AdPlugg.push(function() { + const {AdPlugg} = global; + // Register the renderStart event listener. + AdPlugg.on( + adTag, + 'adplugg:renderStart', + function(event) { + // Create the opt_data object. + const optData = {}; + if (hasOwn(event, 'width')) { + optData.width = event.width; + } + if (hasOwn(event, 'height')) { + optData.height = event.height; + } + global.context.renderStart(optData); + } + ); + + // Register the noContentAvailable event listener. + AdPlugg.on(adTag, 'adplugg:noContentAvailable', + function() { + global.context.noContentAvailable(); + } + ); + + }); + + // Fill the tag. + AdPlugg.push({'command': 'run'}); + +} diff --git a/ads/adplugg.md b/ads/adplugg.md new file mode 100644 index 000000000000..b995566447e9 --- /dev/null +++ b/ads/adplugg.md @@ -0,0 +1,40 @@ + + +# AdPlugg + +## Example + +```html + + +``` + +## Configuration + +For additional info, see AdPlugg's [AMP Ad Help](https://www.adplugg.com/support/help/amp-ads). + +### Required parameters + +- `data-access-code`: your AdPlugg access code. + +### Optional parameters + +- `data-zone`: your AdPlugg zone machine name. Recommended. + diff --git a/ads/adreactor.js b/ads/adreactor.js index 1a1888a9526d..806b9863aa6d 100644 --- a/ads/adreactor.js +++ b/ads/adreactor.js @@ -14,20 +14,21 @@ * limitations under the License. */ -import {writeScript, checkData} from '../src/3p'; +import {validateData, writeScript} 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/adreactor.md b/ads/adreactor.md index 6255a8f73d13..c5979a9781e4 100644 --- a/ads/adreactor.md +++ b/ads/adreactor.md @@ -19,20 +19,20 @@ limitations under the License. ## Example ```html - ``` ## Configuration -For semantics of configuration, please see ad network documentation. +For details on the configuration semantics, please contact the ad network or refer to their documentation. Supported parameters: -- data-pid -- data-zid -- data-custom3 +- `data-pid` +- `data-zid` +- `data-custom3` diff --git a/ads/ads.extern.js b/ads/ads.extern.js new file mode 100644 index 000000000000..270722278ce1 --- /dev/null +++ b/ads/ads.extern.js @@ -0,0 +1,735 @@ +/** + * 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. + */ + +/** @externs */ + +// 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. + +// Under 3p folder + +// beopinion.js +data.account; +data.content; +data.name; +//data['my-content']; +window.BeOpinionSDK; + +// facebook.js +data.embedAs; +data.href; + +// reddit.js +data.uuid; +data.embedcreated; +data.embedparent; +data.embedlive; +data.embedtype; +data.src; + +//twitter.js +data.tweetid + +//mathml.js +data.formula +var mathjax +mathjax.Hub +mathjax.Hub.Config +mathjax.Hub.Queue +window.MathJax + +//3d-gltf/index.js +var THREE; + +THREE.LoaderUtils +THREE.LoaderUtils.extractUrlBase + +THREE.WebGLRenderer = class { + /** @param {!JsonObject} opts */ + constructor(opts) { + /** @type {?Element} */ this.domElement = null;}}; +THREE.WebGLRenderer.prototype.setSize +THREE.WebGLRenderer.prototype.setPixelRatio +THREE.WebGLRenderer.prototype.setClearColor +THREE.WebGLRenderer.prototype.render +/** @type {boolean} */ +THREE.WebGLRenderer.prototype.gammaOutput +/** @type {number} */ +THREE.WebGLRenderer.prototype.gammaFactor + +THREE.Light = class extends THREE.Object3D {}; +THREE.DirectionalLight = class extends THREE.Light {}; +THREE.AmbientLight = class extends THREE.Light {}; + +THREE.Box3 = class {}; +THREE.Box3.prototype.getSize +THREE.Box3.prototype.getCenter +THREE.Box3.prototype.setFromObject +THREE.Box3.prototype.min +THREE.Box3.prototype.max + +THREE.Vector3 = class { + /** @param {number=} opt_x + * @param {number=} opt_y + * @param {number=} opt_z */ + constructor(opt_x, opt_y, opt_z) {} +}; +THREE.Vector3.prototype.lerpVectors +THREE.Vector3.prototype.copy +THREE.Vector3.prototype.clone +THREE.Vector3.prototype.subVectors +THREE.Vector3.prototype.multiplyScalar +THREE.Vector3.prototype.setFromMatrixColumn +THREE.Vector3.prototype.add +THREE.Vector3.prototype.set +THREE.Vector3.prototype.applyQuaternion +THREE.Vector3.prototype.setFromSpherical +THREE.Vector3.prototype.distanceToSquared +THREE.Vector3.prototype.length +THREE.Vector3.prototype.fromArray + +THREE.Euler = class { + constructor() { + this.x = 0; + this.y = 0; + this.z = 0;}}; + +THREE.Euler.prototype.set; + +THREE.Object3D = class { + constructor() { + this.position = new THREE.Vector3(); + this.rotation = new THREE.Euler(); + this.children = [];}}; + +THREE.Object3D.prototype.applyMatrix +THREE.Object3D.prototype.add +THREE.Object3D.prototype.updateMatrixWorld +THREE.Object3D.prototype.lookAt +THREE.Object3D.prototype.clone + +THREE.OrbitControls = class { + /** @param {THREE.Camera} camera + * @param {Element} domElement */ + constructor(camera, domElement) { + this.target = new THREE.Vector3(); }}; +THREE.OrbitControls.prototype.update +THREE.OrbitControls.prototype.addEventListener + +THREE.Scene = class extends THREE.Object3D {}; +THREE.Group = class extends THREE.Object3D {}; + +THREE.Camera = class extends THREE.Object3D { + constructor() { + super(); + this.fov = 0; + this.far = 0; + this.near = 0; + this.aspect = 0; + this.zoom = 0;}}; +THREE.Camera.prototype.updateProjectionMatrix +THREE.Camera.prototype.setFromUnitVectors + +THREE.PerspectiveCamera = class extends THREE.Camera {}; + +THREE.GLTFLoader = class { + constructor() { + this.crossOrigin = false;}}; +THREE.GLTFLoader.prototype.load + +// Under ads/google folder + +// adsense.js + +// csa.js +var _googCsa; +window._googCsa; + +// doubleclick.js +var googletag; +window.googletag; +googletag.cmd; +googletag.cmd.push; +googletag.pubads; +googletag.defineSlot; +var pubads; +pubads.addService; +pubads.markAsGladeOptOut; +pubads.markAsAmp; +pubads.setCorrelator; +pubads.markAsGladeControl; +googletag.enableServices; +pubads.setCookieOptions; +pubads.setTagForChildDirectedTreatment; +data.slot.setCategoryExclusion; +data.slot.setTargeting; +data.slot.setAttribute; +data.useSameDomainRenderingUntilDeprecated; +data.multiSize; +data.overrideWidth; +data.width; +data.overrideHeight; +data.height; +data.multiSizeValidation; +data.categoryExclusions; +data.categoryExclusions.length;; +data.cookieOptions; +data.tagForChildDirectedTreatment; +data.targeting; +data.slot; + +// imaVideo.js +var google; +google.ima; +google.ima.Ad; +google.ima.Ad.getSkipTimeOffset; +google.ima.AdDisplayContainer; +google.ima.AdDisplayContainer.initialize; +google.ima.ImaSdkSettings; +google.ima.ImaSdkSettings.setPlayerType; +google.ima.ImaSdkSettings.setPlayerVersion; +google.ima.AdsLoader; +google.ima.AdsLoader.getSettings; +google.ima.AdsLoader.requestAds; +google.ima.AdsManagerLoadedEvent; +google.ima.AdsManagerLoadedEvent.Type +google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED; +google.ima.AdsManagerLoadedEvent.getAdsManager; +google.ima.AdErrorEvent; +google.ima.AdErrorEvent.Type; +google.ima.AdErrorEvent.Type.AD_ERROR; +google.ima.AdsRequest; +google.ima.ViewMode; +google.ima.ViewMode.NORMAL; +google.ima.ViewMode.FULLSCREEN; +google.ima.AdsRenderingSettings; +google.ima.UiElements; +google.ima.UiElements.AD_ATTRIBUTION; +google.ima.UiElements.COUNTDOWN; +google.ima.AdEvent; +google.ima.AdEvent.getAd; +google.ima.AdEvent.getAdData; +google.ima.AdEvent.Type; +google.ima.AdEvent.Type.AD_PROGRESS; +google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED; +google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED; +google.ima.AdEvent.Type.LOADED; +google.ima.AdEvent.Type.ALL_ADS_COMPLETED; +google.ima.AdsManager; +google.ima.AdsManager.getRemainingTime; +google.ima.AdsManager.setVolume; +google.ima.AdProgressData; +google.ima.AdProgressData.adPosition; +google.ima.AdProgressData.totalAds; +google.ima.settings; +google.ima.settings.setLocale; +google.ima.settings.setVpaidMode; + +// 3P ads +// Please sort by alphabetic order of the ad server name to avoid conflict + +// a9.js +data.aax_size; +data.aax_pubname; +data.aax_src; + +// adblade.js +data.cid; + +// adform.js +data.bn; +data.mid; + +// adfox.js +data.bundleName; +data.adfoxParams; +data.bundleParams; +data.bundleParams.blockId; +data.bundleParams.data; + +// adgeneration.js +data.option; +data.id; +data.adtype; +data.adtype.toUpperCase; +data.async; +data.async.toLowerCase; +data.displayid; +data.targetid; + +// adman.js +data.ws; +data.host; +data.s; + +// adpicker.js +data.ph; + +// adreactor.js +data.zid; +data.pid; +data.custom3; + +// adsnative.js +data.ankv; +data.ankv.split; +data.ancat; +data.ancat.split; +data.anapiid; +data.annid; +data.anwid; +data.antid; + +// adtech.js +data.atwco; +data.atwdiv; +data.atwheight; +data.atwhtnmat; +data.atwmn; +data.atwmoat; +data.atwnetid; +data.atwothat; +data.atwplid; +data.atwpolar; +data.atwsizes; +data.atwwidth; + +// adthrive.js +data.siteId; + +// aduptech.js +window.uAd = {}; +window.uAd.embed; +data.responsive; +data.onAds; +data.onNoAds; + +// amoad.js +data.sid; + +// appnexus.js +data.tagid; +data.member; +data.code; +data.pageOpts; +data.debug; +data.adUnits; +data.target; + +// adventive.js +const adventive = {}; +adventive.Ad; +adventive.addArgs = () => {}; +adventive.addInstance = () => {}; +adventive.ads; +adventive.args; +adventive.instances; +adventive.isLibLoaded; +adventive.modes; +adventive.Plugin; +adventive.plugins; +adventive.utility; +window.adventive = adventive; + +// colombia.js +data.clmb_slot; +data.clmb_position; +data.clmb_section; +data.clmb_divid; + +// contentad.js +data.d; +data.wid; +data.url; + +// criteo.js +var Criteo; +Criteo.DisplayAd; +Criteo.Log.Debug; +Criteo.CallRTA; +Criteo.ComputeDFPTargetingForAMP; +Criteo.PubTag = {}; +Criteo.PubTag.Adapters = {}; +Criteo.PubTag.Adapters.AMP = {}; +Criteo.PubTag.Adapters.AMP.Standalone; +Criteo.PubTag.RTA = {}; +Criteo.PubTag.RTA.DefaultCrtgContentName; +Criteo.PubTag.RTA.DefaultCrtgRtaCookieName +data.tagtype; +data.networkid; +data.cookiename; +data.varname; +data.zone; +data.adserver; + +// distroscale.js +data.tid; + +// eplanning.js +data.epl_si; +data.epl_isv; +data.epl_sv; +data.epl_sec; +data.epl_kvs; +data.epl_e; + +// ezoic.js +/** + * @constructor + * @param {!Window} global + * @param {!Object} data + */ +window.EzoicAmpAd = function(global, data) {}; +window.EzoicAmpAd.prototype.createAd; + +// flite.js +data.guid; +data.mixins; + +// fusion.js +var ev; +ev.msg; +var Fusion; +Fusion.on; +Fusion.on.warning; +Fusion.loadAds; +data.mediaZone; +data.layout; +data.space; +data.parameters; + +// holder.js +data.queue; + +// imedia.js +data.positions + +// imonomy.js +data.pid; +data.subId; + +// improvedigital.js +data.placement; +data.optin; +data.keyvalue; + +// inmobi.js +var _inmobi; +window._inmobi; +_inmobi.getNewAd; +data.siteid; +data.slotid; + +// innity.js +var innity_adZone; +var innityAMPZone; +var innityAMPTag; +data.pub; +data.zone; +data.channel; + +// ix.js +data.ixId; +data.ixId; +data.ixSlot; +data.ixSlot; + +// kargo.js +data.options; +data.slot; + +// kixer.js +data.adslot; + +// mads.js +window.MADSAdrequest = {}; +window.MADSAdrequest.adrequest; +data.adrequest; + +// mediaimpact.js +var asmi; +asmi.sas; +asmi.sas.call; +asmi.sas.setup; +data.site; +data.page; +data.format; +data.slot.replace; + +// medianet.js +data.crid; +data.hasOwnProperty; +data.requrl; +data.refurl; +data.versionId; +data.timeout; + +// microad.js +var MicroAd; +MicroAd.Compass; +MicroAd.Compass.showAd; +data.spot; + +// mixpo.js +data.subdomain; +data.guid; +data.embedv; +data.clicktag; +data.customtarget; +data.dynclickthrough; +data.viewtracking; +data.customcss; +data.local; +data.enablemraid; +data.jsplayer; + +// nativo.js +var PostRelease; +PostRelease.Start; +PostRelease.checkIsAdVisible; +var _prx; +data.delayByTime; +data.delayByTime; + +// nokta.js +data.category; + +// openadstream.js +data.adhost +data.sitepage; +data.pos; +data.query; + +// openx.js +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.addVariable; +oxRequest.setAdSizes; +oxRequest.getOrCreateAdUnit; +var dfpData; +dfpData.dfp; +dfpData.targeting; +data.dfpSlot; +data.nc; +data.auid; + +// pixels.js +var pixelsAd; +var pixelsAMPAd; +var pixelsAMPTag; +pixelsAMPTag.renderAmp; +data.origin; +data.sid; +data.tag; +data.clickTracker; +data.viewability; + +// plista.js +data.widgetname; +data.publickey; +data.urlprefix; +data.item; +data.geo; +data.categories; + +// pressboard.js +data.media; +data.baseUrl; + +// pubguru.js +data.height; +data.publisher; +data.slot; +data.width; + +// pubmatic.js +data.kadpageurl; + +// pubmine.js +data.adsafe; +data.wordads; +data.section; + +// pulsepoint.js +window.PulsePointHeaderTag; + +// rubicon.js +data.method; +data.width; +data.height; +data.account; +data.kw; +data.visitor; +data.inventory; +data.size; +data.site; +data.zone; +data.callback; + +// sharethrough.js +data.pkey; + +// sklik.js +data.elm; + +// smartadserver.js +var sas; +sas.callAmpAd; + +// smartclip.js +data.plc; +data.sz; +data.extra; + +// sortable.js +data.name; + +// sovrn.js +data.domain; +data.u; +data.iid; +data.aid; +data.z; +data.tf; + +// swoop.js +var Swoop +Swoop.announcePlace + +// taboola.js +data.referrer; +data.publisher; +data.mode; + +// teads.js +data.tag; +data.tag; +data.tag.tta; +data.tag.ttp; + +// uas.js +var Phoenix; +window.Phoenix; +Phoenix.EQ; +Phoenix.EQ.push; +Phoenix.enableSingleRequestCallMode; +Phoenix.setInfo; +Phoenix.defineAdSlot; +Phoenix.display; +data.accId; +data.adUnit; +data.targetings; +data.extraParams; +data.slot.setVisibility; +data.slot.setTargeting; +data.slot.setExtraParameters; + +// webediads.js +var wads; +wads.init; +data.position; + +// weborama.js +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; + +// yandex.js +var Ya; +Ya.Context; +Ya.Context.AdvManager; +Ya.Context.AdvManager.render; +Ya.adfoxCode; +Ya.adfoxCode.onRender; +data.isAdfox; + +// yieldbot.js +var yieldbot; +yieldbot.psn; +yieldbot.enableAsync; +yieldbot.defineSlot; +yieldbot.go; +yieldbot.nextPageview; +yieldbot.getSlotCriteria; +data.psn; +data.ybSlot; + +// yieldmo.js +data.ymid; + +// zedo.js +var ZGTag; +var geckoTag; +var placement; +geckoTag.setAMP; +geckoTag.addPlacement; +placement.includeRenderer; +geckoTag.loadAds; +geckoTag.placementReady; +data.charset; +data.superId; +data.network; +data.placementId; +data.channel; +data.publisher; +data.dim; +data.renderer; + +// zen.js +var YandexZen; +YandexZen.renderWidget; +data.clid; +data.successCalback; +data.failCallback; + +// zergnet.js +window.zergnetWidgetId; +data.zergid; + +// _ping_.js +window.networkIntegrationDataParamForTesting; diff --git a/ads/adsense.js b/ads/adsense.js deleted file mode 100644 index 5c4e4369ae57..000000000000 --- a/ads/adsense.js +++ /dev/null @@ -1,47 +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 {checkData} from '../src/3p'; - -/** - * @param {!Window} global - * @param {!Object} data - */ -export function adsense(global, data) { - checkData(data, ['adClient', 'adSlot']); - if (global.context.clientId) { - // Read by GPT for GA/GPT integration. - global.gaGlobal = { - vid: global.context.clientId, - hid: global.context.pageViewId, - }; - } - /*eslint "google-camelcase/google-camelcase": 0*/ - global.google_page_url = global.context.canonicalUrl; - const s = document.createElement('script'); - s.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'; - global.document.body.appendChild(s); - - const i = document.createElement('ins'); - i.setAttribute('data-ad-client', data['adClient']); - if (data['adSlot']) { - i.setAttribute('data-ad-slot', data['adSlot']); - } - i.setAttribute('class', 'adsbygoogle'); - i.style.cssText = 'display:inline-block;width:100%;height:100%;'; - global.document.getElementById('c').appendChild(i); - (global.adsbygoogle = global.adsbygoogle || []).push({}); -} diff --git a/ads/adsense.md b/ads/adsense.md deleted file mode 100644 index 58c648987e64..000000000000 --- a/ads/adsense.md +++ /dev/null @@ -1,36 +0,0 @@ - - -# AdSense - -## Example - -```html - - -``` - -## Configuration - -For semantics of configuration, please see ad network documentation. - -Supported parameters: - -- data-ad-client -- data-ad-slot diff --git a/ads/adsnative.js b/ads/adsnative.js new file mode 100644 index 000000000000..5dd5b9892879 --- /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 {validateData, writeScript} 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..769ed56b9245 --- /dev/null +++ b/ads/adsnative.md @@ -0,0 +1,46 @@ + + +# AdsNative + +## Example + +```html + + +``` + +## Configuration + +For configuration details, see [AdsNative's documentation](http://dev.adsnative.com). + +### Required parameters + +- `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 parameters + +- `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/adspeed.js b/ads/adspeed.js new file mode 100644 index 000000000000..bb33d4dbb05e --- /dev/null +++ b/ads/adspeed.js @@ -0,0 +1,29 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adspeed(global, data) { + validateData(data, ['zone', 'client']); + + const url = 'https://g.adspeed.net/ad.php?do=amphtml&zid=' + data.zone + '&oid=' + data.client + '&cb=' + Math.random(); + + writeScript(global, url); +} diff --git a/ads/adspeed.md b/ads/adspeed.md new file mode 100644 index 000000000000..d288e3f11fab --- /dev/null +++ b/ads/adspeed.md @@ -0,0 +1,39 @@ + + +# AdSpeed + +## Example + +```html + +
Loading ad.
+
Ad could not be loaded.
+
+``` + +## Configuration + +For configuration semantics, please see [AdSpeed documentation](https://www.adspeed.com/Knowledges/1950/Ad-Tag/Accelerated-Mobile-Pages-Project-AMP-Ad.html). + +Supported parameters: + +- `data-zone`: the zone ID +- `data-client`: the publisher ID + diff --git a/ads/adspirit.js b/ads/adspirit.js new file mode 100644 index 000000000000..05497a9e6ae3 --- /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 {setStyles} from '../src/style'; +import {validateData} from '../3p/3p'; + +/** + * @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..12db41cbcd86 --- /dev/null +++ b/ads/adspirit.md @@ -0,0 +1,37 @@ + + +# AdSpirit + +## Example + +```html + + +``` + +## Configuration + + +For details on the configuration semantics, please see [AdSpirit's documentation](http://help.adspirit.de/help/). + +Supported parameters: + +- `data-asm-params` +- `data-asm-host` diff --git a/ads/adstir.js b/ads/adstir.js new file mode 100644 index 000000000000..248210dad982 --- /dev/null +++ b/ads/adstir.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 {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adstir(global, data) { + // TODO: check mandatory fields + validateData(data, [], ['appId', 'adSpot']); + + const v = '4.0'; + + const d = global.document.createElement('div'); + d.setAttribute('class', 'adstir-ad-async'); + d.setAttribute('data-ver', v); + d.setAttribute('data-app-id', data['appId']); + d.setAttribute('data-ad-spot', data['adSpot']); + d.setAttribute('data-amp', true); + d.setAttribute('data-origin', global.context.location.href); + global.document.getElementById('c').appendChild(d); + + loadScript(global, 'https://js.ad-stir.com/js/adstir_async.js'); +} diff --git a/ads/adstir.md b/ads/adstir.md new file mode 100644 index 000000000000..a1905c3c71db --- /dev/null +++ b/ads/adstir.md @@ -0,0 +1,36 @@ + + +# AdStir + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please refer to [your publisher account](https://ad-stir.com/login) or [contact AdStir](https://ad-stir.com/contact). + +Supported parameters: + +- `data-app-id` +- `data-ad-spot` diff --git a/ads/adtech.js b/ads/adtech.js index 1f6c261fb008..dc673bbe6d73 100644 --- a/ads/adtech.js +++ b/ads/adtech.js @@ -14,15 +14,41 @@ * limitations under the License. */ -import {writeScript, validateSrcPrefix, validateSrcContains} from '../src/3p'; +import { + validateData, + validateSrcContains, + validateSrcPrefix, + writeScript, +} 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..ba1f468be777 100644 --- a/ads/adtech.md +++ b/ads/adtech.md @@ -19,16 +19,36 @@ limitations under the License. ## Example ```html - + data-atwMN="2842475" + data-atwDiv="adtech-ad-container" + > ``` ## Configuration -For semantics of configuration, please see ad network documentation. +For details on the configuration semantics, please contact the ad network or refer to their 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 multiple 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/adthrive.js b/ads/adthrive.js new file mode 100644 index 000000000000..6449bbd3ef21 --- /dev/null +++ b/ads/adthrive.js @@ -0,0 +1,27 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adthrive(global, data) { + validateData(data, ['siteId', 'adUnit'], ['sizes']); + loadScript(global, 'https://ads.adthrive.com/sites/' + + encodeURIComponent(data.siteId) + '/amp.min.js'); +} diff --git a/ads/adthrive.md b/ads/adthrive.md new file mode 100644 index 000000000000..1f0660abbc55 --- /dev/null +++ b/ads/adthrive.md @@ -0,0 +1,61 @@ + + +# AdThrive + +Your site must be approved and active with [AdThrive](http://www.adthrive.com) prior to use. AdThrive will install or provide specific tags for your site. + +## Examples + +### Render an ad with the default sizes +```html + + +``` + +### Render an ad with a fixed size 320x50 +```html + + +``` + +### Render an ad with multiple sizes (320x50,320x100,300x250) +```html + + +``` + +## Configuration + +### Required parameters + +* `data-site-id` - Your AdThrive site id. +* `data-ad-unit` - AdThrive provided ad unit. + +### Optional parameters + +`data-sizes` - Comma separated list of ad sizes this ad slot should support. The iFrame will be resized if allowed. diff --git a/ads/adunity.js b/ads/adunity.js new file mode 100644 index 000000000000..f040016ce18b --- /dev/null +++ b/ads/adunity.js @@ -0,0 +1,114 @@ +/** + * Copyright 2018 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'; +import {startsWith} from '../src/string'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adunity(global, data) { + const doc = global.document; + + validateData(data, [ + 'auAccount', + 'auSite', + ], + [ + 'auSection', + 'auZone', + 'auDemo', + 'auIsdemo', + 'auAd', + 'auOrder', + 'auSegment', + 'auOptions', + 'auSources', + 'auAds', + 'auTriggerFn', + 'auTriggerVal', + 'auCallbackVal', + 'auCallbackFn', + 'auPassbackFn', + 'auPassbackVal', + 'auClick', + 'auDual', + 'auImpression', + 'auVideo', + ] + ); + + //prepare tag structure + const tag = doc.createElement('div'); + tag.classList.add('au-tag'); + tag.setAttribute('data-au-width', data['width']); + tag.setAttribute('data-au-height', data['height']); + + if (data != null) { + for (const key in data) { + //skip not valid attributes + if (!hasOwnProperty.call(data, key)) { + continue; + } + + //skip if attribute is type or ampSlotIndex + if (startsWith(key, 'type') || startsWith(key, 'ampSlotIndex')) { + continue; + } + + if (startsWith(key, 'au')) { + if (key == 'auVideo') { + tag.setAttribute('class', 'au-video'); + } + else { + const auKey = key.substring(2).toLowerCase(); + tag.setAttribute('data-au-' + auKey, data[key]); + } + } + } + } + + //make sure is executed only once + let libAd = false; + + //execute tag only if in view + const inViewCb = global.context.observeIntersection(function(changes) { + changes.forEach(function(c) { + if (!libAd && c.intersectionRect.height > data['height'] / 2) { + libAd = true; + inViewCb(); + renderTags(global, data); + } + }); + }); + const tagPh = doc.getElementById('c'); + tagPh.appendChild(tag); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function renderTags(global, data) { + if (data == null) {return;} + + global.context.renderStart({ + width: data.width, + height: data.height, + }); + loadScript(global, 'https://content.adunity.com/aulib.js'); +} diff --git a/ads/adunity.md b/ads/adunity.md new file mode 100644 index 000000000000..ff2a608de117 --- /dev/null +++ b/ads/adunity.md @@ -0,0 +1,46 @@ + + +# AdUnity + +## Example + +```html + + +``` + +## Configuration + +### Required attributes + +`data-au-account` - account number +`data-au-site` - site id + +### Optional attributes +`data-au-section` - section of the site (i.e. hp - homepage), can be empty value +`data-au-zone` - zone id of the ad +`data-au-dual` - this is used to fetch ads for both desktop and mobile devices automatically +`data-au-isdemo` - used to fetch demo ad + +### Other attributes +For more information please see ad network documentation at diff --git a/ads/aduptech.js b/ads/aduptech.js new file mode 100644 index 000000000000..6cde94c6a128 --- /dev/null +++ b/ads/aduptech.js @@ -0,0 +1,52 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function aduptech(global, data) { + const elementId = 'aduptech'; + + validateData(data, ['placementkey'], ['query', 'mincpc', 'adtest']); + + // add id attriubte to given container (required) + global.document.getElementById('c').setAttribute('id', elementId); + + // load aduptech js api + loadScript(global, 'https://s.d.adup-tech.com/jsapi', () => { + + // force responsive ads for amp + data.responsive = true; + + // 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/ads/aduptech.md b/ads/aduptech.md new file mode 100644 index 000000000000..ec450d48b06f --- /dev/null +++ b/ads/aduptech.md @@ -0,0 +1,104 @@ + + +# Ad Up Technology + +Please visit [www.adup-tech.com](http://www.adup-tech.com) for more information +on how to get required ad tag or placement keys. + +## Examples + +### Fixed size + +Uses fixed size by the given `width` and `height`. + +```html + + +``` + +### Filled size + +Uses available space of parent html container. + +```html + +
+ + +
+``` + +### Fixed height + +Uses available width and the given `height`. + +```html + + +``` + +### Responsive + +Uses available space but respecting aspect ratio by given `width` and `height` (for example 10:3). + +```html + + +``` + +## Configuration + +### Required parameters + +* ```data-placementkey``` + +### Optional parameters + +* ```data-query``` +* ```data-mincpc``` +* ```data-adtest``` + +## Design/Layout + +Please visit [www.adup-tech.com](http://www.adup-tech.com) and sign up as publisher to create your own placement. diff --git a/ads/adventive.js b/ads/adventive.js new file mode 100644 index 000000000000..75fcaed58ddf --- /dev/null +++ b/ads/adventive.js @@ -0,0 +1,157 @@ +/** + * 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 {addParamsToUrl} from '../src/url.js'; +import {dict, hasOwn} from '../src/utils/object'; +import {endsWith, startsWith} from '../src/string'; +import {loadScript, validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adventive(global, data) { + if (hasOwn(data, 'isDev')) { + adventive_(global, data); + } else { + validateData(data, ['src'], ['isDev']); + writeScript(global, `${data.src}&isAmp=1`); + } +} + +const adv = { + addInstance: () => {}, + args: {}, + isLibLoaded: false, + mode: { + dev: false, + live: false, + localDev: false, + preview: false, + prod: false, + testing: false, + }, + }, + requiredData = ['pid'], + optionalData = ['click', 'async', 'isDev'], + sld = {true: 'adventivedev', false: 'adventive'}, + thld = {true: 'amp', false: 'ads'}, + cacheTime = 5; + +/** + * Future data: + * - async + * - click + * - height + * - isDev + * - width + * - pid + * + * Considerations: + * - Recipe reuse for multi-placed Ads. + * - Reduce request to only what is needed + * - Mitigate risk of data corruption + * + * @todo implement multi-size handling for multi-slotted ads. @see doubleclick + * + * @param {!Window} global + * @param {!Object} data + */ +function adventive_(global, data) { + validateData(data, requiredData, optionalData); + + if (!hasOwn(global, 'adventive')) { global.adventive = adv; } + const ns = global.adventive; + if (!hasOwn(ns, 'context')) { ns.context = global.context; } + + if (!Object.isFrozen(ns.mode)) { + updateMode(ns.mode, global.context.location.hostname); + } + + const cb = callback.bind(this, data.pid, ns), + url = getUrl(global.context, data, ns); + url ? + (hasOwn(data, 'async') ? loadScript : writeScript)(global, url, cb) : cb(); +} + +/** + * @param {!Object} mode + * @param {string} hostname + */ +function updateMode(mode, hostname) { + mode.localDev = hostname === 'localhost'; + mode.dev = !mode.localDev && endsWith(hostname, `${sld[false]}.com`); + mode.prod = !mode.localDev && endsWith(hostname, `${sld[true]}.com`); + mode.preview = (mode.dev || mode.prod) && startsWith(hostname, '/ad'); + mode.testing = (mode.dev || mode.prod) && startsWith(hostname, '/testing'); + mode.live = (mode.testing || !mode.preview) && !mode.localDev; + + Object.freeze(mode); +} + +/** + * @param {string} id + * @param {!Object} ns + */ +function callback(id, ns) { ns.addInstance(id); } + +/** + * @param {!Object} context + * @param {!Object} data + * @param {!Object} ns + * @return {string|boolean} if a search query is generated, a full url is + * provided, otherwise false + */ +function getUrl(context, data, ns) { + const {mode} = ns, + isDev = hasOwn(data, 'isDev'), + sld_ = sld[!mode.dev], + thld_ = thld[isDev && !mode.live], + search = reduceSearch(ns, data.pid, data.click, context.referrer), + url = search ? + addParamsToUrl(`https://${thld_}.${sld_}.com/ad`, search) : false; + return url; +} + +/** + * @todo determine if we can reduce the request to nothing & return false + * @todo usage of RTC may be applicable here for macros + * @todo check if click-macros can be offloaded to amp-analytics (i.e recipe) + * + * @param {!Object} ns + * @param {string} placementId + * @param {string} click + * @param {string} referrer + * @return {JsonObject} if no more data is needed, false, otherwise JSON + * representation of the url search query. + */ +function reduceSearch(ns, placementId, click, referrer) { + const isRecipeLoaded = hasOwn(ns.args, 'placementId'); + let isRecipeStale = true; + if (isRecipeLoaded) { + const info = ns.args[placementId]; + isRecipeStale = (Date.now() - info['requestTime']) > (60 * cacheTime); + } + const needsRequest = !isRecipeLoaded || isRecipeStale; + + return !needsRequest ? null : dict({ + 'click': click, + 'referrer': referrer, + 'isAmp': '1', + 'lib': !ns.isLibLoaded ? '1' : '', // may be prefetchable via _config + 'pid': needsRequest ? placementId : '', + }); +} diff --git a/ads/adventive.md b/ads/adventive.md new file mode 100644 index 000000000000..b19f53efc59d --- /dev/null +++ b/ads/adventive.md @@ -0,0 +1,37 @@ + + +# Adventive + +## Example + +### Single Ad + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `data-src`: provided ad url + diff --git a/ads/adverline.js b/ads/adverline.js new file mode 100644 index 000000000000..fa5dfc0f01eb --- /dev/null +++ b/ads/adverline.js @@ -0,0 +1,27 @@ +/** + * 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, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +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..26ef97c76aef --- /dev/null +++ b/ads/adverline.md @@ -0,0 +1,44 @@ + + +# Adverline + +## Examples + +### Single ad + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `data-id`: site ID +- `data-plc`: format ID (unique per page) + +### Optional parameters + +- `data-section`: tag list, separated by commas +- `data-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..fcc8a57c28b3 --- /dev/null +++ b/ads/adverticum.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 {setStyle} from '../src/style'; +import {validateData, writeScript} from '../3p/3p'; +/** + * @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..f4c11d95522e --- /dev/null +++ b/ads/adverticum.md @@ -0,0 +1,44 @@ + + +# Adverticum + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the Adverticum support team via e-mail: support@adverticum.com. + + +### Required parameters + + - `data-goa3zone`: The zoneID, which can be found at the Adverticum AdServer. + +### Optional parameters + + - `data-costumetargetstring:`: The value must be Base64Encoded. + +## Support and contact + +For further information and specific configuration please contact the Adverticum support team via e-mail: support@adverticum.com diff --git a/ads/advertserve.js b/ads/advertserve.js new file mode 100644 index 000000000000..03e07faafd19 --- /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 {validateData, writeScript} 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..3c8fc350fd33 --- /dev/null +++ b/ads/advertserve.md @@ -0,0 +1,40 @@ + + +# AdvertServe + +## Example + +```html + +
Loading ad.
+
Ad could not be loaded.
+
+``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +Supported parameters: + +- `data-client`: client id +- `data-pid`: publisher id +- `data-zid`: zone id diff --git a/ads/adyoulike.js b/ads/adyoulike.js new file mode 100644 index 000000000000..93ffc43191c7 --- /dev/null +++ b/ads/adyoulike.js @@ -0,0 +1,28 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function adyoulike(global, data) { + validateData(data, ['placement'], ['dc', 'campaign']); + global.adyoulikeParams = data; + + writeScript(global, 'https://pixels.omnitagjs.com/amp.js'); +} diff --git a/ads/adyoulike.md b/ads/adyoulike.md new file mode 100644 index 000000000000..fe022b602fed --- /dev/null +++ b/ads/adyoulike.md @@ -0,0 +1,43 @@ + +# Adyoulike + +Serves ads from [Adyoulike](https://www.adyoulike.com/). + +## Example + +### Normal ad + +```html + +``` + +## Configuration + +For configuration details and to generate your tags, please contact [Adyoulike](https://www.adyoulike.com/#contact). + +### Required parameters + +- `data-placement` + +### Optional parameters + +- `data-dc` +- `data-campaign` + diff --git a/ads/affiliateb.js b/ads/affiliateb.js new file mode 100644 index 000000000000..5fbdf81dfe9f --- /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 {validateData, writeScript} 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..600fdce4025c --- /dev/null +++ b/ads/affiliateb.md @@ -0,0 +1,36 @@ + + +# AffiliateB + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact [AffiliateB](https://www.affiliate-b.com/web/contact/form.php). + +Supported parameters: + +- `data-nend_params` diff --git a/ads/aja.js b/ads/aja.js new file mode 100644 index 000000000000..7f25fb04f333 --- /dev/null +++ b/ads/aja.js @@ -0,0 +1,41 @@ +/** + * Copyright 2018 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 aja(global, data) { + + // ensure we have valid widgetIds value + validateData(data, ['widgetids']); + + (global._aja = global._aja || { + viewId: global.context.pageViewId, + widgetIds: data['widgetids'], + htmlURL: data['htmlurl'] || global.context.canonicalUrl, + ampURL: data['ampurl'] || global.context.sourceUrl, + fbk: data['fbk'] || '', + testMode: data['testmode'] || 'false', + styleFile: data['stylefile'] || '', + referrer: data['referrer'] || global.context.referrer, + }); + + // load the Aja AMP JS file + loadScript(global, 'https://cdn.as.amanad.adtdp.com/sdk/asot-v2.js'); +} diff --git a/ads/aja.md b/ads/aja.md new file mode 100644 index 000000000000..af815d033674 --- /dev/null +++ b/ads/aja.md @@ -0,0 +1,49 @@ + + +# Aja + +## Example installation of the Aja widget + +### Basic + +```html + + +``` + +The above code must be accompanied by AMP-enabled widgets delivered by Aja’s Account Management Team, +do not directly install this code with existing widgets. + +## Parameters + +- widgetIds *(**mandatory**)* - Widget Id/s Provided by Account Manager. +- htmlURL *(optional)* - The URL of the standard html version of the page. +- ampURL *(optional)* - The URL of the AMP version of the page. +- styleFile *(optional)* - Provide publisher an option to pass CSS file in order to inherit the design for the AMP displayed widget. **Consult with Account Manager regarding CSS options**. + +## Troubleshooting + +### Widget is cut off + +According to the AMP API, "resizes are honored when the resize will not adjust the content the user is currently reading. That is, if the ad is above the viewport's contents, it'll resize. Same if it's below. If it's in the viewport, it ignores it." + +**Resolution** + + You can set an initial height of what the widget height is supposed to be. That is, instead of ```height="100"```, if the widget's final height is 600px, then set ```height="600"```. Setting the initial height ***will not*** finalize the widget height if it's different from the actual. The widget will resize to it's true dimensions after the widget leaves the viewport. diff --git a/ads/alp/handler.js b/ads/alp/handler.js new file mode 100644 index 000000000000..610a944a33b1 --- /dev/null +++ b/ads/alp/handler.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. + */ + +import { + addParamToUrl, + isLocalhostOrigin, + isProxyOrigin, + parseQueryString, + parseUrlDeprecated, +} from '../../src/url'; +import {closest, openWindowDialog} from '../../src/dom'; +import {dev} from '../../src/log'; +import {dict} from '../../src/utils/object'; +import {startsWith} from '../../src/string'; +import {urls} from '../../src/config'; + +/** + * Install a click listener that transforms navigation to the AMP cache + * to a form that directly navigates to the doc and transmits the original + * URL as a click logging info passed via a fragment param. + * Expects to find a URL starting with "https://cdn.ampproject.org/c/" + * to be available via a param call "adurl" (or defined by the + * `data-url-param-name` attribute on the a tag. + * @param {!Window} win + */ +export function installAlpClickHandler(win) { + win.document.documentElement.addEventListener('click', handleClick); + // Start loading destination doc when finger is down. + // Needs experiment whether this is a good idea. + win.document.documentElement.addEventListener('touchstart', warmupDynamic); +} + +/** + * 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, opt_viewerNavigate) { + if (e.defaultPrevented) { + return; + } + // Only handle simple clicks with the left mouse button/touch and without + // modifier keys. + if (e.buttons != 0 && e.buttons != 1) { + return; + } + if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) { + return; + } + + const link = getLinkInfo(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. + const fragment = 'click=' + encodeURIComponent( + addParamToUrl(link.a.href, 'amp', '1', /* opt_addToFront */ true)); + let destination = link.eventualUrl; + if (link.eventualUrl.indexOf('#') == -1) { + destination += '#' + fragment; + } else { + destination += '&' + fragment; + } + const win = link.a.ownerDocument.defaultView; + const ancestors = win.location.ancestorOrigins; + if (ancestors && ancestors[ancestors.length - 1] == 'http://localhost:8000') { + destination = destination.replace( + `${parseUrlDeprecated(link.eventualUrl).host}/c/`, + 'http://localhost:8000/max/'); + } + e.preventDefault(); + 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); + } +} + +/** + * For an event, see if there is an anchor tag in the target + * ancestor chain and if yes, check whether we can figure + * out an AMP target URL. + * @param {!Event} e + * @return {{ + * eventualUrl: (string|undefined), + * a: !Element + * }|undefined} A URL on the AMP Cache. + */ +function getLinkInfo(e) { + const a = closest(dev().assertElement(e.target), element => { + return element.tagName == 'A' && element.href; + }); + if (!a) { + return; + } + return { + eventualUrl: getEventualUrl(a), + a, + }; +} + +/** + * Given an anchor tag, figure out whether this goes to an AMP destination + * via a redirect. + * @param {!Element} a An anchor tag. + * @return {string|undefined} A URL on the AMP Cache. + */ +function getEventualUrl(a) { + const urlParamName = a.getAttribute('data-url-param-name') || 'adurl'; + const eventualUrl = parseQueryString(a.search)[urlParamName]; + if (!eventualUrl) { + return; + } + if (!isProxyOrigin(eventualUrl) || + !startsWith(parseUrlDeprecated(eventualUrl).pathname, '/c/')) { + return; + } + return eventualUrl; +} + +/** + * Navigate to the given URL. Infers the target from the given anchor + * tag. + * @param {!Window} win + * @param {!Element} a Anchor element + * @param {string} url + */ +function navigateTo(win, a, url) { + const target = (a.target || '_top').toLowerCase(); + const a2aAncestor = getA2AAncestor(win); + if (a2aAncestor) { + a2aAncestor.win./*OK*/postMessage('a2a;' + JSON.stringify(dict({ + 'url': url, + })), a2aAncestor.origin); + return; + } + openWindowDialog(win, url, target); +} + +/** + * Establishes a connection to the AMP Cache and makes sure + * the AMP JS is cached. + * @param {!Window} win + */ +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 = `${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 = `${urls.cdn}/v0.js`; + getHeadOrFallback(win.document).appendChild(linkRel); +} + +/** + * For events (such as touch events) that point to an eligible URL, preload + * that URL. + * @param {!Event} e + * @visibleForTesting + */ +export function warmupDynamic(e) { + const link = getLinkInfo(e); + if (!link || !link.eventualUrl) { + return; + } + // Preloading with empty as and newly specced value `fetch` meaning the same + // thing. `document` would be the right value, but this is not yet supported + // in browsers. + const linkRel0 = /*OK*/document.createElement('link'); + linkRel0.rel = 'preload'; + linkRel0.href = link.eventualUrl; + const linkRel1 = /*OK*/document.createElement('link'); + linkRel1.rel = 'preload'; + linkRel1.as = 'fetch'; + linkRel1.href = link.eventualUrl; + const head = getHeadOrFallback(e.target.ownerDocument); + head.appendChild(linkRel0); + head.appendChild(linkRel1); +} + +/** + * Return if present or just the document element. + * @param {!Document} doc + * @return {!Element} + */ +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 new file mode 100644 index 000000000000..993d9d9712a9 --- /dev/null +++ b/ads/alp/install-alp.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. + */ + +// Utility file that generates URLs suitable for AMP's impression tracking. + +import {initLogConstructor, setReportError} from '../../src/log'; +import {installAlpClickHandler, warmupStatic} from './handler'; +import {reportError} from '../../src/error'; + +initLogConstructor(); +setReportError(reportError); +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..19e720a56003 --- /dev/null +++ b/ads/amoad.md @@ -0,0 +1,52 @@ + + +# AMoAd + +## Examples + +### Banner + +```html + + +``` + +### InFeed + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact [AMoAd](http://www.amoad.com/form2/). + +Supported parameters: + +- `width` +- `height` +- `data-sid` +- `data-ad-type` diff --git a/ads/appnexus.js b/ads/appnexus.js new file mode 100644 index 000000000000..8fa22599904d --- /dev/null +++ b/ads/appnexus.js @@ -0,0 +1,139 @@ +/** + * 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, writeScript} from '../3p/3p'; +import {setStyles} from '../src/style'; + +const APPNEXUS_AST_URL = 'https://acdn.adnxs.com/ast/ast.js'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function appnexus(global, data) { + const args = []; + args.push('size=' + data.width + 'x' + data.height); + if (data.tagid) { + validateData(data, ['tagid']); + args.push('id=' + encodeURIComponent(data.tagid)); + writeScript(global, constructTtj(args)); + return; + } else if (data.member && data.code) { + validateData(data, ['member', 'code']); + args.push('member=' + encodeURIComponent(data.member)); + args.push('inv_code=' + encodeURIComponent(data.code)); + writeScript(global, constructTtj(args)); + return; + } + + /** + * Construct the TTJ URL. + * Note params should be properly encoded first (use encodeURIComponent); + * @param {!Array} args query string params to add to the base URL. + * @return {string} Formated TTJ URL. + */ + function constructTtj(args) { + let url = 'https://ib.adnxs.com/ttj?'; + for (let i = 0; i < args.length; i++) { + //append arg to query. Please encode arg first. + url += args[i] + '&'; + } + + return url; + } + + appnexusAst(global, data); + +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function appnexusAst(global, data) { + 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 || {}; + context.master.apntag.anq = context.master.apntag.anq || []; + apntag = context.master.apntag; + + apntag.anq.push(() => { + if (data.pageOpts) { + apntag.anq.push(() => { + //output console information + apntag.debug = data.debug || false; + apntag.setPageOpts(data.pageOpts); + }); + } + + data.adUnits.forEach(adUnit => { + apntag.defineTag(adUnit); + }); + + }); + loadScript(global, APPNEXUS_AST_URL, () => { + apntag.anq.push(() => { + apntag.loadTags(); + }); + }); + } + + const div = global.document.createElement('div'); + div.setAttribute('id', data.target); + const divContainer = global.document.getElementById('c'); + if (divContainer) { + divContainer.appendChild(div); + setStyles(divContainer, { + top: '50%', + left: '50%', + bottom: '', + right: '', + transform: 'translate(-50%, -50%)', + }); + } + + if (!apntag) { + apntag = context.master.apntag; + //preserve a global reference + /** @type {{showTag: function(string, Object)}} global.apntag */ + global.apntag = context.master.apntag; + } + + // check for ad responses received for a slot but before listeners are + // registered, for example when an above-the-fold ad is scrolled into view + apntag.anq.push(() => { + if (typeof apntag.checkAdAvailable === 'function') { + const getAd = apntag.checkAdAvailable(data.target); + getAd({resolve: isAdAvailable, reject: context.noContentAvailable}); + } + }); + + apntag.anq.push(() => { + apntag.onEvent('adAvailable', data.target, isAdAvailable); + apntag.onEvent('adNoBid', data.target, context.noContentAvailable); + }); + + /** + * resolve getAd with an available ad object + * + * @param {{targetId: string}} adObj + */ + function isAdAvailable(adObj) { + global.context.renderStart({width: adObj.width, height: adObj.height}); + global.apntag.showTag(adObj.targetId, global.window); + } +} diff --git a/ads/appnexus.md b/ads/appnexus.md new file mode 100644 index 000000000000..462fc1266e54 --- /dev/null +++ b/ads/appnexus.md @@ -0,0 +1,90 @@ + + +# AppNexus + +## Examples + +### Basic single ad - tagid + +```html + + +``` + +### Basic single ad - member and code + +```html + + +``` + +### AST single ad call with keywords + +Note: you should use either the basic setup or AST setup. Do not mix types on the same page. + +```html + + +``` + +### AST for multiple sync ads on the page + +```html + + + + + +``` + +## Configuration + +See AppNexus [Tiny Tag documentation](https://wiki.appnexus.com/display/adnexusdocumentation/Dynamic+TinyTag+Parameters) or [AST documentation](https://wiki.appnexus.com/pages/viewpage.action?pageId=75793258) for details on input parameters. + +### Enable debugging + +To enable debugging with the AST type of tags, just set `data-debug=true` in all your amp-ad tags. + +```html + + + + + +``` diff --git a/ads/appvador.js b/ads/appvador.js new file mode 100644 index 000000000000..5bd056f55a15 --- /dev/null +++ b/ads/appvador.js @@ -0,0 +1,47 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function appvador(global, data) { + validateData(data, ['id'], ['options', 'jsType', 'customScriptSrc']); + + const container = global.document.getElementById('c'); + const apvDiv = global.document.createElement('div'); + apvDiv.setAttribute('id', 'apvad-' + data.id); + container.appendChild(apvDiv); + + const scriptUrl = data.customScriptSrc ? data.customScriptSrc : + 'https://cdn.apvdr.com/js/' + + (data.jsType ? encodeURIComponent(data.jsType) : 'VastAdUnit') + + '.min.js'; + const apvScript = 'new APV.' + + (data.jsType ? data.jsType : 'VASTAdUnit') + + '({s:"' + data.id + '",isAmpAd:true' + + (data.options ? (',' + data.options) : '') + '}).load();'; + + const cb = function() { + const apvLoadScript = global.document.createElement('script'); + apvLoadScript.text = apvScript; + container.appendChild(apvLoadScript); + }; + + writeScript(global, scriptUrl, cb); +} diff --git a/ads/appvador.md b/ads/appvador.md new file mode 100644 index 000000000000..eca189614179 --- /dev/null +++ b/ads/appvador.md @@ -0,0 +1,40 @@ + + +# AppVador + +## Example + +```html + + +``` + +## Configuration + +For configuration semantics, contact [AppVador](http://www.appvador.com/). + +### Required Parameters + +- `data-id` + +### Optional parameters + +- `data-options` +- `data-js-type` +- `data-custom-script-src` diff --git a/ads/atomx.js b/ads/atomx.js new file mode 100644 index 000000000000..d6fb63290890 --- /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 {validateData, writeScript} 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..a947c3458b35 --- /dev/null +++ b/ads/atomx.md @@ -0,0 +1,41 @@ + + +# Atomx + +## Example + +```html + + +``` + +## Configuration + +For configuration information, see [atomx documentation](https://wiki.atomx.com/tags). + +### Required Parameters + +* `data-id` - placement ID + +### Optional parameters + +* `data-click` - URL to pre-pend 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/bidtellect.js b/ads/bidtellect.js new file mode 100644 index 000000000000..a05105db3c80 --- /dev/null +++ b/ads/bidtellect.js @@ -0,0 +1,54 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function bidtellect(global, data) { + const requiredParams = ['t', 'pid', 'sid']; + const optionalParams = [ + 'sname', + 'pubid', + 'pubname', + 'renderid', + 'bestrender', + 'autoplay', + 'playbutton', + 'videotypeid', + 'videocloseicon', + 'targetid', + 'bustframe']; + validateData(data, requiredParams, optionalParams); + let params = '?t=' + encodeURIComponent(data.t); + params += '&pid=' + encodeURIComponent(data.pid); + params += '&sid=' + encodeURIComponent(data.sid); + if (data.width) { + params += '&w=' + encodeURIComponent(data.width); + } + if (data.height) { + params += '&h=' + encodeURIComponent(data.height); + } + optionalParams.forEach(function(param) { + if (data[param]) { + params += '&' + param + '=' + encodeURIComponent(data[param]); + } + }); + const url = 'https://cdn.bttrack.com/js/infeed/2.0/infeed.min.js' + params; + writeScript(global, url); +} diff --git a/ads/bidtellect.md b/ads/bidtellect.md new file mode 100644 index 000000000000..f9541b886b97 --- /dev/null +++ b/ads/bidtellect.md @@ -0,0 +1,52 @@ + + +# Bidtellect + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, contact [Bidtellect](mailto:technology@bidtellect.com). + +### Required parameters + +- `data-t`: Parent publisher security token. +- `data-pid`: The unique identifier for your placement. +- `data-sid`: Unique identifier for the site. + +### Optional Parameters: + +- `data-sname`: Name of site that corresponds to the Site ID. +- `data-pubid`: Unique identifier for the publisher. +- `data-pubname`: Name of publisher that corresponds to the Publisher ID. +- `data-renderid`: Unique identifier of the placement widget. +- `data-bestrender`: Provides the best size and cropping for the placement. +- `data-autoplay`: Enables autoplay for video placements. +- `data-playbutton`: Onscreen play button for video placements. +- `data-videotypeid`: Defines how it will be rendered the video player. +- `data-videocloseicon`: Enable close button on the video player. +- `data-targetid`: Allows the placement to render inside a target HTML element. +- `data-bustframe`: Allows the placement to bust out of nested iframes recursively. diff --git a/ads/brainy.js b/ads/brainy.js new file mode 100644 index 000000000000..15a746f28cc6 --- /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 {validateData, writeScript} 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..d8630630cda2 --- /dev/null +++ b/ads/brainy.md @@ -0,0 +1,36 @@ + + +# brainy + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, contact http://www.opt.ne.jp/contact_detail/id=8. + +Supported parameters: + +- `data-aid` +- `data-slot-id` diff --git a/ads/bringhub.js b/ads/bringhub.js new file mode 100644 index 000000000000..1cb93de7c41d --- /dev/null +++ b/ads/bringhub.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 {loadScript, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function bringhub(global, data) { + (global._bringhub = global._bringhub || { + viewId: global.context.pageViewId, + htmlURL: data['htmlurl'] || global.context.canonicalUrl, + ampURL: data['ampurl'] || global.context.sourceUrl, + referrer: data['referrer'] || global.context.referrer, + }); + + writeScript(global, `https://static.bh-cdn.com/msf/amp-loader.js?v=${Date.now()}`, function() { + loadScript(global, `https://static.bh-cdn.com/msf/amp-widget.js?v=${global._bringhub.hash}`); + }); +} diff --git a/ads/bringhub.md b/ads/bringhub.md new file mode 100644 index 000000000000..50e3a137e71b --- /dev/null +++ b/ads/bringhub.md @@ -0,0 +1,37 @@ + + +# Bringhub + +## Example installation of the Bringhub Mini-Storefront + +### Basic + +```html + + +``` + +## Configuration + +### Optional parameters + +- `htmlURL`: The URL of the standard html version of the page. Defaults to `global.context.canonicalURL`. +- `ampURL`: The URL of the AMP version of the page. Defaults to `global.context.sourceUrl`. +- `articleSelector`: The CSS Selector of the article body on the page. Contact your Bringhub Account Manager for requirements. diff --git a/ads/broadstreetads.js b/ads/broadstreetads.js new file mode 100644 index 000000000000..cf745330ae81 --- /dev/null +++ b/ads/broadstreetads.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 broadstreetads(global,data) { + validateData( + data, + ['network', 'zone', 'width', 'height'], + ['keywords', 'place'] + ); + + data.place = data.place || 0; + + const 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.broadstreet = global.broadstreet || {}; + global.broadstreet.loadZone = global.broadstreet.loadZone || (() => ({})); + global.broadstreet.run = global.broadstreet.run || []; + global.broadstreet.run.push(() => { + global.broadstreet.loadZone(d, { + amp: true, + height: data.height, + keywords: data.keywords, + networkId: data.network, + place: data.place, + softKeywords: true, + width: data.width, + zoneId: data.zone, + }); + }); + loadScript(global, 'https://cdn.broadstreetads.com/init-2.min.js'); +} diff --git a/ads/broadstreetads.md b/ads/broadstreetads.md new file mode 100644 index 000000000000..ac00407da2db --- /dev/null +++ b/ads/broadstreetads.md @@ -0,0 +1,42 @@ + + +# Broadstreet Ads + +## Example + +```html + + +``` +## Configuration + +For configuration semantics, see the [Broadstreet Ads documentation](https://information.broadstreetads.com/amp-configuration/). + +### Required parameters + +- `width` +- `height` +- `data-network` +- `data-zone` + +### Optional parameters + +- `data-place` +- `data-keywords` diff --git a/ads/caajainfeed.js b/ads/caajainfeed.js new file mode 100644 index 000000000000..68cd1136d2bd --- /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.js'); + +} diff --git a/ads/caajainfeed.md b/ads/caajainfeed.md new file mode 100644 index 000000000000..6be2c2d2aa7e --- /dev/null +++ b/ads/caajainfeed.md @@ -0,0 +1,57 @@ + + +# CA A.J.A. Infeed + +## Example + +```html + + +``` + +## Configuration + +For configuration details, please email amb-nad@cyberagent.co.jp. + +### Required parameters + +- `data-ad-spot` + +### Optional parameters + +- `data-format` +- `data-test` +- `data-optout` +- `data-offset` +- `data-ipv4` +- `data-ipv6` +- `data-network-reachability` +- `data-os-name` +- `data-os-version` +- `data-os-lang` +- `data-os-timezone` +- `data-device-version` +- `data-app-id` +- `data-app-version` +- `data-kv` +- `data-uids` +- `data-template` +- `data-protocol` +- `data-fields` diff --git a/ads/capirs.js b/ads/capirs.js new file mode 100644 index 000000000000..ebb00129edcc --- /dev/null +++ b/ads/capirs.js @@ -0,0 +1,97 @@ +/** + * 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 capirs(global, data) { + validateData(data, ['begunAutoPad', 'begunBlockId']); + + if (data['customCss']) { + const style = global.document.createElement('style'); + + if (style.styleSheet) { + style.styleSheet.cssText = data['customCss']; + } else { + style.appendChild(global.document.createTextNode(data['customCss'])); + } + + global.document.getElementById('c').appendChild(style); + } + + global['begun_callbacks'] = { + lib: { + init: () => { + const block = global.document.createElement('div'); + block.id = 'x-' + Math.round(Math.random() * 1e8).toString(36); + + global.document.getElementById('c').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]; + + global.context.renderStart({ + width: getWidth(global, banner), + height: banner.height, + }); + + const reportId = 'capirs-' + banner['banner_id']; + global.context.reportRenderedEntityIdentifier(reportId); + }, + unexist: function() { global.context.noContentAvailable(); }, + }, + }; + + loadScript(global, '//ssp.rambler.ru/lpdid.js'); + loadScript(global, '//ssp.rambler.ru/capirs_async.js'); +} + +/** + * @param {!Window} global + * @param {!Object} banner + */ +function getWidth(global, banner) { + let width; + + if (isResponsiveAd(banner)) { + width = Math.max( + global.document.documentElement./*OK*/clientWidth, + global.window./*OK*/innerWidth || 0 + ); + } else { + width = banner.width; + } + + return width; +} + +/** + * @param {!Object} banner + * @return {boolean} + */ +function isResponsiveAd(banner) { + return banner.width.indexOf('%') !== -1; +} diff --git a/ads/capirs.md b/ads/capirs.md new file mode 100644 index 000000000000..d292ba02e4cc --- /dev/null +++ b/ads/capirs.md @@ -0,0 +1,40 @@ + + +# Rambler&Co + +## Example + +```html + +``` + +## Configuration + +For semantics of configuration, please see Rambler SSP documentation. + +Supported parameters: + +- `data-begun-auto-pad` +- `data-begun-block-id` +- `data-custom-css` diff --git a/ads/caprofitx.js b/ads/caprofitx.js new file mode 100644 index 000000000000..5516a8d8cad4 --- /dev/null +++ b/ads/caprofitx.js @@ -0,0 +1,28 @@ +/** + * 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 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..5a5b1594ed36 --- /dev/null +++ b/ads/caprofitx.md @@ -0,0 +1,40 @@ + + +# CA ProFit-X + +## Example + +```html + + +``` + +## Configuration +For configuration details, please email ca_profitx_support@cyberagent.co.jp. + +### Required parameters + +- `data-tagid` + +### Optional parameters + +- `data-placeid` diff --git a/ads/cedato.js b/ads/cedato.js new file mode 100644 index 000000000000..ba5bb2e12cdb --- /dev/null +++ b/ads/cedato.js @@ -0,0 +1,75 @@ +/** + * Copyright 2018 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 {parseUrlDeprecated} from '../src/url'; +import {setStyles} from '../src/style'; +import {validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function cedato(global, data) { + const requiredParams = ['id']; + const optionalParams = [ + 'domain', + 'servingDomain', + 'subid', + 'version', + 'extraParams', + ]; + validateData(data, requiredParams, optionalParams); + + if (!data || !data.id) { + global.context.noContentAvailable(); + return; + } + + const cb = Math.floor(Math.random() * 10000); + const domain = data.domain || + parseUrlDeprecated(global.context.sourceUrl).origin; + + /* Create div for ad to target */ + const playerDiv = global.document.createElement('div'); + playerDiv.id = 'video' + data.id + cb; + setStyles(playerDiv, { + width: '100%', + height: '100%', + }); + const playerScript = global.document.createElement('script'); + const servingDomain = data.servingDomain ? + encodeURIComponent(data.servingDomain) : + 'algovid.com'; + const srcParams = [ + 'https://p.' + servingDomain + '/player/player.js', + '?p=' + encodeURIComponent(data.id), + '&cb=' + cb, + '&w=' + encodeURIComponent(data.width), + '&h=' + encodeURIComponent(data.height), + (data.version ? '&pv=' + encodeURIComponent(data.version) : ''), + (data.subid ? '&subid=' + encodeURIComponent(data.subid) : ''), + (domain ? '&d=' + encodeURIComponent(domain) : ''), + (data.extraParams || ''), // already encoded url query string + ]; + + playerScript.onload = () => { + global.context.renderStart(); + }; + + playerScript.src = srcParams.join(''); + playerDiv.appendChild(playerScript); + global.document.getElementById('c').appendChild(playerDiv); +} diff --git a/ads/cedato.md b/ads/cedato.md new file mode 100644 index 000000000000..30fd53790aa6 --- /dev/null +++ b/ads/cedato.md @@ -0,0 +1,43 @@ + + +# Cedato + +## Example + +```html + + +``` + +## Configuration + +For additional details and support contact support@cedato.com. + +### Required parameters + +- `data-id`: the id of the player - supply ID + +### Optional parameters + +- `data-domain`: page domain reported to the player +- `data-serving-domain`: the domain from which the player is served +- `data-subid`: player subid +- `data-version`: version of the player that is being used +- `data-extra-params`: additional player tag parameters can be set in the 'extra-params' query string, all parts have to be encoded. diff --git a/ads/chargeads.js b/ads/chargeads.js new file mode 100644 index 000000000000..cc1ff638d77c --- /dev/null +++ b/ads/chargeads.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 {validateSrcPrefix, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function chargeads(global, data) { + const {src} = data; + validateSrcPrefix(['https://www.chargeplatform.com/', 'https://tags.chargeplatform.com/'], src); + writeScript(global, src); +} diff --git a/ads/chargeads.md b/ads/chargeads.md new file mode 100644 index 000000000000..43038a88010d --- /dev/null +++ b/ads/chargeads.md @@ -0,0 +1,37 @@ + + +# Chargeads + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For configuration semantics, please [contact Chargeads](http://chargeads.com). + +Supported parameters: + +- `src` diff --git a/ads/colombia.js b/ads/colombia.js new file mode 100644 index 000000000000..1f2ec93ffd8b --- /dev/null +++ b/ads/colombia.js @@ -0,0 +1,47 @@ +/** + * 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 colombia(global, data) { + validateData(data, [ + 'clmb_slot', 'clmb_position', 'clmb_section', + 'clmb_divid', 'loadingStrategy', + ]); + // push the two object into the '_colombia' global + (global._colombia = global._colombia || []).push({ + clmbslot: data.clmb_slot, + clmbposition: data.clmb_position, + clmbsection: data.clmb_section, + clmbdivid: data.clmb_divid, + }); + // install observation on entering/leaving the view + global.context.observeIntersection(function(newrequest) { + newrequest.forEach(function(d) { + if (d.intersectionRect.height > 0) { + global._colombia.push({ + visible: true, + rect: d, + }); + } + }); + }); + loadScript(global, 'https://static.clmbtech.com/ad/commons/js/colombia-amp.js'); +} diff --git a/ads/colombia.md b/ads/colombia.md new file mode 100644 index 000000000000..ccb7fd4c74c5 --- /dev/null +++ b/ads/colombia.md @@ -0,0 +1,39 @@ + + +# Colombia + +## Example + +```html + + +``` + +## Configuration + +For configuration semantics, contact care@timesadcenter.com. + +Supported parameters: + +- `data-clmb_slot`: Ad slot +- `data-clmb_position` : Ad position +- `data-clmb_section` : Ad sections diff --git a/ads/connatix.js b/ads/connatix.js new file mode 100644 index 000000000000..271677b3c2fa --- /dev/null +++ b/ads/connatix.js @@ -0,0 +1,50 @@ +/** + * 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 {hasOwn} from '../src/utils/object.js'; +import {tryParseJson} from '../src/json.js'; +import {validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function connatix(global, data) { + + validateData(data, ['connatix']); + + // Because 3p's loadScript does not allow for data attributes, + // we will write the JS tag ourselves. + const script = global.document.createElement('script'); + const cnxData = Object.assign(Object(tryParseJson(data['connatix']))); + global.cnxAmpAd = true; + for (const key in cnxData) { + if (hasOwn(cnxData, key)) { + script.setAttribute(key, cnxData[key]); + } + } + + window.addEventListener('connatix_no_content', function() { + window.context.noContentAvailable(); + }, false); + + script.onload = () => { + global.context.renderStart(); + }; + + script.src = 'https://cdn.connatix.com/min/connatix.renderer.infeed.min.js'; + global.document.getElementById('c').appendChild(script); +} diff --git a/ads/connatix.md b/ads/connatix.md new file mode 100644 index 000000000000..74341cd55b5e --- /dev/null +++ b/ads/connatix.md @@ -0,0 +1,35 @@ + + +# Connatix + +## Example + +```html + + +``` + +## Configuration + +For configuration semantics, contact contact@connatix.com. + +### Required parameters + +- `data-connatix` diff --git a/ads/contentad.js b/ads/contentad.js new file mode 100644 index 000000000000..e6bf8110d803 --- /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 {parseUrlDeprecated} from '../src/url'; +import {validateData, writeScript} 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; + if (data.url) { + const domain = data.url || window.atob(data.d); + sourceUrl = sourceUrl.replace(parseUrlDeprecated(sourceUrl).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..80b019875f5f --- /dev/null +++ b/ads/contentad.md @@ -0,0 +1,40 @@ + + +# Content.ad Inline Ad + +## Example + +```html + + +``` + +## Configuration + +For configuration semantics, 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 new file mode 100644 index 000000000000..e323db60642b --- /dev/null +++ b/ads/criteo.js @@ -0,0 +1,84 @@ +/** + * 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 {computeInMasterFrame, loadScript} from '../3p/3p'; +import {doubleclick} from '../ads/google/doubleclick'; +import {tryParseJson} from '../src/json'; + +/* global Criteo: false */ + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function criteo(global, data) { + loadScript(global, 'https://static.criteo.net/js/ld/publishertag.js', () => { + 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, null); + } else if (data.tagtype === 'standalone') { + Criteo.PubTag.Adapters.AMP.Standalone(data, () => {}, targ => { + setTargeting(global, data, targ); + }); + } else if (!data.tagtype || data.tagtype === 'passback') { + Criteo.DisplayAd({ + zoneid: data.zone, + containerid: 'c', + integrationmode: 'amp', + }); + } + }); +} + +/** + * @param {!Window} global + * @param {!Object} data + * @param {?Object} targeting + */ +function setTargeting(global, data, targeting) { + if (data.adserver === 'DFP') { + const dblParams = tryParseJson(data.doubleclick) || {}; + dblParams['slot'] = data.slot; + dblParams['targeting'] = dblParams['targeting'] || {}; + dblParams['width'] = data.width; + dblParams['height'] = data.height; + dblParams['type'] = 'criteo'; + + if (!targeting && data.tagtype === 'rta') { + targeting = Criteo.ComputeDFPTargetingForAMP( + data.cookiename || Criteo.PubTag.RTA.DefaultCrtgRtaCookieName, + data.varname || Criteo.PubTag.RTA.DefaultCrtgContentName); + } + for (const i in targeting) { + dblParams['targeting'][i] = targeting[i]; + } + + doubleclick(global, dblParams); + } +} + + diff --git a/ads/criteo.md b/ads/criteo.md new file mode 100644 index 000000000000..a39c6ae1da56 --- /dev/null +++ b/ads/criteo.md @@ -0,0 +1,94 @@ + + +# Criteo + +Criteo support for AMP covers Real Time Audience (RTA), Standalone, 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 + + +``` + +## Example - Standalone + +```html + + +``` + +## 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 + +Supported parameters: + +- `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. +- `data-doubleclick`: custom options to send to doubleclick, in JSON format. Optional. See [doubleclick documentation](google/doubleclick.md) for details. + +### PuMP and Passback + +Supported parameters: + +- `data-tagtype`: identifies the used Criteo technology. Must be “passback”. Required. +- `data-zone`: your Criteo zone identifier. Required. + +### Standalone + +Supported parameters: + +- `data-tagtype`: identifies the used Criteo technology. Must be "standalone". Required. +- `data-adserver`: the name of your adserver. Required. Only "DFP" is supported at this stage. +- `data-slot`: adserver (DFP) slot. Required. +- `data-zone`: your Criteo zone identifier. Required. +- `data-line-item-ranges`: your line item ranges. Required. +- `data-timeout`: bid timeout override. Optional. +- `data-doubleclick`: custom options to send to doubleclick, in JSON format. Optional. See [doubleclick documentation](google/doubleclick.md) for details. + diff --git a/ads/custom.md b/ads/custom.md new file mode 100644 index 000000000000..5f7b78021033 --- /dev/null +++ b/ads/custom.md @@ -0,0 +1,223 @@ + + +# 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 +The template can be specified outside the `amp-ad` tag for sharing. You can refer to the template using its ID via the `template` attribute of `amp-ad`. You can also provide a `data-slot` attribute for each `amp-ad`, so they can share one single remote request to fetch the ads data. + +```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. This endpoint must be available +cross-origin. (See [CORS in AMP](https://www.ampproject.org/docs/fundamentals/amp-cors-requests).) + +### 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" +} +``` +The ad server must enforce [AMP CORS](https://github.com/ampproject/amphtml/blob/master/spec/amp-cors-requests.md#cors-security-in-amp). +Here is an example set of the relevant response headers: +``` +Access-Control-Allow-Origin:https://cdn.ampproject.org +Access-Control-Expose-Headers:AMP-Access-Control-Allow-Source-Origin +AMP-Access-Control-Allow-Source-Origin:https://my-ad-server.com +``` + +## 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/dable.js b/ads/dable.js new file mode 100644 index 000000000000..798b0c0d38f8 --- /dev/null +++ b/ads/dable.js @@ -0,0 +1,64 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function dable(global, data) { + // check required props + validateData(data, ['widgetId']); + + (global.dable = global.dable || function() { + (global.dable.q = global.dable.q || []).push(arguments); + }); + global.dable('setService', data['serviceName'] || + global.window.context.location.hostname); + global.dable('setURL', global.window.context.sourceUrl); + global.dable('setRef', global.window.context.referrer); + + const slot = global.document.createElement('div'); + slot.id = '_dbl_' + Math.floor(Math.random() * 100000); + slot.setAttribute('data-widget_id', data['widgetId']); + + const divContainer = global.document.getElementById('c'); + if (divContainer) { + divContainer.appendChild(slot); + } + + const itemId = data['itemId'] || ''; + const opts = {}; + + if (itemId) { + global.dable('sendLog', 'view', {id: itemId}); + } else { + opts.ignoreItems = true; + } + + // call render widget + global.dable('renderWidget', slot.id, itemId, opts, function(hasAd) { + if (hasAd) { + global.context.renderStart(); + } else { + global.context.noContentAvailable(); + } + }); + + // load the Dable script asynchronously + loadScript(global, 'https://static.dable.io/dist/plugin.min.js'); +} diff --git a/ads/dable.md b/ads/dable.md new file mode 100644 index 000000000000..5a795ee70632 --- /dev/null +++ b/ads/dable.md @@ -0,0 +1,42 @@ + + +# Dable + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://admin.dable.io. + +### Required parameters + +- `data-widget-id` + +### Optional parameters + +- `data-item-id` +- `data-service-name` diff --git a/ads/dianomi.md b/ads/dianomi.md new file mode 100644 index 000000000000..02fdbb04177f --- /dev/null +++ b/ads/dianomi.md @@ -0,0 +1,43 @@ + + +# Dianomi Exchange + +## Example + +```html + +``` + +## Configuration + +For configuration semantics and to learn more about how we can help you deliver AMP Ads, please [contact dianomi](http://www.dianomi.com/). + +### Supported parameters + +- `data-id` +- `width` +- `height` +- `data-cf-a4a` +- `src` + +### Required parameters + +- `data-cf-network` +- `type` diff --git a/ads/directadvert.js b/ads/directadvert.js new file mode 100644 index 000000000000..0e460ae8432b --- /dev/null +++ b/ads/directadvert.js @@ -0,0 +1,35 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function directadvert(global, data) { + validateData(data, ['blockId']); + + const url = 'https://code.directadvert.ru/data/' + + encodeURIComponent(data['blockId']) + '.js?async=1&div=c'; + + loadScript(global, url, () => { + global.context.renderStart(); + }, () => { + global.context.noContentAvailable(); + }); + +} diff --git a/ads/directadvert.md b/ads/directadvert.md new file mode 100644 index 000000000000..f5a9070a9f96 --- /dev/null +++ b/ads/directadvert.md @@ -0,0 +1,34 @@ + + +# Directadvert + +## Example + +```html + + +``` + +## Configuration + +For more information, please [see the Directadvert FAQ](https://www.directadvert.ru/text/help). + +### Required parameters + +- `data-block-id` 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..e41c0a4946d0 --- /dev/null +++ b/ads/distroscale.md @@ -0,0 +1,35 @@ + + +# DistroScale + +## Example + +```html + +``` + +## Configuration + +For configuration semantics, please [contact DistroScale](http://www.distroscale.com). + +### Required parameters + +- `data-pid`: Partner ID diff --git a/ads/dotandads.js b/ads/dotandads.js index e54b01a9c682..8c2e82f51eb9 100644 --- a/ads/dotandads.js +++ b/ads/dotandads.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {writeScript} from '../src/3p'; +import {writeScript} from '../3p/3p'; /** * @param {!Window} global diff --git a/ads/dotandads.md b/ads/dotandads.md index 43b80ded8240..121a890ad611 100644 --- a/ads/dotandads.md +++ b/ads/dotandads.md @@ -16,11 +16,12 @@ limitations under the License. # DotAndAds -## Example +## Examples + +#### 300x250 box -300x250 box: ```html - ``` -980x250 masthead: +#### 980x250 masthead + ```html - { - global.googletag.cmd.push(function() { - const googletag = global.googletag; - const dimensions = [[ - parseInt(data.overrideWidth || data.width, 10), - parseInt(data.overrideHeight || data.height, 10) - ]]; - const pubads = googletag.pubads(); - const slot = googletag.defineSlot(data.slot, dimensions, 'c') - .addService(pubads); - pubads.enableSingleRequest(); - pubads.markAsAmp(); - pubads.set('page_url', context.canonicalUrl); - googletag.enableServices(); - - if (data.targeting) { - for (const key in data.targeting) { - slot.setTargeting(key, data.targeting[key]); - } - } - - if (data.categoryExclusion) { - slot.setCategoryExclusion(data.categoryExclusion); - } - - if (data.tagForChildDirectedTreatment != undefined) { - pubads.setTagForChildDirectedTreatment( - data.tagForChildDirectedTreatment); - } - - if (data.cookieOptions) { - pubads.setCookieOptions(data.cookieOptions); - } - - pubads.addEventListener('slotRenderEnded', function(event) { - let creativeId = event.creativeId || - // Full for backfill or empty case. Empty is handled below. - '_backfill_'; - if (event.isEmpty) { - context.noContentAvailable(); - creativeId = '_empty_'; - } - context.reportRenderedEntityIdentifier('dfp-' + creativeId); - }); - - // Exported for testing. - c.slot = slot; - googletag.display('c'); - }); - }); -} diff --git a/ads/doubleclick.md b/ads/doubleclick.md deleted file mode 100644 index 1a3eb5fc323b..000000000000 --- a/ads/doubleclick.md +++ /dev/null @@ -1,69 +0,0 @@ - - -# Doubleclick - -## Example - -### Basic - -```html - - -``` - -### With additional targeting - -```html - - -``` - -## Configuration - -For semantics of configuration, please see [ad network documentation](https://developers.google.com/doubleclick-gpt/reference). - - -### Ad size - -By default the ad size is based on the `width` and `height` attributes of the `amp-ad` tag. In order to explicitly request different ad dimensions from those values, pass the attributes `data-override-width` and `data-override-height` to the ad. - -Example: - -```html - - -``` - -### Supported parameters - -- `data-slot` - -Supported via `json` attribute: - -- `categoryExclusion` -- `cookieOptions` -- `tagForChildDirectedTreatment` -- `targeting` diff --git a/ads/eadv.js b/ads/eadv.js new file mode 100644 index 000000000000..a012355bfeb7 --- /dev/null +++ b/ads/eadv.js @@ -0,0 +1,26 @@ +/** + * 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, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function eadv(global, data) { + validateData(data, ['x', 'u'], []); + writeScript(global, 'https://www.eadv.it/track/?x=' + data.x + '&u=' + data.u); +} diff --git a/ads/eadv.md b/ads/eadv.md new file mode 100644 index 000000000000..e2c4bcb04416 --- /dev/null +++ b/ads/eadv.md @@ -0,0 +1,36 @@ + + +# eADV + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +Supported parameters: + +- `data-x` +- `data-u` diff --git a/ads/eas.js b/ads/eas.js new file mode 100644 index 000000000000..8a689a0eb77f --- /dev/null +++ b/ads/eas.js @@ -0,0 +1,27 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function eas(global, data) { + validateData(data, ['easDomain']); + global.easAmpParams = data; + writeScript(global, 'https://amp.emediate.eu/amp.v0.js'); +} diff --git a/ads/eas.md b/ads/eas.md new file mode 100644 index 000000000000..f7936a5d2530 --- /dev/null +++ b/ads/eas.md @@ -0,0 +1,59 @@ + + +# CxenseDisplay + +## Example + +### Basic call + +Corresponds to `https://eas4.emediate.eu/eas?cu=12345` + +```html + + +``` + +### With targeting parameters + +```html + + +``` + +## Configuration + +### Required parameters + +- `data-eas-domain`: Specify your ad-server domain (e.g., `eas3.emediate.se`); If you're using a custom domain-name (like, `eas.somesite.com`) you should NOT use that one unless you already have an SSL-certificate installed on our ad servers. + +### Optional parameters + +- `data-eas-[parameter]`: Any ad-request parameter, like 'cu'. + + + + + + diff --git a/ads/engageya.js b/ads/engageya.js new file mode 100644 index 000000000000..2197a46e48c4 --- /dev/null +++ b/ads/engageya.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 engageya(global, data) { + + validateData(data, ['widgetids']); + + (global._engageya = global._engageya || { + viewId: global.context.pageViewId, + widgetIds: data['widgetids'], + websiteId: data['websiteid'], + publisherId: data['publisherid'], + url: data['url'] || global.context.canonicalUrl, + ampURL: data['ampurl'] || global.context.sourceUrl, + mode: data['mode'] || 1, + style: data['stylecss'] || '', + referrer: global.context.referrer, + }); + + loadScript(global, 'https://widget.engageya.com/engageya_amp_loader.js'); +} diff --git a/ads/engageya.md b/ads/engageya.md new file mode 100644 index 000000000000..339f6743cd02 --- /dev/null +++ b/ads/engageya.md @@ -0,0 +1,47 @@ + + +# Engageya + +## Example of Engageya's widget implementation + +### Basic + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `data-widgetIds`: Widget ids +- `data-websiteId`: Website Id +- `data-publisherId`: Publisher Id + +### Optional parameters + +- `data-url`: Current none amp version URL +- `data-ampUrl`: Current AMP page URL +- `data-styleCSS`: Additional style diff --git a/ads/epeex.js b/ads/epeex.js new file mode 100644 index 000000000000..503049748ed9 --- /dev/null +++ b/ads/epeex.js @@ -0,0 +1,35 @@ +/** + * Copyright 2018 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'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function epeex(global, data) { + + (global._epeex = global._epeex || { + account: data['account'] || 'demoepeex', + channel: data['channel'] || '1', + htmlURL: data['htmlurl'] || encodeURIComponent(global.context.canonicalUrl), + ampURL: data['ampurl'] || encodeURIComponent(global.context.sourceUrl), + testMode: data['testmode'] || 'false', + }); + + // load the epeex AMP remote js file + loadScript(global, 'https://epeex.com/related/service/widget/amp/remote.js'); +} diff --git a/ads/epeex.md b/ads/epeex.md new file mode 100644 index 000000000000..790170122a55 --- /dev/null +++ b/ads/epeex.md @@ -0,0 +1,43 @@ + + +### Basic + +```html + + +``` + +## Configuration + +For semantics of configuration, please contact info@epeex.com. + + +### Required parameters + +- `data-account` +- `data-channel` + +### Optional parameters + +- `data-htmlurl` +- `data-ampurl` +- `data-testmode` diff --git a/ads/eplanning.js b/ads/eplanning.js new file mode 100644 index 000000000000..04972711af23 --- /dev/null +++ b/ads/eplanning.js @@ -0,0 +1,37 @@ +/** + * 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 eplanning(global, data) { + validateData(data, [ + 'epl_si', 'epl_isv', 'epl_sv', 'epl_sec', 'epl_kvs', 'epl_e', + ]); + // push the two object into the '_eplanning' global + (global._eplanning = global._eplanning || []).push({ + sI: data.epl_si, + isV: data.epl_isv, + sV: data.epl_sv, + sec: data.epl_sec, + kVs: data.epl_kvs, + e: data.epl_e, + }); + loadScript(global, 'https://us.img.e-planning.net/layers/epl-amp.js'); +} diff --git a/ads/eplanning.md b/ads/eplanning.md new file mode 100644 index 000000000000..ee31c1ebe914 --- /dev/null +++ b/ads/eplanning.md @@ -0,0 +1,46 @@ + + +# e-planning + + +## Example + +```html + + +``` + +## Configuration + +For configuration semantics, please see [e-planning's documentation](https://www.e-planning.net). For support contact support@e-planning.net. + +Supported parameters: + +- `data-epl_si`: Site ID +- `data-epl_sv`: Default adserver +- `data-epl_isv`: Default CDN +- `data-epl_sec`: Section +- `data-epl_kvs`: Data keywords +- `data-epl_e`: Space name 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..544fa7521d13 --- /dev/null +++ b/ads/ezoic.md @@ -0,0 +1,54 @@ + + +# 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` + +## Consent Support + +Ezoic amp-ad adhere to a user's consent in the following ways: + +- `CONSENT_POLICY_STATE.SUFFICIENT`: Ezoic amp-ad will display a personalized ad to the user. +- `CONSENT_POLICY_STATE.INSUFFICIENT`: Ezoic amp-ad will display a non-personalized ad to the user. +- `CONSENT_POLICY_STATE.UNKNOWN_NOT_REQUIRED`: Ezoic amp-ad will display a personalized ad to the user. +- `CONSENT_POLICY_STATE.UNKNOWN`: Ezoic amp-ad will not display an ad. diff --git a/ads/f1e.js b/ads/f1e.js new file mode 100644 index 000000000000..180ab3d69f4e --- /dev/null +++ b/ads/f1e.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 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..f4b3fedbf39f --- /dev/null +++ b/ads/f1e.md @@ -0,0 +1,38 @@ + + + +# FlexOneELEPHANT + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +Supported parameters: + +- `data-url` - Must start with "https:" +- `data-target` + diff --git a/ads/f1h.js b/ads/f1h.js new file mode 100644 index 000000000000..0c38b485d6ba --- /dev/null +++ b/ads/f1h.js @@ -0,0 +1,35 @@ +/** + * 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 f1h(global, data) { + validateData(data, ['sectionId', 'slot']); + + const scriptUrl = + data['debugsrc'] || 'https://img.ak.impact-ad.jp/fh/f1h_amp.js'; + + global.f1hData = data; + loadScript(global, scriptUrl); +} + diff --git a/ads/f1h.md b/ads/f1h.md new file mode 100644 index 000000000000..8dce8e4a461c --- /dev/null +++ b/ads/f1h.md @@ -0,0 +1,63 @@ + + +# F1H + +## Examples + +### Single ad + +```html + + +``` + +### Using custom params and custom ad server url + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `sectionId`: ID of this section in inventory system. +- `slot`: ID of slot that will be showed in this ad block. +- `pubnetwork-lib`: Filepath of ad library. + +### Optional parameters + +- `custom`: usage example + +```text +{ + "arrayKey":["value1",1], + "stringKey":"stringValue" +} +``` + diff --git a/ads/felmat.js b/ads/felmat.js new file mode 100644 index 000000000000..dc7532c0833c --- /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 {validateData, writeScript} 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..451c71634fd7 --- /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 new file mode 100644 index 000000000000..602842d3001f --- /dev/null +++ b/ads/flite.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 {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function flite(global, data) { + // TODO: check mandatory fields + validateData(data, [], ['guid','mixins']); + const {guid} = data, o = global, e = encodeURIComponent, x = 0; + let r = '', dep = ''; + o.FLITE = o.FLITE || {}; + o.FLITE.config = o.FLITE.config || {}; + o.FLITE.config[guid] = o.FLITE.config[guid] || {}; + o.FLITE.config[guid].cb = Math.random(); + o.FLITE.config[guid].ts = (+Number(new Date())); + r = global.context.location.href; + const m = r.match(new RegExp('[A-Za-z]+:[/][/][A-Za-z0-9.-]+')); + dep = data.mixins ? '&dep=' + data.mixins : ''; + const url = ['https://r.flite.com/syndication/uscript.js?i=',e(guid), + '&v=3',dep,'&x=us',x,'&cb=',o.FLITE.config[guid].cb,'&d=', + e((m && m[0]) || r), '&tz=', (new Date()).getTimezoneOffset()].join(''); + loadScript(o, url); +} diff --git a/ads/flite.md b/ads/flite.md new file mode 100644 index 000000000000..cf70bad9b4b7 --- /dev/null +++ b/ads/flite.md @@ -0,0 +1,36 @@ + + +# Flite + +## Example + +```html + + +``` + +## Configuration + +For configuration and implementation details, please [contact Flite Support](http://www.flite.com/). + +Supported parameters: + +- `data-guid` +- `data-mixins` diff --git a/ads/fluct.js b/ads/fluct.js new file mode 100644 index 000000000000..4b0ddd7839a4 --- /dev/null +++ b/ads/fluct.js @@ -0,0 +1,32 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/* global adingoFluct: false */ + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function fluct(global, data) { + validateData(data, ['g', 'u']); + writeScript(global, + `https://cdn-fluct.sh.adingo.jp/f.js?G=${encodeURIComponent(data['g'])}`, + function() { + adingoFluct.showAd(data['u']); + }); +} diff --git a/ads/fluct.md b/ads/fluct.md new file mode 100644 index 000000000000..74ad7722b118 --- /dev/null +++ b/ads/fluct.md @@ -0,0 +1,36 @@ + + +# fluct + +## Example + +```html + + +``` + +## Configuration + +For more information, please [contact fluct](https://corp.fluct.jp/en/contact.php). + +### Required parameters + +- `data-g` +- `data-u` diff --git a/ads/fusion.js b/ads/fusion.js new file mode 100644 index 000000000000..5c3842281925 --- /dev/null +++ b/ads/fusion.js @@ -0,0 +1,57 @@ +/** + * 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 {string=} input + * @return {JsonObject|undefined} + */ +function queryParametersToObject(input) { + if (!input) { + return undefined; + } + return input.split('&').filter(_ => _).reduce((obj, val) => { + const kv = val.split('='); + return Object.assign(obj, {[kv[0]]: kv[1] || true}); + }, {}); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function fusion(global, data) { + validateData(data, [], + ['mediaZone', 'layout', 'adServer', 'space', 'parameters']); + + const container = global.document.getElementById('c'); + const ad = global.document.createElement('div'); + ad.setAttribute('data-fusion-space', data.space); + container.appendChild(ad); + const parameters = queryParametersToObject(data.parameters); + + writeScript(global, + 'https://assets.adtomafusion.net/fusion/latest/fusion-amp.min.js', () => { + global.Fusion.apply(container, global.Fusion.loadAds(data, parameters)); + + 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..7733d399ec26 --- /dev/null +++ b/ads/fusion.md @@ -0,0 +1,44 @@ + + +# Fusion + +## Example + +```html + + +``` + +## Configuration + +For configuration and implementation details, please contact the Fusion support team: support@adtoma.com + +Supported parameters: + +- `data-ad-server` +- `data-media-zone` +- `data-layout` +- `data-space` +- `data-parameters` + +Parameters should be passed as `key&value` pairs `&` separated. Missing value equals `true`. So `...&isMobile&...` from the example above stands for `...&isMobile=true&...`. 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..a9585c4835fb --- /dev/null +++ b/ads/genieessp.md @@ -0,0 +1,38 @@ + + +# Geniee SSP + +Please visit [Geniee SSP website](https://www.geniee.co.jp/) for more information. + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see Geniee SSP documentation or [contact Geniee SSP](http://en.geniee.co.jp/inquiry/). + +Supported parameters: + +- `data-vid` +- `data-zid` diff --git a/ads/giraff.js b/ads/giraff.js new file mode 100644 index 000000000000..160a0a3e4250 --- /dev/null +++ b/ads/giraff.js @@ -0,0 +1,41 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function giraff(global, data) { + validateData(data, ['blockName']); + + const serverName = data['serverName'] || 'code.giraff.io'; + const url = '//' + encodeURIComponent(serverName) + '/data/widget-' + + encodeURIComponent(data['blockName']) + '.js'; + + loadScript(global, url, () => { + global.context.renderStart(); + }, () => { + global.context.noContentAvailable(); + }); + + const anchorEl = global.document.createElement('div'); + const widgetId = data['widgetId'] ? '_' + data['widgetId'] : ''; + anchorEl.id = 'grf_' + data['blockName'] + widgetId; + global.document.getElementById('c').appendChild(anchorEl); + +} diff --git a/ads/giraff.md b/ads/giraff.md new file mode 100644 index 000000000000..9833215db1c5 --- /dev/null +++ b/ads/giraff.md @@ -0,0 +1,40 @@ + + +# Giraff + +## Example + +```html + + +``` + +## Configuration + +For more information, see [Giraff's documentation](https://www.giraff.io/help). + +### Optional parameters + +- `data-widget-id` +- `data-server-name` + +### Required parameters + +- `data-block-name` diff --git a/ads/gmossp.js b/ads/gmossp.js new file mode 100644 index 000000000000..9b8ce85062e6 --- /dev/null +++ b/ads/gmossp.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 {validateData, writeScript} from '../3p/3p'; + +const gmosspFields = ['id']; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function gmossp(global, data) { + validateData(data, gmosspFields, []); + + global.gmosspParam = data; + writeScript(global, 'https://cdn.gmossp-sp.jp/ads/amp.js'); +} diff --git a/ads/gmossp.md b/ads/gmossp.md new file mode 100644 index 000000000000..c62b1fa1aa4c --- /dev/null +++ b/ads/gmossp.md @@ -0,0 +1,34 @@ + + +# GMOSSP + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact dev@ml.gmo-am.jp + +Supported parameters: + +- `data-id` diff --git a/ads/google/OWNERS.yaml b/ads/google/OWNERS.yaml new file mode 100644 index 000000000000..89198b759b68 --- /dev/null +++ b/ads/google/OWNERS.yaml @@ -0,0 +1 @@ +- ampproject/a4a diff --git a/ads/google/a4a/.eslintrc b/ads/google/a4a/.eslintrc new file mode 100644 index 000000000000..fcbba908cd92 --- /dev/null +++ b/ads/google/a4a/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "amphtml-internal/no-style-display": 2, + } +} 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..419f5224b5c8 --- /dev/null +++ b/ads/google/a4a/docs/Network-Impl-Guide.md @@ -0,0 +1,243 @@ +# Fast Fetch Network Implementation Guide + +This guide outlines the requirements and steps for ad networks to implement Fast +Fetch for early ad request and support for AMP ads returned by the ad network to +be given preferential rendering. + +* *Status: Draft* +* *Authors: [kjwright@google.com](mailto:kjwright@google.com), +[bradfrizzell@google.com](mailto:bradfrizzell@google.com)* +* *Last Updated: 1-27-2016* + +## Contents + +* [Background](#background) +* [Overview](#overview) +* [Detailed design](#detailed-design) + + [Ad server requirements](#ad-server-requirements) + - [SSL](#ssl) + - [AMPHTML ad creative signature](#amphtml-ad-creative-signature) + - [Ad response headers](#ad-response-headers) + + [Creating an AMPHTML ad extension implementation](#creating-an-amphtml-ad-extension-implementation) + - [Create the implementation script](#create-the-implementation-script) + - [Create the configuration file](#create-the-configuration-file) + - [Create documentation](#create-documentation) + - [Create tests](#create-tests) +* [Checklist for ad network implementation](#checklist-for-ad-network-implementation) + + +## Background + +If you haven’t already, please read the [AMPHTML ads readme](./a4a-readme.md) to +learn about why all networks should implement Fast Fetch. + +Relevant design documents: [AMPHTML ads readme](./a4a-readme.md), +[AMPHTML ads spec](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md) +& [intent to implement](https://github.com/ampproject/amphtml/issues/3133). + +## Overview + +Fast Fetch provides preferential treatment to verified AMPHTML ads over legacy +ads, unlike the current 3P rendering flow which treats AMPHTML 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 AMPHTML 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. + +To support Fast Fetch, ad networks are required to implement the following: + +1. An [XHR CORS](https://www.w3.org/TR/cors/) for the ad request. +2. The JavaScript to build the ad request, which 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 + +*Figure 1: Fast Fetch rendering flow* + + + + + +### Ad server requirements + +#### SSL + +All network communication via the AMP HTML runtime (resources or XHR) require SSL. + +#### AMPHTML ad creative signature + +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 that the creative conforms to the +[AMPHTML ad specification](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md). +If the ad conforms, the creative is rewritten by the validation service and the +rewritten creative and a cryptographic signature are returned to the ad network. +The rewritten creative and signature must be included in the response to the AMP +runtime from the ad network. The AMP runtime then parses out the creative and +the signature from the ad response. Lack of, or invalid signature causes the +runtime to treat it as a legacy ad, rendering it within a cross domain iframe +and using delayed ad rendering. + +Client side verification of the signature, and thus preferential rendering, +requires a browser to have Web Crypto. However, if a browser does not have Web +Crypto, Fast Fetch is still able to be used if the ad network permits it. In +this case, the ad will simply be guaranteed to render in a cross-domain iframe. + +#### Ad response headers + +*See [Figure 1 above, Part C](#detailed-design)* + +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 the 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 https://cdn.ampproject.org + domain.
+
AMP-Access-Control-Allow-Source-Origin
+
With the value matching the value of the "__amp_source_origin" + request parameter, which is added + by the AMP Runtime and matches the origin of the request had the page not + been served from Google AMP Cache + (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 + by the AMP Runtime.
+
Access-Control-Allow-Credentials
+
With the value "true" if cookies should be included in the request.
+
Access-Control-Expose-Headers
+
With the value matching a comma-separated list of 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.
+
+ + +### Creating an AMPHTML ad extension implementation + +The [``](https://www.ampproject.org/docs/reference/components/amp-ad) +element differentiates between different ad network implementations via the +`type` attribute. For example, the following amp-ad tag utilizes the DoubleClick +ad network: + +```html + +``` + +To create an ad network implementation, you must perform the following: + +1. Create a new extension in the `extensions` directory of the AMP HTML Github + [repository](https://github.com/ampproject/amphtml/tree/master/extensions) + whose path and name match the `type` attribute given for the amp-ad element + as follows: + + *Figure 2: File hierarchy for an AMPHTML ad implementation* + + + + + +2. Ad networks that want to add support for Fast Fetch within AMP must add the + file hierarchy to the AMP repository as show in Figure 2, 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. + +#### Create the implementation script + +*For reference, see [Figure 1 Parts B and D](#detailed-design).* + +1. Create a file named `amp-ad-network--impl.js`, which implement the + `AmpAdNetworkImpl` class. +2. This class must extend [AmpA4A](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/0.1/amp-a4a.js). +3. This class must overwrite the super class method **getAdUrl()**. + + ``` javascript + getAdUrl() - must construct and return the ad url for ad request. + // @return {string} - the ad url + ``` + +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` 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). + +#### Create the configuration file + +*For reference, see [Figure 1: Part A](#figure-1-fast-fetch-rendering-flow)*. + +1. Create a `-a4a-config.js` file that implements and exports the + 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. + ``` + +2. Once this file is implemented, you must also update [amphtml/ads/_a4a-config.js](https://github.com/ampproject/amphtml/blob/master/ads/_a4a-config.js). + 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: [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). + +#### Create documentation + +Create a file named `amp-ad-network--impl-internal.md`, and within this +file provide thorough documentation for the use of your implementation. + +Examples: See [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). + +#### Create tests + +Create the `test-amp-ad-network--impl.js` file, and write thorough testing +for your AMP ad network implementation. + +## Checklist for ad network implementation + +- [ ] 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()` +- [ ] `-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/RTBExchangeGuide.md b/ads/google/a4a/docs/RTBExchangeGuide.md new file mode 100644 index 000000000000..0ac01572cbd9 --- /dev/null +++ b/ads/google/a4a/docs/RTBExchangeGuide.md @@ -0,0 +1,82 @@ +# AMPHTML Ad Implementation Guide for RTB Ad Exchanges +--- +## Objective + +This guide is designed to provide additional information for SSPs and Ad Exchanges that want to support AMPHTML ads in a Real-Time Bidding (RTB) environment. The IAB's OpenRTB 2.5 spec is [here](http://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf). + +AMP pages must ensure all content conforms to the AMP format. When non-AMP content is included in an AMP page, there can be a delay as the content is verified. A major benefit for AMPHTML ads on AMP pages is that the ad can be rendered early by splicing the ad into the surrounding AMP page, without affecting the UX of the page and without delay. + +For those new to AMPHTML ads, see the background docs at the bottom of this article. + +## AMPHTML Ads in RTB: High-Level Approach + +### RTB Bid Request + +Exchanges will need to indicate in the RTB bid request whether a page is built in AMP, and any specific requirements or treatment of AMPHTML ads. As of [OpenRTB 2.5](http://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf), this is not yet included in the spec, but the proposed implementation to the IAB committee is as follows. + +**`Site` Object additional field: `amp`** + +A new field is added to the `Site` object of the OpenRTB standard to indicate whether a webpage is built on AMP. In OpenRTB 2.5, this is section 3.2.13. + +| Field | Scope | Type | Default | Description | +| ------------- |------ |----- |:-------------:|------------- | +| `amp` | optional | integer | - | Whether the request is for an Accelerated Mobile Page. 0 = page is non-AMP, 1 = page is built with AMP HTML. AMP status unknown if omitted. | + +**`Imp` Object additional field: `ampad`** + +A new field is added to the `Imp` object of the OpenRTB standard to provide more detail around AMP ad requirements and how AMP ads will load. In OpenRTB 2.5, this is section 3.2.4. + +| Field | Scope | Type | Default | Description | +| ------------- |------ |----- |:-------------:|------------- | +| `ampad` | optional | integer | 1 | AMPHTML ad requirements and rendering behavior. See AMPHTML Ad Status Table. | + +**AMPHTML Ad Status Table** + +| Value | Description | +| ------------- |------------- | +| 1 | AMPHTML ad requirements are unknown.| +| 2 | AMPHTML ads are not allowed. | +| 3 | Either AMPHTML or non-AMPHTML ads are allowed; AMPHTML ads are not early rendered. | +| 4 | Either AMPHTML or non-AMPHTML ads are allowed, and AMPHTML ads are early rendered.| +| 5 | AMPHTML ads are required. Ads that are non-AMPHTML may be rejected by the publisher.| +| 500+ | Exchange-specific values; should be communicated to bidders *a priori* | + +### RTB Bid Response + +SSPs will need to provide a new field in the bid response to allow bidders to return AMPHTML content, and RTB bidders will need to populate that field in order to return AMPHTML ads. As of [OpenRTB 2.5](http://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf), this is not yet included in the spec, but the proposed workflow is a new field that accepts a URL pointing to AMP ad content. + +**`Bid` Object additional field: `ampadurl`** + +| Field | Type | Description | +| ------------- |------ |----- | +| `ampadurl` | string | Optional means of conveying AMPHTML ad markup in case the bid wins; only one of `ampadurl` or `adm` should be set. Substitution macros (Section 4.4) may be included. URL should point to a creative server containing valid AMP Ad html. | + +### Verification of valid AMP + +* For AMPHTML ads to be rendered early, the exchange is required to verify and sign in real time that the ad is written in amp4ads `` creative format. +* See "[Proposed Design](https://github.com/ampproject/amphtml/issues/3133)" for signing. +* Ads that are valid AMPHTML ads will be allowed to render early by AMP pages. Ads that are not verified as valid AMPHTML ads will render at the same speed as non-AMPHTML ads. +* Only AMPHTML ads should be returned in the `ampadurl`. + +### Server-side fetch + +* For AMPHTML ads to be rendered early, AMPHTML ad content must be fetched with 0 additional "hops" from the client. This is designed to avoid poor user experiences due to ad latency and extra client-side calls. +* The exchange's servers (not the client browser) will request the AMPHTML ad content located at the URL provided in `ampadurl` after a bidder wins the auction. +* Creative servers must respond and return content within some reasonable SLA, recommended at 150ms. +* The AMPHTML ad will be injected into the adslot and subsequently rendered. Note that since a valid AMPHTML ad cannot contain an iframe or another ad tag, the server-side fetch must retrieve the actual HTML of the creative. + +### Impression Tracking and Billing URLs + +* RTB buyers often include impression trackers as a structured field in the bid response (for example `Bid.burl`, the "billing notice URL" in OpenRTB 2.5). +* It is up to the exchange or publisher ad server to determine how these URLs are fired, but <[amp-pixel](https://www.ampproject.org/docs/reference/components/amp-pixel)> and <[amp-analytics](https://www.ampproject.org/docs/reference/components/amp-analytics)> can handle most impression tracking and analytics use cases. + +## Background Docs +* [AMPHTML Ads for AMP Pages (Github)](https://github.com/ampproject/amphtml/issues/3133) +* [AMPHTML Ad Creative Format Spec (Github)](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/amp-a4a-format.md) +* [AMPHTML Ads Overview (Github)](https://github.com/ampproject/amphtml/blob/master/ads/google/a4a/docs/a4a-readme.md) +* [AMPHTML Ads Website from the AMP Project](https://www.ampproject.org/learn/who-uses-amp/amp-ads/) +* [Example AMPHTML Ads](https://ampbyexample.com/amp-ads/#amp-ads/introduction) +* [Speed comparison](https://ampbyexample.com/amp-ads/introduction/amphtml_ads_vs_non-amp_ads/): see how fast an AMP Ad loads in comparison to a regular ad. Best viewed on a 3G connection. +* [Discussion in OpenRTB Dev Forum](https://groups.google.com/forum/#!topic/openrtb-dev/0wyPsF5D07Q): RTB Specific Proposal + + diff --git a/ads/google/a4a/docs/a4a-readme.md b/ads/google/a4a/docs/a4a-readme.md new file mode 100644 index 000000000000..193ba7f18dc5 --- /dev/null +++ b/ads/google/a4a/docs/a4a-readme.md @@ -0,0 +1,6 @@ +# AMPHTML ads + +For details, please update your link to point to the latest doc available at: https://www.ampproject.org/docs/ads/amphtml_ads. + +*Need to make changes to the doc*? The doc is now hosted in the [docs repo]( https://github.com/ampproject/docs/blob/master/content/docs/ads/amphtml_ads.md). + diff --git a/ads/google/a4a/experiment-utils.js b/ads/google/a4a/experiment-utils.js new file mode 100644 index 000000000000..ba8a264bbd8a --- /dev/null +++ b/ads/google/a4a/experiment-utils.js @@ -0,0 +1,71 @@ +/** + * Copyright 2018 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 { + ExperimentInfo, // eslint-disable-line no-unused-vars + forceExperimentBranch, + getExperimentBranch, + randomlySelectUnsetExperiments, +} from '../../../src/experiments'; +import { + addExperimentIdToElement, +} from './traffic-experiments'; + +/** + * Attempts to select into experiment and forces branch if selected. + * @param {!Window} win + * @param {!Element} element + * @param {!Array} branches + * @param {string} expName + * @param {boolean=} optAddExpIdToElement + */ +export function selectAndSetExperiments( + win, element, branches, expName, optAddExpIdToElement) { + const experimentId = expUtils.maybeSelectExperiment( + win, element, branches, expName); + if (!!experimentId) { + addExperimentIdToElement(optAddExpIdToElement ? + experimentId : undefined, element); + forceExperimentBranch(win, expName, experimentId); + } + return experimentId; +} + +export class ExperimentUtils { + /** + * @param {!Window} win + * @param {!Element} element + * @param {!Array} selectionBranches + * @param {string} experimentName + */ + maybeSelectExperiment( + win, element, selectionBranches, experimentName) { + const experimentInfoMap = + /** @type {!Object} */ ({}); + experimentInfoMap[experimentName] = { + isTrafficEligible: () => true, + branches: selectionBranches, + }; + randomlySelectUnsetExperiments(win, experimentInfoMap); + return getExperimentBranch(win, experimentName); + } +} + +/** + * ExperimentUtils singleton. + * @type {!ExperimentUtils} +*/ +const expUtils = new ExperimentUtils(); diff --git a/ads/google/a4a/line-delimited-response-handler.js b/ads/google/a4a/line-delimited-response-handler.js new file mode 100644 index 000000000000..df5ccaf09a23 --- /dev/null +++ b/ads/google/a4a/line-delimited-response-handler.js @@ -0,0 +1,104 @@ +/** + * 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 {tryParseJson} from '../../../src/json'; + +/** + * Handles an XHR response by calling lineCallback for each line delineation. + * Uses streaming where possible otherwise falls back to text. + * @param {!Window} win + * @param {!Response} response + * @param {function(string, boolean)} lineCallback + * @private + */ +export function lineDelimitedStreamer(win, response, lineCallback) { + let line = ''; + /** + * @param {string} text + * @param {boolean} done + */ + function streamer(text, done) { + const regex = /([^\n]*)(\n)?/g; + let match; + while ((match = regex.exec(text))) { + line += match[1]; + if (match[2]) { + lineCallback(line, done && regex.lastIndex === text.length); + line = ''; + } + if (regex.lastIndex === text.length) { + break; + } + } + } + if (!response.body || !win.TextDecoder) { + response.text().then(text => streamer(text, true)); + return; + } + + const decoder = new TextDecoder('utf-8'); + const reader = /** @type !ReadableStreamDefaultReader */ ( + response.body.getReader()); + reader.read().then(function chunk(result) { + if (result.value) { + streamer( + decoder.decode( + /** @type {!ArrayBuffer} */(result.value), {'stream': true}), + result.done); + } + if (!result.done) { + // More chunks to read. + reader.read().then(chunk); + } + }); +} + +/** + * Given each line, groups such that the first is JSON parsed and second + * html unescaped. + * @param {function(string, !Object, boolean)} callback + * @private + */ +export function metaJsonCreativeGrouper(callback) { + let first; + return function(line, done) { + if (first) { + const metadata = + /** @type {!Object} */(tryParseJson(first) || {}); + const lowerCasedMetadata = + Object.keys(metadata).reduce((newObj, key) => { + newObj[key.toLowerCase()] = metadata[key]; + return newObj; + }, {}); + callback(unescapeLineDelimitedHtml_(line), lowerCasedMetadata, done); + first = null; + } else { + first = line; + } + }; +} + +/** + * Unescapes characters that are escaped in line-delimited JSON-HTML. + * @param {string} html An html snippet. + * @return {string} + * @private + */ +function unescapeLineDelimitedHtml_(html) { + return html.replace( + /\\(n|r|\\)/g, + (_, match) => match == 'n' ? '\n' : match == 'r' ? '\r' : '\\'); +} diff --git a/ads/google/a4a/test/test-line-delimited-response-handler.js b/ads/google/a4a/test/test-line-delimited-response-handler.js new file mode 100644 index 000000000000..2742274b5eaf --- /dev/null +++ b/ads/google/a4a/test/test-line-delimited-response-handler.js @@ -0,0 +1,217 @@ +/** + * 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 { + lineDelimitedStreamer, + metaJsonCreativeGrouper, +} from '../line-delimited-response-handler'; + +describe('#line-delimited-response-handler', () => { + + let chunkHandlerStub; + let slotData; + let sandbox; + let win; + let response; + + /** + * @return {string} slot data written in expected stream response format + */ + function generateResponseFormat() { + let slotDataString = ''; + slotData.forEach(slot => { + // TODO: escape creative returns + const creative = slot.creative.replace(/\\/g, '\\\\') + .replace(/\n/g, '\\n').replace(/\r/g, '\\r'); + slotDataString += `${JSON.stringify(slot.headers)}\n${creative}\n`; + }); + return slotDataString; + } + + function executeAndVerifyResponse() { + // Streamed response calls chunk handlers after returning so need to + // wait on chunks. + let chunkResolver; + const chunkPromise = new Promise(resolver => chunkResolver = resolver); + const chunkHandlerWrapper = (creative, metaData) => { + chunkHandlerStub(creative, metaData); + if (chunkHandlerStub.callCount == slotData.length) { + chunkResolver(); + } + }; + // If no slots then callback will never execute so we need to resolve + // immediately. + if (slotData.length == 0) { + chunkResolver(); + } + lineDelimitedStreamer( + win, response, metaJsonCreativeGrouper(chunkHandlerWrapper)); + return chunkPromise.then(() => { + expect(chunkHandlerStub.callCount).to.equal(slotData.length); + // Could have duplicate responses so need to iterate and get counts. + // TODO: can't use objects as keys :( + const calls = {}; + slotData.forEach(slot => { + const normalizedHeaderNames = + Object.keys(slot.headers).map(s => [s.toLowerCase(), s]); + slot.normalizedHeaders = {}; + normalizedHeaderNames.forEach( + namePair => + slot.normalizedHeaders[namePair[0]] = slot.headers[namePair[1]]); + const key = slot.creative + JSON.stringify(slot.normalizedHeaders); + calls[key] ? calls[key]++ : (calls[key] = 1); + }); + slotData.forEach(slot => { + expect(chunkHandlerStub.withArgs( + slot.creative, slot.normalizedHeaders).callCount) + .to.equal(calls[slot.creative + + JSON.stringify(slot.normalizedHeaders)]); + }); + }); + } + + beforeEach(() => { + sandbox = sinon.sandbox; + chunkHandlerStub = sandbox.stub(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('stream not supported', () => { + beforeEach(() => { + response = { + text: () => Promise.resolve(generateResponseFormat()), + }; + win = { + // TextDecoder should exist but not be called + TextDecoder: () => { throw new Error('fail'); }, + }; + }); + + it('should fallback to text if no stream support', () => { + slotData = [ + {headers: {foo: 'bar'}, creative: 'baz\n'}, + {headers: {hello: 'world'}, creative: 'do not\n chunk me'}, + ]; + return executeAndVerifyResponse(); + }); + + it('should fallback to text if no stream support w/ empty response', () => { + slotData = []; + return executeAndVerifyResponse(); + }); + + it('should fallback to text if no TextDecoder', () => { + slotData = [ + {headers: {foo: 'bar', hello: 'world'}, creative: 'baz\n'}, + {headers: {hello: 'world'}, + creative: '\r\n\nchunk\b me\r\b\n\r'}, + {headers: {foo: 'bar'}, creative: ''}, + {headers: {}, creative: '\r\n\r\b\n\r'}, + ]; + return executeAndVerifyResponse(); + }); + }); + + describe('streaming', () => { + let readStub; + let sandbox; + + function setup() { + const responseString = generateResponseFormat(); + const textEncoder = new TextEncoder('utf-8'); + const CHUNK_SIZE = 5; + let chunk = 0; + do { + const value = textEncoder.encode(responseString.substr( + chunk * CHUNK_SIZE, CHUNK_SIZE), {'stream': true}); + const done = chunk * CHUNK_SIZE >= responseString.length - 1; + readStub.onCall(chunk).returns(Promise.resolve({value, done})); + } while (chunk++ * CHUNK_SIZE < responseString.length); + } + + beforeEach(() => { + sandbox = sinon.sandbox; + readStub = sandbox.stub(); + response = { + text: () => Promise.resolve(), + body: { + getReader: () => { + return { + read: readStub, + }; + }, + }, + }; + win = { + TextDecoder, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + // TODO(lannka, #15748): Fails on Safari 11.1.0. + it.configure().skipSafari('should handle empty streamed ' + + 'response properly', () => { + slotData = []; + setup(); + return executeAndVerifyResponse(); + }); + + // TODO(lannka, #15748): Fails on Safari 11.1.0. + it.configure().skipSafari('should handle no fill response properly', () => { + slotData = [{headers: {}, creative: ''}]; + setup(); + return executeAndVerifyResponse(); + }); + + // TODO(lannka, #15748): Fails on Safari 11.1.0. + it.configure().skipSafari('should handle multiple no fill responses ' + + 'properly', () => { + slotData = [ + {headers: {}, creative: ''}, + {headers: {}, creative: ''}, + ]; + setup(); + return executeAndVerifyResponse(); + }); + + // TODO(lannka, #15748): Fails on Safari 11.1.0. + it.configure().skipSafari('should stream properly', () => { + slotData = [ + {headers: {}, creative: ''}, + {headers: {foo: 'bar', hello: 'world'}, + creative: '\t\n\r\bbaz\r\n\n'}, + {headers: {Foo: 'bar', hello: 'world'}, + creative: '\t\n\r\bbaz\r\n\n'}, + {headers: {}, creative: ''}, + {headers: {Foo: 'bar', HELLO: 'Le Monde'}, + creative: '\t\n\r\bbaz\r\n\n'}, + {headers: {FOO: 'bar', Hello: 'Le Monde'}, + creative: '\t\n\r\bbaz\r\n\n'}, + {headers: {hello: 'world'}, + creative: '\nchu\nnk me'}, + {headers: {}, creative: ''}, + ]; + setup(); + return executeAndVerifyResponse(); + }); + }); +}); 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..416e4b671483 --- /dev/null +++ b/ads/google/a4a/test/test-traffic-experiments.js @@ -0,0 +1,121 @@ +/** + * 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} from '../utils'; +import { + addExperimentIdToElement, + isInExperiment, + validateExperimentIds, +} from '../traffic-experiments'; + +describe('all-traffic-experiments-tests', () => { + 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 do nothing to already valid single experiment', () => { + const element = document.createElement('div'); + element.setAttribute(EXPERIMENT_ATTRIBUTE, '99'); + addExperimentIdToElement(undefined, element); + expect(element.getAttribute(EXPERIMENT_ATTRIBUTE)).to.equal('99'); + }); + + 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('#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..a40f454b6b60 --- /dev/null +++ b/ads/google/a4a/test/test-utils.js @@ -0,0 +1,990 @@ +/** + * 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 '../../../../extensions/amp-ad/0.1/amp-ad-ui'; +import '../../../../extensions/amp-ad/0.1/amp-ad-xorigin-iframe-handler'; +import {CONSENT_POLICY_STATE} from '../../../../src/consent-state'; +import { + EXPERIMENT_ATTRIBUTE, + TRUNCATION_PARAM, + ValidAdContainerTypes, + addCsiSignalsToAmpAnalyticsConfig, + additionalDimensions, + extractAmpAnalyticsConfig, + extractHost, + getAmpRuntimeTypeParameter, + getCorrelator, + getCsiAmpAnalyticsVariables, + getEnclosingContainerTypes, + getIdentityToken, + getIdentityTokenRequestUrl, + googleAdUrl, + groupAmpAdsByType, + maybeAppendErrorParameter, + mergeExperimentIds, +} from '../utils'; +import { + MockA4AImpl, +} from '../../../../extensions/amp-a4a/0.1/test/utils'; +import {Services} from '../../../../src/services'; +import {buildUrl} from '../url-builder'; +import {createElementWithAttributes} from '../../../../src/dom'; +import {createIframePromise} from '../../../../testing/iframe'; +import {installDocService} from '../../../../src/service/ampdoc-impl'; +import { + installExtensionsService, +} from '../../../../src/service/extensions-impl'; +import {installXhrService} from '../../../../src/service/xhr-impl'; +import {toggleExperiment} from '../../../../src/experiments'; + +function setupForAdTesting(fixture) { + installDocService(fixture.win, /* isSingleDoc */ true); + installExtensionsService(fixture.win); + const {doc} = fixture; + // 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); +} + +// Because of the way the element is constructed, it doesn't have all of the +// machinery that AMP expects it to have, so just no-op the irrelevant +// functions. +function noopMethods(impl, doc, sandbox) { + const noop = () => {}; + impl.element.build = noop; + impl.element.getPlaceholder = noop; + impl.element.createPlaceholder = noop; + sandbox.stub(impl, 'getAmpDoc').callsFake(() => doc); + sandbox.stub(impl, 'getPageLayoutBox').callsFake(() => { + return { + top: 11, left: 12, right: 0, bottom: 0, width: 0, height: 0, + }; + }); +} + +describe('Google A4A utils', () => { + + //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'); + }); + }); + + describe('#ActiveView AmpAnalytics integration', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox; + }); + + afterEach(() => { + sandbox.restore(); + }); + + const builtConfig = { + transport: {beacon: false, xhrpost: false}, + requests: { + visibility1: 'https://foo.com?hello=world', + visibility2: 'https://bar.com?a=b', + }, + triggers: { + continuousVisible: { + on: 'visible', + request: ['visibility1', 'visibility2'], + visibilitySpec: { + selector: 'amp-ad', + selectionMethod: 'closest', + visiblePercentageMin: 50, + continuousTimeMin: 1000, + }, + }, + }, + }; + + it('should extract correct config from header', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + let url; + const headers = { + get(name) { + if (name == 'X-AmpAnalytics') { + return JSON.stringify({url}); + } + if (name == 'X-QQID') { + return 'qqid_string'; + } + }, + has(name) { + if (name == 'X-AmpAnalytics') { + return true; + } + if (name == 'X-QQID') { + return true; + } + }, + }; + const element = createElementWithAttributes(fixture.doc, 'amp-a4a', { + 'width': '200', + 'height': '50', + 'type': 'adsense', + 'data-experiment-id': '00000001,0000002', + }); + const a4a = new MockA4AImpl(element); + url = 'not an array'; + allowConsoleError(() => + expect(extractAmpAnalyticsConfig(a4a, headers)).to.not.be.ok); + allowConsoleError(() => + expect(extractAmpAnalyticsConfig(a4a, headers)).to.be.null); + url = []; + expect(extractAmpAnalyticsConfig(a4a, headers)).to.not.be.ok; + expect(extractAmpAnalyticsConfig(a4a, headers)).to.be.null; + + url = ['https://foo.com?hello=world', 'https://bar.com?a=b']; + const config = extractAmpAnalyticsConfig(a4a, headers); + expect(config).to.deep.equal(builtConfig); + headers.has = function(name) { + expect(name).to.equal('X-AmpAnalytics'); + return false; + }; + expect(extractAmpAnalyticsConfig(a4a, headers)).to.not.be.ok; + }); + }); + + it('should add the correct CSI signals', () => { + sandbox.stub(Services, 'documentInfoForDoc').returns({pageViewId: 777}); + const mockElement = { + getAttribute: function(name) { + switch (name) { + case EXPERIMENT_ATTRIBUTE: + return '00000001,00000002'; + case 'type': + return 'fake-type'; + case 'data-amp-slot-index': + return '0'; + } + return null; + }, + }; + const qqid = 'qqid_string'; + let newConfig = addCsiSignalsToAmpAnalyticsConfig( + window, mockElement, builtConfig, qqid, + /* isVerifiedAmpCreative */ true); + + expect(newConfig.requests.iniLoadCsi).to.not.be.null; + expect(newConfig.requests.renderStartCsi).to.not.be.null; + expect(newConfig.triggers.continuousVisibleIniLoad.request) + .to.equal('iniLoadCsi'); + expect(newConfig.triggers.continuousVisibleRenderStart.request) + .to.equal('renderStartCsi'); + const getRegExps = metricName => [ + /^https:\/\/csi\.gstatic\.com\/csi\?/, + /(\?|&)s=a4a(&|$)/, + /(\?|&)c=[0-9]+(&|$)/, + /(\?|&)slotId=0(&|$)/, + /(\?|&)qqid\.0=[a-zA-Z_]+(&|$)/, + new RegExp(`(\\?|&)met\\.a4a\\.0=${metricName}\\.-?[0-9]+(&|$)`), + /(\?|&)dt=-?[0-9]+(&|$)/, + /(\?|&)e\.0=00000001%2C00000002(&|$)/, + /(\?|&)rls=\$internalRuntimeVersion\$(&|$)/, + /(\?|&)adt.0=fake-type(&|$)/, + ]; + getRegExps('visibilityCsi').forEach(regExp => { + expect(newConfig.requests.visibilityCsi).to.match(regExp); + }); + getRegExps('iniLoadCsiFriendly').forEach(regExp => { + expect(newConfig.requests.iniLoadCsi).to.match(regExp); + }); + getRegExps('renderStartCsiFriendly').forEach(regExp => { + expect(newConfig.requests.renderStartCsi).to.match(regExp); + }); + newConfig = addCsiSignalsToAmpAnalyticsConfig( + window, mockElement, builtConfig, qqid, + /* isVerifiedAmpCreative */ false, + /* lifecycle time events; not relevant here */ -1, -1); + getRegExps('iniLoadCsiCrossDomain').forEach(regExp => { + expect(newConfig.requests.iniLoadCsi).to.match(regExp); + }); + getRegExps('renderStartCsiCrossDomain').forEach(regExp => { + expect(newConfig.requests.renderStartCsi).to.match(regExp); + }); + + }); + }); + + describe('#getAmpRuntimeTypeParameter', () => { + it('should specify that this is canary', () => { + expect(getAmpRuntimeTypeParameter({ + AMP_CONFIG: {type: 'canary'}, + location: {origin: 'https://www-example-com.cdn.ampproject.org'}, + })).to.equal('2'); + }); + it('should specify that this is control', () => { + expect(getAmpRuntimeTypeParameter({ + AMP_CONFIG: {type: 'control'}, + location: {origin: 'https://www-example-com.cdn.ampproject.org'}, + })).to.equal('1'); + }); + it('should not have `art` parameter when AMP_CONFIG is undefined', () => { + expect(getAmpRuntimeTypeParameter({ + location: {origin: 'https://www-example-com.cdn.ampproject.org'}, + })).to.be.null; + }); + it('should not have `art` parameter when binary type is production', () => { + expect(getAmpRuntimeTypeParameter({ + AMP_CONFIG: {type: 'production'}, + location: {origin: 'https://www-example-com.cdn.ampproject.org'}, + })).to.be.null; + }); + it('should not have `art` parameter when canonical', () => { + expect(getAmpRuntimeTypeParameter({ + AMP_CONFIG: {type: 'canary'}, + location: {origin: 'https://www.example.com'}, + })).to.be.null; + }); + }); + + describe('#googleAdUrl', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should set ad position', function() { + // When ran locally, this test tends to exceed 2000ms timeout. + this.timeout(5000); + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = window; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + return fixture.addElement(elem).then(() => { + return googleAdUrl(impl, '', 0, [], []).then(url1 => { + expect(url1).to.match(/ady=11/); + expect(url1).to.match(/adx=12/); + }); + }); + }); + }); + + it('should include scroll position', function() { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = window; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + const getRect = () => { return {'width': 100, 'height': 200}; }; + const getSize = () => { return {'width': 100, 'height': 200}; }; + const getScrollLeft = () => 12; + const getScrollTop = () => 34; + const viewportStub = sandbox.stub(Services, 'viewportForDoc'); + viewportStub.returns({getRect, getSize, getScrollTop, getScrollLeft}); + return fixture.addElement(elem).then(() => { + return googleAdUrl(impl, '', 0, {}, []).then(url1 => { + expect(url1).to.match(/scr_x=12&scr_y=34/); + }); + }); + }); + }); + + it('should include all experiment ids', function() { + // When ran locally, this test tends to exceed 2000ms timeout. + this.timeout(5000); + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = window; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + 'data-experiment-id': '123,456', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + return fixture.addElement(elem).then(() => { + return googleAdUrl(impl, '', 0, {}, ['789', '098']).then(url1 => { + expect(url1).to.match(/eid=123%2C456%2C789%2C098/); + }); + }); + }); + }); + + it('should include debug_experiment_id if local mode w/ deid hash', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = fixture.win; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + impl.win.AMP_CONFIG = {type: 'production'}; + impl.win.location.hash = 'foo,deid=123456,654321,bar'; + return fixture.addElement(elem).then(() => { + return googleAdUrl(impl, '', 0, [], []).then(url1 => { + expect(url1).to.match(/[&?]debug_experiment_id=123456%2C654321/); + }); + }); + }); + }); + + it('should include GA cid/hid', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = fixture.win; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + impl.win.gaGlobal = {cid: 'foo', hid: 'bar'}; + return fixture.addElement(elem).then(() => { + return googleAdUrl(impl, '', 0, [], []).then(url => { + expect(url).to.match(/[&?]ga_cid=foo[&$]/); + expect(url).to.match(/[&?]ga_hid=bar[&$]/); + }); + }); + }); + }); + + it('should have correct bc value when everything supported', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = fixture.win; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + const createElementStub = + sandbox.stub(impl.win.document, 'createElement'); + createElementStub.withArgs('iframe').returns({ + sandbox: { + supports: () => true, + }, + }); + return fixture.addElement(elem).then(() => { + return expect(googleAdUrl(impl, '', 0, {}, [])).to.eventually.match( + /[&?]bc=7[&$]/); + }); + }); + }); + + it('should have correct bc value when sandbox not supported', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = fixture.win; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + const createElementStub = + sandbox.stub(impl.win.document, 'createElement'); + createElementStub.withArgs('iframe').returns({ + sandbox: {}, + }); + return fixture.addElement(elem).then(() => { + return expect(googleAdUrl(impl, '', 0, {}, [])).to.eventually.match( + /[&?]bc=1[&$]/); + }); + }); + }); + + it('should not include bc when nothing supported', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = fixture.win; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + impl.win.SVGElement = undefined; + const createElementStub = + sandbox.stub(impl.win.document, 'createElement'); + createElementStub.withArgs('iframe').returns({ + sandbox: { + supports: () => false, + }, + }); + return fixture.addElement(elem).then(() => { + return expect( + googleAdUrl(impl, '', 0, {}, [])).to.eventually.not.match( + /[&?]bc=1[&$]/); + }); + }); + }); + + it('should handle referrer url promise timeout', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = fixture.win; + const elem = createElementWithAttributes(doc, 'amp-a4a', { + 'type': 'adsense', + 'width': '320', + 'height': '50', + }); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + sandbox.stub(Services.viewerForDoc(impl.getAmpDoc()), 'getReferrerUrl') + .returns(new Promise(() => {})); + const createElementStub = + sandbox.stub(impl.win.document, 'createElement'); + createElementStub.withArgs('iframe').returns({ + sandbox: { + supports: () => false, + }, + }); + expectAsyncConsoleError(/Referrer timeout/, 1); + return fixture.addElement(elem).then(() => { + return expect( + googleAdUrl(impl, '', 0, {}, [])).to.eventually.not.match( + /[&?]ref=[&$]/); + }); + }); + }); + + it('should include domLoading time', () => { + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const {doc} = fixture; + doc.win = fixture.win; + const elem = createElementWithAttributes(doc, 'amp-a4a', {}); + const impl = new MockA4AImpl(elem); + noopMethods(impl, doc, sandbox); + return fixture.addElement(elem).then(() => { + return googleAdUrl(impl, '', Date.now(), [], []).then(url => { + expect(url).to.match(/[&?]bdt=[1-9][0-9]*[&$]/); + }); + }); + }); + }); + }); + + 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 merge multiple IDs into a list', () => { + expect(mergeExperimentIds(['12345','6789'], '3,4,5,6')) + .to.equal('3,4,5,6,12345,6789'); + }); + 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('#maybeAppendErrorParameter', () => { + const url = 'https://foo.com/bar?hello=world&one=true'; + it('should append parameter', () => { + expect(maybeAppendErrorParameter(url, 'n')).to.equal(url + '&aet=n'); + }); + it('should not append parameter if already present', () => { + expect(maybeAppendErrorParameter(url + '&aet=already', 'n')).to.not.be.ok; + }); + it('should not append parameter if truncated', () => { + const truncUrl = buildUrl( + 'https://foo.com/bar', {hello: 'world'}, 15, TRUNCATION_PARAM); + expect(truncUrl.indexOf(TRUNCATION_PARAM.name)).to.not.equal(-1); + expect(maybeAppendErrorParameter(truncUrl, 'n')).to.not.be.ok; + }); + }); + + describes.realWin('#getEnclosingContainerTypes', {}, env => { + it('should return empty if no containers', () => { + expect(getEnclosingContainerTypes( + env.win.document.createElement('amp-ad')).length).to.equal(0); + }); + + Object.keys(ValidAdContainerTypes).forEach(container => { + it(`should return container: ${container}`, () => { + const containerElem = env.win.document.createElement(container); + env.win.document.body.appendChild(containerElem); + const ampAdElem = env.win.document.createElement('amp-ad'); + containerElem.appendChild(ampAdElem); + expect(getEnclosingContainerTypes(ampAdElem)) + .to.deep.equal([ValidAdContainerTypes[container]]); + }); + }); + + it('should include ALL containers', () => { + let prevContainer; + Object.keys(ValidAdContainerTypes).forEach(container => { + const containerElem = env.win.document.createElement(container); + (prevContainer || env.win.document.body).appendChild(containerElem); + prevContainer = containerElem; + }); + const ampAdElem = env.win.document.createElement('amp-ad'); + prevContainer.appendChild(ampAdElem); + const ValidAdContainerTypeValues = + Object.keys(ValidAdContainerTypes).map(function(key) { + return ValidAdContainerTypes[key]; + }); + expect(getEnclosingContainerTypes(ampAdElem).sort()) + .to.deep.equal(ValidAdContainerTypeValues.sort()); + }); + }); + + describes.fakeWin('#getIdentityTokenRequestUrl', {}, () => { + let doc; + let fakeWin; + beforeEach(() => { + const documentInfoStub = sandbox.stub(Services, 'documentInfoForDoc'); + doc = {}; + fakeWin = {location: {}}; + documentInfoStub.withArgs(doc) + .returns({canonicalUrl: 'http://f.blah.com?some_site'}); + }); + + it('should use google.com if at top', () => { + fakeWin.top = fakeWin; + fakeWin.location.ancestorOrigins = ['foo.google.com.eu']; + expect(getIdentityTokenRequestUrl(fakeWin, doc)).to.equal( + 'https://adservice.google.com/adsid/integrator.json?' + + 'domain=f.blah.com'); + }); + + it('should use google.com if no ancestorOrigins', () => { + expect(getIdentityTokenRequestUrl(fakeWin, doc)).to.equal( + 'https://adservice.google.com/adsid/integrator.json?' + + 'domain=f.blah.com'); + }); + + it('should use google.com if non-google top', () => { + fakeWin.location.ancestorOrigins = ['foo.google2.com']; + expect(getIdentityTokenRequestUrl(fakeWin, doc)).to.equal( + 'https://adservice.google.com/adsid/integrator.json?' + + 'domain=f.blah.com'); + }); + + it('should use google ancestor origin based top domain', () => { + fakeWin.location.ancestorOrigins = + ['foo.google.eu', 'blah.google.fr']; + expect(getIdentityTokenRequestUrl(fakeWin, doc)).to.equal( + 'https://adservice.google.fr/adsid/integrator.json?' + + 'domain=f.blah.com'); + }); + + it('should use supplied domain', () => { + fakeWin.location.ancestorOrigins = ['foo.google.fr']; + expect(getIdentityTokenRequestUrl(fakeWin, doc, '.google.eu')).to.equal( + 'https://adservice.google.eu/adsid/integrator.json?' + + 'domain=f.blah.com'); + }); + }); + + describes.fakeWin('#getIdentityToken', {amp: true, mockFetch: true}, env => { + beforeEach(() => { + installXhrService(env.win); + const documentInfoStub = sandbox.stub(Services, 'documentInfoForDoc'); + documentInfoStub.withArgs(env.win.document) + .returns({canonicalUrl: 'http://f.blah.com?some_site'}); + }); + + afterEach(() => { + // Verify fetch mocks are all consumed. + expect(env.fetchMock.done()).to.be.true; + }); + + const getUrl = domain => { + domain = domain || 'google\.com'; + return `https:\/\/adservice\.${domain}\/adsid\/integrator\.json\?` + + 'domain=f\.blah\.com'; + }; + + it('should ignore response if required fields are missing', () => { + env.expectFetch(getUrl(), JSON.stringify({newToken: 'abc'})); + return getIdentityToken(env.win, env.win.document).then(result => { + expect(result.token).to.not.be.ok; + expect(result.jar).to.not.be.ok; + expect(result.pucrd).to.not.be.ok; + expect(result.freshLifetimeSecs).to.not.be.ok; + expect(result.validLifetimeSecs).to.not.be.ok; + expect(result.fetchTimeMs).to.be.at.least(0); + }); + }); + + it('should fetch full token as expected', () => { + env.expectFetch(getUrl(), JSON.stringify({ + newToken: 'abc', + '1p_jar': 'some_jar', + pucrd: 'some_pucrd', + freshLifetimeSecs: '1234', + validLifetimeSecs: '5678', + })); + return getIdentityToken(env.win, env.win.document).then(result => { + expect(result.token).to.equal('abc'); + expect(result.jar).to.equal('some_jar'); + expect(result.pucrd).to.equal('some_pucrd'); + expect(result.freshLifetimeSecs).to.equal(1234); + expect(result.validLifetimeSecs).to.equal(5678); + expect(result.fetchTimeMs).to.be.at.least(0); + }); + }); + + it('should redirect as expected', () => { + env.expectFetch(getUrl(), JSON.stringify({altDomain: '.google.fr'})); + env.expectFetch(getUrl('google\.fr'), JSON.stringify({ + newToken: 'abc', + freshLifetimeSecs: '1234', + validLifetimeSecs: '5678', + })); + return getIdentityToken(env.win, env.win.document, '').then(result => { + expect(result.token).to.equal('abc'); + expect(result.jar).to.equal(''); + expect(result.pucrd).to.equal(''); + expect(result.freshLifetimeSecs).to.equal(1234); + expect(result.validLifetimeSecs).to.equal(5678); + expect(result.fetchTimeMs).to.be.at.least(0); + }); + }); + + it('should stop after 1 redirect', () => { + env.expectFetch(getUrl(), JSON.stringify({altDomain: '.google.fr'})); + env.expectFetch( + getUrl('google\.fr'), JSON.stringify({altDomain: '.google.com'})); + return getIdentityToken(env.win, env.win.document).then(result => { + expect(result.token).to.not.be.ok; + expect(result.jar).to.not.be.ok; + expect(result.pucrd).to.not.be.ok; + expect(result.fetchTimeMs).to.be.at.least(0); + }); + }); + + it('should use previous execution', () => { + const ident = { + newToken: 'foo', + freshLifetimeSecs: '1234', + validLifetimeSecs: '5678', + }; + env.win['goog_identity_prom'] = Promise.resolve(ident); + return getIdentityToken(env.win, env.win.document) + .then(result => expect(result).to.jsonEqual(ident)); + }); + + it('should handle fetch error', () => { + sandbox.stub(Services, 'xhrFor').returns( + {fetchJson: () => Promise.reject('some network failure')}); + return getIdentityToken(env.win, env.win.document) + .then(result => expect(result).to.jsonEqual({})); + }); + + it('should fetch if SUFFICIENT consent', () => { + env.expectFetch(getUrl(), JSON.stringify({ + newToken: 'abc', + '1p_jar': 'some_jar', + pucrd: 'some_pucrd', + freshLifetimeSecs: '1234', + validLifetimeSecs: '5678', + })); + sandbox.stub(Services, 'consentPolicyServiceForDocOrNull').returns( + Promise.resolve({ + whenPolicyResolved: () => CONSENT_POLICY_STATE.SUFFICIENT, + })); + return getIdentityToken(env.win, env.win.document, 'default').then( + result => expect(result.token).to.equal('abc')); + }); + + it('should not fetch if INSUFFICIENT consent', () => { + sandbox.stub(Services, 'consentPolicyServiceForDocOrNull').returns( + Promise.resolve({ + whenPolicyResolved: () => CONSENT_POLICY_STATE.INSUFFICIENT, + })); + return expect(getIdentityToken(env.win, env.win.document, 'default')) + .to.eventually.jsonEqual({}); + }); + + it('should not fetch if UNKNOWN consent', () => { + sandbox.stub(Services, 'consentPolicyServiceForDocOrNull').returns( + Promise.resolve({ + whenPolicyResolved: () => CONSENT_POLICY_STATE.UNKNOWN, + })); + return expect(getIdentityToken(env.win, env.win.document, 'default')) + .to.eventually.jsonEqual({}); + }); + }); + + describe('variables for amp-analytics', () => { + let a4a; + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox; + return createIframePromise().then(fixture => { + setupForAdTesting(fixture); + const element = createElementWithAttributes(fixture.doc, 'amp-a4a', { + 'width': '200', + 'height': '50', + 'type': 'adsense', + 'data-amp-slot-index': '4', + }); + element.getAmpDoc = () => fixture.doc; + a4a = new MockA4AImpl(element); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should include the correlator', () => { + const vars = getCsiAmpAnalyticsVariables('trigger', a4a, null); + expect(vars['correlator']).not.to.be.undefined; + expect(vars['correlator']).to.be.greaterThan(0); + }); + + it('should include the slot index', () => { + const vars = getCsiAmpAnalyticsVariables('trigger', a4a, null); + expect(vars['slotId']).to.equal('4'); + }); + + it('should include the qqid when provided', () => { + const vars = getCsiAmpAnalyticsVariables('trigger', a4a, ''); + expect(vars['qqid']).to.equal(''); + }); + + it('should omit the qqid when null', () => { + const vars = getCsiAmpAnalyticsVariables('trigger', a4a, null); + expect(vars['qqid']).to.be.undefined; + }); + + it('should include scheduleTime for ad render start triggers', () => { + a4a.element.layoutScheduleTime = 200; + const vars = getCsiAmpAnalyticsVariables( + 'ad-render-start', a4a, null); + expect(vars['scheduleTime']).to.be.a('number'); + expect(vars['scheduleTime']).not.to.equal(0); + }); + + it('should omit scheduleTime by default', () => { + a4a.element.layoutScheduleTime = 200; + const vars = getCsiAmpAnalyticsVariables('trigger', a4a, null); + expect(vars['scheduleTime']).to.be.undefined; + }); + + it('should include viewer lastVisibleTime', () => { + const getLastVisibleTime = () => 300; + const viewerStub = sandbox.stub(Services, 'viewerForDoc'); + viewerStub.returns({getLastVisibleTime}); + + const vars = getCsiAmpAnalyticsVariables('trigger', a4a, null); + expect(vars['viewerLastVisibleTime']).to.be.a('number'); + expect(vars['viewerLastVisibleTime']).not.to.equal(0); + }); + }); + + describe('#extractHost', () => { + [ + {in: 'http://foo.com/sl?lj=fl', out: 'foo.com'}, + {in: 'Http://bar.com?lj=fl', out: 'bar.com'}, + {in: 'htTps://foo.com?lj=fl', out: 'foo.com'}, + {in: 'http://bar.com', out: 'bar.com'}, + {in: 'https://foo.com', out: 'foo.com'}, + {in: 'https://foo.com:8080', out: 'foo.com'}, + {in: 'https://bar.com:8080/lkjs?a=b', out: 'bar.com'}, + {in: 'bar.com:8080/lkjs?a=b', out: 'bar.com'}, + {in: 'bar.com:8080/', out: 'bar.com'}, + {in: 'bar.com/sl?lj=fl', out: 'bar.com'}, + {in: 'foo.com/sl/lj=fl?ls=f', out: 'foo.com'}, + {in: 'bar.com?lj=fl', out: 'bar.com'}, + {in: 'foo.com?lj=fl', out: 'foo.com'}, + {in: 'hello.com', out: 'hello.com'}, + {in: '', out: ''}, + ].forEach(test => + it(test.in, () => expect(extractHost(test.in)).to.equal(test.out))); + }); + + describes.realWin('#getCorrelator', {}, env => { + let win; + + beforeEach(() => { + win = env.win; + }); + + afterEach(() => { + toggleExperiment(win, 'exp-new-correlator', false); + }); + + it('should return cached value if it exists', () => { + const correlator = '12345678910'; + win.ampAdPageCorrelator = correlator; + expect(getCorrelator(win, win.document)).to.equal(correlator); + }); + + it('should calculate correlator from PVID and CID if possible', () => { + const pageViewId = '818181'; + sandbox.stub(Services, 'documentInfoForDoc').callsFake(() => { + return {pageViewId}; + }); + const cid = '12345678910'; + const correlator = getCorrelator(win, win.document, cid); + expect(String(correlator).includes(pageViewId)).to.be.true; + }); + + it('should calculate randomly if experiment on', () => { + toggleExperiment(win, 'exp-new-correlator', true); + const correlator = getCorrelator(win, win.document); + expect(correlator).to.be.below(2 ** 52); + expect(correlator).to.be.above(0); + }); + }); +}); + +describes.realWin('#groupAmpAdsByType', {amp: true}, env => { + let doc, win; + beforeEach(() => { + win = env.win; + doc = win.document; + }); + + function createResource(config, tagName = 'amp-ad', parent = doc.body) { + const element = createElementWithAttributes(doc, tagName, config); + parent.appendChild(element); + element.getImpl = () => Promise.resolve({element}); + return {element}; + } + + it('should find amp-ad of only given type', () => { + const resources = [createResource({type: 'doubleclick'}), + createResource({type: 'blah'}), createResource({}, 'amp-foo')]; + sandbox.stub(Services.resourcesForDoc(doc), 'getMeasuredResources') + .callsFake((doc, fn) => Promise.resolve(resources.filter(fn))); + return groupAmpAdsByType(win, 'doubleclick', () => 'foo').then(result => { + expect(Object.keys(result).length).to.equal(1); + expect(result['foo']).to.be.ok; + expect(result['foo'].length).to.equal(1); + return result['foo'][0].then(baseElement => + expect(baseElement.element.getAttribute('type')) + .to.equal('doubleclick')); + }); + }); + + it('should find amp-ad within sticky container', () => { + const stickyResource = createResource({}, 'amp-sticky-ad'); + const resources = [stickyResource, createResource({}, 'amp-foo')]; + // Do not expect ampAdResource to be returned by getMeasuredResources + // as its owned by amp-sticky-ad. It will locate associated element + // and block on whenUpgradedToCustomElement so override createdCallback + // to cause it to return immediately. + const ampAdResource = + createResource({type: 'doubleclick'}, 'amp-ad', stickyResource.element); + ampAdResource.element.createdCallback = true; + sandbox.stub(Services.resourcesForDoc(doc), 'getMeasuredResources') + .callsFake((doc, fn) => Promise.resolve(resources.filter(fn))); + return groupAmpAdsByType(win, 'doubleclick', () => 'foo').then( + result => { + expect(Object.keys(result).length).to.equal(1); + expect(result['foo']).to.be.ok; + expect(result['foo'].length).to.equal(1); + return result['foo'][0].then(baseElement => + expect(baseElement.element.getAttribute('type')) + .to.equal('doubleclick')); + }); + }); + + it('should find and group multiple, some in containers', () => { + const stickyResource = createResource({}, 'amp-sticky-ad'); + const resources = [stickyResource, createResource({}, 'amp-foo'), + createResource({type: 'doubleclick', foo: 'bar'}), + createResource({type: 'doubleclick', foo: 'hello'})]; + // Do not expect ampAdResource to be returned by getMeasuredResources + // as its owned by amp-sticky-ad. It will locate associated element + // and block on whenUpgradedToCustomElement so override createdCallback + // to cause it to return immediately. + const ampAdResource = createResource({type: 'doubleclick', foo: 'bar'}, + 'amp-ad', stickyResource.element); + ampAdResource.element.createdCallback = true; + sandbox.stub(Services.resourcesForDoc(doc), 'getMeasuredResources') + .callsFake((doc, fn) => Promise.resolve(resources.filter(fn))); + return groupAmpAdsByType( + win, 'doubleclick', element => element.getAttribute('foo')).then( + result => { + expect(Object.keys(result).length).to.equal(2); + expect(result['bar']).to.be.ok; + expect(result['bar'].length).to.equal(2); + expect(result['hello']).to.be.ok; + expect(result['hello'].length).to.equal(1); + return Promise.all(result['bar'].concat(result['hello'])).then( + baseElements => baseElements.forEach(baseElement => + expect(baseElement.element.getAttribute('type')) + .to.equal('doubleclick'))); + }); + }); +}); diff --git a/ads/google/a4a/traffic-experiments.js b/ads/google/a4a/traffic-experiments.js new file mode 100644 index 000000000000..57767c6a652b --- /dev/null +++ b/ads/google/a4a/traffic-experiments.js @@ -0,0 +1,170 @@ +/** + * 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 { + EXPERIMENT_ATTRIBUTE, + mergeExperimentIds, +} from './utils'; +import { + ExperimentInfo, // eslint-disable-line no-unused-vars + isExperimentOn, +} from '../../../src/experiments'; +import {Services} from '../../../src/services'; +import {parseQueryString} from '../../../src/url'; + +/** @typedef {{ + * control: string, + * experiment: string + * }} */ +export let A4aExperimentBranches; + +/** @type {string} @private */ +export const MANUAL_EXPERIMENT_ID = '117152632'; + +/** + * @param {!Window} win + * @param {!Element} element Ad tag Element. + * @return {?string} experiment extracted from page url. + */ +export function extractUrlExperimentId(win, element) { + const expParam = Services.viewerForDoc(element).getParam('exp') || + parseQueryString(win.location.search)['exp']; + if (!expParam) { + return null; + } + // Allow for per type experiment control with Doubleclick key set for 'da' + // and AdSense using 'aa'. Fallback to 'a4a' if type specific is missing. + const expKeys = [ + (element.getAttribute('type') || '').toLowerCase() == 'doubleclick' ? + 'da' : 'aa', + 'a4a', + ]; + let arg; + let match; + expKeys.forEach(key => arg = arg || + ((match = new RegExp(`(?:^|,)${key}:(-?\\d+)`).exec(expParam)) && + match[1])); + return arg || null; +} + +/** + * 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. + * @return {!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 {?string} id 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. + * @return {boolean} + */ +export function isInManualExperiment(element) { + return isInExperiment(element, MANUAL_EXPERIMENT_ID); +} + +/** + * Predicate to check whether A4A has launched yet or not. + * If it has not yet launched, then the experimental branch serves A4A, and + * control/filler do not. If it has not, then the filler and control branch do + * serve A4A, and the experimental branch does not. + * + * @param {!Window} win Host window for the ad. + * @param {!Element} element Element to check for pre-launch membership. + * @return {boolean} + */ +export function hasLaunched(win, element) { + switch (element.getAttribute('type')) { + case 'adsense': + return isExperimentOn(win, 'a4aFastFetchAdSenseLaunched'); + case 'doubleclick': + return isExperimentOn(win, 'a4aFastFetchDoubleclickLaunched'); + default: + return false; + } +} + +/** + * 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. + * @return {boolean} Whether all list elements are valid experiment IDs. + */ +export function validateExperimentIds(idList) { + return idList.every(id => { return !isNaN(parseInt(id, 10)); }); +} + +/** + * Adds a single experimentID to an element iff it's a valid experiment ID. + * No-ops if the experimentId is undefined. + * + * @param {string|undefined} experimentId ID to add to the element. + * @param {Element} element to add the experiment ID to. + */ +export function addExperimentIdToElement(experimentId, element) { + if (!experimentId) { + return; + } + 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..e60a04503f35 --- /dev/null +++ b/ads/google/a4a/url-builder.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. + */ + +/** @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 {!Object} 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; + } + const keys = Object.keys(queryParams); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = queryParams[key]; + if (value == null || value === '') { + continue; + } + const encodedNameAndSep = encodeURIComponent(key) + '='; + const encodedValue = encodeURIComponent(String(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..0e6e90f69c73 --- /dev/null +++ b/ads/google/a4a/utils.js @@ -0,0 +1,959 @@ +/** + * 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 {CONSENT_POLICY_STATE} from '../../../src/consent-state'; +import {DomFingerprint} from '../../../src/utils/dom-fingerprint'; +import {Services} from '../../../src/services'; +import {buildUrl} from './url-builder'; +import {dev} from '../../../src/log'; +import {dict} from '../../../src/utils/object'; +import { + getBinaryType, + isExperimentOn, + toggleExperiment, +} from '../../../src/experiments'; +import {getConsentPolicyState} from '../../../src/consent'; +import {getMode} from '../../../src/mode'; +import {getOrCreateAdCid} from '../../../src/ad-cid'; +import {getTimingDataSync} from '../../../src/service/variable-source'; +import {parseJson} from '../../../src/json'; +import {whenUpgradedToCustomElement} from '../../../src/dom'; + +/** @type {string} */ +const AMP_ANALYTICS_HEADER = 'X-AmpAnalytics'; + +/** @const {number} */ +const MAX_URL_LENGTH = 16384; + +/** @enum {string} */ +const AmpAdImplementation = { + AMP_AD_XHR_TO_IFRAME: '2', + AMP_AD_XHR_TO_IFRAME_OR_AMP: '3', + AMP_AD_IFRAME_GET: '5', +}; + +/** @const {!Object} */ +export const ValidAdContainerTypes = { + 'AMP-CAROUSEL': 'ac', + 'AMP-FX-FLYING-CARPET': 'fc', + 'AMP-LIGHTBOX': 'lb', + 'AMP-STICKY-AD': 'sa', +}; + +/** + * See `VisibilityState` enum. + * @const {!Object} + */ +const visibilityStateCodes = { + 'visible': '1', + 'hidden': '2', + 'prerender': '3', + 'unloaded': '5', +}; + +/** @const {string} */ +export const QQID_HEADER = 'X-QQID'; + +/** @type {string} */ +export const SANDBOX_HEADER = 'amp-ff-sandbox'; + +/** + * 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'; + +/** @typedef {{urls: !Array}} + */ +export let AmpAnalyticsConfigDef; + +/** + * @typedef {{instantLoad: boolean, writeInBody: boolean}} + */ +export let NameframeExperimentConfig; + +/** + * @const {!./url-builder.QueryParameterDef} + * @visibleForTesting + */ +export const TRUNCATION_PARAM = {name: 'trunc', value: '1'}; + +/** @const {Object} */ +const CDN_PROXY_REGEXP = /^https:\/\/([a-zA-Z0-9_-]+\.)?cdn\.ampproject\.org((\/.*)|($))+/; + +/** + * Returns the value of some navigation timing parameter. + * Feature detection is used for safety on browsers that do not support the + * performance API. + * @param {!Window} win + * @param {string} timingEvent The name of the timing event, e.g. + * 'navigationStart' or 'domContentLoadEventStart'. + * @return {number} + */ +function getNavigationTiming(win, timingEvent) { + return (win['performance'] && win['performance']['timing'] && + win['performance']['timing'][timingEvent]) || 0; +} + +/** + * Check whether Google Ads supports the A4A rendering pathway is valid for the + * environment by ensuring native crypto support and page originated in the + * {@code cdn.ampproject.org} CDN or we must be running in local + * dev mode. + * + * @param {!Window} win Host window for the ad. + * @return {boolean} Whether Google Ads should attempt to render via the A4A + * pathway. + */ +export function isGoogleAdsA4AValidEnvironment(win) { + return supportsNativeCrypto(win) && ( + !!isCdnProxy(win) || getMode(win).localDev || getMode(win).test); +} + +/** + * Checks whether native crypto is supported for win. + * @param {!Window} win Host window for the ad. + * @return {boolean} Whether native crypto is supported. + */ +export function supportsNativeCrypto(win) { + return win.crypto && (win.crypto.subtle || win.crypto.webkitSubtle); +} + +/** + * @param {!AMP.BaseElement} ampElement The element on whose lifecycle this + * reporter will be reporting. + * @return {boolean} whether reporting is enabled for this element + */ +export function isReportingEnabled(ampElement) { + // 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; + // In local dev mode, neither the canary nor prod config files is available, + // so manually set the profiling rate, for testing/dev. + if (getMode(ampElement.win).localDev && !getMode(ampElement.win).test) { + toggleExperiment(win, 'a4aProfilingRate', true, true); + } + return (type == 'doubleclick' || type == 'adsense') && + isExperimentOn(win, 'a4aProfilingRate'); +} + +/** + * Has side-effect of incrementing ifi counter on window. + * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a + * @param {!Array=} opt_experimentIds Any experiments IDs (in addition + * to those specified on the ad element) that should be included in the + * request. + * @return {!Object} block level parameters + */ +export function googleBlockParameters(a4a, opt_experimentIds) { + const {element: adElement, win} = a4a; + const slotRect = a4a.getPageLayoutBox(); + const iframeDepth = iframeNestingDepth(win); + const enclosingContainers = getEnclosingContainerTypes(adElement); + let eids = adElement.getAttribute('data-experiment-id'); + if (opt_experimentIds) { + eids = mergeExperimentIds(opt_experimentIds, eids); + } + return { + 'adf': DomFingerprint.generate(adElement), + 'nhd': iframeDepth, + 'eid': eids, + 'adx': slotRect.left, + 'ady': slotRect.top, + 'oid': '2', + 'act': enclosingContainers.length ? enclosingContainers.join() : null, + }; +} + +/** + * @param {!Window} win + * @param {string} type matching typing attribute. + * @param {function(!Element):string} groupFn + * @return {!Promise>>>} + */ +export function groupAmpAdsByType(win, type, groupFn) { + // Look for amp-ad elements of correct type or those contained within + // standard container type. Note that display none containers will not be + // included as they will never be measured. + // TODO(keithwrightbos): what about slots that become measured due to removal + // of display none (e.g. user resizes viewport and media selector makes + // visible). + const ampAdSelector = + r => r.element./*OK*/querySelector(`amp-ad[type=${type}]`); + return Services.resourcesForDoc(win.document).getMeasuredResources(win, + r => { + const isAmpAdType = r.element.tagName == 'AMP-AD' && + r.element.getAttribute('type') == type; + if (isAmpAdType) { + return true; + } + const isAmpAdContainerElement = + Object.keys(ValidAdContainerTypes).includes(r.element.tagName) && + !!ampAdSelector(r); + return isAmpAdContainerElement; + }) + // Need to wait on any contained element resolution followed by build + // of child ad. + .then(resources => Promise.all(resources.map( + resource => { + if (resource.element.tagName == 'AMP-AD') { + return resource.element; + } + // Must be container element so need to wait for child amp-ad to + // be upgraded. + return whenUpgradedToCustomElement( + dev().assertElement(ampAdSelector(resource))); + }))) + // Group by networkId. + .then(elements => elements.reduce((result, element) => { + const groupId = groupFn(element); + (result[groupId] || (result[groupId] = [])).push(element.getImpl()); + return result; + }, {})); +} + +/** + * @param {! ../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a + * @param {number} startTime + * @return {!Promise>} + */ +export function googlePageParameters(a4a, startTime) { + const {win} = a4a; + const ampDoc = a4a.getAmpDoc(); + // Do not wait longer than 1 second to retrieve referrer to ensure + // viewer integration issues do not cause ad requests to hang indefinitely. + const referrerPromise = Services.timerFor(win).timeoutPromise( + 1000, Services.viewerForDoc(ampDoc).getReferrerUrl()) + .catch(() => { + dev().expectedError('AMP-A4A', 'Referrer timeout!'); + return ''; + }); + const domLoading = getNavigationTiming(win, 'domLoading'); + return Promise.all([ + getOrCreateAdCid(ampDoc, 'AMP_ECID_GOOGLE', '_ga'), referrerPromise]) + .then(promiseResults => { + const clientId = promiseResults[0]; + const referrer = promiseResults[1]; + const {pageViewId, canonicalUrl} = Services.documentInfoForDoc(ampDoc); + // Read by GPT for GA/GPT integration. + win.gaGlobal = win.gaGlobal || {cid: clientId, hid: pageViewId}; + const {screen} = win; + const viewport = Services.viewportForDoc(ampDoc); + const viewportRect = viewport.getRect(); + const viewportSize = viewport.getSize(); + const visibilityState = Services.viewerForDoc(ampDoc) + .getVisibilityState(); + return { + 'is_amp': a4a.isXhrAllowed() ? + AmpAdImplementation.AMP_AD_XHR_TO_IFRAME_OR_AMP : + AmpAdImplementation.AMP_AD_IFRAME_GET, + 'amp_v': '$internalRuntimeVersion$', + 'd_imp': '1', + 'c': getCorrelator(win, ampDoc, clientId), + 'ga_cid': win.gaGlobal.cid || null, + 'ga_hid': win.gaGlobal.hid || null, + 'dt': startTime, + 'biw': viewportRect.width, + 'bih': viewportRect.height, + 'u_aw': screen ? screen.availWidth : null, + 'u_ah': screen ? screen.availHeight : null, + 'u_cd': screen ? screen.colorDepth : null, + 'u_w': screen ? screen.width : null, + 'u_h': screen ? screen.height : null, + 'u_tz': -new Date().getTimezoneOffset(), + 'u_his': getHistoryLength(win), + 'isw': win != win.top ? viewportSize.width : null, + 'ish': win != win.top ? viewportSize.height : null, + 'art': getAmpRuntimeTypeParameter(win), + 'vis': visibilityStateCodes[visibilityState] || '0', + 'scr_x': viewport.getScrollLeft(), + 'scr_y': viewport.getScrollTop(), + 'bc': getBrowserCapabilitiesBitmap(win) || null, + 'debug_experiment_id': + (/(?:#|,)deid=([\d,]+)/i.exec(win.location.hash) || [])[1] || + null, + 'url': canonicalUrl || null, + 'top': win != win.top ? topWindowUrlOrDomain(win) : null, + 'loc': win.location.href == canonicalUrl ? null : win.location.href, + 'ref': referrer || null, + 'bdt': domLoading ? startTime - domLoading : null, + }; + }); +} + +/** + * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a + * @param {string} baseUrl + * @param {number} startTime + * @param {!Object} parameters + * @param {!Array=} opt_experimentIds Any experiments IDs (in addition + * to those specified on the ad element) that should be included in the + * request. + * @return {!Promise} + */ +export function googleAdUrl( + a4a, baseUrl, startTime, parameters, opt_experimentIds) { + // TODO: Maybe add checks in case these promises fail. + const blockLevelParameters = googleBlockParameters(a4a, opt_experimentIds); + return googlePageParameters(a4a, startTime) + .then(pageLevelParameters => { + Object.assign(parameters, blockLevelParameters, pageLevelParameters); + return truncAndTimeUrl(baseUrl, parameters, startTime); + }); +} + +/** + * @param {string} baseUrl + * @param {!Object} parameters + * @param {number} startTime + * @return {string} + */ +export function truncAndTimeUrl(baseUrl, parameters, startTime) { + return buildUrl( + baseUrl, parameters, MAX_URL_LENGTH - 10, TRUNCATION_PARAM) + + '&dtd=' + elapsedTimeWithCeiling(Date.now(), startTime); +} + +/** + * @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 {string} url + * @return {string} hostname portion of url + * @visibleForTesting + */ +export function extractHost(url) { + return (/^(?:https?:\/\/)?([^\/\?:]+)/i.exec(url) || [])[1] || url; +} + +/** + * @param {!Window} win + * @return {?string} + */ +function topWindowUrlOrDomain(win) { + const {ancestorOrigins} = win.location; + if (ancestorOrigins) { + const {origin} = win.location; + const topOrigin = ancestorOrigins[ancestorOrigins.length - 1]; + if (origin == topOrigin) { + return win.top.location.hostname; + } + const secondFromTop = secondWindowFromTop(win); + if (secondFromTop == win || + origin == ancestorOrigins[ancestorOrigins.length - 2]) { + return extractHost(secondFromTop./*OK*/document.referrer); + } + return extractHost(topOrigin); + } else { + try { + return win.top.location.hostname; + } catch (e) {} + const secondFromTop = secondWindowFromTop(win); + try { + return extractHost(secondFromTop./*OK*/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'; +} + +/** + * `nodeOrDoc` must be passed for correct behavior in shadow AMP (PWA) case. + * @param {!Window} win + * @param {!Element|!../../../src/service/ampdoc-impl.AmpDoc} elementOrAmpDoc + * @param {string=} opt_cid + * @return {number} The correlator. + */ +export function getCorrelator(win, elementOrAmpDoc, opt_cid) { + if (!win.ampAdPageCorrelator) { + win.ampAdPageCorrelator = isExperimentOn(win, 'exp-new-correlator') ? + Math.floor(4503599627370496 * Math.random()) : + makeCorrelator( + Services.documentInfoForDoc(elementOrAmpDoc).pageViewId, opt_cid); + } + return win.ampAdPageCorrelator; +} + +/** + * @param {string} pageViewId + * @param {string=} opt_clientId + * @return {number} + */ +function makeCorrelator(pageViewId, opt_clientId) { + const pageViewIdNumeric = Number(pageViewId || 0); + if (opt_clientId) { + return pageViewIdNumeric + ((opt_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()); + } +} + + +/** + * 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(); +} + +/** + * Returns amp-analytics config for a new CSI trigger. + * @param {string} on The name of the analytics trigger. + * @param {!Object} params Params to be included on the ping. + * @return {!JsonObject} + */ +function csiTrigger(on, params) { + return dict({ + 'on': on, + 'request': 'csi', + 'sampleSpec': { + // Pings are sampled on a per-pageview basis. A prefix is included in the + // sampleOn spec so that the hash is orthogonal to any other sampling in + // amp. + 'sampleOn': 'a4a-csi-${pageViewId}', + 'threshold': 1, // 1% sample + }, + 'selector': 'amp-ad', + 'selectionMethod': 'closest', + 'extraUrlParams': params, + }); +} + +/** + * Returns amp-analytics config for Google ads network impls. + * @return {!JsonObject} + */ +export function getCsiAmpAnalyticsConfig() { + return dict({ + 'requests': { + 'csi': 'https://csi.gstatic.com/csi?', + }, + 'transport': {'xhrpost': false}, + 'triggers': { + 'adRequestStart': csiTrigger('ad-request-start', { + // afs => ad fetch start + 'met.a4a': 'afs_lvt.${viewerLastVisibleTime}~afs.${time}', + }), + 'adResponseEnd': csiTrigger('ad-response-end', { + // afe => ad fetch end + 'met.a4a': 'afe.${time}', + }), + 'adRenderStart': csiTrigger('ad-render-start', { + // ast => ad schedule time + // ars => ad render start + 'met.a4a': + 'ast.${scheduleTime}~ars_lvt.${viewerLastVisibleTime}~ars.${time}', + 'qqid': '${qqid}', + }), + 'adIframeLoaded': csiTrigger('ad-iframe-loaded', { + // ail => ad iframe loaded + 'met.a4a': 'ail.${time}', + }), + }, + 'extraUrlParams': { + 's': 'ampad', + 'ctx': '2', + 'c': '${correlator}', + 'slotId': '${slotId}', + // Time that the beacon was actually sent. Note that there can be delays + // between the time at which the event is fired and when ${nowMs} is + // evaluated when the URL is built by amp-analytics. + 'puid': '${requestCount}~${timestamp}', + }, + }); +} + +/** + * Returns variables to be included in the amp-analytics event for A4A. + * @param {string} analyticsTrigger The name of the analytics trigger. + * @param {!AMP.BaseElement} a4a The A4A element. + * @param {?string} qqid The query ID or null if the query ID has not been set + * yet. + */ +export function getCsiAmpAnalyticsVariables(analyticsTrigger, a4a, qqid) { + const {win} = a4a; + const ampdoc = a4a.getAmpDoc(); + const viewer = Services.viewerForDoc(ampdoc); + const navStart = getNavigationTiming(win, 'navigationStart'); + const vars = { + 'correlator': getCorrelator(win, ampdoc), + 'slotId': a4a.element.getAttribute('data-amp-slot-index'), + 'viewerLastVisibleTime': viewer.getLastVisibleTime() - navStart, + }; + if (qqid) { + vars['qqid'] = qqid; + } + if (analyticsTrigger == 'ad-render-start') { + vars['scheduleTime'] = a4a.element.layoutScheduleTime - navStart; + } + return vars; +} + +/** + * Extracts configuration used to build amp-analytics element for active view. + * + * @param {!../../../extensions/amp-a4a/0.1/amp-a4a.AmpA4A} a4a + * @param {!Headers} responseHeaders + * XHR service FetchResponseHeaders object containing the response + * headers. + * @return {?JsonObject} config or null if invalid/missing. + */ +export function extractAmpAnalyticsConfig(a4a, responseHeaders) { + if (!responseHeaders.has(AMP_ANALYTICS_HEADER)) { + return null; + } + try { + const analyticsConfig = + parseJson(responseHeaders.get(AMP_ANALYTICS_HEADER)); + dev().assert(Array.isArray(analyticsConfig['url'])); + const urls = analyticsConfig['url']; + if (!urls.length) { + return null; + } + + const config = /** @type {JsonObject}*/ ({ + 'transport': {'beacon': false, 'xhrpost': false}, + 'triggers': { + 'continuousVisible': { + 'on': 'visible', + 'visibilitySpec': { + 'selector': 'amp-ad', + 'selectionMethod': 'closest', + 'visiblePercentageMin': 50, + 'continuousTimeMin': 1000, + }, + }, + }, + }); + + // Discover and build visibility endpoints. + const requests = dict(); + for (let idx = 1; idx <= urls.length; idx++) { + // TODO: Ensure url is valid and not freeform JS? + requests[`visibility${idx}`] = `${urls[idx - 1]}`; + } + // Security review needed here. + config['requests'] = requests; + config['triggers']['continuousVisible']['request'] = + Object.keys(requests); + return config; + } catch (err) { + dev().error('AMP-A4A', 'Invalid analytics', err, + responseHeaders.get(AMP_ANALYTICS_HEADER)); + } + return null; +} + +/** + * Add new experiment IDs 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 {!Array} newIds IDs to merge in. Should contain stringified + * integer (base 10) experiment IDs. + * @param {?string} currentIdString If present, a string containing a + * comma-separated list of integer experiment IDs. + * @return {string} New experiment list string, including newId iff it is + * a valid (integer) experiment ID. + * @see parseExperimentIds, validateExperimentIds + */ +export function mergeExperimentIds(newIds, currentIdString) { + const newIdString = newIds.filter(newId => Number(newId)).join(','); + currentIdString = currentIdString || ''; + return currentIdString + (currentIdString && newIdString ? ',' : '') + + newIdString; +} + +/** + * Adds two CSI signals to the given amp-analytics configuration object, one + * for render-start, and one for ini-load. + * + * @param {!Window} win + * @param {!Element} element The ad slot. + * @param {!JsonObject} config The original config object. + * @param {?string} qqid + * @param {boolean} isVerifiedAmpCreative + * @return {?JsonObject} config or null if invalid/missing. + */ +export function addCsiSignalsToAmpAnalyticsConfig( + win, element, config, qqid, isVerifiedAmpCreative) { + // Add CSI pingbacks. + const correlator = getCorrelator(win, element); + const slotId = Number(element.getAttribute('data-amp-slot-index')); + const eids = encodeURIComponent( + element.getAttribute(EXPERIMENT_ATTRIBUTE)); + const adType = element.getAttribute('type'); + const initTime = + Number(getTimingDataSync(win, 'navigationStart') || Date.now()); + const deltaTime = Math.round(win.performance && win.performance.now ? + win.performance.now() : (Date.now() - initTime)); + const baseCsiUrl = 'https://csi.gstatic.com/csi?s=a4a' + + `&c=${correlator}&slotId=${slotId}&qqid.${slotId}=${qqid}` + + `&dt=${initTime}` + + (eids != 'null' ? `&e.${slotId}=${eids}` : '') + + `&rls=$internalRuntimeVersion$&adt.${slotId}=${adType}`; + const isAmpSuffix = isVerifiedAmpCreative ? 'Friendly' : 'CrossDomain'; + config['triggers']['continuousVisibleIniLoad'] = { + 'on': 'ini-load', + 'selector': 'amp-ad', + 'selectionMethod': 'closest', + 'request': 'iniLoadCsi', + }; + config['triggers']['continuousVisibleRenderStart'] = { + 'on': 'render-start', + 'selector': 'amp-ad', + 'selectionMethod': 'closest', + 'request': 'renderStartCsi', + }; + config['requests']['iniLoadCsi'] = baseCsiUrl + + `&met.a4a.${slotId}=iniLoadCsi${isAmpSuffix}.${deltaTime}`; + config['requests']['renderStartCsi'] = baseCsiUrl + + `&met.a4a.${slotId}=renderStartCsi${isAmpSuffix}.${deltaTime}`; + + // Add CSI ping for visibility. + config['requests']['visibilityCsi'] = baseCsiUrl + + `&met.a4a.${slotId}=visibilityCsi.${deltaTime}`; + config['triggers']['continuousVisible']['request'].push('visibilityCsi'); + return config; +} + +/** + * Returns an array of two-letter codes representing the amp-ad containers + * enclosing the given ad element. + * + * @param {!Element} adElement + * @return {!Array} + */ +export function getEnclosingContainerTypes(adElement) { + const containerTypeSet = {}; + for (let el = adElement.parentElement, counter = 0; + el && counter < 20; el = el.parentElement, counter++) { + const tagName = el.tagName.toUpperCase(); + if (ValidAdContainerTypes[tagName]) { + containerTypeSet[ValidAdContainerTypes[tagName]] = true; + } + } + return Object.keys(containerTypeSet); +} + +/** + * Appends parameter to ad request indicating error state so long as error + * parameter is not already present or url has been truncated. + * @param {string} adUrl used for network request + * @param {string} parameterValue to be appended + * @return {string|undefined} potentially modified url, undefined + */ +export function maybeAppendErrorParameter(adUrl, parameterValue) { + dev().assert(!!adUrl && !!parameterValue); + // Add parameter indicating error so long as the url has not already been + // truncated and error parameter is not already present. Note that we assume + // that added, error parameter length will be less than truncation parameter + // so adding will not cause length to exceed maximum. + if (new RegExp(`[?|&](${encodeURIComponent(TRUNCATION_PARAM.name)}=` + + `${encodeURIComponent(String(TRUNCATION_PARAM.value))}|aet=[^&]*)$`) + .test(adUrl)) { + return; + } + const modifiedAdUrl = adUrl + `&aet=${parameterValue}`; + dev().assert(modifiedAdUrl.length <= MAX_URL_LENGTH); + return modifiedAdUrl; +} + +/** + * Returns a numerical code representing the binary type. + * @param {string} type + * @return {?string} + */ +export function getBinaryTypeNumericalCode(type) { + return { + 'production': '0', + 'control': '1', + 'canary': '2', + }[type] || null; +} + +/** @const {!RegExp} */ +const IDENTITY_DOMAIN_REGEXP_ = /\.google\.(?:com?\.)?[a-z]{2,3}$/; + +/** @typedef {{ + token: (string|undefined), + jar: (string|undefined), + pucrd: (string|undefined), + freshLifetimeSecs: (number|undefined), + validLifetimeSecs: (number|undefined), + fetchTimeMs: (number|undefined) + }} */ +export let IdentityToken; + +/** + * @param {!Window} win + * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampDoc + * @param {?string} consentPolicyId + * @return {!Promise} + */ +export function getIdentityToken(win, ampDoc, consentPolicyId) { + // If configured to use amp-consent, delay request until consent state is + // resolved. + win['goog_identity_prom'] = win['goog_identity_prom'] || + (consentPolicyId ? getConsentPolicyState(ampDoc, consentPolicyId) : + Promise.resolve(CONSENT_POLICY_STATE.UNKNOWN_NOT_REQUIRED)) + .then(consentState => + consentState == CONSENT_POLICY_STATE.INSUFFICIENT || + consentState == CONSENT_POLICY_STATE.UNKNOWN ? + /** @type{!IdentityToken} */({}) : + executeIdentityTokenFetch(win, ampDoc)); + return /** @type {!Promise} */(win['goog_identity_prom']); +} + +/** + * @param {!Window} win + * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampDoc + * @param {number=} redirectsRemaining (default 1) + * @param {string=} domain + * @param {number=} startTime + * @return {!Promise} + */ +function executeIdentityTokenFetch(win, ampDoc, redirectsRemaining = 1, + domain = undefined, startTime = Date.now()) { + const url = getIdentityTokenRequestUrl(win, ampDoc, domain); + return Services.xhrFor(win).fetchJson(url, { + mode: 'cors', + method: 'GET', + ampCors: false, + credentials: 'include', + }).then(res => res.json()) + .then(obj => { + const token = obj['newToken']; + const jar = obj['1p_jar'] || ''; + const pucrd = obj['pucrd'] || ''; + const freshLifetimeSecs = parseInt(obj['freshLifetimeSecs'] || '', 10); + const validLifetimeSecs = parseInt(obj['validLifetimeSecs'] || '', 10); + const altDomain = obj['altDomain']; + const fetchTimeMs = Date.now() - startTime; + if (IDENTITY_DOMAIN_REGEXP_.test(altDomain)) { + if (!redirectsRemaining--) { + // Max redirects, log? + return {fetchTimeMs}; + } + return executeIdentityTokenFetch( + win, ampDoc, redirectsRemaining, altDomain, startTime); + } else if (freshLifetimeSecs > 0 && validLifetimeSecs > 0 && + typeof token == 'string') { + return {token, jar, pucrd, freshLifetimeSecs, validLifetimeSecs, + fetchTimeMs}; + } + // returning empty + return {fetchTimeMs}; + }) + .catch(unusedErr => { + // TODO log? + return {}; + }); +} + +/** + * @param {!Window} win + * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampDoc + * @param {string=} domain + * @return {string} url + * @visibleForTesting + */ +export function getIdentityTokenRequestUrl(win, ampDoc, domain = undefined) { + if (!domain && win != win.top && win.location.ancestorOrigins) { + const matches = IDENTITY_DOMAIN_REGEXP_.exec( + win.location.ancestorOrigins[win.location.ancestorOrigins.length - 1]); + domain = (matches && matches[0]) || undefined; + } + domain = domain || '.google.com'; + const canonical = + extractHost(Services.documentInfoForDoc(ampDoc).canonicalUrl); + return `https://adservice${domain}/adsid/integrator.json?domain=${canonical}`; +} + +/** + * Returns whether we are running on the AMP CDN. + * @param {!Window} win + * @return {boolean} + */ +export function isCdnProxy(win) { + return CDN_PROXY_REGEXP.test(win.location.origin); +} + +/** + * Populates the fields of the given Nameframe experiment config object. + * @param {!Headers} headers + * @param {!NameframeExperimentConfig} nameframeConfig + */ +export function setNameframeExperimentConfigs(headers, nameframeConfig) { + const nameframeExperimentHeader = headers.get('amp-nameframe-exp'); + if (nameframeExperimentHeader) { + nameframeExperimentHeader.split(';').forEach(config => { + if (config == 'instantLoad' || config == 'writeInBody') { + nameframeConfig[config] = true; + } + }); + } +} + +/** + * Enum for browser capabilities. NOTE: Since JS is 32-bit, do not add anymore + * than 32 capabilities to this enum. + * @enum {number} + */ +const Capability = { + SVG_SUPPORTED: 1 << 0, + SANDBOXING_ALLOW_TOP_NAVIGATION_BY_USER_ACTIVATION_SUPPORTED: 1 << 1, + SANDBOXING_ALLOW_POPUPS_TO_ESCAPE_SANDBOX_SUPPORTED: 1 << 2, +}; + +/** + * Returns a bitmap representing what features are supported by this browser. + * @param {!Window} win + * @return {number} + */ +function getBrowserCapabilitiesBitmap(win) { + let browserCapabilities = 0; + const doc = win.document; + if (win.SVGElement && doc.createElementNS) { + browserCapabilities |= Capability.SVG_SUPPORTED; + } + const iframeEl = doc.createElement('iframe'); + if (iframeEl.sandbox && iframeEl.sandbox.supports) { + if (iframeEl.sandbox.supports('allow-top-navigation-by-user-activation')) { + browserCapabilities |= + Capability.SANDBOXING_ALLOW_TOP_NAVIGATION_BY_USER_ACTIVATION_SUPPORTED; + } + if (iframeEl.sandbox.supports('allow-popups-to-escape-sandbox')) { + browserCapabilities |= + Capability.SANDBOXING_ALLOW_POPUPS_TO_ESCAPE_SANDBOX_SUPPORTED; + } + } + return browserCapabilities; +} + +/** + * Returns an enum value representing the AMP binary type, or null if this is a + * canonical page. + * @param {!Window} win + * @return {?string} The binary type enum. + * @visibleForTesting + */ +export function getAmpRuntimeTypeParameter(win) { + const art = getBinaryTypeNumericalCode(getBinaryType(win)); + return isCdnProxy(win) && art != '0' ? art : null; +} diff --git a/ads/google/adsense-amp-auto-ads-responsive.js b/ads/google/adsense-amp-auto-ads-responsive.js new file mode 100644 index 000000000000..37ba79476042 --- /dev/null +++ b/ads/google/adsense-amp-auto-ads-responsive.js @@ -0,0 +1,63 @@ +/** + * Copyright 2018 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 { + ExperimentInfo, // eslint-disable-line no-unused-vars + getExperimentBranch, + randomlySelectUnsetExperiments, +} from '../../src/experiments'; + + +/** @const {string} */ +export const ADSENSE_AMP_AUTO_ADS_RESPONSIVE_EXPERIMENT_NAME = + 'amp-auto-ads-adsense-responsive'; + + +/** + * @enum {string} + */ +export const AdSenseAmpAutoAdsResponsiveBranches = { + CONTROL: '19861210', // don't attempt to expand auto ads to responsive format + EXPERIMENT: '19861211', // do attempt to expand auto ads to responsive format +}; + + +/** @const {!ExperimentInfo} */ +const ADSENSE_AMP_AUTO_ADS_RESPONSIVE_EXPERIMENT_INFO = { + isTrafficEligible: win => !!win.document.querySelector('AMP-AUTO-ADS'), + branches: [ + AdSenseAmpAutoAdsResponsiveBranches.CONTROL, + AdSenseAmpAutoAdsResponsiveBranches.EXPERIMENT, + ], +}; + + +/** + * This has the side-effect of selecting the page into a branch of the + * experiment, which becomes sticky for the entire pageview. + * + * @param {!Window} win + * @return {?string} + */ +export function getAdSenseAmpAutoAdsResponsiveExperimentBranch(win) { + const experiments = /** @type {!Object} */ ({}); + experiments[ADSENSE_AMP_AUTO_ADS_RESPONSIVE_EXPERIMENT_NAME] = + ADSENSE_AMP_AUTO_ADS_RESPONSIVE_EXPERIMENT_INFO; + randomlySelectUnsetExperiments(win, experiments); + return getExperimentBranch(win, + ADSENSE_AMP_AUTO_ADS_RESPONSIVE_EXPERIMENT_NAME) || null; +} diff --git a/ads/google/adsense-amp-auto-ads.js b/ads/google/adsense-amp-auto-ads.js new file mode 100644 index 000000000000..30fb59389e72 --- /dev/null +++ b/ads/google/adsense-amp-auto-ads.js @@ -0,0 +1,63 @@ +/** + * 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 { + ExperimentInfo, // eslint-disable-line no-unused-vars + getExperimentBranch, + randomlySelectUnsetExperiments, +} from '../../src/experiments'; + + +/** @const {string} */ +export const ADSENSE_AMP_AUTO_ADS_HOLDOUT_EXPERIMENT_NAME = + 'amp-auto-ads-adsense-holdout'; + + +/** + * @enum {string} + */ +export const AdSenseAmpAutoAdsHoldoutBranches = { + CONTROL: '3782001', // don't run amp-auto-ads + EXPERIMENT: '3782002', // do run amp-auto-ads +}; + + +/** @const {!../../src/experiments.ExperimentInfo} */ +const ADSENSE_AMP_AUTO_ADS_EXPERIMENT_INFO = { + isTrafficEligible: win => !!win.document.querySelector('AMP-AUTO-ADS'), + branches: [ + AdSenseAmpAutoAdsHoldoutBranches.CONTROL, + AdSenseAmpAutoAdsHoldoutBranches.EXPERIMENT, + ], +}; + + +/** + * This has the side-effect of selecting the page into a branch of the + * experiment, which becomes sticky for the entire pageview. + * + * @param {!Window} win + * @return {?string} + */ +export function getAdSenseAmpAutoAdsExpBranch(win) { + const experiments = /** @type {!Object} */ ({}); + experiments[ADSENSE_AMP_AUTO_ADS_HOLDOUT_EXPERIMENT_NAME] = + ADSENSE_AMP_AUTO_ADS_EXPERIMENT_INFO; + randomlySelectUnsetExperiments(win, experiments); + return getExperimentBranch(win, ADSENSE_AMP_AUTO_ADS_HOLDOUT_EXPERIMENT_NAME) + || null; +} diff --git a/ads/google/adsense.js b/ads/google/adsense.js new file mode 100644 index 000000000000..51e0dcbc99a1 --- /dev/null +++ b/ads/google/adsense.js @@ -0,0 +1,104 @@ +/** + * 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 { + ADSENSE_MCRSPV_TAG, + ADSENSE_RSPV_TAG, + ADSENSE_RSPV_WHITELISTED_HEIGHT, +} from './utils'; +import {CONSENT_POLICY_STATE} from '../../src/consent-state'; +import {camelCaseToDash} from '../../src/string'; +import {hasOwn} from '../../src/utils/object'; +import {setStyles} from '../../src/style'; +import {user} from '../../src/log'; +import {validateData} from '../../3p/3p'; + +/** + * Make an adsense iframe. + * @param {!Window} global + * @param {!Object} data + */ +export function adsense(global, data) { + // TODO: check mandatory fields + validateData(data, [], + ['adClient', 'adSlot', 'adHost', 'adtest', 'tagOrigin', 'experimentId', + 'ampSlotIndex', 'adChannel', 'autoFormat', 'fullWidth', 'package', + 'npaOnUnknownConsent', 'matchedContentUiType', 'matchedContentRowsNum', + 'matchedContentColumnsNum']); + + if (data['autoFormat'] == ADSENSE_RSPV_TAG || + data['autoFormat'] == ADSENSE_MCRSPV_TAG) { + user().assert(hasOwn(data, 'fullWidth'), + 'Responsive AdSense ad units require the attribute data-full-width.'); + + user().assert(data['height'] == ADSENSE_RSPV_WHITELISTED_HEIGHT, + `Specified height ${data['height']} in tag is not equal to ` + + `the required height of ${ADSENSE_RSPV_WHITELISTED_HEIGHT} for ` + + 'responsive AdSense ad units.'); + } + + if (global.context.clientId) { + // Read by GPT for GA/GPT integration. + global.gaGlobal = { + cid: global.context.clientId, + hid: global.context.pageViewId, + }; + } + const s = global.document.createElement('script'); + s.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'; + global.document.body.appendChild(s); + + const i = global.document.createElement('ins'); + ['adChannel', 'adClient', 'adSlot', 'adHost', 'adtest', 'tagOrigin', + 'package', 'matchedContentUiType', 'matchedContentRowsNum', + 'matchedContentColumnsNum'] + .forEach(datum => { + if (data[datum]) { + i.setAttribute('data-' + camelCaseToDash(datum), data[datum]); + } + }); + i.setAttribute('data-page-url', global.context.canonicalUrl); + i.setAttribute('class', 'adsbygoogle'); + setStyles(i, { + display: 'inline-block', + width: '100%', + height: '100%', + }); + const initializer = {}; + switch (global.context.initialConsentState) { + case CONSENT_POLICY_STATE.UNKNOWN: + if (data['npaOnUnknownConsent'] != 'true') { + // Unknown w/o NPA results in no ad request. + return; + } + case CONSENT_POLICY_STATE.INSUFFICIENT: + (global.adsbygoogle = global.adsbygoogle || []) + ['requestNonPersonalizedAds'] = true; + break; + } + 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(initializer); +} diff --git a/ads/google/adsense.md b/ads/google/adsense.md new file mode 100644 index 000000000000..2a2a1d93ef3b --- /dev/null +++ b/ads/google/adsense.md @@ -0,0 +1,40 @@ + + +# AdSense + +## Example + +```html + + +``` + +## Configuration + +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: + +- data-ad-client +- data-ad-slot +- data-ad-host +- data-adtest +- data-tag-origin +- data-language diff --git a/ads/google/csa.js b/ads/google/csa.js new file mode 100644 index 000000000000..f1ed923418be --- /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 {getStyle, setStyle, setStyles} from '../../src/style'; +import {loadScript, validateData} from '../../3p/3p'; +import {tryParseJson} from '../../src/json.js'; + +// 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-next-line no-unused-vars + const ignore = global.document.body./*OK*/offsetHeight; + // 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(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 {!Window} global + * @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(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..9df0e3b59a61 --- /dev/null +++ b/ads/google/csa.md @@ -0,0 +1,94 @@ + +# Custom Search Ads + +## 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. + + + diff --git a/ads/google/doubleclick.js b/ads/google/doubleclick.js new file mode 100644 index 000000000000..da1191d910d7 --- /dev/null +++ b/ads/google/doubleclick.js @@ -0,0 +1,28 @@ +/** + * 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 {dev} from '../../src/log'; + +const TAG = 'DOUBLECLICK - DEPRECATED'; +/** + * @param {!Window} opt_global + * @param {!Object} opt_data + */ +export function doubleclick(opt_global, opt_data) { + dev().error(TAG, 'The use of doubleclick.js has been deprecated. Please ' + + 'switch to Fast Fetch. See documentation here: ' + + 'https://github.com/ampproject/amphtml/issues/11834'); + +} diff --git a/ads/google/doubleclick.md b/ads/google/doubleclick.md new file mode 100644 index 000000000000..a64c6445c507 --- /dev/null +++ b/ads/google/doubleclick.md @@ -0,0 +1,19 @@ + + +# Doubleclick + +Support for DoubleClick Delayed Fetch has been deprecated as of March 29, 2018. Please upgrade to Fast Fetch. Visit the DoubleClick Fast Fetch documentation page for more details. diff --git a/ads/google/ima-player-data.js b/ads/google/ima-player-data.js new file mode 100644 index 000000000000..e0064557036a --- /dev/null +++ b/ads/google/ima-player-data.js @@ -0,0 +1,57 @@ +/** + * 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. + */ + +export class ImaPlayerData { + + /** + * Create a new ImaPlayerData object. + */ + constructor() { + /* {!Number} */ + this.currentTime = 0; + + /* {!Number} */ + this.duration = 1; + + /* {!Array} */ + this.playedRanges = []; + } + + /** + * Update from the provided video player. + * + * @param {!Element} videoPlayer + */ + update(videoPlayer) { + this.currentTime = videoPlayer.currentTime; + this.duration = videoPlayer.duration; + + // Adapt videoPlayer.played for the playedRanges format AMP wants. + const {played} = videoPlayer; + const {length} = played; + this.playedRanges = []; + for (let i = 0; i < length; i++) { + this.playedRanges.push([played.start(i), played.end(i)]); + } + } +} + +/** + * Unique identifier for messages from the implementation iframe with data + * about the player. + * @const + */ +ImaPlayerData.IMA_PLAYER_DATA = 'imaPlayerData'; diff --git a/ads/google/imaVideo.js b/ads/google/imaVideo.js new file mode 100644 index 000000000000..1f317b4c356e --- /dev/null +++ b/ads/google/imaVideo.js @@ -0,0 +1,1688 @@ +/** + * 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 {CONSENT_POLICY_STATE} from '../../src/consent-state'; +import {ImaPlayerData} from './ima-player-data'; +import {camelCaseToTitleCase, px, setStyle, setStyles} from '../../src/style'; +import {isObject} from '../../src/types'; +import {loadScript} from '../../3p/3p'; +import {tryParseJson} from '../../src/json'; + +/** + * Possible player states. + * @enum {number} + * @private + */ +const PlayerStates = { + PLAYING: 1, + PAUSED: 2, +}; + +/** + * Icons from Google Material Icons + * https://material.io/tools/icons + */ +/*eslint-disable*/ +const icons = { + 'play': + ` + `, + 'pause': + ` + `, + 'fullscreen': + ` + `, + 'mute': + ` + `, + 'volume_max': + ` + `, + 'seek': + `` +}; + +/*eslint-enable */ + +const bigPlayDivDisplayStyle = 'table-cell'; + +// Div wrapping our entire DOM. +let wrapperDiv; + +// Div containing big play button. Rendered before player starts. +let bigPlayDiv; + +// Div contianing play button. Double-nested for alignment. +let playButtonDiv; + +// Div containing player controls. +let controlsDiv; + +// Wrapper for ad countdown element. +let countdownWrapperDiv; + +// Div containing ad countdown timer. +let countdownDiv; + +// Div containing play or pause button. +let playPauseDiv; + +// Div containing player time. +let timeDiv; + +// Node containing the player time text. +let timeNode; + +// Wrapper for progress bar DOM elements. +let progressBarWrapperDiv; + +// Line for progress bar. +let progressLine; + +// Line for total time in progress bar. +let totalTimeLine; + +// Div containing the marker for the progress. +let progressMarkerDiv; + +// Div for fullscreen icon. +let fullscreenDiv; + +// Div for mute/unmute icon. +let muteUnmuteDiv; + +// Div for ad container. +let adContainerDiv; + +// Div for content player. +let contentDiv; + +// Content player. +let videoPlayer; + +// Event indicating user interaction. +let interactEvent; + +// Event for mouse down. +let mouseDownEvent; + +// Event for mouse move. +let mouseMoveEvent; + +// Event for mouse up. +let mouseUpEvent; + +// Percent of the way through the video the user has seeked. Used for seek +// events. +let seekPercent; + +// Flag tracking whether or not content has played to completion. +let contentComplete; + +// Flag tracking whether or not all ads have been played and been completed. +let allAdsCompleted; + +// Flag tracking if an ad request has failed. +let adRequestFailed; + +// IMA SDK Ad object +let currentAd; + +// IMA SDK AdDisplayContainer object. +let adDisplayContainer; + +// IMA SDK AdsRequest object. +let adsRequest; + +// IMA SDK AdsLoader object. +let adsLoader; + +// IMA SDK AdsManager object; +let adsManager; + +// Timer for UI updates. +let uiTicker; + +// Tracks the current state of the player. +let playerState; + +// Flag for whether or not we are currently in fullscreen mode. +let fullscreen; + +// Width the player should be in fullscreen mode. +let fullscreenWidth; + +// Height the player should be in fullscreen mode. +let fullscreenHeight; + +// "Ad" label used in ad controls. +let adLabel; + +// Flag tracking if ads are currently active. +let adsActive; + +// Flag tracking if playback has started. +let playbackStarted; + +// Timer used to hide controls after user action. +let hideControlsTimeout; + +// Flag tracking if we need to mute the ads manager once it loads. Used for +// autoplay. +let muteAdsManagerOnLoaded; + +// Flag tracking if we are in native fullscreen mode. Used for iPhone. +let nativeFullscreen; + +// Flag tracking if the IMA library was allowed to load. Will be set to false +// when e.g. a user is using an ad blocker. +let imaLoadAllowed; + +// Used if the adsManager needs to be resized on load. +let adsManagerWidthOnLoad, adsManagerHeightOnLoad; + +// Initial video dimensions. +let videoWidth, videoHeight; + +// IMASettings provided via + ` + ); +} diff --git a/ads/pubmine.md b/ads/pubmine.md new file mode 100644 index 000000000000..c85fcbd465e3 --- /dev/null +++ b/ads/pubmine.md @@ -0,0 +1,56 @@ + + +# Pubmine + +## Example + +### Basic + +```html + + +``` + +### With all attributes + +```html + + +``` + +## Configuration + +For further configuration information, please [contact Pubmine](https://wordpress.com/help/contact). + +Please note that the height parameter should be 15 greater than your ad size to ensure there is enough room for the "Report this ad" link. + +### Required parameters + +* `data-siteid`: Pubmine publisher site number. + +### Optional parameters + +* `data-section`: Pubmine slot identifier +* `data-pt`: Enum value for page type +* `data-ht`: Enum value for hosting type diff --git a/ads/pulsepoint.js b/ads/pulsepoint.js new file mode 100644 index 000000000000..12ffe4fb8462 --- /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 {doubleclick} from '../ads/google/doubleclick'; +import {loadScript, validateData, writeScript} from '../3p/3p'; + +/** + * @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(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..5d585ec1dd38 --- /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 [PulsePoint's 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. diff --git a/ads/purch.js b/ads/purch.js new file mode 100644 index 000000000000..28644a576725 --- /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 { + validateData, + validateSrcPrefix, + writeScript} 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..3e0eb224a50b --- /dev/null +++ b/ads/purch.md @@ -0,0 +1,36 @@ + + +# Purch + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +Supported parameters: + +- `data-pid`: placement id +- `data-divid`: div id of unit diff --git a/ads/quoraad.js b/ads/quoraad.js new file mode 100644 index 000000000000..d7fd0f7891aa --- /dev/null +++ b/ads/quoraad.js @@ -0,0 +1,27 @@ +/** + * Copyright 2018 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 quoraad(global, data) { + validateData(data, ['adid']); + global.ampAdParam = data; + writeScript(global, 'https://a.quora.com/amp_ad.js'); +} diff --git a/ads/quoraad.md b/ads/quoraad.md new file mode 100644 index 000000000000..4bb70d09e457 --- /dev/null +++ b/ads/quoraad.md @@ -0,0 +1,36 @@ + + +# Quora + +## Example + +### Normal Ad + +```html + + +``` + +## Configuration + +Please consult internal reference documents on AMP. This is used only for first-party ads on quora.com. + +Required parameter: + +- `data-aid` diff --git a/ads/realclick.js b/ads/realclick.js new file mode 100644 index 000000000000..24fb1d5f2cd4 --- /dev/null +++ b/ads/realclick.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 realclick(global, data) { + validateData(data, ['mcode']); + global.rcParams = data; + loadScript(global, 'https://ssp.realclick.co.kr/amp/ad.js'); +} diff --git a/ads/realclick.md b/ads/realclick.md new file mode 100644 index 000000000000..a41fee23ed4c --- /dev/null +++ b/ads/realclick.md @@ -0,0 +1,35 @@ + + +# Realclick + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact [Realclick](http://www.realclick.co.kr/) + + +Supported parameters: + +- `data-mcode` diff --git a/ads/recomad.js b/ads/recomad.js new file mode 100644 index 000000000000..117a5b9dc2df --- /dev/null +++ b/ads/recomad.js @@ -0,0 +1,70 @@ +/** + * Copyright 2018 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'; + +/** + * Add a container for the recomAD widget, + * which will be discovered by the script automatically. + * + * @param {Element} container + * @param {string} appId + * @param {string} widgetId + * @param {string} searchTerm + * @param {string} origin + * @param {string} baseUrl + * @param {string} puid + */ +function createWidgetContainer( + container, + appId, + widgetId, + searchTerm, + origin, + baseUrl, + puid +) { + container.className = 's24widget'; + + container.setAttribute('data-app-id', appId); + container.setAttribute('data-widget-id', widgetId); + searchTerm && container.setAttribute('data-search-term', searchTerm); + origin && container.setAttribute('data-origin', origin); + baseUrl && container.setAttribute('data-base-url', baseUrl); + puid && container.setAttribute('data-puid', puid); + + window.document.body.appendChild(container); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function recomad(global, data) { + validateData(data, ['appId', 'widgetId', ['searchTerm', 'origin']]); + + createWidgetContainer( + window.document.createElement('div'), + data['appId'], + data['widgetId'], + data['searchTerm'] || '', + data['origin'] || '', + data['baseUrl'] || '', + data['puid'] || '' + ); + + loadScript(window, 'https://widget.s24.com/js/s24widget.min.js'); +} diff --git a/ads/recomad.md b/ads/recomad.md new file mode 100644 index 000000000000..9795e875fd91 --- /dev/null +++ b/ads/recomad.md @@ -0,0 +1,58 @@ + + +# recomAD widget +This `` tag contains a widget called +recomAD, which can be integrated to add product +recommendations for your visitors that fit the +content of your website. + +See [https://recomad.de](https://recomad.de) for details. + +## Example + +```html + +``` + +## Configuration + +For details on the configuration semantics, please contact the [ad network](#configuration) or refer to their [documentation](#ping). + +### Required parameters + +- `data-app-id` : Your app id +- `data-widget-id` : Your widget id + +Please contact us at cooperations@s24.com to receive an `app id` and a `widget id`. + +### Required _at least one of these two_ parameters + +- `data-search-term` : Required if _recomAD Search_. The search term you would like to get products for +- `data-origin` : Required if _recomAD Semantic_. Your canonical link of your original page + +### Optional parameters + +- `data-puid` : Your tracking id for the end user diff --git a/ads/relap.js b/ads/relap.js new file mode 100644 index 000000000000..2242c98661a0 --- /dev/null +++ b/ads/relap.js @@ -0,0 +1,69 @@ +/** + * 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', 'version']); + + const urlParam = data['url'] || window.context.canonicalUrl; + + if (data['version'] === 'v7') { + window.onRelapAPIReady = function(relapAPI) { + relapAPI['init']({ + token: data['token'], + url: urlParam, + }); + }; + + window.onRelapAPIInit = function(relapAPI) { + relapAPI['addWidget']({ + cfgId: data['anchorid'], + anchorEl: global.document.getElementById('c'), + position: 'append', + events: { + onReady: function() { + window.context.renderStart(); + }, + onNoContent: function() { + window.context.noContentAvailable(); + }, + }, + }); + }; + + loadScript(global, 'https://v7.relap.io/relap.js'); + } else { + window.relapV6WidgetReady = function() { + window.context.renderStart(); + }; + + window.relapV6WidgetNoSimilarPages = function() { + window.context.noContentAvailable(); + }; + + const anchorEl = global.document.createElement('div'); + anchorEl.id = data['anchorid']; + global.document.getElementById('c').appendChild(anchorEl); + + const url = `https://relap.io/api/v6/head.js?token=${encodeURIComponent(data['token'])}&url=${encodeURIComponent(urlParam)}`; + loadScript(global, url); + } +} diff --git a/ads/relap.md b/ads/relap.md new file mode 100644 index 000000000000..2800c1b04812 --- /dev/null +++ b/ads/relap.md @@ -0,0 +1,40 @@ + + +# Relap + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see Relap's documentation. Currently supports all anchor-based widgets. + +Supported parameters: + +- `data-token` +- `data-url` +- `data-anchorid` +- `data-version` \ No newline at end of file diff --git a/ads/revcontent.js b/ads/revcontent.js new file mode 100644 index 000000000000..a42a54aaa350 --- /dev/null +++ b/ads/revcontent.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, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function revcontent(global, data) { + 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, endpoint); +} diff --git a/ads/revcontent.md b/ads/revcontent.md new file mode 100644 index 000000000000..939aadef0f13 --- /dev/null +++ b/ads/revcontent.md @@ -0,0 +1,57 @@ + + +# Revcontent + +## Example + +```html + +
Loading ...
+
+``` + +## Configuration + +For semantics of configuration, please see [Revcontent's documentation](https://faq.revcontent.com/). + +Supported parameters: + +- `data-id` +- `data-wrapper` +- `data-endpoint` +- `data-ssl` +- `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. diff --git a/ads/revjet.js b/ads/revjet.js new file mode 100644 index 000000000000..b3045c27c565 --- /dev/null +++ b/ads/revjet.js @@ -0,0 +1,35 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function revjet(global, data) { + validateData(data, ['tag', 'key'], ['plc', 'opts', 'params']); + + global._revjetData = Object.assign({}, data); + + loadScript( + global, + 'https://cdn.revjet.com/~cdn/JS/03/amp.js', + /* opt_cb */ undefined, + () => { + global.context.noContentAvailable(); + }); +} diff --git a/ads/revjet.md b/ads/revjet.md new file mode 100644 index 000000000000..045de746c3a3 --- /dev/null +++ b/ads/revjet.md @@ -0,0 +1,45 @@ + + +# RevJet + +Serves ads from the [RevJet Marketing Creative Platform](https://www.revjet.com/). + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `data-tag` +- `data-key` + +### Optional parameters + +- `data-plc` +- `data-opts` +- `data-params` diff --git a/ads/rfp.js b/ads/rfp.js new file mode 100644 index 000000000000..624b36f76902 --- /dev/null +++ b/ads/rfp.js @@ -0,0 +1,27 @@ +/** + * Copyright 2018 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 rfp(global, data) { + validateData(data, ['adspotId'], ['stylesheetUrl', 'country']); + global.rfpData = data; + writeScript(global, 'https://js.rfp.fout.jp/rfp-amp.js'); +} diff --git a/ads/rfp.md b/ads/rfp.md new file mode 100644 index 000000000000..6262c5694b51 --- /dev/null +++ b/ads/rfp.md @@ -0,0 +1,39 @@ + + +# Red for Publishers + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://www.fout.co.jp/freakout/contact/. + +### Required parameters + +- `data-adspot-id` + +### Optional parameters + +- `data-stylesheet-url` +- `data-country` diff --git a/ads/rubicon.js b/ads/rubicon.js new file mode 100644 index 000000000000..cc73f11af8bf --- /dev/null +++ b/ads/rubicon.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, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function rubicon(global, data) { + // TODO: check mandatory fields + validateData(data, [], [ + 'account', 'site', 'zone', 'size', + 'kw', 'visitor', 'inventory', + 'method', 'callback', + ]); + + if (data.method === 'smartTag') { + smartTag(global, data); + } +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function smartTag(global, data) { + /* eslint-disable */ + global.rp_account = data.account; + global.rp_site = data.site; + global.rp_zonesize = data.zone + '-' + data.size; + global.rp_adtype = 'js'; + global.rp_page = context.sourceUrl; + global.rp_kw = data.kw; + global.rp_visitor = data.visitor; + global.rp_inventory = data.inventory; + global.rp_amp = 'st'; + global.rp_callback = data.callback; + /* eslint-enable */ + writeScript(global, 'https://ads.rubiconproject.com/ad/' + + encodeURIComponent(data.account) + '.js'); +} diff --git a/ads/rubicon.md b/ads/rubicon.md new file mode 100644 index 000000000000..eb924cdf66fe --- /dev/null +++ b/ads/rubicon.md @@ -0,0 +1,75 @@ + + +# Rubicon Project + +If you want to serve ads via your Ad Server then there is no need to use the adapter when using Smart Tags. These can simply be served via your Ad Server in the normal fashion. You simply need to ensure that you are using secure tags (https). + +The Rubicon Project adapter supports Smart Tags directly on the page. + +**Please note that Fastlane is no longer supported.** + +## Examples + +### Smart Tag: Basic + +```html + + +``` + +#### Smart Tag: With additional targeting + +```html + + +``` + +### Configuration + +For semantics of configuration, please contact your Rubicon Account Director @ +[Rubicon Project](http://platform.rubiconproject.com]) + +#### Ad size + +By default the ad size is based on the `width` and `height` attributes of the `amp-ad` tag. + +#### Supported parameters + +#### Smart Tag +- `data-method` +- `data-account` +- `data-site` +- `data-zone` +- `data-size` + +##### First Party Data & Keywords +- `data-kw` +- `json` - for visitor and inventory data diff --git a/ads/runative.js b/ads/runative.js new file mode 100644 index 000000000000..19e5364a01db --- /dev/null +++ b/ads/runative.js @@ -0,0 +1,112 @@ +/** + * Copyright 2018 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'; + +const requiredParams = ['spot']; +const optionsParams = [ + 'keywords', + 'adType', + 'param1', + 'param2', + 'param3', + 'subid', + 'cols', + 'rows', + 'title', + 'titlePosition', + 'adsByPosition', +]; +const adContainerId = 'runative_id'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function runative(global, data) { + // ensure we have valid widgetIds value + validateData( + data, + requiredParams, + optionsParams + ); + + const adContainer = global.document.getElementById('c'); + const adNativeContainer = getAdContainer(global); + const initScript = getInitAdScript(global, data); + + adContainer.appendChild(adNativeContainer); + + // load the RUNative AMP JS file + loadScript( + global, + '//cdn.run-syndicate.com/sdk/v1/n.js', + () => { + global + .document + .body + .appendChild(initScript); + } + ); +} + +/** + * @param {!Object} data + */ +function getInitData(data) { + const initKeys = requiredParams.concat(optionsParams); + const initParams = {}; + + initKeys + .forEach(key => { + if (key in data) { + const initKey = key === 'adType' ? 'type' : key; + + initParams[initKey] = data[key]; + } + }); + + initParams['element_id'] = adContainerId; + + return initParams; +} + +/** + * @param {!Window} global + */ +function getAdContainer(global) { + const container = global.document.createElement('div'); + + container['id'] = adContainerId; + + return container; +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function getInitAdScript(global, data) { + const scriptElement = global.document.createElement('script'); + const initData = getInitData(data); + const initScript = global + .document + .createTextNode(`NativeAd(${ JSON.stringify(initData) });`); + + scriptElement.appendChild(initScript); + + return scriptElement; +} diff --git a/ads/runative.md b/ads/runative.md new file mode 100644 index 000000000000..c9c442962563 --- /dev/null +++ b/ads/runative.md @@ -0,0 +1,52 @@ + + +# RUNative + +Serves ads from the [RUNative](https://www.runative.com/). + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `data-spot` - code spot + +### Optional parameters + +- `data-ad-type` - types of ads: `img-left`, `img-right`, `label-over`, `label-under` +- `data-keywords` - title of ad +- `data-title` - title of ad +- `data-cols` - number of cols 1 till 6 +- `data-rows` - number of rows 1 till 6 +- `data-title-position` - position of ad title (`left` or `right`) +- `data-ads-by-position` - position of runative logo (`left` or `right`) diff --git a/ads/sekindo.js b/ads/sekindo.js new file mode 100644 index 000000000000..51b22032dedf --- /dev/null +++ b/ads/sekindo.js @@ -0,0 +1,47 @@ +/** + * Copyright 2018 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 {hasOwn} from '../src/utils/object'; +import {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function sekindo(global, data) { + validateData(data, ['spaceid']); + const pubUrl = encodeURIComponent(global.context.sourceUrl); + const excludesSet = {ampSlotIndex: 1, type: 1}; + const customParamMap = {spaceid: 's',width: 'x',height: 'y'}; + let query = 'isAmpProject=1&pubUrl=' + pubUrl + '&cbuster=' + + global.context.startTime + '&'; + let getParam = ''; + for (const key in data) { + if (hasOwn(data, key)) { + if (typeof excludesSet[key] == 'undefined') { + getParam = (typeof customParamMap[key] == 'undefined') ? + key : customParamMap[key]; + query += getParam + '=' + encodeURIComponent(data[key]) + '&'; + } + } + } + loadScript(global, + 'https://live.sekindo.com/live/liveView.php?' + query, () => { + global.context.renderStart(); + }, () => { + global.context.noContentAvailable(); + }); +} diff --git a/ads/sekindo.md b/ads/sekindo.md new file mode 100644 index 000000000000..b90cf9ed67c2 --- /dev/null +++ b/ads/sekindo.md @@ -0,0 +1,34 @@ + + +# Sekindo + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please visit [www.sekindo.com](http://www.sekindo.com). + +### Required parameters + +- `data-spaceId` - Ad unit unique id diff --git a/ads/sharethrough.js b/ads/sharethrough.js new file mode 100644 index 000000000000..baa152d1bef9 --- /dev/null +++ b/ads/sharethrough.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 sharethrough(global, data) { + validateData(data, ['pkey'], []); + global.pkey = data.pkey; + writeScript(global, 'https://native.sharethrough.com/iframe/amp.js'); +} diff --git a/ads/sharethrough.md b/ads/sharethrough.md new file mode 100644 index 000000000000..01bf1f2ae52b --- /dev/null +++ b/ads/sharethrough.md @@ -0,0 +1,37 @@ + + +# Sharethrough + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For semantics of configuration, please [contact Sharethrough](mailto:pubsupport@sharethrough.com). + +### Required parameters + +- `data-pkey`: (String, non-empty) The unique identifier for your placement diff --git a/ads/sklik.js b/ads/sklik.js new file mode 100644 index 000000000000..d4fe68ff4a8c --- /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.canonicalUrl; + + sklikProvider.show(data); + }); +} diff --git a/ads/sklik.md b/ads/sklik.md new file mode 100644 index 000000000000..e75749b38c6e --- /dev/null +++ b/ads/sklik.md @@ -0,0 +1,37 @@ + + +# Sklik.cz + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see [Sklik.czdocumentation](https://napoveda.sklik.cz/partner/reklamni-kod/). + +Supported parameters: + +- `width` +- `height` +- `json` + diff --git a/ads/slimcutmedia.js b/ads/slimcutmedia.js new file mode 100644 index 000000000000..93219ec7ba12 --- /dev/null +++ b/ads/slimcutmedia.js @@ -0,0 +1,35 @@ +/** + * 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 slimcutmedia(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + global._scm_amp = { + allowed_data: ['pid', 'ffc'], + mandatory_data: ['pid'], + data, + }; + + validateData(data, + global._scm_amp.mandatory_data, global._scm_amp.allowed_data); + + loadScript(global, 'https://static.freeskreen.com/publisher/' + encodeURIComponent(data.pid) + '/freeskreen.min.js'); +} diff --git a/ads/slimcutmedia.md b/ads/slimcutmedia.md new file mode 100644 index 000000000000..4aa3db04c44a --- /dev/null +++ b/ads/slimcutmedia.md @@ -0,0 +1,38 @@ + + +# SlimCut Media + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `data-pid` + +### Optional parameters +- `data-ffc` diff --git a/ads/smartadserver.js b/ads/smartadserver.js new file mode 100644 index 000000000000..03983799f199 --- /dev/null +++ b/ads/smartadserver.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 {loadScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function smartadserver(global, data) { + // 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 new file mode 100644 index 000000000000..8cfd48c1be53 --- /dev/null +++ b/ads/smartadserver.md @@ -0,0 +1,67 @@ + + +# SmartAdServer + + + +## Example + +### Basic call + +```html + + +``` + +### With targeting + +```html + + +``` + +## Configuration + +For ````, 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/). + +### Supported parameters + +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. 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..706964ba2c75 --- /dev/null +++ b/ads/smartclip.md @@ -0,0 +1,39 @@ + + +# 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/smi2.js b/ads/smi2.js new file mode 100644 index 000000000000..1f0cdef3a8e2 --- /dev/null +++ b/ads/smi2.js @@ -0,0 +1,47 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function smi2(global, data) { + validateData(data, ['blockid']); + (global._smi2 = global._smi2 || { + viewId: global.context.pageViewId, + blockId: data['blockid'], + htmlURL: data['canonical'] || global.context.canonicalUrl, + ampURL: data['ampurl'] || global.context.sourceUrl, + testMode: data['testmode'] || 'false', + referrer: data['referrer'] || global.context.referrer, + hostname: global.window.context.location.hostname, + clientId: window.context.clientId, + domFingerprint: window.context.domFingerprint, + location: window.context.location, + startTime: window.context.startTime, + + }); + global._smi2.AMPCallbacks = { + renderStart: global.context.renderStart, + noContentAvailable: global.context.noContentAvailable, + }; + // load the smi2 AMP JS file script asynchronously + const rand = Math.round(Math.random() * 100000000); + loadScript(global, 'https://amp.smi2.ru/ampclient/ampfecth.js?rand=' + rand, () => {}, + global.context.noContentAvailable); +} diff --git a/ads/smi2.md b/ads/smi2.md new file mode 100644 index 000000000000..51f8aef1f330 --- /dev/null +++ b/ads/smi2.md @@ -0,0 +1,37 @@ + + +# Smi2 + +SMI2 is a service for personalizing content network. Please visit our [smi2.net](https://smi2.net) for more information. + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `data-blockid` - insert your block_id +- `height` diff --git a/ads/sogouad.js b/ads/sogouad.js new file mode 100644 index 000000000000..f9d1058c0d4c --- /dev/null +++ b/ads/sogouad.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 {loadScript, validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function sogouad(global, data) { + validateData(data, ['slot', 'w', 'h'], ['responsive']); + const slot = global.document.getElementById('c'); + const ad = global.document.createElement('div'); + const sogouUn = 'sogou_un'; + global[sogouUn] = window[sogouUn] || []; + if (data.w === '100%') { + global[sogouUn].push({ + id: data.slot, + ele: ad, + }); + } else { + global[sogouUn].push({ + id: data.slot, + ele: ad, + w: data.w, + h: data.h, + }); + } + slot.appendChild(ad); + loadScript(global, 'https://theta.sogoucdn.com/wap/js/aw.js'); +} diff --git a/ads/sogouad.md b/ads/sogouad.md new file mode 100644 index 000000000000..29242da4f60b --- /dev/null +++ b/ads/sogouad.md @@ -0,0 +1,56 @@ + + +# Sogou + +## Examples + +```html + + + + + + + +``` + +## Configuration + + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +Responsive mode: + +- `data-slot`: slot id of Sogou ads +- `data-w`: always be 20 +- `data-h`: slot's height info from Sogou ads + +Fixed-height mode: + +- `data-slot`: slot id of Sogou ads +- `data-w`: always be 100% +- `data-h` slot's height info from Sogou ads diff --git a/ads/sortable.js b/ads/sortable.js new file mode 100644 index 000000000000..1a79b58e6586 --- /dev/null +++ b/ads/sortable.js @@ -0,0 +1,37 @@ +/** + * 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 sortable(global, data) { + validateData(data, ['site', 'name'], ['responsive']); + + const slot = global.document.getElementById('c'); + const ad = global.document.createElement('div'); + const size = (data.responsive === 'true') ? + 'auto' + : data.width + 'x' + data.height; + ad.className = 'ad-tag'; + ad.setAttribute('data-ad-name', data.name); + ad.setAttribute('data-ad-size', size); + slot.appendChild(ad); + loadScript(global, 'https://tags-cdn.deployads.com/a/' + + encodeURIComponent(data.site) + '.js'); +} diff --git a/ads/sortable.md b/ads/sortable.md new file mode 100644 index 000000000000..67955b805922 --- /dev/null +++ b/ads/sortable.md @@ -0,0 +1,59 @@ + + +# Sortable + +## Examples + +```html + + + + + + + + + +``` + +## Configuration + +No explicit configuration is needed for a given sortable amp-ad, though each site must be set up beforehand with [Sortable](http://sortable.com). The site name `ampproject.org` can be used for testing. Note that only the two examples above will show an ad properly. + +### Required parameters + +* `data-name`: The name of the ad unit. +* `data-site`: The site/domain this ad will be served on (effectively an account id) +* `width` + `height`: Required for all `` units. Specifies the ad size. +* `type`: Always set to "sortable" + +### Optional parameters + +* `data-reponsive`: When set to true indicates that the ad slot has multiple potential sizes. + + + diff --git a/ads/sovrn.js b/ads/sovrn.js new file mode 100644 index 000000000000..abc16baf74ce --- /dev/null +++ b/ads/sovrn.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. +*/ +/* + ********* + * Existing sovrn customers feel free to contact amp-implementations@sovrn.com + * for assistance with setting up your amp-ad tagid New customers please see + * www.sovrn.com to sign up and get started! + ********* + */ +import {writeScript} from '../3p/3p'; +/** + * @param {!Window} global + * @param {!Object} data + */ +export function sovrn(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + global.width = data.width; + global.height = data.height; + global.domain = data.domain; + global.u = data.u; + global.iid = data.iid; + global.aid = data.aid; + global.z = data.z; + global.tf = data.tf; + writeScript(global, 'https://ap.lijit.com/www/sovrn_amp/sovrn_ads.js'); +} diff --git a/ads/sovrn.md b/ads/sovrn.md new file mode 100644 index 000000000000..466657823eef --- /dev/null +++ b/ads/sovrn.md @@ -0,0 +1,55 @@ + + +# sovrn + +## Example + +```html + + +``` + +## Configuration + +For existing sovrn customers, feel free to contact amp-implementations@sovrn.com for assistance with setting up your amp-ad tagid. + +For new customers, please see www.sovrn.com to sign up and get started! + +Supported parameters: + +- `data-width` +- `data-height` +- `data-domain` +- `data-u` +- `data-iid` +- `data-aid` +- `data-tf: A Boolean value used only for testing. Either remove it or set it to false for real world production work. +- `data-z` diff --git a/ads/spotx.js b/ads/spotx.js new file mode 100644 index 000000000000..9b20acf9c111 --- /dev/null +++ b/ads/spotx.js @@ -0,0 +1,57 @@ +/** + * 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 {hasOwn} from '../src/utils/object'; +import {startsWith} from '../src/string'; +import {validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function spotx(global, data) { + // ensure we have valid channel id + validateData(data, ['spotx_channel_id', 'width', 'height']); + + // Because 3p's loadScript does not allow for data attributes, + // we will write the JS tag ourselves. + const script = global.document.createElement('script'); + + data['spotx_content_width'] = data.spotx_content_width || data.width; + data['spotx_content_height'] = data.spotx_content_height || data.height; + data['spotx_content_page_url'] = global.context.location.href || + global.context.sourceUrl; + + // Add data-* attribute for each data value passed in. + for (const key in data) { + if (hasOwn(data, key) && startsWith(key, 'spotx_')) { + script.setAttribute(`data-${key}`, data[key]); + } + } + + global['spotx_ad_done_function'] = function(spotxAdFound) { + if (!spotxAdFound) { + global.context.noContentAvailable(); + } + }; + + // TODO(KenneyE): Implement AdLoaded callback in script to accurately trigger + // renderStart() + script.onload = global.context.renderStart; + + script.src = `//js.spotx.tv/easi/v1/${data['spotx_channel_id']}.js`; + global.document.body.appendChild(script); +} diff --git a/ads/spotx.md b/ads/spotx.md new file mode 100644 index 000000000000..ad2e48edad2d --- /dev/null +++ b/ads/spotx.md @@ -0,0 +1,49 @@ + + +# SpotX + +## Example + +### Basic + +```html + + +``` + +### Using Custom Key-Value Pairs + +```html + + +``` + +## Configuration + +The SpotX `amp-ad` integration has many of the same capabilities and options as our SpotX EASI integration. For full list of options, please see the [SpotX EASI integration documentation](https://developer.spotxchange.com/content/local/docs/sdkDocs/EASI/README.md#common-javascript-attributes). + +### Required parameters + +- `data-spotx_channel_id` +- `width` +- `height` diff --git a/ads/sunmedia.js b/ads/sunmedia.js new file mode 100644 index 000000000000..dddfcec17b19 --- /dev/null +++ b/ads/sunmedia.js @@ -0,0 +1,35 @@ +/** + * 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 sunmedia(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + global._sunmedia_amp = { + allowed_data: ['cskp', 'crst', 'cdb', 'cid'], + mandatory_data: ['cid'], + data, + }; + + validateData(data, + global._sunmedia_amp.mandatory_data, global._sunmedia_amp.allowed_data); + + loadScript(global, 'https://vod.addevweb.com/sunmedia/amp/ads/SMIntextAMP.js'); +} diff --git a/ads/sunmedia.md b/ads/sunmedia.md new file mode 100644 index 000000000000..68b93c351bb3 --- /dev/null +++ b/ads/sunmedia.md @@ -0,0 +1,42 @@ + + +# SunMedia + +## Example + +```html + + +``` + +## Configuration + +For further information, please contact [SunMedia](http://sunmedia.tv/#contact). + +### Required parameters + +- `data-cid`: Client ID provided by SunMedia + +### Optional parameters + +- `data-cskp`: Indicates skip button enabled +- `data-crst`: Indicates restart option enabled diff --git a/ads/swoop.js b/ads/swoop.js new file mode 100644 index 000000000000..0d77ead40d5c --- /dev/null +++ b/ads/swoop.js @@ -0,0 +1,50 @@ +/** + * 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 { + computeInMasterFrame, + loadScript, + validateData, +} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +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..a4908f81335b --- /dev/null +++ b/ads/swoop.md @@ -0,0 +1,43 @@ + + +# Swoop + +## Example + +```html + +
+
+
+``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their 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 333a1829f485..2d25c4c72a75 100644 --- a/ads/taboola.js +++ b/ads/taboola.js @@ -1,5 +1,5 @@ /** - * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * Copyright 2018 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,7 +14,7 @@ * limitations under the License. */ -import {loadScript, validateDataExists, validateExactlyOne} from '../src/3p'; +import {loadScript, validateData} from '../3p/3p'; /** * @param {!Window} global @@ -23,19 +23,17 @@ import {loadScript, validateDataExists, validateExactlyOne} from '../src/3p'; export function taboola(global, data) { // do not copy the following attributes from the 'data' object // to _tablloa global object - const blackList = ['height', 'initialWindowHeight', 'initialWindowWidth', - 'type', 'width', 'placement', 'mode']; + const blackList = ['height', 'type', 'width', 'placement', 'mode']; // 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', 'other']]); // setup default values for referrer and url const params = { referrer: data.referrer || global.context.referrer, - url: data.url || global.context.canonicalUrl + url: data.url || global.context.canonicalUrl, }; // copy none blacklisted attribute to the 'params' map @@ -52,9 +50,11 @@ export function taboola(global, data) { placement: data.placement, mode: data.mode, framework: 'amp', - container: 'c' + container: 'c', }, - params]); + params, + {flush: true}] + ); // install observation on entering/leaving the view global.context.observeIntersection(function(changes) { @@ -63,7 +63,7 @@ export function taboola(global, data) { global._taboola.push({ visible: true, rects: c, - placement: data.placement + placement: data.placement, }); } }); diff --git a/ads/taboola.md b/ads/taboola.md index c325381b00d0..1d05890eb4cc 100644 --- a/ads/taboola.md +++ b/ads/taboola.md @@ -21,31 +21,31 @@ limitations under the License. ### Basic ```html - + ``` ## Configuration -For semantics of configuration, please see ad network documentation. +For details on the configuration semantics, please contact the ad network or refer to their documentation. Supported parameters: -- data-publisher -- data-placement -- data-mode -- data-article -- data-video -- data-photo -- data-home -- data-category -- data-others -- data-url -- data-referrer +- `data-publisher` +- `data-placement` +- `data-mode` +- `data-article` +- `data-video` +- `data-photo` +- `data-home` +- `data-category` +- `data-others` +- `data-url` +- `data-referrer` diff --git a/ads/teads.js b/ads/teads.js new file mode 100644 index 000000000000..7872d60cdb7c --- /dev/null +++ b/ads/teads.js @@ -0,0 +1,44 @@ +/** + * 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 teads(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + global._teads_amp = { + allowed_data: ['pid', 'tag'], + mandatory_data: ['pid'], + mandatory_tag_data: ['tta', 'ttp'], + data, + }; + + validateData(data, + global._teads_amp.mandatory_data, global._teads_amp.allowed_data); + + if (data.tag) { + validateData(data.tag, global._teads_amp.mandatory_tag_data); + global._tta = data.tag.tta; + global._ttp = data.tag.ttp; + + loadScript(global, 'https://a.teads.tv/media/format/' + encodeURI(data.tag.js || 'v3/teads-format.min.js')); + } else { + loadScript(global, 'https://a.teads.tv/page/' + encodeURIComponent(data.pid) + '/tag'); + } +} diff --git a/ads/teads.md b/ads/teads.md new file mode 100644 index 000000000000..e96d2ce12827 --- /dev/null +++ b/ads/teads.md @@ -0,0 +1,44 @@ + + +# Teads + +## Example + +```html + + +``` + +## Configuration + +For configuration semantics, please contact [Teads](http://teads.tv/fr/contact/). + +Supported parameters: + +- `data-pid` + +## User Consent Integration + +When [user consent](https://github.com/ampproject/amphtml/blob/master/extensions/amp-consent/amp-consent.md#blocking-behaviors) is required, Teads ad approaches user consent in the following ways: + +- `CONSENT_POLICY_STATE.SUFFICIENT`: Serve a personalized ad to the user. +- `CONSENT_POLICY_STATE.INSUFFICIENT`: Serve a non-personalized ad to the user. +- `CONSENT_POLICY_STATE.UNKNOWN_NOT_REQUIRED`: Serve a personalized ad to the user. +- `CONSENT_POLICY_STATE.UNKNOWN`: Serve a non-personalized ad to the user.. diff --git a/ads/triplelift.js b/ads/triplelift.js new file mode 100644 index 000000000000..cddd6af052a1 --- /dev/null +++ b/ads/triplelift.js @@ -0,0 +1,27 @@ +/** + * 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, validateSrcPrefix} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function triplelift(global, data) { + const {src} = data; + validateSrcPrefix('https://ib.3lift.com/', src); + loadScript(global, src); +} diff --git a/ads/triplelift.md b/ads/triplelift.md new file mode 100644 index 000000000000..61aed02e8eef --- /dev/null +++ b/ads/triplelift.md @@ -0,0 +1,37 @@ + + +# TripleLift + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For configuration semantics, please [contact TripleLift](http://triplelift.com). + +Supported parameters: + +- `src` diff --git a/ads/trugaze.js b/ads/trugaze.js new file mode 100644 index 000000000000..c2d7683e18e7 --- /dev/null +++ b/ads/trugaze.js @@ -0,0 +1,26 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + */ +export function trugaze(global) { + // For simplicity and flexibility, all validations are performed in the + // Trugaze's URL based on the data received + loadScript(global, 'https://cdn.trugaze.io/amp-init-v1.js'); +} diff --git a/ads/trugaze.md b/ads/trugaze.md new file mode 100644 index 000000000000..8f4f26d1006d --- /dev/null +++ b/ads/trugaze.md @@ -0,0 +1,69 @@ + + +# Trugaze + + +## Example + +### Basic sample + +```html + + +``` + +### Sample with multisize + +```html + + +``` + +### Sample with targeting + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +## Supported parameters + +| Parameter name | Description | Required | +|-----------------|-------------------------------------|----------| +| width | Primary size width | Yes | +| height | Primary size height | Yes | +| data-public-id | Application public id | Yes | +| data-slot | Ad unit code | Yes | +| data-multi-size | Comma separated list of other sizes | No | +| json | Custom targeting map | No | + +Note: if any of the required parameters is not present, the ad slot will not be filled. diff --git a/ads/uas.js b/ads/uas.js new file mode 100644 index 000000000000..da87aab2379e --- /dev/null +++ b/ads/uas.js @@ -0,0 +1,90 @@ +/** + * Copyright 2018 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 {hasOwn} from '../src/utils/object'; +import {loadScript, validateData} from '../3p/3p'; +import {setStyles} from '../src/style'; + +/** + * @param {!Object} theObject + * @param {!Function} callback + */ +function forEachOnObject(theObject, callback) { + if (typeof theObject === 'object' && theObject !== null) { + if (typeof callback === 'function') { + for (const key in theObject) { + if (hasOwn(theObject, key)) { + callback(key, theObject[key]); + } + } + } + } +} + +/** + * @param {!Window} global + */ +function centerAd(global) { + const e = global.document.getElementById('c'); + if (e) { + setStyles(e, { + top: '50%', + left: '50%', + bottom: '', + right: '', + transform: 'translate(-50%, -50%)', + }); + } +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function uas(global, data) { + validateData( + data, + ['accId', 'adUnit', 'sizes'], + ['locLat', 'locLon', 'locSrc', 'pageURL', 'targetings', 'extraParams', + 'visibility'] + ); + global.Phoenix = {EQ: []}; + const uasDivId = 'uas-amp-slot'; + global.document.write('
'); + loadScript(global, 'https://ads.pubmatic.com/AdServer/js/phoenix.js', () => { + global.Phoenix.EQ.push(function() { + global.Phoenix.enableSingleRequestCallMode(); + global.Phoenix.setInfo('AMP', 1);// Need to set the AMP flag + global.Phoenix.setInfo('ACCID', data.accId); + global.Phoenix.setInfo('PAGEURL', global.context.location.href); + data.pageURL && global.Phoenix.setInfo('PAGEURL', data.pageURL); + data.locLat && global.Phoenix.setInfo('LAT', data.locLat); + data.locLon && global.Phoenix.setInfo('LON', data.locLon); + data.locSrc && global.Phoenix.setInfo('LOC_SRC', data.locSrc); + const slot = global.Phoenix.defineAdSlot(data.adUnit, data.sizes, + uasDivId); + slot.setVisibility(1); + forEachOnObject(data.targetings, function(key, value) { + slot.setTargeting(key, value); + }); + forEachOnObject(data.extraParams, function(key, value) { + slot.setExtraParameters(key, value); + }); + global.Phoenix.display(uasDivId); + }); + }); + centerAd(global); +} diff --git a/ads/uas.md b/ads/uas.md new file mode 100644 index 000000000000..ed2b1c0e3743 --- /dev/null +++ b/ads/uas.md @@ -0,0 +1,81 @@ + + +# UAS + +## Examples + +### Basic + +```html + + +``` + +### Multi-size Ad + +```html + + +``` +Note that the `width` and `height` mentioned should be maximum of the width-hight combinations mentioned in `json.sizes`. + +### Targetings + +```html + + +``` + +### Sample tag + +```html + + +``` +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Supported parameters via `json` attribute + +- `accId` Account Id (mandatory) +- `adUnit` AdUnitId (mandatory) +- `sizes` Array of sizes (mandatory) +- `locLat` Geo-location latitude +- `locLon` Geo-location longitude +- `locSrc` Geo-location source +- `pageURL` Set custom page URL +- `targetings` key-value pairs +- `extraParams` key-value pairs to be passed to PubMatic SSP + + +### Unsupported Ad Formats +- Interstitials +- Expandables. Work is in progress +- Flash +- Creatives served over HTTP + + + diff --git a/ads/unruly.js b/ads/unruly.js new file mode 100644 index 000000000000..4419cf14268b --- /dev/null +++ b/ads/unruly.js @@ -0,0 +1,34 @@ +/* + Copyright 2018 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 + * @param {!Function} [scriptLoader=loadScript] + */ +export function unruly(global, data, scriptLoader = loadScript) { + validateData(data, ['siteId']); + + global.unruly = global.unruly || {}; + global.unruly.native = { + siteId: data.siteId, + }; + + scriptLoader(global, 'https://video.unrulymedia.com/native/native-loader.js'); +} diff --git a/ads/unruly.md b/ads/unruly.md new file mode 100644 index 000000000000..33e905461a0a --- /dev/null +++ b/ads/unruly.md @@ -0,0 +1,31 @@ + + +# Unruly + +## Example + +```html + + +``` + +### Required Ad Parameters + +- `data-site-id` - unique publisher identifier, supplied by Unruly. + diff --git a/ads/uzou.js b/ads/uzou.js new file mode 100644 index 000000000000..27c603a68815 --- /dev/null +++ b/ads/uzou.js @@ -0,0 +1,64 @@ +/** + * 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'; +import {parseJson} from '../src/json'; + +/** +* @param {!Window} global +* @param {!Object} data +*/ +export function uzou(global, data) { + validateData(data, ['widgetParams'], []); + + const akamaiHost = 'speee-ad.akamaized.net'; + const prefixMap = { + test: 'dev-', + development: 'dev-', + staging: 'staging-', + production: '', + }; + + const widgetParams = parseJson(data['widgetParams']); + const placementCode = widgetParams['placementCode']; + const mode = widgetParams['mode'] || 'production'; + const entryPoint = `https://${prefixMap[mode]}${akamaiHost}/tag/${placementCode}/js/outer-frame.min.js`; + + const d = global.document.createElement('div'); + d.className = `uz-${placementCode} uz-ny`; + + const container = global.document.getElementById('c'); + container.appendChild(d); + + const uzouInjector = { + url: ( + widgetParams['url'] || + global.context.canonicalUrl || + global.context.sourceUrl + ), + referer: widgetParams['referer'] || global.context.referrer, + }; + ['adServerHost', 'akamaiHost', 'iframeSrcPath'].forEach(function(elem) { + if (widgetParams[elem]) { + uzouInjector[elem] = widgetParams[elem]; + } + }); + global.UzouInjector = uzouInjector; + + loadScript(global, entryPoint, () => { + global.context.renderStart(); + }); +} diff --git a/ads/uzou.md b/ads/uzou.md new file mode 100644 index 000000000000..9296ee948851 --- /dev/null +++ b/ads/uzou.md @@ -0,0 +1,40 @@ + + +# UZOU + +For initial installation, let us know from [this form](https://docs.google.com/forms/d/e/1FAIpQLSdq18-oOnVZNuJG2pAAzMyjyfCVU69RryUJWwjwMbYLkOY4Zg/viewform). + +## Example + + +```html + + +``` + +Uzou widget code must be published by [our administration screen](https://uzou.speee-ad.jp/). Please do not directly install the above code. + +## Parameters + +For the widget design or configuration details, please contact your account manager. + +Supported parameters: + +- widget-params + - JSON string including your placement code. diff --git a/ads/valuecommerce.js b/ads/valuecommerce.js new file mode 100644 index 000000000000..e773acd81b15 --- /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 {validateData, writeScript} 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..918960e6e6a5 --- /dev/null +++ b/ads/valuecommerce.md @@ -0,0 +1,46 @@ + + +# 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/videointelligence.js b/ads/videointelligence.js new file mode 100644 index 000000000000..a5a097590c54 --- /dev/null +++ b/ads/videointelligence.js @@ -0,0 +1,28 @@ +/** + * Copyright 2018 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 videointelligence(global, data) { + + validateData(data, ['publisherId', 'channelId']); + + loadScript(global, 'https://s.vi-serve.com/tagLoaderAmp.js'); +} diff --git a/ads/videointelligence.md b/ads/videointelligence.md new file mode 100644 index 000000000000..ef1559c9035c --- /dev/null +++ b/ads/videointelligence.md @@ -0,0 +1,43 @@ + + +# video intelligence + +## Example + +```html + + +``` + +## Configuration + +For configuration information, please check [docs.vi.ai/](https://docs.vi.ai/general/integrations/). + +### Required parameters + +* `data-publisher-id` +* `data-channel-id` + +### Optional parameters + +* `data-placement-id` +* `data-iab-category` +* `data-language` \ No newline at end of file diff --git a/ads/videonow.js b/ads/videonow.js new file mode 100644 index 000000000000..136e5e50b1c5 --- /dev/null +++ b/ads/videonow.js @@ -0,0 +1,47 @@ +/** + * 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 videonow(global, data) { + const mandatoryAttributes = ['pid', 'width', 'height']; + const optionalAttributes = ['kind', 'src']; + + validateData(data, mandatoryAttributes, optionalAttributes); + + const profileId = data.pid || 1; + const kind = data.type || 'prod'; + + // production version by default + let script = (data.src && decodeURI(data.src)) || + ('https://static.videonow.ru/dev/vn_init_module.js?profileId=' + profileId); + + if (kind === 'local') { + script = 'https://localhost:8085/vn_init.js?profileId=' + profileId + + '&url=' + encodeURIComponent('https://localhost:8085/init'); + } else if (kind === 'dev') { + // this part can be changed late + script = 'https://static.videonow.ru/dev/vn_init_module.js?profileId=' + + profileId + + '&url=' + encodeURIComponent('https://data.videonow.ru/?init'); + } + + loadScript(global, script); +} diff --git a/ads/videonow.md b/ads/videonow.md new file mode 100644 index 000000000000..9746b0d5b991 --- /dev/null +++ b/ads/videonow.md @@ -0,0 +1,35 @@ + + +# VideoNow + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please contact [Videonow](http://videonow.ru/html/advertisers/). + +Supported parameters: + +- `data-pid` diff --git a/ads/viralize.js b/ads/viralize.js new file mode 100644 index 000000000000..8e82336ef100 --- /dev/null +++ b/ads/viralize.js @@ -0,0 +1,50 @@ +/** + * 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 {addParamsToUrl} from '../src/url'; +import {loadScript, validateData} from '../3p/3p'; +import {parseJson} from '../src/json'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function viralize(global, data) { + const endpoint = 'https://ads.viralize.tv/display/'; + const required = ['zid']; + const optional = ['extra']; + + validateData(data, required, optional); + + const defaultLocation = 'sel-#c>script'; + const pubPlatform = 'amp'; + + const queryParams = parseJson(data.extra || '{}'); + queryParams['zid'] = data.zid; + queryParams['pub_platform'] = pubPlatform; + if (!queryParams['location']) { + queryParams['location'] = defaultLocation; + } + if (!queryParams['u']) { + queryParams['u'] = global.context.sourceUrl; + } + + const scriptUrl = addParamsToUrl(endpoint, queryParams); + + loadScript(global, scriptUrl, + () => global.context.renderStart(), + () => global.context.noContentAvailable()); +} diff --git a/ads/viralize.md b/ads/viralize.md new file mode 100644 index 000000000000..11d28650fc8b --- /dev/null +++ b/ads/viralize.md @@ -0,0 +1,40 @@ + + +# Viralize + +## Example + +```html + + +``` + +## Configuration + +For further configuration details, please contact [Viralize](https://viralize.com/contact-us/). + + +## Required parameters + +- `data-zid`: Id of the unit. + +## Optional parameters + +- `data-extra`: JSON object representing any other query parameter that could be passed to the unit. diff --git a/ads/vmfive.js b/ads/vmfive.js new file mode 100644 index 000000000000..463d8df78e2a --- /dev/null +++ b/ads/vmfive.js @@ -0,0 +1,72 @@ +/** + * 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 {validateData} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function vmfive(global, data) { + /*eslint "google-camelcase/google-camelcase": 0*/ + const mandatory_fields = ['appKey', 'placementId', 'adType']; + const optional_fields = []; + + const {appKey, placementId, adType} = data; + + global._vmfive_amp = {appKey, placementId, adType}; + + validateData(data, mandatory_fields, optional_fields); + + createAdUnit(global, placementId, adType); + setupSDKReadyCallback(global, appKey); + parallelDownloadScriptsAndExecuteInOrder(global); +} + +/** + * @param {!Window} win + */ +function parallelDownloadScriptsAndExecuteInOrder(win) { + [ + 'https://vawpro.vm5apis.com/man.js', + 'https://man.vm5apis.com/dist/adn-web-sdk.js', + ].forEach(function(src) { + const script = document.createElement('script'); + script.src = src; + script.async = false; + win.document.head.appendChild(script); + }); +} + +/** + * @param {!Window} win + * @param {string} placementId + * @param {string} adType + */ +function createAdUnit(win, placementId, adType) { + const el = document.createElement('vmfive-ad-unit'); + el.setAttribute('placement-id', placementId); + el.setAttribute('ad-type', adType); + win.document.getElementById('c').appendChild(el); +} + +/** + * @param {!Window} win + * @param {string} appKey + */ +function setupSDKReadyCallback(win, appKey) { + win.onVM5AdSDKReady = sdk => sdk.init({appKey}); +} diff --git a/ads/vmfive.md b/ads/vmfive.md new file mode 100644 index 000000000000..c5d0ed942d40 --- /dev/null +++ b/ads/vmfive.md @@ -0,0 +1,63 @@ + + +# VMFIve + +## Example + +### Native ad + +```html + + +``` + +### Top ad + +```html + + +``` + +### Interstitial Embedded ad + +```html + + +``` + + +## Configuration + +For configuration details and to generate your tags, please contact http://vmfive.com/index.html#contact + +Supported parameters: + +- `data-app-key` +- `data-placement-id` +- `data-ad-type` diff --git a/ads/webediads.js b/ads/webediads.js new file mode 100644 index 000000000000..c16845ca3f7c --- /dev/null +++ b/ads/webediads.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 {loadScript, validateData} from '../3p/3p'; + +/** + * Created by Webedia on 07/03/16 - last updated on 11/10/16 + * @param {!Window} global + * @param {!Object} data + */ +export function webediads(global, data) { + validateData(data, ['site', 'page', 'position'], ['query']); + loadScript(global, 'https://eu1.wbdds.com/amp.min.js', () => { + global.wads.amp.init({ + 'site': data.site, + 'page': data.page, + 'position': data.position, + 'query': (data.query) ? data.query : '', + }); + }); +} diff --git a/ads/webediads.md b/ads/webediads.md new file mode 100644 index 000000000000..ffb612af187d --- /dev/null +++ b/ads/webediads.md @@ -0,0 +1,79 @@ + + +# Webedia Adserver + +Private ad system deployed for all Webedia websites. + +## One call method + +This method allow you to call one ad position with a specific configuration. + +### Basic example + +```html + + +``` + +### Query example + +```html + + +``` + +### Placeholder and fallback example + +```html + +
Loading...
+
No ad
+
+``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Supported parameters + +All parameters are mandatory, only "query" can be empty. + +- `data-site` (String, non-empty) +- `data-page` (String, non-empty) +- `data-position` (String, non-empty) +- `data-query` (String) + - `key` are separated with `&` + - `value` are separted with `|` + - **Example**: `key1=value1|value2|value3&key2=value4&key3=value5|value6` + + diff --git a/ads/weborama.js b/ads/weborama.js new file mode 100644 index 000000000000..eecf96612b88 --- /dev/null +++ b/ads/weborama.js @@ -0,0 +1,87 @@ +/** + * 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, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function weboramaDisplay(global, data) { + const mandatoryFields = [ + 'width', + 'height', + 'wbo_account_id', + 'wbo_tracking_element_id', + 'wbo_fullhost', + ]; + + const optionalFields = [ + 'wbo_bid_price', + 'wbo_price_paid', + 'wbo_random', + 'wbo_debug', + 'wbo_host', + 'wbo_publisherclick', + // 'wbo_clicktrackers', + // 'wbo_imptrackers', + // 'wbo_zindex', + 'wbo_customparameter', + 'wbo_disable_unload_event', + 'wbo_donottrack', + 'wbo_script_variant', + 'wbo_is_mobile', + 'wbo_vars', + 'wbo_weak_encoding', + ]; + + validateData(data, mandatoryFields, optionalFields); + + /*eslint "google-camelcase/google-camelcase": 0*/ + global.weborama_display_tag = { + // Default settings to work with AMP + forcesecure: true, + bursttarget: 'self', + burst: 'never', + + // Settings taken from component config + width: data.width, + height: data.height, + account_id: data.wbo_account_id, + customparameter: data.wbo_customparameter, + tracking_element_id: data.wbo_tracking_element_id, + host: data.wbo_host, + fullhost: data.wbo_fullhost, + bid_price: data.wbo_bid_price, + price_paid: data.wbo_price_paid, + random: data.wbo_random, + debug: data.wbo_debug, + publisherclick: data.wbo_publisherclick, + // clicktrackers: data.wbo_clicktrackers, + // imptrackers: data.wbo_imptrackers, + // This is actually quite useless for now, since we are launced in a + // non-friendly iframe. + // zindex: data.wbo_zindex, + disable_unload_event: data.wbo_disable_unload_event, + donottrack: data.wbo_donottrack, + script_variant: data.wbo_script_variant, + is_mobile: data.wbo_is_mobile, + vars: data.wbo_vars, + weak_encoding: data.wbo_weak_encoding, + }; + + writeScript(global, 'https://cstatic.weborama.fr/js/advertiserv2/adperf_launch_1.0.0_scrambled.js'); +} diff --git a/ads/weborama.md b/ads/weborama.md new file mode 100644 index 000000000000..f1be1c4d34d7 --- /dev/null +++ b/ads/weborama.md @@ -0,0 +1,102 @@ + + +# Weborama + +## Example + +**Display tag:** + +See below for an example of usage or our display tag, adapted for use with AMP websites: + +```html + +
Loading ad.
+
Ad could not be loaded.
+
+``` + +**Conversion tag:** + +In order to add conversion trackers to your page, please use the AMP pixel component that will be supplied to you by your Weborama contact. +The values mentioned between brackets `[]` should be replaced by the proper values. + +`DOCUMENT_REFERRER`, `SOURCE_URL` and `RANDOM` should remain in the URL, as AMP takes care of the automatic substitution of these values. + +```html + +``` + + +## Configuration + +**Placeholder and fallback:** + +The placeholder and fallback `div` elements are completely optional and can be left out. + +- The placeholder is shown while the ad is loading. +- The fallback is served when there is no ad to show for some reason. + +**Dimensions:** + +By default the ad size is based on the `width` and `height` attributes of the `amp-ad` tag. These are listed as mandatory parameters. + +The AMP ad component requires the following HTML attributes to be added before the ad will be parsed as a Weborama ad: + +- width +- height +- type="weborama-display" + +**Mandatory data parameters:** + +Without valid values for these parameters we won't be able to display an ad: + +- `data-wbo_account_id` +- `data-wbo_tracking_element_id` +- `data-wbo_fullhost` + +**Examples of optional parameters:** + +Here are some extra parameters that might be set on the AMP ad: + +- `data-wbo_random` - Used as session identifier by some 3rd party ad systems +- `data-wbo_publisherclick` - Adds a publisher redirect for the exit click. +- `data-wbo_vars` - Sends variables to the creative. e.g.: `color=green&weather=rainy` +- `data-wbo_debug` - Launch the ad in debug mode. +- ... ask your contact at Weborama for more details. + +## Current restrictions + +- AMP ads are launched in a cross-origin iframe, so there currently is no support for some rich media, amongst which: + - Expandables + - Floorads, Interstitials and other types of layers + - Flash ads + - Creatives served over HTTP. +- Click and impression trackers can't be added to the tag. They need to be added through Weborama's ad platform: WCM. +- At the moment we don't support dynamic resizing of the iframe. Support for this will be added on request. + +## Support + +If you have any questions, please refer to your contact at Weborama. Otherwise you can contact our TIER 2 support: + +- E: tier2support@weborama.nl +- T: +31 (0) 20 52 46 690 diff --git a/ads/widespace.js b/ads/widespace.js new file mode 100644 index 000000000000..b0774d199051 --- /dev/null +++ b/ads/widespace.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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function widespace(global, data) { + + const WS_AMP_CODE_VER = '1.0.1'; + // Optional demography parameters. + let demo = []; + + demo = ['Gender', 'Country', 'Region', 'City', 'Postal', 'Yob'].map(d => { + return 'demo' + d; + }); + + validateData(data, ['sid'], demo); + + const url = 'https://engine.widespace.com/map/engine/dynamic?isamp=1' + + '&ver=' + WS_AMP_CODE_VER + + '&#sid=' + encodeURIComponent(data.sid); + + writeScript(global, url); +} diff --git a/ads/widespace.md b/ads/widespace.md new file mode 100644 index 000000000000..87ff57d41449 --- /dev/null +++ b/ads/widespace.md @@ -0,0 +1,62 @@ + + +# Widespace + + +## Examples + +### Basic + +```html + + +``` + +### More parameters + +```html + + +``` + +## Configuration + +For configuration details, please contact integrations@widespace.com. + +### Required parameters + +- `data-sid` + +### Optional demography parameters + +- `data-demo-Gender` +- `data-demo-Country` +- `data-demo-Region` +- `data-demo-City` +- `data-demo-Yob` + + + diff --git a/ads/wisteria.js b/ads/wisteria.js new file mode 100644 index 000000000000..44046c38ce6c --- /dev/null +++ b/ads/wisteria.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 { + loadScript, + validateData, +} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function wisteria(global, data) { + const d = global.document.createElement('div'); + d.id = '_wisteria_recommend_contents'; + global.document.getElementById('c').appendChild(d); + //get canonical url + const originalUrl = global.context.canonicalUrl; + validateData(data, ['siteId', 'templateNumber']); + loadScript(global, 'https://wisteria-js.excite.co.jp/wisteria.js?site_id=' + data['siteId'] + '&template_no=' + data['templateNumber'] + '&original_url=' + originalUrl); +} diff --git a/ads/wisteria.md b/ads/wisteria.md new file mode 100644 index 000000000000..c304fdb8689a --- /dev/null +++ b/ads/wisteria.md @@ -0,0 +1,42 @@ + + + +# Wisteria + +## Example + +```html + + +``` + +### Configuration + +For configuration details, please contact https://www.excite.co.jp/guide/wisteria/ + +Supported parameters: + +- data-site-id (required) +- data-template-number (required) +- height (optional) +- width (optional) diff --git a/ads/wpmedia.js b/ads/wpmedia.js new file mode 100644 index 000000000000..03a78cf0e17d --- /dev/null +++ b/ads/wpmedia.js @@ -0,0 +1,32 @@ +/** + * 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 {validateData, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function wpmedia(global, data) { + validateData(data, ['slot','bunch'], ['sn','slots']); + + // const url = 'http://localhost/wpjslib.js'; + const url = 'https://std.wpcdn.pl/wpjslib/wpjslib-amp.js'; + + writeScript(global, url, function() { + window.run(data); + }); +} diff --git a/ads/wpmedia.md b/ads/wpmedia.md new file mode 100644 index 000000000000..3f901201003b --- /dev/null +++ b/ads/wpmedia.md @@ -0,0 +1,57 @@ + + +# WP Media (Wirtualna Polska) + + +## Examples + +```html + + +``` + +```html + + +``` + + +### Required parameter + +- `data-slot` +- `data-bunch` + +### Optional parameters + +- `data-slots` +- `data-sn` + + + +## Configuration + +For configuration details please contact cbfd@grupawp.pl 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..9c5bd639eb7e --- /dev/null +++ b/ads/xlift.md @@ -0,0 +1,34 @@ + + +# Xlift + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact https://www.x-lift.jp/#contact. + +#### Required parameters + +- `data-mediaid`: For loading JavaScript for each media. 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..108064a02c44 --- /dev/null +++ b/ads/yahoo.md @@ -0,0 +1,42 @@ + + +# yahoo + +## Display ad + +```html + + +``` + +### Configuration + +For configuration details, please contact https://advertising.yahoo.com/contact. + +Supported parameters: + +- `height` +- `width` +- `data-sid` +- `data-site` +- `data-sa` diff --git a/ads/yahoojp.js b/ads/yahoojp.js new file mode 100644 index 000000000000..22d79ca7f9ef --- /dev/null +++ b/ads/yahoojp.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 yahoojp(global, data) { + validateData(data, ['yadsid'], []); + global.yahoojpParam = data; + writeScript(global, + 'https://s.yimg.jp/images/listing/tool/yads/ydn/amp/amp.js'); +} diff --git a/ads/yahoojp.md b/ads/yahoojp.md new file mode 100644 index 000000000000..3ee45912326a --- /dev/null +++ b/ads/yahoojp.md @@ -0,0 +1,36 @@ + + +# Yahoo JP + +## Example + +```html + + +``` + +## Configuration + +For configuration details and to generate your tags, please contact http://marketing.yahoo.co.jp/contact/. + + + +Supported parameters: + +- `data-yadsid` diff --git a/ads/yandex.js b/ads/yandex.js new file mode 100644 index 000000000000..19bc98ad6f6b --- /dev/null +++ b/ads/yandex.js @@ -0,0 +1,79 @@ +/** + * 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} from '../3p/3p'; + +const n = 'yandexContextAsyncCallbacks'; +const renderTo = 'yandex_rtb'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function yandex(global, data) { + validateData( + data, + ['blockId'], + ['data', 'onRender', 'onError'] + ); + + addToQueue(global, data); + loadScript(global, + 'https://yastatic.net/partner-code/loaders/context_amp.js'); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function addToQueue(global, data) { + global[n] = global[n] || []; + global[n].push(() => { + + // Create container + createContainer(global, renderTo); + + // Show Ad in container + global.Ya.Context.AdvManager.render({ + blockId: data.blockId, + statId: data.statId, + renderTo, + data: data.data, + async: true, + onRender: () => { + if (typeof data.onRender === 'function') { + data.onRender(); + } + global.context.renderStart(); + }, + }, () => { + if (typeof data.onError === 'function') { + data.onError(); + } else { + global.context.noContentAvailable(); + } + }); + }); +} + +/** + * @param {!Window} global + * @param {string} id + */ +function createContainer(global, id) { + const d = global.document.createElement('div'); + d.id = id; + global.document.getElementById('c').appendChild(d); +} diff --git a/ads/yandex.md b/ads/yandex.md new file mode 100644 index 000000000000..ea01a65a7643 --- /dev/null +++ b/ads/yandex.md @@ -0,0 +1,40 @@ + + +# Yandex + +## Example + +```html + + +``` + +## Configuration + +For semantics of configuration, please see [Yandex's documentation](https://yandex.ru/support/direct/index.html). + +### Required parameters + +- `data-block-id` + +### Optional parameters +- `data-data` +- `data-html-access-allowed` +- `data-on-render` +- `data-on-error` diff --git a/ads/yengo.js b/ads/yengo.js new file mode 100644 index 000000000000..7c8cbf0198d9 --- /dev/null +++ b/ads/yengo.js @@ -0,0 +1,35 @@ +/** + * 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} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function yengo(global, data) { + validateData(data, ['blockId']); + + const url = 'https://code.yengo.com/data/' + + encodeURIComponent(data['blockId']) + '.js?async=1&div=c'; + + loadScript(global, url, () => { + global.context.renderStart(); + }, () => { + global.context.noContentAvailable(); + }); + +} diff --git a/ads/yengo.md b/ads/yengo.md new file mode 100644 index 000000000000..415352079548 --- /dev/null +++ b/ads/yengo.md @@ -0,0 +1,34 @@ + + +# Yengo + +## Example + +```html + + +``` + +## Configuration + +For more information, please [see Yengo's FAQ](http://www.yengo.com/text/faqs?publishers). + +### Required parameters + +- `data-block-id` diff --git a/ads/yieldbot.js b/ads/yieldbot.js new file mode 100644 index 000000000000..b0a2d456ae1d --- /dev/null +++ b/ads/yieldbot.js @@ -0,0 +1,82 @@ +/** + * 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 {getMultiSizeDimensions} from '../ads/google/utils'; +import {loadScript, validateData} from '../3p/3p'; +import {rethrowAsync, user} from '../src/log'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function yieldbot(global, data) { + validateData(data, ['psn', 'ybSlot', 'slot']); + + global.ybotq = global.ybotq || []; + + loadScript(global, 'https://cdn.yldbt.com/js/yieldbot.intent.amp.js', () => { + global.ybotq.push(() => { + try { + const multiSizeDataStr = data.multiSize || null; + const primaryWidth = parseInt(data.overrideWidth || data.width, 10); + const primaryHeight = parseInt(data.overrideHeight || data.height, 10); + let dimensions; + + if (multiSizeDataStr) { + dimensions = getMultiSizeDimensions(multiSizeDataStr, + primaryWidth, + primaryHeight, + false); + dimensions.unshift([primaryWidth, primaryHeight]); + } else { + dimensions = [[primaryWidth, primaryHeight]]; + } + + global.yieldbot.psn(data.psn); + global.yieldbot.enableAsync(); + if (window.context.isMaster) { + global.yieldbot.defineSlot(data.ybSlot, {sizes: dimensions}); + global.yieldbot.go(); + } else { + const slots = {}; + slots[data.ybSlot] = dimensions; + global.yieldbot.nextPageview(slots); + } + } catch (e) { + rethrowAsync(e); + } + }); + + global.ybotq.push(() => { + try { + const targeting = global.yieldbot.getSlotCriteria(data['ybSlot']); + data['targeting'] = data['targeting'] || {}; + for (const key in targeting) { + data.targeting[key] = targeting[key]; + } + } catch (e) { + rethrowAsync(e); + } + user().warn('AMP-AD', 'type="yieldbot" will no longer ' + + 'be supported starting on March 29, 2018.' + + ' Please use your amp-ad-network and RTC to configure a' + + ' Yieldbot callout vendor. Refer to' + + ' https://github.com/ampproject/amphtml/blob/master/' + + 'extensions/amp-a4a/rtc-publisher-implementation-guide.md' + + '#setting-up-rtc-config for more information.'); + }); + }); +} diff --git a/ads/yieldbot.md b/ads/yieldbot.md new file mode 100644 index 000000000000..ef6603a208d5 --- /dev/null +++ b/ads/yieldbot.md @@ -0,0 +1,68 @@ + + +# Yieldbot +Yieldbot can be configured as a demand source by using the Real Time Config (RTC) callout vendor specification. To be a demand source, Yieldbot is configured as an `rtc-config` vendor within an `amp-ad` network tag configuration. Specific Yieldbot publisher identifier and slot name configuration is made using callout vendor substitution macros listed in the table below. + +## Yieldbot Vendor Callout Macros + +| Parameter | Description | +|:------------- |:-------------| +| **`YB_PSN`** | Yieldbot publisher site number | +| **`YB_SLOT`** | Yieldbot slot identifier | + +For further information on RTC please see the [RTC Publisher Implementation Guide](https://github.com/ampproject/amphtml/blob/master/extensions/amp-a4a/rtc-publisher-implementation-guide.md) and see the example Doubleclick configuration below. + +## Doubleclick RTC Configuration + +To specify a Doubleclick `amp-ad` integration with Yieldbot, include the vendor `"yieldbot"` in your `rtc-config` tag attributed as shown in the example below. This particular example shows that the Yieldbot demand configuration for the respective Doubleclick slot, `/2476204/medium-rectangle` where: + + - `"YB_PSN": "1234"`, Yieldbot publisher number + - `"YB_SLOT": "medrec"`, Yieldbot slot name + + +```html + + +``` + +### Yieldbot Integration Testing + +For integration testing, the Yieldbot Platform can be set to always return a bid for requested slots. + +- **Enable** integration testing mode: + - http://i.yldbt.com/integration/start +- **Disable** integration testing mode: + - http://i.yldbt.com/integration/stop + +When Yieldbot testing mode is enabled, a cookie (`__ybot_test`) on the domain `.yldbt.com` tells the Yieldbot ad server to always return a bid and when creative is requested, return a static integration testing creative. + +***Note:*** +- No ad serving metrics are impacted when integration testing mode is enabled. +- The `__ybot_test` cookie expires in 24 hours. +- It is good practice to action the "Stop testing" Url when testing is complete to return to normal ad delivery. diff --git a/ads/yieldmo.js b/ads/yieldmo.js new file mode 100644 index 000000000000..da44aa770e9d --- /dev/null +++ b/ads/yieldmo.js @@ -0,0 +1,35 @@ +/** + * 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'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function yieldmo(global, data) { + + const ymElem = global.document.createElement('div'); + ymElem.id = 'ym_' + data.ymid; + ymElem.className = 'ym'; + ymElem.dataset['ampEnabled'] = true; + global.document.getElementById('c').appendChild(ymElem); + + const swimLane = Math.round(5 * Math.random() / 3); + const ymJs = 'https://static.yieldmo.com/ym.' + swimLane + '.js'; + + loadScript(global, ymJs); +} diff --git a/ads/yieldmo.md b/ads/yieldmo.md new file mode 100644 index 000000000000..5d021ae2413d --- /dev/null +++ b/ads/yieldmo.md @@ -0,0 +1,54 @@ + + +# Yieldmo + +## Example + +```html + + +``` + +## Configuration + +For semantics configuration, please [contact Yieldmo](https://yieldmo.com/#contact). + +Supported parameters: + +- `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..7f4bb10e4243 --- /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 {validateData, writeScript} 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..0dc302f88a9a --- /dev/null +++ b/ads/yieldone.md @@ -0,0 +1,37 @@ + + +# Yield One + +## Example + +```html + + +``` + +## Configuration + + +For configuration details and to generate your tags, please contact [YieldOne](https://yieldone.com/service/contact/media/index.php) or email: `y1dev@platform-one.co.jp`. + +Supported parameters: + +- `data-pubid` +- `data-pid` diff --git a/ads/yieldpro.js b/ads/yieldpro.js new file mode 100644 index 000000000000..f4935793609c --- /dev/null +++ b/ads/yieldpro.js @@ -0,0 +1,73 @@ +/** + * 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 { + computeInMasterFrame, + loadScript, + validateData, +} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function yieldpro(global, data) { + + validateData(data, ['sectionId', 'slot', 'pubnetwork'], [ + 'instance', + 'custom', + 'adServerUrl', + 'cacheSafe', + 'pageIdModifier', + 'click3rd', + 'debugsrc', + ]); + //TODO support dmp and cookie + + const SCRIPT_HOST = 'creatives.yieldpro.eu/showad_'; + + let scriptUrl = 'https://' + SCRIPT_HOST + + data['pubnetwork'] + '.js'; + + if (data['debugsrc']) { + scriptUrl = data['debugsrc']; + } + + computeInMasterFrame(global, 'yieldpro-request', done => { + let success = false; + const masterWin = this; + if (!masterWin.showadAMPAdapter) { + masterWin.showadAMPAdapter = { + registerSlot: () => {}, + }; + loadScript(this, scriptUrl, () => { + if (masterWin.showadAMPAdapter.inited) { + success = true; + } + done(success); + }); + } else { + done(true); + } + }, success => { + if (success) { + global.showadAMPAdapter = global.context.master.showadAMPAdapter; + global.showadAMPAdapter.registerSlot(data, global); + } else { + throw new Error('Yieldpro AdTag failed to load'); + } + }); +} diff --git a/ads/yieldpro.md b/ads/yieldpro.md new file mode 100644 index 000000000000..d51736acbee1 --- /dev/null +++ b/ads/yieldpro.md @@ -0,0 +1,91 @@ + + +# YieldPro + +## Examples + +### Single ad + +```html + + +``` + +### Multi instance ads + +```html + + + + +``` + +### Using custom params and custom ad server url + +```html + + +``` + +## Configuration + + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +### Required parameters + +- `sectionId`: ID of this section in ad server. +- `slot`: ID of ad slot in ad server. +- `pubnetwork`: ID of the publisher that in ad server. + +### Optional parameters + +- `instance`: ID of section instance in case we multiple times used the same section on the same page. Can contain only letters and numbers.Strictly required to use the same section multiple times per page. +- `click3rd`: 3rd party click watcher. +- `adServerUrl` +- `cacheSafe` +- `pageIdModifier` +- `custom`: Custom targeting properties. You may use 3 types for its properties: `{String}`, `{Number}` and `{Array}`. The following array usage example translates into: `arrayKey=value1&arrayKey=1&stringKey=stringValue...` + + ```text + { + arrayKey: [ "value1", 1 ], + stringKey: 'stringValue' + } + ``` + + diff --git a/ads/zedo.js b/ads/zedo.js new file mode 100644 index 000000000000..99486e469162 --- /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 {loadScript, validateData} 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; + 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..bda00dc6d8fe --- /dev/null +++ b/ads/zedo.md @@ -0,0 +1,54 @@ + + +# Zedo + +## Example + +### Basic + +```html + + +``` + + +## Configuration + +For semantics of configuration, please contact 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/zen.js b/ads/zen.js new file mode 100644 index 000000000000..38f81513ae24 --- /dev/null +++ b/ads/zen.js @@ -0,0 +1,76 @@ +/** + * 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} from '../3p/3p'; + +const n = 'yandexZenAsyncCallbacks'; +const renderTo = 'zen-widget'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function zen(global, data) { + validateData( + data, + ['clid'], + ['size', 'orientation', 'successCallback', 'failCallback'] + ); + + addToQueue(global, data); + loadScript(global, 'https://zen.yandex.ru/widget-loader'); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +function addToQueue(global, data) { + global[n] = global[n] || []; + global[n].push(() => { + + // Create container + createContainer(global, renderTo); + + const {YandexZen} = global; + const config = Object.assign(data, { + clid: JSON.parse(data.clid), + container: `#${renderTo}`, + isAMP: true, + successCallback: () => { + if (typeof data.successCallback === 'function') { + data.successCallback(); + } + }, + failCallback: () => { + if (typeof data.failCallback === 'function') { + data.failCallback(); + } + }, + }); + + YandexZen.renderWidget(config); + }); +} + +/** + * @param {!Window} global + * @param {string} id + */ +function createContainer(global, id) { + const d = global.document.createElement('div'); + d.id = id; + global.document.getElementById('c').appendChild(d); +} diff --git a/ads/zen.md b/ads/zen.md new file mode 100644 index 000000000000..9e7b87b848d5 --- /dev/null +++ b/ads/zen.md @@ -0,0 +1,40 @@ + + +# Zen + +## Example + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please see [Zen's documentation](https://yandex.ru/support/zen/index.html). + +### Required parameters + +- `data-clid` + +### Optional parameters +- `data-size` +- `data-orientation` +- `data-on-render` +- `data-on-error` diff --git a/ads/zergnet.js b/ads/zergnet.js new file mode 100644 index 000000000000..10477ac43c42 --- /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 {validateData, writeScript} 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..c6617efaeb4e --- /dev/null +++ b/ads/zergnet.md @@ -0,0 +1,38 @@ + + +# ZergNet + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For details on the configuration semantics, please contact the ad network or refer to their documentation. + +Supported parameters: + +- `data-zergid` diff --git a/ads/zucks.js b/ads/zucks.js new file mode 100644 index 000000000000..b4697400a472 --- /dev/null +++ b/ads/zucks.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, writeScript} from '../3p/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function zucks(global, data) { + validateData(data, ['frameId']); + + let script = `https://j.zucks.net.zimg.jp/j?f=${data['frameId']}`; + + if (data['adtype'] === 'native') { + script = `https://j.zucks.net.zimg.jp/n?f=${data['frameId']}`; + } + + writeScript(global, script); +} diff --git a/ads/zucks.md b/ads/zucks.md new file mode 100644 index 000000000000..1574a62a74fa --- /dev/null +++ b/ads/zucks.md @@ -0,0 +1,34 @@ + + +# Zucks + +## Example + +```html + + +``` + +## Configuration + +For more information, please [contact Zucks](https://zucks.co.jp/contact/). + +Supported parameters: + +- `data-frame-id` diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000000..3c356ca6cee2 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,51 @@ +/** + * Copyright 2018 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 Global configuration file for the babelify transform. + * + * Notes: From https://babeljs.io/docs/en/plugins#plugin-ordering: + * 1. Plugins run before Presets. + * 2. Plugin ordering is first to last. + * 3. Preset ordering is reversed (last to first). + */ + +'use strict'; + +const minimist = require('minimist'); +const argv = minimist(process.argv.slice(2)); + +module.exports = function(api) { + api.cache(true); + // Single pass builds do not use any of the default settings below. + if (argv._.includes('dist') && argv.single_pass) { + return {}; + } + return { + 'presets': [ + ['@babel/env', { + 'modules': 'commonjs', + 'loose': true, + 'targets': { + 'browsers': process.env.TRAVIS ? + ['Last 2 versions', 'safari >= 9'] : ['Last 2 versions'], + }, + }], + ], + 'compact': false, + 'sourceType': 'module', + }; +}; diff --git a/build-system/.eslintrc b/build-system/.eslintrc new file mode 100644 index 000000000000..9e0077f44be6 --- /dev/null +++ b/build-system/.eslintrc @@ -0,0 +1,17 @@ +{ + "env": { + "node": true + }, + "globals": { + "require": false, + "process": false, + "exports": false + }, + "rules": { + "amphtml-internal/no-array-destructuring": 0, + "amphtml-internal/no-for-of-statement": 0, + "amphtml-internal/no-has-own-property-method": 0, + "amphtml-internal/no-spread": 0, + "require-jsdoc": 0 + } +} diff --git a/build-system/OWNERS.yaml b/build-system/OWNERS.yaml new file mode 100644 index 000000000000..6a6b14ec7b20 --- /dev/null +++ b/build-system/OWNERS.yaml @@ -0,0 +1,3 @@ +- erwinmombay +- rsimha +- danielrozenberg diff --git a/build-system/SERVING.md b/build-system/SERVING.md new file mode 100644 index 000000000000..eb034bc338f6 --- /dev/null +++ b/build-system/SERVING.md @@ -0,0 +1,58 @@ + + +# How AMP HTML is deployed + +## Requirements +Go through the initial [one time setup](../contributing/getting-started-quick.md#one-time-setup). + +## 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 new file mode 100644 index 000000000000..4dea865062f7 --- /dev/null +++ b/build-system/amp.extern.js @@ -0,0 +1,776 @@ +/** + * 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. + */ + +/** @externs */ + +/** + * The "init" argument of the Fetch API. Externed due to being passes across + * component/runtime boundary. + * + * Currently, only "credentials: include" is implemented. + * + * Note ampCors === false indicates that __amp_source_origin should not be + * appended to the URL to allow for potential caching or response across pages. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch + * + * @typedef {{ + * body: (!JsonObject|!FormData|!FormDataWrapperInterface|undefined|string), + * cache: (string|undefined), + * credentials: (string|undefined), + * headers: (!JsonObject|undefined), + * method: (string|undefined), + * requireAmpResponseSourceOrigin: (boolean|undefined), + * ampCors: (boolean|undefined) + * }} + */ +var FetchInitDef; + +/** + * Externed due to being passed across component/runtime boundary. + * @typedef {{xhrUrl: string, fetchOpt: !FetchInitDef}} + */ +var FetchRequestDef; + +/** @constructor **/ +var FormDataWrapperInterface = function() {}; + +FormDataWrapperInterface.prototype.entries = function() {}; +FormDataWrapperInterface.prototype.getFormData = function() {}; + +FormData.prototype.entries = function () {}; +/** + * @param {string} unusedName + */ +FormData.prototype.delete = function (unusedName) {}; + +/** + * A type for Objects that can be JSON serialized or that come from + * JSON serialization. Requires the objects fields to be accessed with + * bracket notation object['name'] to make sure the fields do not get + * obfuscated. + * @constructor + * @dict + */ +function JsonObject() {} + +/** + * Force the dataset property to be handled as a JsonObject. + * @type {!JsonObject} + */ +Element.prototype.dataset; + +/** + * - n is the name. + * - f is the function body of the extension. + * - p is the priority. Only supported value is "high". + * high means, that the extension is not subject to chunking. + * This should be used for work, that should always happen + * as early as possible. Currently this is primarily used + * for viewer communication setup. + * - v is the release version + * @constructor @struct + */ +function ExtensionPayload() {} + +/** @type {string} */ +ExtensionPayload.prototype.n; + +/** @type {function(!Object,!Object)} */ +ExtensionPayload.prototype.f; + +/** @type {string|undefined} */ +ExtensionPayload.prototype.p; + +/** @type {string} */ +ExtensionPayload.prototype.v; + +/** @type {!Array|string|undefined} */ +ExtensionPayload.prototype.i; + + +/** + * @typedef {?JsonObject|undefined|string|number|!Array} + */ +var JsonValue; + +/** + * @constructor + * @dict + */ +function VideoAnalyticsDetailsDef() {}; +/** @type {boolean} */ +VideoAnalyticsDetailsDef.prototype.autoplay; +/** @type {number} */ +VideoAnalyticsDetailsDef.prototype.currentTime; +/** @type {number} */ +VideoAnalyticsDetailsDef.prototype.duration; +/** @type {number} */ +VideoAnalyticsDetailsDef.prototype.height; +/** @type {string} */ +VideoAnalyticsDetailsDef.prototype.id; +/** @type {string} */ +VideoAnalyticsDetailsDef.prototype.playedRangesJson; +/** @type {number} */ +VideoAnalyticsDetailsDef.prototype.playedTotal; +/** @type {boolean} */ +VideoAnalyticsDetailsDef.prototype.muted; +/** @type {string} */ +VideoAnalyticsDetailsDef.prototype.state; +/** @type {number} */ +VideoAnalyticsDetailsDef.prototype.width; + + +// Node.js global +var process = {}; +process.env; +process.env.NODE_ENV; +process.env.SERVE_MODE; + +/** @type {boolean|undefined} */ +window.IS_AMP_ALT; + +// Exposed to ads. +window.context = {}; +window.context.sentinel; +window.context.clientId; +window.context.initialLayoutRect; +window.context.initialIntersection; +window.context.sourceUrl; +window.context.experimentToggles; +window.context.master; +window.context.isMaster; + +// Service Holder +window.services; + +// Safeframe +// TODO(bradfrizzell) Move to its own extern. Not relevant to all AMP. +/* @type {?Object} */ +window.sf_ = {}; +/* @type {?Object} */ +window.sf_.cfg; + +// Exposed to custom ad iframes. +/* @type {!Function} */ +window.draw3p; + +// AMP's globals +window.AMP_TEST; +window.AMP_TEST_IFRAME; +window.AMP_TAG; +window.AMP = {}; +window.AMP._ = {}; +window.AMP.push; +window.AMP.title; +window.AMP.canonicalUrl; +window.AMP.extension; +window.AMP.ampdoc; +window.AMP.config; +window.AMP.config.urls; +window.AMP.BaseElement; +window.AMP.BaseTemplate; +window.AMP.registerElement; +window.AMP.registerTemplate; +window.AMP.registerServiceForDoc; +window.AMP.isExperimentOn; +window.AMP.toggleExperiment; +window.AMP.setLogLevel; +window.AMP.setTickFunction; +window.AMP.viewer; +window.AMP.viewport = {}; +window.AMP.viewport.getScrollLeft; +window.AMP.viewport.getScrollWidth; +window.AMP.viewport.getWidth; +window.AMP.attachShadowDoc; +window.AMP.attachShadowDocAsStream; + + +/** @constructor */ +function AmpConfigType() {} + +/* @public {string} */ +AmpConfigType.prototype.thirdPartyUrl; +/* @public {string} */ +AmpConfigType.prototype.thirdParty; +/* @public {string} */ +AmpConfigType.prototype.thirdPartyFrameHost; +/* @public {string} */ +AmpConfigType.prototype.thirdPartyFrameRegex; +/* @public {string} */ +AmpConfigType.prototype.errorReporting; +/* @public {string} */ +AmpConfigType.prototype.cdn; +/* @public {string} */ +AmpConfigType.prototype.cdnUrl; +/* @public {string} */ +AmpConfigType.prototype.errorReportingUrl; +/* @public {string} */ +AmpConfigType.prototype.localDev; +/* @public {string} */ +AmpConfigType.prototype.v; +/* @public {boolean} */ +AmpConfigType.prototype.canary; +/* @public {string} */ +AmpConfigType.prototype.runtime; +/* @public {boolean} */ +AmpConfigType.prototype.test; +/* @public {string|undefined} */ +AmpConfigType.prototype.spt; + +/** @type {!AmpConfigType} */ +window.AMP_CONFIG; + +window.AMP_CONTEXT_DATA; + +/** @constructor @struct */ +function AmpViewerMessage() {} + +/** @public {string} */ +AmpViewerMessage.prototype.app; +/** @public {string} */ +AmpViewerMessage.prototype.type; +/** @public {number} */ +AmpViewerMessage.prototype.requestid; +/** @public {string} */ +AmpViewerMessage.prototype.name; +/** @public {*} */ +AmpViewerMessage.prototype.data; +/** @public {boolean|undefined} */ +AmpViewerMessage.prototype.rsvp; +/** @public {string|undefined} */ +AmpViewerMessage.prototype.error; + +// AMP-Analytics Cross-domain iframes +let IframeTransportEvent; + +/** @constructor @struct */ +function IframeTransportContext() {} +IframeTransportContext.onAnalyticsEvent; +IframeTransportContext.sendResponseToCreative; + +/** @typedef {function(!JsonObject)} */ +let VegaChartFactory; + +// amp-viz-vega related externs. +/** + * @typedef {{spec: function(!JsonObject, function(?Error, !VegaChartFactory))}} + */ +let VegaParser; +/** + * @typedef {{parse: VegaParser}} + */ +let VegaObject; +/* @type {!VegaObject} */ +window.vg; + +// amp-date-picker externs +/** + * @type {function(*)} + */ +let ReactRender = function() {}; + +/** + * @dict + */ +let PropTypes = {}; + +/** + * @@dict + */ +let ReactDates = {}; + +/** @constructor */ +ReactDates.DayPickerSingleDateController; + +/** @dict */ +ReactDates.DayPickerRangeController; + +/** @type {function(*):boolean} */ +ReactDates.isInclusivelyAfterDay; + +/** @type {function(*):boolean} */ +ReactDates.isInclusivelyBeforeDay; + +/** @type {function(*,*):boolean} */ +ReactDates.isSameDay; + +/** + * @dict + */ +let ReactDatesConstants = {}; + +/** @const {string} */ +ReactDatesConstants.ANCHOR_LEFT; + +/** @const {string} */ +ReactDatesConstants.HORIZONTAL_ORIENTATION; + +// Should have been defined in the closure compiler's extern file for +// IntersectionObserverEntry, but appears to have been omitted. +IntersectionObserverEntry.prototype.rootBounds; + +// TODO (remove after we update closure compiler externs) +window.PerformancePaintTiming; +window.PerformanceObserver; +Object.prototype.entryTypes + +// Externed explicitly because this private property is read across +// binaries. +Element.prototype.implementation_ = {}; +Element.prototype.signals; +window.whenSignal; + +/** @typedef {number} */ +var time; + +/** + * This type signifies a callback that can be called to remove the listener. + * @typedef {function()} + */ +var UnlistenDef; + + +/** + * Just an element, but used with AMP custom elements.. + * @typedef {!Element} + */ +var AmpElement; + +// Temp until we figure out forward declarations +/** @constructor */ +var AccessService = function() {}; +/** @constructor @struct */ +var UserNotificationManager = function() {}; +UserNotificationManager.prototype.get; +/** @constructor @struct */ +var Cid = function() {}; +/** @constructor @struct */ +var Activity = function() {}; +/** @constructor */ +var AmpStoryVariableService = function() {}; + +// data +var data; +data.tweetid; +data.requestedHeight; +data.requestedWidth; +data.pageHidden; +data.changes; +data._context; +data.inViewport; +data.numposts; +data.orderBy; +data.colorscheme; +data.tabs; +data.hideCover; +data.hideCta; +data.smallHeader; +data.showFacepile; +data.showText; +data.productId; +data.imageUrl; +data.yotpoElementId; +data.backgroudColor; +data.reviewIds; +data.showBottomLine; +data.autoplayEnabled; +data.autoplaySpeed; +data.showNavigation; +data.layoutScroll; +data.spacing; +data.hoverColor; +data.hoverOpacity; +data.hoverIcon; +data.ctaText; +data.ctaColor; +data.appKey; +data.widgetType; +data.layoutRows; +data.demo; +data.uploadButton; +data.reviews; +data.headerText; +data.headerBackgroundColor; +data.bodyBackgroundColor; +data.data.fontColor; +data.width; +data.sitekey; +data.fortesting; + +// 3p code +var twttr; +twttr.events; +twttr.events.bind; +twttr.widgets; +twttr.widgets.createTweet; +twttr.widgets.createMoment; +twttr.widgets.createTimeline; + +var FB; +FB.init; + +var gist; +gist.gistid; + +var bodymovin; +bodymovin.loadAnimation; +var animationHandler; +animationHandler.play; +animationHandler.pause; +animationHandler.stop; +animationHandler.goToAndStop; +animationHandler.totalFrames; + +var grecaptcha; +grecaptcha.execute; + +// Validator +var amp; +amp.validator; +amp.validator.validateUrlAndLog = function(string, doc, filter) {} + +// Temporary Access types (delete when amp-access is compiled +// for type checking). +Activity.prototype.getTotalEngagedTime = function() {}; +Activity.prototype.getIncrementalEngagedTime = function(name, reset) {}; +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; +/** + * @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) {} + +AmpStoryVariableService.prototype.onStateChange = function(event) {}; +AmpStoryVariableService.pageIndex; +AmpStoryVariableService.pageId; + +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 @struct + * @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 @struct + * @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 @struct + * @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; + +AMP.RealTimeConfigManager; + +/** + * Actual filled values for this exists in + * extensions/amp-a4a/0.1/real-time-config-manager.js + * @enum {string} + */ +const RTC_ERROR_ENUM = {}; + +/** @typedef {{ + response: (Object|undefined), + rtcTime: number, + callout: string, + error: (RTC_ERROR_ENUM|undefined)}} */ +var rtcResponseDef; + +/** + * This symbol is exposed by browserify bundles transformed by + * `scoped-require.js` to avoid polluting the global namespace with `require`. + * It allows AMP extensions to consume code injected into their binaries that + * cannot be run through Closure Compiler, e.g. React code with JSX. + * @type {!function(string):?} + */ +AMP.require; + +/** + * 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; + +/////////////////// +// amp-bind externs +/////////////////// + +/** + * @typedef {{method: string, args: !Array, scope: number, id: number}} + */ +let ToWorkerMessageDef; + +/** + * @typedef {{method: string, returnValue: *, id: number}} + */ +let FromWorkerMessageDef; + +/** + * Structured cloneable representation of an element. + * @typedef {{id: string, argumentNames: Array, expressionString: string}} + */ +let BindMacroDef; + +/** + * Structured cloneable representation of a binding e.g.

'; + }).join('\n'); + } + + const {experiments} = req.query; + let metaTag = ''; + let experimentString = ''; + if (experiments) { + metaTag = ''; + experimentString = '"' + experiments.split(',').join('","') + '"'; + } + const {css} = req.query; + const cssTag = css ? `` : ''; + + res.send(` + + + + + + AMP TEST + + ${metaTag} + + + + + ${extensionScripts} + ${cssTag} + + +${req.query.body} + + +`); +}); + +/** + * A server side temporary request storage which is useful for testing + * browser sent HTTP requests. + */ +const bank = {}; + +/** + * Deposit a request. An ID has to be specified. Will override previous request + * if the same ID already exists. + */ +app.use('/request-bank/deposit/', (req, res) => { + // req.url is relative to the path specified in app.use + const key = req.url; + log('SERVER-LOG [DEPOSIT]: ', key); + if (typeof bank[key] === 'function') { + bank[key](req); + } else { + bank[key] = req; + } + res.end(); +}); + +/** + * Withdraw a request. If the request of the given ID is already in the bank, + * return it immediately. Otherwise wait until it gets deposited + * The same request cannot be withdrawn twice at the same time. + */ +app.use('/request-bank/withdraw/', (req, res) => { + // req.url is relative to the path specified in app.use + const key = req.url; + log('SERVER-LOG [WITHDRAW]: ' + key); + const result = bank[key]; + if (typeof result === 'function') { + return res.status(500).send('another client is withdrawing this ID'); + } + const callback = function(result) { + res.json({ + headers: result.headers, + body: result.body, + }); + delete bank[key]; + }; + if (result) { + callback(result); + } else { + bank[key] = callback; + } +}); diff --git a/build-system/app-index/boilerplate.js b/build-system/app-index/boilerplate.js new file mode 100644 index 000000000000..ddfea76a2672 --- /dev/null +++ b/build-system/app-index/boilerplate.js @@ -0,0 +1,17 @@ +/** + * Copyright 2018 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. + */ +/* eslint-disable max-len */ +module.exports = ''; diff --git a/build-system/app-index/bundler.js b/build-system/app-index/bundler.js new file mode 100644 index 000000000000..138d968dd15f --- /dev/null +++ b/build-system/app-index/bundler.js @@ -0,0 +1,59 @@ +/** + * Copyright 2018 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'; + +const babel = require('rollup-plugin-babel'); +const commonjs = require('rollup-plugin-commonjs'); +const resolve = require('rollup-plugin-node-resolve'); +const rollup = require('rollup'); + +const plugins = [ + resolve(), + babel({ + exclude: '**/node_modules/**', + plugins: [ + ['transform-react-jsx', {'pragma': 'h'}], + ['@babel/plugin-proposal-class-properties'], + ], + presets: [['@babel/preset-env', {modules: false}]], + }), + commonjs(), +]; + +const inputOptions = { + plugins, +}; + +const outputOptions = { + format: 'iife', +}; + +module.exports = { + bundleComponent: async componentEntryFile => { + + console/*OK*/.log('Generating bundle for: ' + componentEntryFile); + + inputOptions.input = componentEntryFile; + + const bundle = await rollup.rollup(inputOptions); + const {code} = await bundle.generate(outputOptions); + + console/*OK*/.log('Generated bundle for: ' + componentEntryFile); + + return code; + }, +}; + diff --git a/build-system/app-index/components/main.js b/build-system/app-index/components/main.js new file mode 100644 index 000000000000..38c0d11a8996 --- /dev/null +++ b/build-system/app-index/components/main.js @@ -0,0 +1,17 @@ +/** + * Copyright 2018 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. + */ + +// TODO(alanorozco): This bundle shall be used for experiment toggles. diff --git a/build-system/app-index/html.js b/build-system/app-index/html.js new file mode 100644 index 000000000000..710a6cccbe98 --- /dev/null +++ b/build-system/app-index/html.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +// pass-through for syntax highlighting +module.exports = (strings, ...values) => + strings.map((string, i) => string + (values[i] || '')).join(''); diff --git a/build-system/app-index/index.js b/build-system/app-index/index.js new file mode 100644 index 000000000000..afdf4259985c --- /dev/null +++ b/build-system/app-index/index.js @@ -0,0 +1,146 @@ +/** + * Copyright 2018 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'; + + +const BBPromise = require('bluebird'); +const bundler = require('./bundler'); +const fs = BBPromise.promisifyAll(require('fs')); +const {join, normalize, sep} = require('path'); +const {renderTemplate} = require('./template'); + +const pc = process; + +// JS Component +const mainComponent = join(__dirname, '/components/main.js'); + +// CSS +const mainCssFile = join(__dirname, '/main.css'); + + +function isMaliciousPath(path, rootPath) { + return (path + sep).substr(0, rootPath.length) !== rootPath; +} + + +async function getListing(rootPath, basepath) { + const path = normalize(join(rootPath, basepath)); + + if (~path.indexOf('\0')) { + return null; + } + + if (isMaliciousPath(path, rootPath)) { + return null; + } + + try { + if ((await fs.statAsync(path)).isDirectory()) { + return fs.readdirAsync(path); + } + } catch (unusedE) { + /* empty catch for fallbacks */ + return null; + } +} + + +let shouldCache = true; +function setCacheStatus(cacheStatus) { + shouldCache = cacheStatus; +} + + +let mainBundleCache; +async function bundleMain() { + if (shouldCache && mainBundleCache) { + return mainBundleCache; + } + const bundle = await bundler.bundleComponent(mainComponent); + if (shouldCache) { + mainBundleCache = bundle; + } + return bundle; +} + + +function isMainPageFromUrl(url) { + return url == '/'; +} + + +/** + * Adds a trailing slash if missing. + * @param {string} basepath + * @return {string} + */ +function formatBasepath(basepath) { + return basepath.replace(/[^\/]$/, lastChar => `${lastChar}/`); +} + + +function serveIndex({root, mapBasepath}) { + const mapBasepathOrPassthru = mapBasepath || (url => url); + + return (req, res, next) => { + if (!root) { + res.status(500); + res.end('Misconfigured: missing `root`.'); + return; + } + + return (async() => { + const isMainPage = isMainPageFromUrl(req.url); + const basepath = mapBasepathOrPassthru(req.url); + + const fileSet = await getListing(root, basepath); + + if (fileSet == null) { + next(); + return; + } + + const css = (await fs.readFileAsync(mainCssFile)).toString(); + + const serveMode = pc.env.SERVE_MODE || 'default'; + + const renderedHtml = renderTemplate({ + basepath: formatBasepath(basepath), + fileSet, + isMainPage, + serveMode, + css, + }); + + res.end(renderedHtml); + + return renderedHtml; // for testing + })(); + }; +} + +// Promises to run before serving +async function beforeServeTasks() { + if (shouldCache) { + await bundleMain(); + } +} + +module.exports = { + setCacheStatus, + serveIndex, + beforeServeTasks, +}; diff --git a/build-system/app-index/main.css b/build-system/app-index/main.css new file mode 100644 index 000000000000..6f980d53a76c --- /dev/null +++ b/build-system/app-index/main.css @@ -0,0 +1,293 @@ +/** + * Copyright 2018 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. + */ +body { + font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + line-height: 20px; + padding: 0; + margin: 0; +} +body.scroll-locked { + overflow: hidden; +} +.amp-logo { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='175' height='60' viewBox='0 0 175 60'%3E%3Ctitle%3EAMP-Brand-Blue%3C/title%3E%3Cg fill='%230379C4' fill-rule='evenodd'%3E%3Cpath d='M92.938 34.265h7.07l-2.38-7.015c-.153-.445-.334-.97-.54-1.574-.21-.603-.414-1.256-.615-1.96-.188.715-.384 1.378-.585 1.988-.202.61-.392 1.136-.57 1.58l-2.38 6.98zm16.632 9.794h-4.656c-.522 0-.95-.122-1.29-.362-.336-.24-.57-.548-.7-.923l-1.528-4.465h-9.844l-1.53 4.465c-.116.328-.348.625-.69.888-.344.264-.765.396-1.263.396h-4.692L93.4 18.44h6.147L109.57 44.06zM130.562 33.736c.22.48.43.975.63 1.48.202-.518.415-1.02.64-1.505.225-.487.456-.96.693-1.416l6.645-12.954c.12-.224.24-.397.365-.52.124-.123.264-.214.417-.273.154-.06.33-.088.524-.088h5.27v25.6h-5.295V29.324c0-.714.035-1.49.106-2.32l-6.858 13.168c-.213.41-.5.72-.862.932-.362.21-.773.316-1.235.316h-.816c-.462 0-.873-.104-1.235-.315-.363-.21-.65-.52-.864-.932l-6.893-13.187c.047.41.083.818.106 1.222.023.405.035.778.035 1.117V44.06h-5.295v-25.6h5.269c.194 0 .37.028.523.087.154.06.293.15.417.274.125.123.246.296.365.52l6.663 13.006c.237.446.464.91.684 1.39M160.99 31.013h3.127c1.563 0 2.688-.37 3.376-1.108.687-.738 1.03-1.77 1.03-3.094 0-.585-.088-1.12-.266-1.6-.178-.48-.448-.893-.808-1.24-.363-.344-.82-.612-1.37-.8-.55-.187-1.206-.28-1.963-.28h-3.127v8.123zm0 4.483v8.562h-6.006V18.442h9.133c1.824 0 3.39.214 4.7.64 1.308.43 2.387 1.018 3.233 1.77.847.75 1.473 1.634 1.875 2.653.403 1.02.604 2.122.604 3.306 0 1.278-.208 2.45-.622 3.518-.416 1.065-1.05 1.98-1.902 2.742-.853.762-1.934 1.357-3.243 1.784-1.308.43-2.857.642-4.646.642h-3.127zM40.674 27.253L27.968 48.196h-2.302l2.276-13.647-7.048.01h-.1c-.633 0-1.148-.51-1.148-1.137 0-.27.254-.727.254-.727l12.664-20.92 2.34.01-2.332 13.668 7.084-.008.112-.002c.635 0 1.15.51 1.15 1.14 0 .254-.1.478-.245.668zM30.288 0C13.56 0 0 13.432 0 30 0 46.57 13.56 60 30.288 60c16.73 0 30.29-13.431 30.29-30 0-16.568-13.56-30-30.29-30z'/%3E%3C/g%3E%3C/svg%3E"); + width: 87px; + height: 40px; + text-indent: -999em; + overflow: hidden; + background-size: 87px; + background-position: left center; + background-repeat: no-repeat; +} +.wrap { + position: relative; + display: block; + max-width: 960px; + padding: 0 40px; + margin: 20px auto; +} +.block { + margin: 40px auto; +} +#proxy-form > label { + display: flex; + flex-wrap: wrap; +} +#proxy-form > label > span { + display: block; + padding-right: 20px; + font-weight: bold; +} +#proxy-input { + display: block; + flex: 1; +} +.file-list-container, +.settings-modal-content { + background: #f8f8f8; +} +h3 { + padding: 40px 0; + line-height: 40px; + font-size: 18px; + margin: 0; + display: flex; +} +#settings-modal > div { + display: flex; + flex-direction: column; +} +.settings-modal-content { + flex: 1; +} +.settings-close-button-container { + background: #fff; + position: absolute; + right: 40px; + top: 30px; + border-radius: 6px; +} +.right-nav .settings-cog-icon, +.settings-modal-header .close-icon { + cursor: pointer; +} +.settings-modal-header { + background: rgba(255, 255, 255, 0.4); + height: 130px; +} +#settings-modal h3 { + padding: 20px 0 0; +} +.settings-modal-main { + max-width: 780px; +} +.selector-block { + display: block; + padding: 20px 20px 20px 48px; + border-radius: 4px; + cursor: pointer; + position: relative; + margin: 0 0 10px; +} +.selector-block > .check-icon { + position: absolute; + left: 12px; + top: 18px; + display: none; +} +.selector-block p { + margin: 10px 0 0; +} +.selector-block[option][selected] { + background-color: #daeeff; + color: #0066c0; + outline: none; +} +.selector-block[option][selected] > .check-icon { + background-color: #0066c0; + display: block; +} +.icon { + mask-position: center center; + -webkit-mask-position: center center; + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + background-color: #999; + width: 24px; + height: 24px; + text-indent: -999em; + display: block; +} +.close-icon { + mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='%23000000'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='%23000000'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); +} +.find-icon { + mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='%23000000'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='%23000000'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); +} +.settings-cog-icon { + mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 20 20' fill='%23000000'%3E%3Cpath fill='none' d='M0 0h20v20H0V0z'/%3E%3Cpath d='M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z'/%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 20 20' fill='%23000000'%3E%3Cpath fill='none' d='M0 0h20v20H0V0z'/%3E%3Cpath d='M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z'/%3E%3C/svg%3E"); +} +.check-icon { + mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); +} +h3 > .find-icon { + height: 40px; + margin: 0 20px; +} +a.find-icon:hover { + background-color: #1c79c0; +} +code, +.code, +.file-list > li > a { + font-family: 'Fira Code', 'Inconsolata', Menlo, Consolas, monospace; +} +a { + color: #1c79c0; + text-decoration: none; + position: relative; +} +a.underlined { + hyphens: manual; + overflow-wrap: normal; + white-space: nowrap; + word-break: normal; + word-wrap: normal; +} +a.underlined::before { + background: linear-gradient(to right, #0389ff, #0dd3ff); + bottom: -3px; + content: ''; + left: 0; + height: 1px; + position: absolute; + right: 0; +} +.push-right-after-heading { + display: flex; + float: right; + margin-top: -70px; + line-height: 20px; +} +#examples-mode-select { + margin: 0 20px 0 10px; +} +.center { + margin: 40px 0; + text-align: center; +} +.text-input { + padding: 6px 0; + margin: -6px 0; + font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + line-height: 20px; + border-color: #ddd; + border-width: 0 0 1px; +} +.text-input:focus { + border-color: #0389ff; + outline: none; +} +.file-list { + padding: 0 0 40px; + margin: 0; + column-count: 3; + column-gap: 20px; +} +.file-list > li { + padding: 0; + margin: 0 0 10px; + display: inline-block; + width: 300px; + display: block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} +.file-list > li > a:hover { + text-decoration: underline; + color: #333; +} +.form-info { + margin: 20px 0 0; + font-size: 12px; + text-align: right; +} +.form-info a { + font-weight: bold; +} +header { + display: flex; +} +.right-of-logo { + flex: 1; + margin: 30px 0 0 40px; +} +header > ul { + display: flex; + margin: 0; + padding: 0; + list-style: none; +} +header > ul > li { + display: flex; + margin: 30px 0; + padding: 0 0 0 20px; +} +header li.divider { + padding-right: 20px; + border-right: 1px solid #ddd; +} +@media (max-width: 1000px) { + .file-list { + column-count: 2; + } +} +@media (max-width: 580px) { + #proxy-form > label > span { + display: block; + padding-right: 0; + flex: 0 0 100%; + margin-bottom: 20px; + } + .file-list { + column-count: 1; + } + .file-list > li { + display: block; + width: 100%; + } + header > ul { + display: block; + flex: 1; + } + header > ul > li { + margin: 20px 0; + padding: 0; + } + header li.divider { + padding-right: 0; + padding-bottom: 20px; + border-right: 0; + border-bottom: 1px solid #ddd; + } +} diff --git a/build-system/app-index/proxy-form.js b/build-system/app-index/proxy-form.js new file mode 100644 index 000000000000..4796cc281444 --- /dev/null +++ b/build-system/app-index/proxy-form.js @@ -0,0 +1,38 @@ +/** + * Copyright 2018 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. + */ +const html = require('./html'); + +module.exports = () => html` +

+
+ + +
+
`; diff --git a/build-system/app-index/settings.js b/build-system/app-index/settings.js new file mode 100644 index 000000000000..f740389533f8 --- /dev/null +++ b/build-system/app-index/settings.js @@ -0,0 +1,114 @@ +/** + * Copyright 2018 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. + */ +/* eslint-disable indent */ +/* eslint-disable amphtml-internal/html-template */ +const html = require('./html'); + +const serveModes = [ + { + value: 'default', + description: `Unminified AMP JavaScript is served from the local server. For + local development you will usually want to serve unminified JS to test your + changes.`, + }, + { + value: 'compiled', + description: `Minified AMP JavaScript is served from the local server. This + is only available after running \`gulp dist--fortesting \`.`, + }, + { + value: 'cdn', + description: 'Minified AMP JavaScript is served from the AMP Project CDN.', + }, +]; + + +const SelectorBlock = ({id, value, selected, children}) => html`
+
+ ${children} +
`; + + +const ServeModeSelector = ({serveMode}) => html` +
+ + ${serveModes.map(({value, description}) => { + const id = `serve_mode_${value}`; + return SelectorBlock({ + id, + value, + selected: serveMode == value, + children: html`${value} +

${description}

`, + }); + }).join('')} +
+
`; + + +const SettingsOpenButton = () => html`
+ Settings +
`; + + +const SettingsCloseButton = () => html`
+ Close Settings +
`; + + +const SettingsModal = ({serveMode}) => html` +
+
+
+ ${SettingsCloseButton()} +
+
+
+
+
+

Settings

+

JavaScript Serve Mode

+ ${ServeModeSelector({serveMode})} +
+
+
`; + + +module.exports = {SettingsModal, SettingsOpenButton}; diff --git a/build-system/app-index/template.js b/build-system/app-index/template.js new file mode 100644 index 000000000000..e1abaf0a0541 --- /dev/null +++ b/build-system/app-index/template.js @@ -0,0 +1,253 @@ +/** + * Copyright 2018 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. + */ + +/* eslint-disable amphtml-internal/html-template */ +/* eslint-disable indent */ + +'use strict'; + +const boilerPlate = require('./boilerplate'); +const html = require('./html'); +const ProxyForm = require('./proxy-form'); +const {SettingsModal, SettingsOpenButton} = require('./settings'); + + +const examplesDocumentModes = { + 'standard': '/', + 'a4a': '/a4a/', + 'a4a-3p': '/a4a-3p/', + 'inabox': '/inabox/1/', +}; + + +const headerLinks = [ + { + 'name': 'Developing', + 'href': 'https://' + + 'github.com/ampproject/amphtml/blob/master/contributing/DEVELOPING.md', + }, + { + 'divider': true, + 'name': 'Contributing', + 'href': 'https://github.com/ampproject/amphtml/blob/master/CONTRIBUTING.md', + }, + { + 'name': 'Github', + 'href': 'https://github.com/ampproject/amphtml/', + }, + { + 'name': 'Travis', + 'href': 'https://travis-ci.org/ampproject/amphtml', + }, + { + 'name': 'Percy', + 'href': 'https://percy.io/ampproject/amphtml/', + }, +]; + + +const requiredExtensions = [ + {name: 'amp-bind'}, + {name: 'amp-form'}, + {name: 'amp-lightbox'}, + {name: 'amp-selector'}, +]; + + +const ExtensionScript = ({name, version}) => + html``; + + +const HeaderLink = ({name, href, divider}) => html` +
  • + + ${name} + +
  • `; + + +const Header = ({isMainPage, links}) => html` +
    +

    AMP

    + +
      + ${links.map(({name, href, divider}, i) => + HeaderLink({ + divider: divider || i == links.length - 1, + name, + href, + })).join('')} +
    • ${SettingsOpenButton()}
    • +
    +
    `; + + +const HeaderBackToMainLink = () => html`← Back to main`; + + +const ExamplesDocumentModeSelectOption = ({value, name}) => html` + `; + + +const ExamplesDocumentModeSelect = ({selectModePrefix}) => html` + + + + `; + + +const SelectModeOptional = ({basepath, selectModePrefix}) => + !/^\/examples/.test(basepath) ? '' : ExamplesDocumentModeSelect({ + selectModePrefix, + }); + + +const FileListItem = ({name, href, selectModePrefix}) => { + if (!/^\/examples/.test(href) || !/\.html$/.test(href)) { + return html`
  • + ${name} +
  • `; + } + + const hrefSufix = href.replace(/^\//, ''); + + return html`
  • + + ${name} + +
  • `; +}; + + +const FileList = ({fileSet, selectModePrefix}) => html` +
      + ${fileSet.map(({name, href}) => + FileListItem({name, href, selectModePrefix})).join('')} +
    `; + + +const getFileSet = ({basepath, fileSet, selectModePrefix}) => { + // Set at top-level so RegEx is compiled once per call. + const documentLinkRegex = /\.html$/; + const examplesLinkRegex = /^\/examples\//; + + return fileSet.map(name => { + const isExamplesDocument = examplesLinkRegex.test(basepath) && + documentLinkRegex.test(name); + + const prefix = isExamplesDocument ? + basepath.replace(/^\//, selectModePrefix) : + basepath; + + return {name, href: prefix + name}; + }); +}; + + +const ProxyFormOptional = ({isMainPage}) => { + return isMainPage ? ProxyForm() : ''; +}; + + +const selectModePrefix = '/'; + +const renderTemplate = ({ + basepath, + css, + fileSet, + isMainPage, + serveMode}) => html` + + + + + AMP Dev Server + + + + + ${boilerPlate} + + ${requiredExtensions.map(({name, version}) => + ExtensionScript({name, version})).join('')} + + +
    + ${Header({isMainPage, links: headerLinks})} + ${ProxyFormOptional({isMainPage})} +
    +
    +
    +

    + ${basepath} + + Find file + +

    +
    + ${SelectModeOptional({basepath, selectModePrefix})} + List root directory +
    + ${FileList({ + fileSet: getFileSet({ + basepath, + selectModePrefix, + fileSet, + }), + selectModePrefix, + })} +
    +
    +
    + Built with 💙 by + the AMP Project. +
    + ${SettingsModal({serveMode})} + + `; + + +module.exports = {renderTemplate}; diff --git a/build-system/app-index/test/test.js b/build-system/app-index/test/test.js new file mode 100644 index 000000000000..5ac9014a103a --- /dev/null +++ b/build-system/app-index/test/test.js @@ -0,0 +1,43 @@ +/** + * Copyright 2018 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. + */ + +const assert = require('assert'); +const {join} = require('path'); + +const bundler = require('../bundler'); +const devDashboard = require('../index'); + +const expressResMock = { + end: () => {}, +}; + +describe('Tests for the dev dashboard', () => { + + it('should bundle', () => { + return bundler.bundleComponent(join(__dirname, '../components/main.js')) + .then(bundle => { + assert.ok(bundle); + }); + }); + + it('should be able to return HTML', () => { + return devDashboard.serveIndex({ + root: __dirname, + })({url: '/'}, expressResMock).then(renderedHtml => { + assert.ok(renderedHtml); + }); + }); +}); diff --git a/build-system/app-utils.js b/build-system/app-utils.js new file mode 100644 index 000000000000..e67902e6cc12 --- /dev/null +++ b/build-system/app-utils.js @@ -0,0 +1,84 @@ +/** + * Copyright 2018 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} mode + * @param {string} file + * @param {string=} hostName + * @param {boolean=} inabox + * @param {boolean=} storyV1 + */ +const replaceUrls = (mode, file, hostName, inabox, storyV1) => { + hostName = hostName || ''; + if (mode == 'default') { + // TODO:(ccordry) remove this when story 0.1 is deprecated + if (storyV1) { + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/v0\/amp-story-0\.1\.js/g, + hostName + '/dist/v0/amp-story-1.0.max.js'); + } + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/v0\.js/g, + hostName + '/dist/amp.js'); + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/shadow-v0\.js/g, + hostName + '/dist/amp-shadow.js'); + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/amp4ads-v0\.js/g, + hostName + '/dist/amp-inabox.js'); + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/v0\/(.+?).js/g, + hostName + '/dist/v0/$1.max.js'); + if (inabox) { + let filename; + if (inabox == '1') { + filename = '/dist/amp-inabox.js'; + } else if (inabox == '2') { + filename = '/dist/amp-inabox-lite.js'; + } + file = file.replace(/]*>/, ''); + file = file.replace(/\/dist\/amp\.js/g, filename); + } + } else if (mode == 'compiled') { + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/v0\.js/g, + hostName + '/dist/v0.js'); + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/shadow-v0\.js/g, + hostName + '/dist/shadow-v0.js'); + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/amp4ads-v0\.js/g, + hostName + '/dist/amp4ads-v0.js'); + file = file.replace( + /https:\/\/cdn\.ampproject\.org\/v0\/(.+?).js/g, + hostName + '/dist/v0/$1.js'); + file = file.replace( + /\/dist.3p\/current\/(.*)\.max.html/g, + hostName + '/dist.3p/current-min/$1.html'); + if (inabox) { + let filename; + if (inabox == '1') { + filename = '/dist/amp4ads-v0.js'; + } else if (inabox == '2') { + filename = '/dist/amp4ads-lite-v0.js'; + } + file = file.replace(/\/dist\/v0\.js/g, filename); + } + } + return file; +}; + +module.exports = {replaceUrls}; diff --git a/build-system/app-video-testbench.js b/build-system/app-video-testbench.js new file mode 100644 index 000000000000..280df64fcb9d --- /dev/null +++ b/build-system/app-video-testbench.js @@ -0,0 +1,391 @@ +/** + * Copyright 2018 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. + */ +/* eslint-disable */ +'use strict'; + +const BBPromise = require('bluebird'); +const fs = BBPromise.promisifyAll(require('fs')); +const {JSDOM} = require('jsdom'); +const {replaceUrls} = require('./app-utils'); + +const sourceFile = 'test/manual/amp-video.amp.html'; + +// These are taken from the respective example or validation files. +/** + * Please keep these alphabetically sorted. + */ +const requiredAttrs = { + 'amp-3q-player': { + 'data-id': 'c8dbe7f4-7f7f-11e6-a407-0cc47a188158', + }, + 'amp-brid-player': { + 'data-partner': '264', + 'data-player': '4144', + 'data-video': '13663', + }, + 'amp-brightcove': { + 'data-account': '1290862519001', + 'data-video-id': 'ref:amp-docs-sample', + 'data-player-id': 'SyIOV8yWM', + }, + 'amp-dailymotion': {'data-videoid': 'x2m8jpp'}, + 'amp-gfycat': {'data-gfyid': 'TautWhoppingCougar'}, + 'amp-ima-video': { + 'data-tag': 'https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=&debug_experiment_id=1269069638', + 'data-poster': '/examples/img/ima-poster.png', + }, + 'amp-mowplayer': {'data-mediaid': 'meeqmaqg5js'}, + + // These one seems obsolete, 404'ing with current params. + // 'amp-nexxtv-player': { + // 'data-mediaid': '71QQG852413DU7J', + // 'data-client': '761', + // }, + + 'amp-ooyala-player': { + 'data-embedcode': 'xkeHRiMjE6ls2aXoPoiqmPO6IU8HtXsg', + 'data-pcode': '5zb2wxOlZcNCe_HVT3a6cawW298X', + 'data-playerid': '26e2e3c1049c4e70ae08a242638b5c40', + 'data-playerversion': 'v4', + }, + 'amp-video-iframe': { + 'src': '/examples/amp-video-iframe/frame.html', + 'poster': 'https://placekitten.com/g/1600/900', + }, + 'amp-vimeo': {'data-videoid': '27246366'}, + 'amp-viqeo-player': { + 'data-profileid': '184', + 'data-videoid': 'b51b70cdbb06248f4438', + }, + 'amp-wistia-player': {'data-media-hashed-id': 'u8p9wq6mq8'}, + 'amp-youtube': {'data-videoid': 'mGENRKrdoGY'}, +}; + +/** + * Please keep these alphabetically sorted. + */ +const requiredInnerHtml = { + 'amp-ima-video': ``, +}; + +/** + * Please keep these alphabetically sorted. + */ +const optionalAttrs = [ + 'autoplay', + 'controls', + 'rotate-to-fullscreen', +]; + +/** + * Please keep these alpahbetically sorted. + */ +const availableExtensions = [ + 'amp-3q-player', + 'amp-brid-player', + 'amp-brightcove', + 'amp-dailymotion', + 'amp-gfycat', + 'amp-ima-video', + 'amp-mowplayer', + 'amp-ooyala-player', + 'amp-video', + 'amp-video-iframe', + 'amp-viqeo-player', + 'amp-wistia-player', + 'amp-youtube', + + // TODO(alanorozco): Reenable with valid params, if possible. + // 'amp-nexxtv-player', +]; + + +const clientScript = ` +var urlParams = new URLSearchParams(window.location.search); + +function logAnalyticsEvent(url) { + var urlParams = new URLSearchParams(url.split('?', 2)[1]); + appendAnalyticsRow(urlParams); +} + +function appendAnalyticsRow(urlParams) { + var container = document.querySelector('.analytics-events-container'); + var table = document.getElementById('analytics-events'); + table.appendChild(createTableRow([ + getHoursMinutesSeconds(), + urlParams.get('autoplay'), + urlParams.get('type'), + urlParams.get('time'), + urlParams.get('total'), + urlParams.get('duration'), + ])); + container./*OK*/scrollTop = container./*OK*/scrollHeight; +} + +function getHoursMinutesSeconds() { + var date = new Date(); + return padTo2(date.getHours()) + ':' + + padTo2(date.getMinutes()) + ':' + + padTo2(date.getSeconds()); +} + +function padTo2(number) { + if (number < 10) { + return '0' + number; + } + return number.toString(); +} + +function createTableRow(cellsContent) { + var row = document.createElement('tr'); + for (var i = 0; i < cellsContent.length; i++) { + row.appendChild(createTableCell(cellsContent[i])); + } + return row; +} + +function createTableCell(contents) { + var cell = document.createElement('td'); + cell./*OK*/innerText = contents; + return cell; +} + +function monkeyPatchXhr(xhrPrototype) { + var defaultOpen = xhrPrototype.open; + xhrPrototype.open = function(unusedMethod, url) { + if ((new RegExp('^https://foo\.com/')).test(url)) { + logAnalyticsEvent(url); + } + return defaultOpen.apply(this, arguments); + }; +} + +function main() { + monkeyPatchXhr(XMLHttpRequest.prototype); + + var dropdown = document.querySelector('select'); + dropdown.onchange = function() { + replaceExtension(urlParams, dropdown.value); + reloadFrom(urlParams); + }; + + var checkboxes = document.querySelectorAll('.optional-attrs-container input'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].addEventListener('change', function(event) { + urlParams.delete(event.target.value); + urlParams.set(event.target.value, event.target.checked ? '1' : '0'); + reloadFrom(urlParams); + }); + } +} + +function reloadFrom(params) { + var baseUrl = + [location.protocol, '//', location.host, location.pathname].join(''); + window.location = baseUrl + '?' + params; +} + +function replaceExtension(params, withExtension) { + params.delete('extension'); + params.set('extension', withExtension); +} + +main(); +`; + + +function getSubstitutable(doc) { + return doc.querySelector('[data-substitutable]'); +} + + +function renderExtensionDropdown(doc, opt_extension) { + const select = doc.createElement('select'); + + const usedExtension = + opt_extension || getSubstitutable(doc).tagName.toLowerCase(); + + availableExtensions.forEach(extension => { + const option = doc.createElement('option'); + option.setAttribute('value', extension); + option./*OK*/innerHTML = extension; + + if (extension == usedExtension) { + option.setAttribute('selected', ''); + } + + select.appendChild(option); + }); + + return select; +} + + +function renderOptionalAttrsCheckboxes(doc) { + const fragment = doc.createDocumentFragment(); + const substitutable = getSubstitutable(doc); + + optionalAttrs.forEach(attr => { + const id = `optional-attr-${attr}`; + const label = doc.createElement('label'); + const input = doc.createElement('input'); + + label.setAttribute('for', id); + + input.id = id; + input.setAttribute('type', 'checkbox'); + input.value = attr; + + if (substitutable.hasAttribute(attr)) { + input.setAttribute('checked', ''); + } + + label.appendChild(input); + label./*OK*/innerHTML += ` ${attr}`; + + fragment.appendChild(label); + }); + + return fragment; +} + + +function replaceTagName(node, withTagName) { + const {tagName} = node; + + node./*OK*/outerHTML = + node./*OK*/outerHTML + .replace(new RegExp(`^\<${tagName}`, 'i'), `<${withTagName}`) + .replace(new RegExp(`\$`, 'i'), ``); +} + + +function replaceCustomElementScript( + doc, fromExtension, toExtension, version = '0.1') { + + const selector = `script[custom-element=${fromExtension}]`; + const script = doc.querySelector(selector); + + script.setAttribute('custom-element', toExtension); + + // TODO(alanorozco): Use config.urls.cdn value. This file is not available + // under the Node.JS context. + script.setAttribute('src', + `https://cdn.ampproject.org/v0/${toExtension}-${version}.js`); +} + + +function removeAttrs(node) { + node.getAttribute('data-removable-attrs').split(',').forEach(attr => { + node.removeAttribute(attr); + }); +} + + +function replaceExtension(doc, toExtension) { + const substitutable = getSubstitutable(doc); + + const substitutableTagNameLowerCase = substitutable.tagName.toLowerCase(); + const toExtensionLowerCase = toExtension.toLowerCase(); + + if (substitutableTagNameLowerCase == toExtensionLowerCase) { + return; + } + + replaceCustomElementScript(doc, substitutableTagNameLowerCase, toExtension); + removeAttrs(substitutable); + + if (requiredAttrs[toExtensionLowerCase]) { + const attrs = requiredAttrs[toExtensionLowerCase]; + Object.keys(attrs).forEach(attr => { + substitutable.setAttribute(attr, attrs[attr]); + }); + } + + substitutable./*OK*/innerHTML = requiredInnerHtml[toExtensionLowerCase] || ''; + + // `replaceTagName` has to run at the end since it manipulates `outerHTML`. + replaceTagName(substitutable, toExtension); +} + + +function setOptionalAttrs(req, doc) { + const substitutable = getSubstitutable(doc); + + optionalAttrs.forEach(attr => { + if (!req.query[attr]) { + return; + } + if (req.query[attr] == '1') { + substitutable.setAttribute(attr, ''); + } else { + substitutable.removeAttribute(attr); + } + }); +} + + +function appendClientScript(doc) { + const script = doc.createElement('script'); + script./*OK*/innerHTML = clientScript; + doc.body.appendChild(script); +} + + +function isValidExtension(extension) { + return availableExtensions.includes(extension); +} + + +function runVideoTestBench(req, res, next) { + const mode = process.env.SERVE_MODE; + fs.readFileAsync(sourceFile).then(contents => { + const dom = new JSDOM(contents); + const {window} = dom; + const doc = window.document; + + const {extension} = req.query; + + setOptionalAttrs(req, doc); + + if (extension) { + if (!isValidExtension(extension)) { + res.status(403); + res.end('Invalid extension parameter.'); + return; + } + replaceExtension(doc, extension); + } + + const dropdownContainer = doc.querySelector('.dropdown-container'); + dropdownContainer.appendChild(renderExtensionDropdown(doc), extension); + + const optionalAttrsContainer = + doc.querySelector('.optional-attrs-container'); + optionalAttrsContainer.appendChild(renderOptionalAttrsCheckboxes(doc)); + + appendClientScript(doc); + + return res.end(replaceUrls(mode, dom.serialize())); + }).error(() => { + next(); + }); +} + + +module.exports = runVideoTestBench; diff --git a/build-system/app.js b/build-system/app.js new file mode 100644 index 000000000000..c26a63fdc903 --- /dev/null +++ b/build-system/app.js @@ -0,0 +1,1314 @@ +/** + * 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. + */ +'use strict'; + +/** + * @fileoverview Creates an http server to handle static + * files and list directories for use with the gulp live server + */ +const app = require('express')(); +const bacon = require('baconipsum'); +const BBPromise = require('bluebird'); +const bodyParser = require('body-parser'); +const devDashboard = require('./app-index/index'); +const formidable = require('formidable'); +const fs = BBPromise.promisifyAll(require('fs')); +const jsdom = require('jsdom'); +const multer = require('multer'); +const path = require('path'); +const request = require('request'); +const pc = process; +const countries = require('../examples/countries.json'); +const runVideoTestBench = require('./app-video-testbench'); +const {replaceUrls} = require('./app-utils'); + +app.use(bodyParser.text()); +app.use('/amp4test', require('./amp4test')); + +// Append ?csp=1 to the URL to turn on the CSP header. +// TODO: shall we turn on CSP all the time? +app.use((req, res, next) => { + if (req.query.csp) { + res.set({ + 'content-security-policy': "default-src * blob: data:; script-src https://cdn.ampproject.org/rtv/ https://cdn.ampproject.org/v0.js https://cdn.ampproject.org/v0/ https://cdn.ampproject.org/viewer/ http://localhost:8000 https://localhost:8000; object-src 'none'; style-src 'unsafe-inline' https://cdn.ampproject.org/rtv/ https://cdn.materialdesignicons.com https://cloud.typography.com https://fast.fonts.net https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://p.typekit.net https://use.fontawesome.com https://use.typekit.net; report-uri https://csp-collector.appspot.com/csp/amp", + }); + } + next(); +}); + +function isValidServeMode(serveMode) { + return ['default', 'compiled', 'cdn'].includes(serveMode); +} + +function setServeMode(serveMode) { + pc.env.SERVE_MODE = serveMode; +} + +app.get('/serve_mode=:mode', (req, res) => { + const newMode = req.params.mode; + if (isValidServeMode(newMode)) { + setServeMode(newMode); + res.send(`

    Serve mode changed to ${newMode}

    `); + } else { + const info = '

    Serve mode ' + newMode + ' is not supported.

    '; + res.status(400).send(info); + } +}); + +if (!global.AMP_TESTING) { + if (process.env.DISABLE_DEV_DASHBOARD_CACHE && + process.env.DISABLE_DEV_DASHBOARD_CACHE !== 'false') { + devDashboard.setCacheStatus(false); + } + + app.get(['/', '/*'], devDashboard.serveIndex({ + // Sitting on build-system/, so we go back one dir for the repo root. + root: path.join(__dirname, '../'), + mapBasepath(url) { + // Serve /examples/ on main page. + if (url == '/') { + return '/examples'; + } + // Serve root on /~ as a fallback. + if (url == '/~') { + return '/'; + } + // Serve basepath from URL otherwise. + return url; + }, + })); + + app.get('/serve_mode.json', (req, res) => { + res.json({serveMode: pc.env.SERVE_MODE || 'default'}); + }); + + app.get('/serve_mode_change', (req, res) => { + const sourceOrigin = req.query['__amp_source_origin']; + if (sourceOrigin) { + res.setHeader('AMP-Access-Control-Allow-Source-Origin', sourceOrigin); + } + const {mode} = req.query; + if (isValidServeMode(mode)) { + setServeMode(mode); + res.json({ok: true}); + return; + } + res.status(400).json({ok: false}); + }); + + app.get('/proxy', (req, res) => { + const sufix = req.query.url.replace(/^http(s?):\/\//i, ''); + res.redirect(`proxy/s/${sufix}`); + }); +} + +// Deprecate usage of .min.html/.max.html +app.get([ + '/examples/*.(min|max).html', + '/test/manual/*.(min|max).html', + '/dist/cache-sw.(min|max).html', +], (req, res) => { + const filePath = req.url; + res.send(generateInfo(filePath)); +}); + +app.use('/pwa', (req, res) => { + let file; + let contentType; + if (!req.url || req.path == '/') { + // 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 if (req.url == '/ampdoc-shell') { + // pwa-ampdoc-shell.html + contentType = 'text/html'; + file = '/examples/pwa/pwa-ampdoc-shell.html'; + } 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(pc.cwd() + file).then(file => { + res.end(file); + }); +}); + +app.use('/api/show', (req, res) => { + res.json({ + showNotification: true, + }); +}); + +app.use('/api/dont-show', (req, res) => { + res.json({ + showNotification: false, + }); +}); + +app.use('/api/echo/post', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.end(req.body); +}); + +app.use('/analytics/:type', (req, res) => { + console.log('Analytics event received: ' + req.params.type); + console.log(req.query); + res.status(204).send(); +}); + +/** + * 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', (req, res) => { + assertCors(req, res, ['POST']); + + const form = new formidable.IncomingForm(); + form.parse(req, (err, fields) => { + res.setHeader('Content-Type', 'text/html'); + if (fields['email'] == 'already@subscribed.com') { + res.statusCode = 500; + res.end(` +

    Sorry ${fields['name']}!

    +

    The email ${fields['email']} is already subscribed!

    + `); + } else { + res.end(` +

    Thanks ${fields['name']}!

    +

    Please make sure to confirm your email ${fields['email']}

    + `); + } + }); +}); + + +app.use('/form/redirect-to/post', (req, res) => { + assertCors(req, res, ['POST'], ['AMP-Redirect-To']); + res.setHeader('AMP-Redirect-To', 'https://google.com'); + res.end('{}'); +}); + + +app.use('/form/echo-json/post', (req, res) => { + assertCors(req, res, ['POST']); + const form = new formidable.IncomingForm(); + form.parse(req, (err, fields) => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + if (fields['email'] == 'already@subscribed.com') { + res.statusCode = 500; + } + res.end(JSON.stringify(fields)); + }); +}); + +app.use('/form/json/poll1', (req, res) => { + assertCors(req, res, ['POST']); + const form = new formidable.IncomingForm(); + form.parse(req, () => { + 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), + }], + })); + }); +}); + +const upload = multer(); + +app.post('/form/json/upload', upload.fields([{name: 'myFile'}]), (req, res) => { + assertCors(req, res, ['POST']); + + /** @type {!Array|undefined} */ + const myFile = req.files['myFile']; + + if (!myFile) { + res.json({message: 'No file data received'}); + return; + } + const fileData = myFile[0]; + const contents = fileData.buffer.toString(); + + res.json({message: contents}); +}); + +app.use('/form/search-html/get', (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', (req, res) => { + assertCors(req, res, ['GET']); + res.json({ + term: req.query.term, + results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}], + }); +}); + +const autosuggestLanguages = ['ActionScript', 'AppleScript', 'Asp', 'BASIC', + 'C', 'C++', 'Clojure', 'COBOL', 'ColdFusion', 'Erlang', 'Fortran', 'Go', + 'Groovy', 'Haskell', 'Java', 'JavaScript', 'Lisp', 'Perl', 'PHP', 'Python', + 'Ruby', 'Scala', 'Scheme']; + +app.use('/form/autosuggest/query', (req, res) => { + assertCors(req, res, ['GET']); + const MAX_RESULTS = 4; + const query = req.query.q; + if (!query) { + res.json({items: [{ + results: autosuggestLanguages.slice(0, MAX_RESULTS), + }]}); + } else { + const lowerCaseQuery = query.toLowerCase(); + const filtered = autosuggestLanguages.filter( + l => l.toLowerCase().includes(lowerCaseQuery)); + res.json({items: [{ + results: filtered.slice(0, MAX_RESULTS)}, + ]}); + } +}); + +app.use('/form/autosuggest/search', (req, res) => { + assertCors(req, res, ['POST']); + const form = new formidable.IncomingForm(); + form.parse(req, function(err, fields) { + res.json({ + query: fields.query, + results: [{title: 'Result 1'}, {title: 'Result 2'}, {title: 'Result 3'}], + }); + }); +}); + +app.use('/form/verify-search-json/post', (req, res) => { + assertCors(req, res, ['POST']); + const form = new formidable.IncomingForm(); + form.parse(req, (err, fields) => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + + const errors = []; + if (!fields.phone.match(/^650/)) { + errors.push({name: 'phone', message: 'Phone must start with 650'}); + } + if (fields.name !== 'Frank') { + errors.push({name: 'name', message: 'Please set your name to be Frank'}); + } + if (fields.error === 'true') { + errors.push({message: 'You asked for an error, you get an error.'}); + } + if (fields.city !== 'Mountain View' || fields.zip !== '94043') { + errors.push({ + name: 'city', + message: 'City doesn\'t match zip (Mountain View and 94043)', + }); + } + + if (errors.length === 0) { + res.end(JSON.stringify({ + results: [ + {title: 'Result 1'}, + {title: 'Result 2'}, + {title: 'Result 3'}, + ], + committed: true, + })); + } else { + res.statusCode = 400; + res.end(JSON.stringify({verifyErrors: errors})); + } + }); +}); + +app.use('/share-tracking/get-outgoing-fragment', (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, mode) { + const url = 'https://cdn.ampproject.org/' + + (req.query['amp_js_v'] ? 'v' : 'c') + + req.url; + console.log('Fetching URL: ' + url); + request(url, function(error, response, body) { + body = body + // Unversion URLs. + .replace(/https\:\/\/cdn\.ampproject\.org\/rtv\/\d+\//g, + 'https://cdn.ampproject.org/') + // href pointing to the proxy, so that images, etc. still work. + .replace('', ''); + const inabox = req.query['inabox']; + // TODO(ccordry): Remove this when story v01 is depricated. + const storyV1 = req.query['story_v'] === '1'; + const urlPrefix = getUrlPrefix(req); + body = replaceUrls(mode, body, urlPrefix, inabox, storyV1); + if (inabox) { + // Allow CORS requests for A4A. + const origin = req.headers.origin || urlPrefix; + enableCors(req, res, origin); + } + res.status(response.statusCode).send(body); + }); +} + + +let itemCtr = 2; +const doctype = '\n'; +const liveListDocs = Object.create(null); +app.use('/examples/live-list-update(-reverse)?.amp.html', (req, res, next) => { + const mode = pc.env.SERVE_MODE; + let liveListDoc = liveListDocs[req.baseUrl]; + if (mode != 'compiled' && mode != 'default') { + // Only handle compile(prev min)/default (prev max) mode + next(); + return; + } + // 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) { + let outerHTML = liveListDoc.documentElement./*OK*/outerHTML; + outerHTML = replaceUrls(mode, outerHTML); + res.send(`${doctype}${outerHTML}`); + return; + } + if (!liveListDoc) { + const liveListUpdateFullPath = `${pc.cwd()}${req.baseUrl}`; + console.log('liveListUpdateFullPath', liveListUpdateFullPath); + const liveListFile = fs.readFileSync(liveListUpdateFullPath); + liveListDoc = liveListDocs[req.baseUrl] = new jsdom.JSDOM(liveListFile) + .window.document; + liveListDoc.ctr = 0; + } + const liveList = liveListDoc.querySelector('#my-live-list'); + const perPage = Number(liveList.getAttribute('data-max-items-per-page')); + const items = liveList.querySelector('[items]'); + const pagination = liveListDoc.querySelector('#my-live-list [pagination]'); + const item1 = liveList.querySelector('#list-item-1'); + if (liveListDoc.ctr != 0) { + if (Math.random() < .8) { + // Always run a replace on the first item + liveListReplace(item1); + + if (Math.random() < .5) { + liveListTombstone(liveList); + } + + if (Math.random() < .8) { + liveListInsert(liveList, item1); + } + pagination.textContent = ''; + const liveChildren = [].slice.call(items.children) + .filter(x => !x.hasAttribute('data-tombstone')); + + const pageCount = Math.ceil(liveChildren.length / perPage); + const pageListItems = Array.apply(null, Array(pageCount)) + .map((_, i) => `
  • ${i + 1}
  • `).join(''); + const newPagination = ''; + pagination./*OK*/innerHTML = newPagination; + } else { + // Sometimes we want an empty response to simulate no changes. + res.send(`${doctype}`); + return; + } + } + let outerHTML = liveListDoc.documentElement./*OK*/outerHTML; + outerHTML = replaceUrls(mode, outerHTML); + liveListDoc.ctr++; + res.send(`${doctype}${outerHTML}`); +}); + +function liveListReplace(item) { + item.setAttribute('data-update-time', Date.now()); + const itemContents = item.querySelectorAll('.content'); + itemContents[0].textContent = Math.floor(Math.random() * 10); + itemContents[1].textContent = Math.floor(Math.random() * 10); +} + +function liveListInsert(liveList, node) { + const iterCount = Math.floor(Math.random() * 2) + 1; + console.log(`inserting ${iterCount} item(s)`); + for (let i = 0; i < iterCount; i++) { + const child = node.cloneNode(true); + child.setAttribute('id', `list-item-${itemCtr++}`); + child.setAttribute('data-sort-time', Date.now()); + liveList.querySelector('[items]').appendChild(child); + } +} + +function liveListTombstone(liveList) { + const tombstoneId = Math.floor(Math.random() * itemCtr); + console.log(`trying to tombstone #list-item-${tombstoneId}`); + // We can tombstone any list item except item-1 since we always do a + // replace example on item-1. + if (tombstoneId != 1) { + const item = liveList./*OK*/querySelector(`#list-item-${tombstoneId}`); + if (item) { + item.setAttribute('data-tombstone', ''); + } + } +} + + +// Generate a random number between min and max +// Value is inclusive of both min and max values. +function range(min, max) { + const values = + Array.apply(null, new Array(max - min + 1)).map((_, i) => min + i); + return values[Math.round(Math.random() * (max - min))]; +} + +// Returns the result of a coin flip, true or false +function flip() { + return !!Math.floor(Math.random() * 2); +} + +function getLiveBlogItem() { + const now = Date.now(); + // Generate a 3 to 7 worded headline + const headline = bacon(range(3, 7)); + const numOfParagraphs = range(1, 2); + const body = Array.apply(null, new Array(numOfParagraphs)).map(() => { + return `

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

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

    + ${headline} +

    +
    + +
    +
    ${body}
    + ${img} + +
    +
    +
    `; +} + +function getLiveBlogItemWithBindAttributes() { + const now = Date.now(); + // Generate a 3 to 7 worded headline + const numOfParagraphs = range(1, 2); + const body = Array.apply(null, new Array(numOfParagraphs)).map(() => { + return `

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

    `; + }).join('\n'); + + return ` + + +
    +
    +
    + ${body} +

    As you can see, bacon is far superior to + everything!!

    +
    +
    +
    +
    `; +} + +app.use('/examples/live-blog(-non-floating-button)?.amp.html', + (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/bind/live-list.amp.html', + (req, res, next) => { + if ('amp_latest_update_time' in req.query) { + res.setHeader('Content-Type', 'text/html'); + res.end(getLiveBlogItemWithBindAttributes()); + return; + } + next(); + }); + +app.use('/impression-proxy/', (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); + + // Or fake response with status 204 if viewer replaceUrl is provided +}); + +app.post('/get-consent-v1/', (req, res) => { + assertCors(req, res, ['POST']); + const body = { + 'promptIfUnknown': true, + 'sharedData': { + 'tfua': true, + 'coppa': true, + }, + }; + res.json(body); +}); + +app.post('/get-consent-no-prompt/', (req, res) => { + assertCors(req, res, ['POST']); + const body = {}; + res.json(body); +}); + +// Proxy with local JS. +// Example: +// http://localhost:8000/proxy/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('/proxy/', (req, res) => { + const mode = pc.env.SERVE_MODE; + proxyToAmpProxy(req, res, mode); +}); + +// Nest the response in an iframe. +// Example: +// http://localhost:8000/iframe/examples/ads.amp.html +app.get('/iframe/*', (req, res) => { + // Returns an html blob with an iframe pointing to the url after /iframe/. + res.send(` + + + + + `); +}); + +app.get('/a4a_template/*', (req, res) => { + assertCors(req, res, ['GET'], undefined, true); + const match = /^\/a4a_template\/([a-z-]+)\/(\d+)$/.exec(req.path); + if (!match) { + res.status(404); + res.end('Invalid path: ' + req.path); + return; + } + const filePath = `${pc.cwd()}/extensions/amp-ad-network-${match[1]}-impl/` + + `0.1/data/${match[2]}.template`; + fs.readFileAsync(filePath).then(file => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('AMP-template-amp-creative', 'amp-mustache'); + res.end(file); + }).error(() => { + res.status(404); + res.end('Not found: ' + filePath); + }); +}); + +// Returns a document that echoes any post messages received from parent. +// An optional `message` query param can be appended for an initial post +// message sent on document load. +// Example: +// http://localhost:8000/iframe-echo-message?message=${payload} +app.get('/iframe-echo-message', (req, res) => { + const {message} = req.query; + res.send( + ` + + + + `); +}); + +// A4A envelope. +// Examples: +// http://localhost:8000/a4a[-3p]/examples/animations.amp.html +// http://localhost:8000/a4a[-3p]/proxy/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)/', (req, res) => { + const force3p = req.baseUrl.indexOf('/a4a-3p') == 0; + let adUrl = req.url; + const templatePath = '/build-system/server-a4a-template.html'; + const urlPrefix = getUrlPrefix(req); + if (!adUrl.startsWith('/proxy') && + 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; + } + adUrl = addQueryParam(adUrl, 'inabox', 1); + fs.readFileAsync(pc.cwd() + templatePath, 'utf8').then(template => { + const result = template + .replace(/CHECKSIG/g, force3p || '') + .replace(/DISABLE3PFALLBACK/g, !force3p) + .replace(/OFFSET/g, req.query.offset || '0px') + .replace(/AD_URL/g, adUrl) + .replace(/AD_WIDTH/g, req.query.width || '300') + .replace(/AD_HEIGHT/g, req.query.height || '250'); + res.end(result); + }); +}); + +// In-a-box envelope. +// Examples: +// http://localhost:8000/inabox/examples/animations.amp.html +// http://localhost:8000/inabox/proxy/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('/inabox/:version/', (req, res) => { + let adUrl = req.url; + const templatePath = '/build-system/server-inabox-template.html'; + const urlPrefix = getUrlPrefix(req); + if (!adUrl.startsWith('/proxy') && // Ignore /proxy + 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; + } + adUrl = addQueryParam(adUrl, 'inabox', req.params['version']); + fs.readFileAsync(pc.cwd() + templatePath, 'utf8').then(template => { + const result = template + .replace(/AD_URL/g, adUrl) + .replace(/OFFSET/g, req.query.offset || '0px') + .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', (req, res, next) => { + res.setHeader('AMP-Access-Control-Allow-Source-Origin', getUrlPrefix(req)); + next(); +}); + +app.use(['/examples/*', '/extensions/*'], (req, res, next) => { + const 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'], (req, res, next) => { + const sleep = parseInt(req.query.sleep || 0, 10) * 1000; + setTimeout(next, sleep); +}); + +/** + * Video testbench endpoint + */ +app.get('/test/manual/amp-video.amp.html', runVideoTestBench); + +app.get(['/examples/*.html', '/test/manual/*.html'], (req, res, next) => { + const filePath = req.path; + const mode = pc.env.SERVE_MODE; + const inabox = req.query['inabox']; + const stream = Number(req.query['stream']); + fs.readFileAsync(pc.cwd() + filePath, 'utf8').then(file => { + if (req.query['amp_js_v']) { + file = addViewerIntegrationScript(req.query['amp_js_v'], file); + } + + + if (inabox && req.headers.origin && req.query.__amp_source_origin) { + // Allow CORS requests for A4A. + enableCors(req, res, req.headers.origin); + } else { + file = replaceUrls(mode, file, '', inabox); + } + + // Extract amp-ad for the given 'type' specified in URL query. + if (req.path.indexOf('/examples/ads.amp.html') == 0 && req.query.type) { + const ads = file.match( + elementExtractor('(amp-ad|amp-embed)', req.query.type)); + file = file.replace( + /[\s\S]+<\/body>/m, '' + ads.join('') + ''); + } + + // Extract amp-analytics for the given 'type' specified in URL query. + if (req.path.indexOf( + '/examples/analytics-vendors.amp.html') == 0 && req.query.type) { + const analytics = file.match( + elementExtractor('amp-analytics', req.query.type)); + file = file.replace( + /
    [\s\S]+<\/div>/m, + '
    ' + analytics.join('') + '
    '); + } + + if (stream > 0) { + res.writeHead(200, {'Content-Type': 'text/html'}); + let pos = 0; + const writeChunk = function() { + const chunk = file.substring(pos, Math.min(pos + stream, file.length)); + res.write(chunk); + pos += stream; + if (pos < file.length) { + setTimeout(writeChunk, 500); + } else { + res.end(); + } + }; + writeChunk(); + } else { + res.send(file); + } + }).catch(() => { + next(); + }); +}); + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function elementExtractor(tagName, type) { + type = escapeRegExp(type); + return new RegExp( + `<${tagName} [^>]*['"]${type}['"][^>]*>([\\s\\S]+?)`, + 'gm'); +} + +// Data for example: http://localhost:8000/examples/bind/xhr.amp.html +app.use('/bind/form/get', (req, res) => { + assertCors(req, res, ['GET']); + res.json({ + bindXhrResult: 'I was fetched from the server!', + }); +}); + +// Data for example: http://localhost:8000/examples/bind/ecommerce.amp.html +app.use('/bind/ecommerce/sizes', (req, res) => { + assertCors(req, res, ['GET']); + setTimeout(() => { + const prices = { + '0': { + 'sizes': { + 'XS': 8.99, + 'S': 9.99, + }, + }, + '1': { + 'sizes': { + 'S': 10.99, + 'M': 12.99, + 'L': 14.99, + }, + }, + '2': { + 'sizes': { + 'L': 11.99, + 'XL': 13.99, + }, + }, + '3': { + 'sizes': { + 'M': 7.99, + 'L': 9.99, + 'XL': 11.99, + }, + }, + '4': { + 'sizes': { + 'XS': 8.99, + 'S': 10.99, + 'L': 15.99, + }, + }, + '5': { + 'sizes': { + 'S': 8.99, + 'L': 14.99, + 'XL': 11.99, + }, + }, + '6': { + 'sizes': { + 'XS': 8.99, + 'S': 9.99, + 'M': 12.99, + }, + }, + '7': { + 'sizes': { + 'M': 10.99, + 'L': 11.99, + }, + }, + }; + const object = {}; + object[req.query.shirt] = prices[req.query.shirt]; + res.json(object); + }, 1000); // Simulate network delay. +}); + +app.use('/list/fruit-data/get', (req, res) => { + assertCors(req, res, ['GET']); + res.json({ + items: [ + {name: 'apple', quantity: 47, unitPrice: '0.33'}, + {name: 'pear', quantity: 538, unitPrice: '0.54'}, + {name: 'tomato', quantity: 0, unitPrice: '0.23'}, + ], + }); +}); + +app.use('/list/vegetable-data/get', (req, res) => { + assertCors(req, res, ['GET']); + res.json({ + items: [ + {name: 'cabbage', quantity: 5, unitPrice: '1.05'}, + {name: 'carrot', quantity: 10, unitPrice: '0.01'}, + {name: 'brocoli', quantity: 7, unitPrice: '0.02'}, + ], + }); +}); + +// Simulated subscription entitlement +app.use('/subscription/:id/entitlements', (req, res) => { + assertCors(req, res, ['GET']); + res.json({ + source: 'local' + req.params.id, + granted: true, + grantedReason: 'NOT_SUBSCRIBED', + data: { + login: true, + }, + }); +}); + +app.use('/subscription/pingback', (req, res) => { + assertCors(req, res, ['POST']); + res.json({ + done: true, + }); +}); + +// Simulated adzerk ad server and AMP cache CDN. +app.get('/adzerk/*', (req, res) => { + assertCors(req, res, ['GET'], ['AMP-template-amp-creative']); + const match = /\/(\d+)/.exec(req.path); + if (!match || !match[1]) { + res.status(404); + res.end('Invalid path: ' + req.path); + return; + } + const filePath = + pc.cwd() + '/extensions/amp-ad-network-adzerk-impl/0.1/data/' + match[1]; + fs.readFileAsync(filePath).then(file => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('AMP-Ad-Template-Extension', 'amp-mustache'); + res.setHeader('AMP-Ad-Response-Type', 'template'); + res.end(file); + }).error(() => { + res.status(404); + res.end('Not found: ' + filePath); + }); +}); + +/* + * Serve extension scripts and their source maps. + */ +app.get(['/dist/rtv/*/v0/*.js', '/dist/rtv/*/v0/*.js.map'], + (req, res, next) => { + const mode = pc.env.SERVE_MODE; + const fileName = path.basename(req.path).replace('.max.', '.'); + let filePath = 'https://cdn.ampproject.org/v0/' + fileName; + if (mode == 'cdn') { + // This will not be useful until extension-location.js change in prod + // Require url from cdn + request(filePath, (error, response) => { + if (error) { + res.status(404); + res.end(); + } else { + res.send(response); + } + }); + return; + } + const isJsMap = filePath.endsWith('.map'); + if (isJsMap) { + filePath = filePath.replace(/\.js\.map$/, '\.js'); + } + filePath = replaceUrls(mode, filePath); + req.url = filePath + (isJsMap ? '.map' : ''); + next(); + }); + +/** + * Serve entry point script url + */ +app.get(['/dist/sw.js', '/dist/sw-kill.js', '/dist/ww.js'], + (req, res, next) => { + // Special case for entry point script url. Use compiled for testing + const mode = pc.env.SERVE_MODE; + const fileName = path.basename(req.path); + if (mode == 'cdn') { + // This will not be useful until extension-location.js change in prod + // Require url from cdn + const filePath = 'https://cdn.ampproject.org/' + fileName; + request(filePath, function(error, response) { + if (error) { + res.status(404); + res.end(); + } else { + res.send(response); + } + }); + return; + } + if (mode == 'default') { + req.url = req.url.replace(/\.js$/, '.max.js'); + } + next(); + }); + +app.get('/dist/iframe-transport-client-lib.js', (req, res, next) => { + req.url = req.url.replace(/dist/, 'dist.3p/current'); + next(); +}); + +/* + * Start Cache SW LOCALDEV section + */ +app.get('/dist/sw(.max)?.js', (req, res, next) => { + const filePath = req.path; + fs.readFileAsync(pc.cwd() + filePath, 'utf8').then(file => { + let n = new Date(); + // Round down to the nearest 5 minutes. + n -= ((n.getMinutes() % 5) * 1000 * 60) + + (n.getSeconds() * 1000) + n.getMilliseconds(); + file = 'self.AMP_CONFIG = {v: "99' + n + '",' + + 'cdnUrl: "http://localhost:8000/dist"};' + + file; + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Date', new Date().toUTCString()); + res.setHeader('Cache-Control', 'no-cache;max-age=150'); + res.end(file); + }).catch(next); +}); + +app.get('/dist/rtv/9[89]*/*.js', (req, res, next) => { + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Date', new Date().toUTCString()); + res.setHeader('Cache-Control', 'no-cache;max-age=31536000'); + + setTimeout(() => { + // Cause a delay, to show the "stale-while-revalidate" + if (req.path.includes('v0.js')) { + const path = req.path.replace(/rtv\/\d+/, ''); + return fs.readFileAsync(pc.cwd() + path, 'utf8') + .then(file => { + res.end(file); + }).catch(next); + } + + res.end(` + const li = document.createElement('li'); + li.textContent = '${req.path}'; + loaded.appendChild(li); + `); + }, 2000); +}); + +app.get(['/dist/cache-sw.html'], (req, res, next) => { + const filePath = '/test/manual/cache-sw.html'; + fs.readFileAsync(pc.cwd() + filePath, 'utf8').then(file => { + let n = new Date(); + // Round down to the nearest 5 minutes. + n -= ((n.getMinutes() % 5) * 1000 * 60) + + (n.getSeconds() * 1000) + n.getMilliseconds(); + const percent = parseFloat(req.query.canary) || 0.01; + let env = '99'; + if (Math.random() < percent) { + env = '98'; + n += 5 * 1000 * 60; + } + file = file.replace(/dist\/v0/g, `dist/rtv/${env}${n}/v0`); + file = file.replace(/CURRENT_RTV/, env + n); + + res.setHeader('Content-Type', 'text/html'); + res.end(file); + }).catch(next); +}); + +app.get('/dist/diversions', (req, res) => { + let n = new Date(); + // Round down to the nearest 5 minutes. + n -= ((n.getMinutes() % 5) * 1000 * 60) + + (n.getSeconds() * 1000) + n.getMilliseconds(); + n += 5 * 1000 * 60; + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Date', new Date().toUTCString()); + res.setHeader('Cache-Control', 'no-cache;max-age=150'); + res.end(JSON.stringify(['98' + n])); +}); + +/* + * End Cache SW LOCALDEV section + */ + +/** + * Web worker binary. + */ +app.get('/dist/ww(.max)?.js', (req, res) => { + fs.readFileAsync(pc.cwd() + req.path).then(file => { + res.setHeader('Content-Type', 'text/javascript'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end(file); + }); +}); + +/** + * Autosuggest endpoint + */ +app.get('/search/countries', function(req, res) { + let filtered = []; + if (req.query.hasOwnProperty('q')) { + const query = req.query.q.toLowerCase(); + + filtered = countries.items + .filter(country => country.name.toLowerCase().startsWith(query)); + } + + const results = { + 'items': [ + { + 'results': filtered, + }, + ], + }; + res.send(results); +}); + +/** + * @param {string} ampJsVersion + * @param {string} file + */ +function addViewerIntegrationScript(ampJsVersion, file) { + ampJsVersion = parseFloat(ampJsVersion); + if (!ampJsVersion) { + return file; + } + let viewerScript; + if (Number.isInteger(ampJsVersion)) { // eslint-disable-line amphtml-internal/no-es2015-number-props + // Viewer integration script from gws, such as + // https://cdn.ampproject.org/viewer/google/v7.js + viewerScript = + ''; + } else { + // Viewer integration script from runtime, such as + // https://cdn.ampproject.org/v0/amp-viewer-integration-0.1.js + viewerScript = ''; + } + file = file.replace('', viewerScript + ''); + return file; +} + +function getUrlPrefix(req) { + return req.protocol + '://' + req.headers.host; +} + +/** + * @param {string} url + * @param {string} param + * @param {*} value + * @return {string} + */ +function addQueryParam(url, param, value) { + const paramValue = + encodeURIComponent(param) + '=' + encodeURIComponent(value); + if (!url.includes('?')) { + url += '?' + paramValue; + } else { + url += '&' + paramValue; + } + return url; +} + +function enableCors(req, res, origin, opt_exposeHeaders) { + 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(', ')); + if (req.query.__amp_source_origin) { + res.setHeader('AMP-Access-Control-Allow-Source-Origin', + req.query.__amp_source_origin); + } +} + +function assertCors(req, res, opt_validMethods, opt_exposeHeaders, + opt_ignoreMissingSourceOrigin) { + // Allow disable CORS check (iframe fixtures have origin 'about:srcdoc'). + if (req.query.cors == '0') { + return; + } + + 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'; + let 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 (!opt_ignoreMissingSourceOrigin && + !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; + } + + enableCors(req, res, origin, opt_exposeHeaders); +} + + +function generateInfo(filePath) { + const mode = pc.env.SERVE_MODE; + filePath = filePath.substr(0, filePath.length - 9) + '.html'; + + return '

    Please note that .min/.max is no longer supported

    ' + + '

    Current serving mode is ' + mode + '

    ' + + '

    Please go to Unversioned Link to view the page

    ' + + '

    ' + + '

    ' + + 'Change to DEFAULT mode (unminified JS)

    ' + + '

    ' + + 'Change to COMPILED mode (minified JS)

    ' + + '

    Change to CDN mode (prod JS)

    '; +} + +module.exports = { + middleware: app, + beforeServeTasks: devDashboard.beforeServeTasks, +}; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/index.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/index.js new file mode 100644 index 000000000000..8592ed7c866a --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/index.js @@ -0,0 +1,84 @@ +/** + * Copyright 2018 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. + */ + +function isRemovableMethod(t, node, names) { + if (!node || !t.isIdentifier(node)) { + return false; + } + return names.some(x => { + return t.isIdentifier(node, {name: x}); + }); +} + +const removableDevAsserts = [ + 'assert', + 'fine', + 'assertElement', + 'assertString', + 'assertNumber', + 'assertBoolean', +]; + +const removableUserAsserts = ['fine']; + + +module.exports = function(babel) { + const {types: t} = babel; + return { + visitor: { + CallExpression(path) { + const {node} = path; + const {callee} = node; + const {parenthesized} = node.extra || {}; + const isMemberAndCallExpression = t.isMemberExpression(callee) + && t.isCallExpression(callee.object); + + if (!isMemberAndCallExpression) { + return; + } + + const logCallee = callee.object.callee; + const {property} = callee; + const isRemovableDevCall = t.isIdentifier(logCallee, {name: 'dev'}) && + isRemovableMethod(t, property, removableDevAsserts); + + const isRemovableUserCall = t.isIdentifier(logCallee, {name: 'user'}) && + isRemovableMethod(t, property, removableUserAsserts); + + if (!(isRemovableDevCall || isRemovableUserCall)) { + return; + } + + // We assume the return is always the resolved expression value. + // This might not be the case like in assertEnum which we currently + // don't remove. + const args = path.node.arguments[0]; + if (args) { + if (parenthesized) { + path.replaceWith(t.parenthesizedExpression(args)); + path.skip(); + } else { + path.replaceWith(args); + } + } else { + // This is to resolve right hand side usage of expression where + // no argument is passed in. + path.replaceWith(t.identifier('undefined')); + } + }, + }, + }; +}; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/input.js new file mode 100644 index 000000000000..b85484887bf3 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/input.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +user().assert(1 + 1); +let result = user().assert(user(), 'hello', 'world'); +let result2 = user().assert(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/output.js new file mode 100644 index 000000000000..b85484887bf3 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform-dev-assert/output.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +user().assert(1 + 1); +let result = user().assert(user(), 'hello', 'world'); +let result2 = user().assert(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/input.js new file mode 100644 index 000000000000..4833e4899415 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/input.js @@ -0,0 +1,19 @@ +/** + * Copyright 2018 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. + */ +dev().assertSomeMethod(dev()); +dev.assert(dev()); +const hello = dev().assertSomeMethod(dev()); +console.log(hello); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/output.js new file mode 100644 index 000000000000..4833e4899415 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/no-transform/output.js @@ -0,0 +1,19 @@ +/** + * Copyright 2018 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. + */ +dev().assertSomeMethod(dev()); +dev.assert(dev()); +const hello = dev().assertSomeMethod(dev()); +console.log(hello); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/input.js new file mode 100644 index 000000000000..0bc923d2126b --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/input.js @@ -0,0 +1,21 @@ +/** + * Copyright 2018 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. + */ +/** @type {x} */ (dev().assertElement(dev())); + +function hello() { + return /** @type {x} */ ( + dev().assertElement(dev())); +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/output.js new file mode 100644 index 000000000000..fd193b570034 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/preserve-parens/output.js @@ -0,0 +1,25 @@ +/** + * Copyright 2018 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. + */ + +/** @type {x} */ +(dev()); + +function hello() { + return ( + /** @type {x} */ + (dev()) + ); +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/input.js new file mode 100644 index 000000000000..52847fabeeb3 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/input.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +dev().assertBoolean(true); +let result = dev().assertBoolean(false, 'hello', 'world'); +let result2 = dev().assertBoolean(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/output.js new file mode 100644 index 000000000000..966b0294367a --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-boolean/output.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +true; +let result = false; +let result2 = undefined; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/input.js new file mode 100644 index 000000000000..24b4b6e067d8 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/input.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +dev().assertElement(element); +let result = dev().assertElement(element, 'hello', 'world'); +let result2 = dev().assertElement(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/output.js new file mode 100644 index 000000000000..e31898597168 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-element/output.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +element; +let result = element; +let result2 = undefined; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/input.js new file mode 100644 index 000000000000..741bf9eadf54 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/input.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +dev().assertNumber(1 + 1); +let result = dev().assertNumber(3, 'hello', 'world'); +let result2 = dev().assertNumber(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/output.js new file mode 100644 index 000000000000..a3e49f5a6d09 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-number/output.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +1 + 1; +let result = 3; +let result2 = undefined; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/input.js new file mode 100644 index 000000000000..aaf5f4d8b6f5 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/input.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +dev().assertString('hello'); +let result = dev().assertString('world'); +let result2 = dev().assertString(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/output.js new file mode 100644 index 000000000000..eb28d2cc6cff --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert-string/output.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +'hello'; +let result = 'world'; +let result2 = undefined; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/input.js new file mode 100644 index 000000000000..1e45447ee8e0 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/input.js @@ -0,0 +1,20 @@ +/** + * Copyright 2018 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. + */ +dev().assert(1 + 1); +dev().assert(dev().assert(2 + 2)); +dev().assert(); +let result = dev().assert(dev(), 'hello', 'world'); +let result2 = dev().assert(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/output.js new file mode 100644 index 000000000000..c7b098fed679 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-dev-assert/output.js @@ -0,0 +1,20 @@ +/** + * Copyright 2018 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. + */ +1 + 1; +2 + 2; +undefined; +let result = dev(); +let result2 = undefined; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/input.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/input.js new file mode 100644 index 000000000000..def60cff9683 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/input.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +user().fine(1); +let result = user().fine(user(), 'hello', 'world'); +let result2 = user().fine(); diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/options.json b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/options.json new file mode 100644 index 000000000000..9f7ccda8f0e7 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../../babel-plugin-transform-amp-asserts"] +} diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/output.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/output.js new file mode 100644 index 000000000000..890202cf580c --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/fixtures/transform-assertions/transform-user-fine/output.js @@ -0,0 +1,18 @@ +/** + * Copyright 2018 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. + */ +1; +let result = user(); +let result2 = undefined; diff --git a/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/index.js b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/index.js new file mode 100644 index 000000000000..7b6d7376a749 --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-amp-asserts/test/index.js @@ -0,0 +1,19 @@ +/** + * Copyright 2018 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. + */ + +const runner = require('@babel/helper-plugin-test-runner').default; + +runner(__dirname); diff --git a/build-system/babel-plugins/babel-plugin-transform-parenthesize-expression/index.js b/build-system/babel-plugins/babel-plugin-transform-parenthesize-expression/index.js new file mode 100644 index 000000000000..05328c94156f --- /dev/null +++ b/build-system/babel-plugins/babel-plugin-transform-parenthesize-expression/index.js @@ -0,0 +1,35 @@ +/** + * Copyright 2018 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(babel) { + const {types: t} = babel; + return { + visitor: { + Expression: { + exit(path) { + const {node} = path; + const {parenthesized} = node.extra || {}; + if (!parenthesized) { + return; + } + + path.replaceWith(t.parenthesizedExpression(node)); + path.skip(); + }, + }, + }, + }; +}; diff --git a/build-system/babel-plugins/testSetupFile.js b/build-system/babel-plugins/testSetupFile.js new file mode 100644 index 000000000000..856ea3b4bdd4 --- /dev/null +++ b/build-system/babel-plugins/testSetupFile.js @@ -0,0 +1,17 @@ +/** + * Copyright 2018 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. + */ + +jest.setTimeout(10000); // eslint-disable-line no-undef diff --git a/build-system/build.conf.js b/build-system/build.conf.js new file mode 100644 index 000000000000..66da5983646d --- /dev/null +++ b/build-system/build.conf.js @@ -0,0 +1,46 @@ +/** + * Copyright 2018 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. + */ + +const defaultPlugins = [ + require.resolve( + './babel-plugins/babel-plugin-transform-parenthesize-expression'), +]; + +module.exports = { + plugins: (isEsmBuild, isCommonJsModule) => { + let pluginsToApply = defaultPlugins; + if (isEsmBuild) { + pluginsToApply = pluginsToApply.concat([ + [require.resolve('babel-plugin-filter-imports'), { + 'imports': { + './polyfills/fetch': ['installFetch'], + './polyfills/domtokenlist-toggle': ['installDOMTokenListToggle'], + './polyfills/document-contains': ['installDocContains'], + './polyfills/math-sign': ['installMathSign'], + './polyfills/object-assign': ['installObjectAssign'], + './polyfills/promise': ['installPromise'], + }, + }], + ]); + } + if (isCommonJsModule) { + pluginsToApply = pluginsToApply.concat([ + [require.resolve('babel-plugin-transform-commonjs-es2015-modules')], + ]); + } + return pluginsToApply; + }, +}; diff --git a/build-system/check-package-manager.js b/build-system/check-package-manager.js new file mode 100644 index 000000000000..da82325c08c2 --- /dev/null +++ b/build-system/check-package-manager.js @@ -0,0 +1,215 @@ +/** + * 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. + */ +'use strict'; + +const https = require('https'); +const {getStdout} = require('./exec'); + +const setupInstructionsUrl = 'https://github.com/ampproject/amphtml/blob/master/contributing/getting-started-quick.md#one-time-setup'; +const nodeDistributionsUrl = 'https://nodejs.org/dist/index.json'; +const gulpHelpUrl = 'https://medium.com/gulpjs/gulp-sips-command-line-interface-e53411d4467'; + +const yarnExecutable = 'npx yarn'; +const gulpExecutable = 'npx gulp'; + +const updatesNeeded = []; + +// Color formatting libraries may not be available when this script is run. +function red(text) {return '\x1b[31m' + text + '\x1b[0m';} +function cyan(text) {return '\x1b[36m' + text + '\x1b[0m';} +function green(text) {return '\x1b[32m' + text + '\x1b[0m';} +function yellow(text) {return '\x1b[33m' + text + '\x1b[0m';} + +/** + * @fileoverview Perform checks on the AMP toolchain. + */ + +// If npm is being run, print a message and cause 'npm install' to fail. +function ensureYarn() { + if (process.env.npm_execpath.indexOf('yarn') === -1) { + console.log(red( + '*** The AMP project uses yarn for package management ***'), '\n'); + console.log(yellow('To install all packages:')); + console.log(cyan('$'), 'yarn', '\n'); + console.log( + yellow('To install a new (runtime) package to "dependencies":')); + console.log(cyan('$'), 'yarn add --exact [package_name@version]', '\n'); + console.log( + yellow('To install a new (toolset) package to "devDependencies":')); + console.log(cyan('$'), + 'yarn add --dev --exact [package_name@version]', '\n'); + console.log(yellow('To upgrade a package:')); + console.log(cyan('$'), 'yarn upgrade --exact [package_name@version]', '\n'); + console.log(yellow('To remove a package:')); + console.log(cyan('$'), 'yarn remove [package_name]', '\n'); + console.log(yellow('For detailed instructions, see'), + cyan(setupInstructionsUrl), '\n'); + process.exit(1); + } +} + +// Check the node version and print a warning if it is not the latest LTS. +function checkNodeVersion() { + const nodeVersion = getStdout('node --version').trim(); + return new Promise(resolve => { + https.get(nodeDistributionsUrl, res => { + res.setEncoding('utf8'); + let distributions = ''; + res.on('data', data => { + distributions += data; + }); + res.on('end', () => { + const distributionsJson = JSON.parse(distributions); + const latestLtsVersion = getNodeLatestLtsVersion(distributionsJson); + if (latestLtsVersion === '') { + console.log(yellow('WARNING: Something went wrong. ' + + 'Could not determine the latest LTS version of node.')); + } else if (nodeVersion !== latestLtsVersion) { + console.log(yellow('WARNING: Detected node version'), + cyan(nodeVersion) + + yellow('. Recommended (latest LTS) version is'), + cyan(latestLtsVersion) + yellow('.')); + console.log(yellow('⤷ To fix this, run'), + cyan('"nvm install --lts"'), yellow('or see'), + cyan('https://nodejs.org/en/download/package-manager'), + yellow('for instructions.')); + updatesNeeded.push('node'); + } else { + console.log(green('Detected'), cyan('node'), green('version'), + cyan(nodeVersion + ' (latest LTS)') + + green('.')); + } + resolve(); + }); + }).on('error', () => { + console.log(yellow('WARNING: Something went wrong. ' + + 'Could not download node version info from ' + + cyan(nodeDistributionsUrl) + yellow('.'))); + console.log(yellow('⤷ Detected node version'), cyan(nodeVersion) + + yellow('.')); + resolve(); + }); + }); +} + +function getNodeLatestLtsVersion(distributionsJson) { + if (distributionsJson) { + // Versions are in descending order, so the first match is the latest lts. + return distributionsJson.find(function(distribution) { + return distribution.hasOwnProperty('version') && + distribution.hasOwnProperty('lts') && + distribution.lts; + }).version; + } else { + return ''; + } +} + +// If yarn is being run, perform a version check and proceed with the install. +function checkYarnVersion() { + const yarnVersion = getStdout(yarnExecutable + ' --version').trim(); + const yarnInfo = getStdout(yarnExecutable + ' info --json yarn').trim(); + const yarnInfoJson = JSON.parse(yarnInfo.split('\n')[0]); // First line + const stableVersion = getYarnStableVersion(yarnInfoJson); + if (stableVersion === '') { + console.log(yellow('WARNING: Something went wrong. ' + + 'Could not determine the stable version of yarn.')); + } else if (yarnVersion !== stableVersion) { + console.log(yellow('WARNING: Detected yarn version'), + cyan(yarnVersion) + yellow('. Recommended (stable) version is'), + cyan(stableVersion) + yellow('.')); + console.log(yellow('⤷ To fix this, run'), + cyan('"curl -o- -L https://yarnpkg.com/install.sh | bash"'), + yellow('or see'), cyan('https://yarnpkg.com/docs/install'), + yellow('for instructions.')); + updatesNeeded.push('yarn'); + } else { + console.log(green('Detected'), cyan('yarn'), green('version'), + cyan(yarnVersion + ' (stable)') + + green('. Installing packages...')); + } +} + +function getYarnStableVersion(infoJson) { + if (infoJson && + infoJson.hasOwnProperty('data') && + infoJson.data.hasOwnProperty('version')) { + return infoJson.data.version; + } else { + return ''; + } +} + +function checkGlobalGulp() { + const globalPackages = getStdout(yarnExecutable + ' global list').trim(); + const globalGulp = globalPackages.match(/"gulp@.*" has binaries/); + const globalGulpCli = globalPackages.match(/"gulp-cli@.*" has binaries/); + if (globalGulp) { + console.log(yellow('WARNING: Detected a global install of'), + cyan('gulp') + yellow('. It is recommended that you use'), + cyan('gulp-cli'), yellow('instead.')); + console.log(yellow('⤷ To fix this, run'), + cyan('"yarn global remove gulp"'), yellow('followed by'), + cyan('"yarn global add gulp-cli"') + yellow('.')); + console.log(yellow('⤷ See'), cyan(gulpHelpUrl), + yellow('for more information.')); + updatesNeeded.push('gulp'); + } else if (!globalGulpCli) { + console.log(yellow('WARNING: Could not find'), + cyan('gulp-cli') + yellow('.')); + console.log(yellow('⤷ To install it, run'), + cyan('"yarn global add gulp-cli"') + yellow('.')); + } else { + const gulpVersions = getStdout(gulpExecutable + ' --version').trim(); + const gulpVersion = gulpVersions.match(/Local version (.*?)$/); + if (gulpVersion && gulpVersion.length == 2) { + console.log(green('Detected'), cyan('gulp'), green('version'), + cyan(gulpVersion[1]) + green('.')); + } else { + console.log(yellow('WARNING: Something went wrong. ' + + 'Could not determine the local version of gulp.')); + } + } +} + +function main() { + // Yarn is already used by default on Travis, so there is nothing more to do. + if (process.env.TRAVIS) { + return 0; + } + ensureYarn(); + return checkNodeVersion().then(() => { + checkGlobalGulp(); + checkYarnVersion(); + if (!process.env.TRAVIS && updatesNeeded.length > 0) { + console.log(yellow('\nWARNING: Detected missing updates for'), + cyan(updatesNeeded.join(', '))); + console.log(yellow('⤷ Continuing install in'), cyan('5'), + yellow('seconds...')); + console.log(yellow('⤷ Press'), cyan('Ctrl + C'), + yellow('to abort and fix...')); + let resolver; + const deferred = new Promise(resolverIn => {resolver = resolverIn;}); + setTimeout(() => { + console.log(yellow('\nAttempting to install packages...')); + resolver(); + }, 5000); + return deferred; + } + }); +} + +main(); diff --git a/build-system/compile-wrappers.js b/build-system/compile-wrappers.js new file mode 100644 index 000000000000..c146ddf060ff --- /dev/null +++ b/build-system/compile-wrappers.js @@ -0,0 +1,58 @@ +/** + * Copyright 2018 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. + */ + +const {VERSION} = require('./internal-version'); + +// If there is a sync JS error during initial load, +// at least try to unhide the body. +exports.mainBinary = 'var global=self;self.AMP=self.AMP||[];' + + 'try{(function(_){\n<%= contents %>})(AMP._=AMP._||{})}catch(e){' + + 'setTimeout(function(){' + + 'var s=document.body.style;' + + 's.opacity=1;' + + 's.visibility="visible";' + + 's.animation="none";' + + 's.WebkitAnimation="none;"},1000);throw e};'; + +exports.extension = function(name, loadPriority, intermediateDeps, + opt_splitMarker) { + opt_splitMarker = opt_splitMarker || ''; + let deps = ''; + if (intermediateDeps) { + deps = 'i:'; + function quote(s) { + return `"${s}"`; + } + if (intermediateDeps.length == 1) { + deps += quote(intermediateDeps[0]); + } else { + deps += `[${intermediateDeps.map(quote).join(',')}]`; + } + deps += ','; + } + let priority = ''; + if (loadPriority) { + if (loadPriority != 'high') { + throw new Error('Unsupported loadPriority: ' + loadPriority); + } + priority = 'p:"high",'; + } + return `(self.AMP=self.AMP||[]).push({n:"${name}",${priority}${deps}` + + `v:"${VERSION}",f:(function(AMP,_){${opt_splitMarker}\n` + + '<%= contents %>\n})});'; +}; + +exports.none = '<%= contents %>'; diff --git a/build-system/config.js b/build-system/config.js index 84acda9b6d81..0eb082c4e765 100644 --- a/build-system/config.js +++ b/build-system/config.js @@ -13,136 +13,143 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; -var path = require('path'); - -var karmaConf = path.resolve('karma.conf.js'); - -var commonTestPaths = [ +const initTestsPath = [ 'test/_init_tests.js', - 'test/fixtures/**/*.html', +]; + +const fixturesExamplesPaths = [ + 'test/fixtures/*.html', { - pattern: 'dist/**/*.js', + pattern: 'test/fixtures/served/*.html', included: false, + nocache: false, + watched: true, }, { - pattern: 'dist.tools/**/*.js', + pattern: 'examples/**/*', included: false, + nocache: false, + watched: true, }, +]; + +const builtRuntimePaths = [ { - pattern: 'build/**/*.js', + pattern: 'dist/**/*.js', included: false, - served: true + nocache: false, + watched: true, }, { - pattern: 'examples/**/*', + pattern: 'dist.3p/**/*', included: false, - served: true + nocache: false, + watched: true, }, { - pattern: 'dist.3p/**/*', + pattern: 'dist.tools/**/*.js', included: false, - served: true - } -] + nocache: false, + watched: true, + }, +]; -var testPaths = commonTestPaths.concat([ +const commonUnitTestPaths = initTestsPath.concat(fixturesExamplesPaths); + +const commonIntegrationTestPaths = + initTestsPath.concat(fixturesExamplesPaths, builtRuntimePaths); + +const testPaths = commonIntegrationTestPaths.concat([ 'test/**/*.js', + 'ads/**/test/test-*.js', 'extensions/**/test/**/*.js', ]); -var integrationTestPaths = commonTestPaths.concat([ +const a4aTestPaths = initTestsPath.concat([ + 'extensions/amp-a4a/**/test/**/*.js', + 'extensions/amp-ad-network-*/**/test/**/*.js', + 'ads/google/a4a/test/*.js', +]); + +const chaiAsPromised = [ + 'test/chai-as-promised/chai-as-promised.js', +]; + +const unitTestPaths = [ + 'test/functional/**/*.js', + 'ads/**/test/test-*.js', + 'extensions/**/test/*.js', +]; + +const unitTestOnSaucePaths = [ + 'test/functional/**/*.js', + 'ads/**/test/test-*.js', +]; + +const integrationTestPaths = [ 'test/integration/**/*.js', + 'test/functional/test-error.js', '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, - }, - captureTimeout: 120000, - browserDisconnectTimeout: 120000, - browserNoActivityTimeout: 120000, - } -}; +const devDashboardTestPaths = [ + 'build-system/app-index/test/**/*.js', +]; + +const lintGlobs = [ + '**/*.js', + // To ignore a file / directory, add it to .eslintignore. +]; /** @const */ module.exports = { - testPaths: testPaths, - integrationTestPaths: integrationTestPaths, - karma: karma, - lintGlobs: [ - '**/*.js', + testPaths, + a4aTestPaths, + chaiAsPromised, + commonUnitTestPaths, + commonIntegrationTestPaths, + unitTestPaths, + unitTestOnSaucePaths, + integrationTestPaths, + devDashboardTestPaths, + lintGlobs, + jsonGlobs: [ + '**/*.json', '!{node_modules,build,dist,dist.3p,dist.tools,' + 'third_party,build-system}/**/*.*', - '!{testing,examples,examples.build}/**/*.*', - // TODO: temporary, remove when validator is up to date - '!validator/**/*.*', - '!gulpfile.js', - '!karma.conf.js', - '!**/local-amp-chrome-extension/background.js', - '!extensions/amp-access/0.1/access-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,' + - 'dist.3p/[0-9]*,dist.3p/current-min}/**/*.*', + '!{node_modules,build,dist,dist.tools,' + + 'dist.3p/[0-9]*,dist.3p/current,dist.3p/current-min}/**/*.*', + '!dist.3p/current/**/ampcontext-lib.js', + '!dist.3p/current/**/iframe-transport-client-lib.js', + '!out/**/*.*', + '!validator/validator.pb.go', + '!validator/dist/**/*.*', '!validator/node_modules/**/*.*', + '!validator/nodejs/node_modules/**/*.*', + '!validator/webui/dist/**/*.*', + '!validator/webui/node_modules/**/*.*', '!build-system/tasks/presubmit-checks.js', + '!build-system/tasks/visual-diff/node_modules/**/*.*', + '!build-system/tasks/visual-diff/snippets/*.js', '!build/polyfills.js', - '!gulpfile.js', + '!build/polyfills/*.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/**/*', + '!examples/visual-tests/**/*', + '!test/coverage/**/*.*', + '!firebase/**/*.*', ], - changelogIgnoreFileTypes: /\.md|\.json|\.yaml|LICENSE|CONTRIBUTORS$/ + 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..03d3369cc67d --- /dev/null +++ b/build-system/conformance-config.textproto @@ -0,0 +1,364 @@ +# 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.' +} + +# Element + +requirement: { + type: BANNED_PROPERTY_READ + error_message: 'Use getStyle, setStyle, or setStyles in src/style.js' + value: 'Element.prototype.style' + whitelist: 'src/style.js' + whitelist: 'src/layout.js' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'Use closestBySelector, closestByTag etc in src/dom.js' + value: 'Element.prototype.closest' + whitelist: 'src/dom.js' +} + +# CSSStyleDeclaration + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'Use setStyle or setStyles in src/style.js' + value: 'CSSStyleDeclaration.prototype.setProperty' + whitelist: 'src/style.js' +} + +# History + +requirement: { + type: BANNED_PROPERTY + error_message: 'History.p.state is broken in IE11. Please use the helper methods provided in src/history.js' + value: 'History.prototype.state' + whitelist: 'src/history.js' +} + +# 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: 'Use string.js#startsWith' + 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: 'String.fromCodePoint' +} + +# Object + +requirement: { + type: BANNED_NAME + error_message: 'Object.values is not allowed' + value: 'Object.values' + whitelist: 'extensions/amp-bind/0.1/bind-expression.js' # Polyfills IE case. +} + +requirement: { + type: BANNED_NAME + error_message: 'Object.entries is not allowed' + value: 'Object.entries' +} + +requirement: { + type: BANNED_NAME + error_message: 'Object.getOwnPropertyDescriptors is not allowed' + value: 'Object.getOwnPropertyDescriptors' +} + +# 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' +} + +# 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' +} + +requirement: { + type: BANNED_NAME + error_message: 'Use jsonParse instead. Usage in 3p ads can be whitelisted for now.' + value: 'JSON.parse' + whitelist: 'ads/adfox.js' + whitelist: 'ads/adincube.js' + whitelist: 'ads/imedia.js' + whitelist: 'ads/kargo.js' + whitelist: 'ads/mads.js' + whitelist: 'ads/google/imaVideo.js' + whitelist: 'ads/zen.js' + whitelist: 'src/json.js' # Where jsonParse itself is implemented. +} + +requirement: { + type: BANNED_PROPERTY_READ + error_message: 'Use eventHelper#getData to read the data property. OK to whitelist 3p ads for now.' + value: 'Event.prototype.data' + value: 'MessageEvent.prototype.data' + whitelist: 'src/event-helper.js' # getData is implemented here + whitelist: 'src/web-worker/amp-worker.js' # OK to use in version bound worker + whitelist: 'src/web-worker/web-worker.js' # OK to use in version bound worker + + # 3p ads are OK + whitelist: 'ads/adfox.js' + whitelist: 'ads/google/imaVideo.js' + whitelist: 'ads/netletix.js' + whitelist: 'ads/yandex.js' + whitelist: 'extensions/amp-access/0.1/amp-access-iframe.js' # False-positive + whitelist: 'extensions/amp-access/0.1/iframe-api/messenger.js' # 3p-intent + whitelist: 'src/service/history-impl.js' # False-positive +} + +requirement: { + type: BANNED_PROPERTY_READ + error_message: 'Use eventHelper#getDetail to read the detail property. OK to whitelist 3p ads for now.' + value: 'Event.prototype.detail' + value: 'MessageEvent.prototype.detail' + whitelist: 'src/event-helper.js' # getDetail is implemented here + + # 3p ads are OK + whitelist: 'ads/adhese.js' + whitelist: 'ads/loka.js' + whitelist: 'ads/xlift.js' +} + +requirement: { + type: RESTRICTED_METHOD_CALL + error_message: 'postMessage must be called with a string or a JsonObject' + value: 'Window.prototype.postMessage:function((string|?JsonObject), string, (Array|Transferable)=)' + # Guaranteed same version call + whitelist: 'src/web-worker/web-worker.js' + # Allowing violations in ads code for now. + # We would have to fix this to property obfuscated the 3p frame code. + whitelist: 'ads/google/deprecated_doubleclick.js' + whitelist: 'ads/google/imaVideo.js' +} + +requirement: { + type: RESTRICTED_NAME_CALL + error_message: 'JSON.stringify must be called with a JsonObject' + # Unfortunately the Array is untyped, because the compiler doesn't check + # for the template type. + value: 'JSON.stringify:function((?JsonObject|AmpViewerMessage|string|number|boolean|undefined|Array),!Function=)' + # Allowing violations in ads code for now. + # We would have to fix this to property obfuscated the 3p frame code. + whitelist: 'ads/google/deprecated_doubleclick.js' +} + +# Cookies + +requirement: { + type: BANNED_PROPERTY + error_message: 'Use cookies#getCookie or cookies#setCookie to read or write cookies. Note that usage of cookies requires dedicated review due to being privacy sensitive. Please file an issue asking for permission to use if you have not yet done so' + value: 'Document.prototype.cookie' + whitelist: 'src/cookies.js' +} + +requirement: { + type: BANNED_NAME + error_message: 'Usage of cookies requires dedicated review due to being privacy sensitive. Please file an issue asking for permission to use if you have not yet done so' + value: 'module$src$cookies.getCookie' + value: 'module$src$cookies.setCookie' + whitelist: 'src/cookies.js' + whitelist: 'src/experiments.js' + whitelist: 'src/service/cid-api.js' + whitelist: 'src/service/cid-impl.js' + whitelist: 'extensions/amp-analytics/0.1/cookie-writer.js' +} + +# Function + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'Function.prototype.name is not allowed due to IE11 non-support.' + value: 'Function.prototype.name' +} + + +# Bans DOM v4 methods, to limit the surface area of the Custom Elements Polyfill + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'ChildNode.p.remove() is unusual. Please use Node.p.removeChild()' + value: 'DocumentType.prototype.remove' + value: 'Element.prototype.remove' + value: 'CharacterData.prototype.remove' + whitelist: 'extensions/amp-inputmask/0.1/mask-impl.js' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'ChildNode.p.replaceWith() is unusual. Please use Node.p.replaceChild()' + value: 'DocumentType.prototype.replaceWith' + value: 'Element.prototype.replaceWith' + value: 'CharacterData.prototype.replaceWith' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'ParentNode.p.append() is unusual. Please use Node.p.appendChild()' + value: 'Element.prototype.append' + value: 'Document.prototype.append' + value: 'DocumentFragment.prototype.append' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'ParentNode.p.prepend() is unusual. Please use Node.p.insertBefore()' + value: 'Element.prototype.prepend' + value: 'Document.prototype.prepend' + value: 'DocumentFragment.prototype.prepend' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'ChildNode.p.before() is unusual. Please use Node.p.insertBefore()' + value: 'Element.prototype.before' + value: 'DocumentType.prototype.before' + value: 'CharacterData.prototype.before' + # It's really hard to properly type check this one. + whitelist: 'extensions/amp-date-picker/0.1/dates-list.js' +} + +requirement: { + type: BANNED_PROPERTY_CALL + error_message: 'ChildNode.p.after() is unusual. Please use Node.p.insertBefore()' + value: 'Element.prototype.after' + value: 'DocumentType.prototype.after' + value: 'CharacterData.prototype.after' +} diff --git a/build-system/ctrlcHandler.js b/build-system/ctrlcHandler.js new file mode 100644 index 000000000000..7819bd8c67b3 --- /dev/null +++ b/build-system/ctrlcHandler.js @@ -0,0 +1,62 @@ +/** + * Copyright 2018 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. + */ + +const colors = require('ansi-colors'); +const log = require('fancy-log'); +const {execScriptAsync, exec} = require('./exec'); + +const {green, cyan} = colors; + +const killCmd = + (process.platform == 'win32') ? 'taskkill /f /pid' : 'kill -KILL'; +const killSuffix = (process.platform == 'win32') ? '>NUL' : ''; + +/** + * Creates an async child process that handles Ctrl + C and immediately cancels + * the ongoing `gulp watch | build | dist` task. + * + * @param {string} command + */ +exports.createCtrlcHandler = function(command) { + if (!process.env.TRAVIS) { + log(green('Running'), cyan(command) + green('. Press'), cyan('Ctrl + C'), + green('to cancel...')); + } + const killMessage = green('\nDetected ') + cyan('Ctrl + C') + + green('. Canceling ') + cyan(command) + green('.'); + const listenerCmd = ` + #!/bin/sh + ctrlcHandler() { + echo -e "${killMessage}" + ${killCmd} ${process.pid} + exit 1 + } + trap 'ctrlcHandler' INT + read _ # Waits until the process is terminated + `; + return execScriptAsync( + listenerCmd, {'stdio': [null, process.stdout, process.stderr]}).pid; +}; + +/** + * Exits the Ctrl C handler process. + * + * @param {string} handlerProcess + */ +exports.exitCtrlcHandler = function(handlerProcess) { + const exitCmd = killCmd + ' ' + handlerProcess + ' ' + killSuffix; + exec(exitCmd); +}; diff --git a/build-system/default-pre-push b/build-system/default-pre-push new file mode 100755 index 000000000000..ef4e882a24eb --- /dev/null +++ b/build-system/default-pre-push @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Copyright 2018 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. +# +# This file contains the default pre-push hook for AMPHTML. To enable it, run: +# "./build-system/enable-git-pre-push.sh" +# +# Note: The checks in this file must not take more than a few seconds to run. +# Time consuming checks that call gulp build, or run all the tests are +# forbidden. If you'd like to add something like that to your pre-push, do so +# by directly editing .git/hooks/pre-push instead of editing this default hook. + +GREEN() { echo -e "\033[0;32m$1\033[0m"; } +CYAN() { echo -e "\033[0;36m$1\033[0m"; } + +GULP_BUNDLE_SIZE="gulp bundle-size" +GULP_LINT_LOCAL="gulp lint --local-changes" +GULP_TEST_LOCAL="gulp test --local-changes --headless" + +echo $(GREEN "Running") $(CYAN "pre-push") $(GREEN "hooks. (Run") $(CYAN "git push --no-verify") $(GREEN "to skip them.)") +echo -e "\n" + +echo $(GREEN "Running") $(CYAN "$GULP_BUNDLE_SIZE") +eval $GULP_BUNDLE_SIZE || exit 1 +echo -e "\n" + +echo $(GREEN "Running") $(CYAN "$GULP_LINT_LOCAL") +eval $GULP_LINT_LOCAL || exit 1 +echo -e "\n" + +echo $(GREEN "Running") $(CYAN "$GULP_TEST_LOCAL") +eval $GULP_TEST_LOCAL || exit 1 +echo -e "\n" + +echo $(GREEN "Done with") $(CYAN "pre-push") $(GREEN "hooks. Pushing commits to GitHub...") diff --git a/build-system/dep-check-config.js b/build-system/dep-check-config.js new file mode 100644 index 000000000000..4f4a30282716 --- /dev/null +++ b/build-system/dep-check-config.js @@ -0,0 +1,413 @@ +/** + * 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'; + +/*eslint "max-len": 0*/ + +/** + * - type - Is assumed to be "forbidden" if not provided. + * - filesMatching - Is assumed to be all files if not provided. + * - mustNotDependOn - If type is "forbidden" (default) then the files + * matched must not match the glob(s) provided. + * - whitelist - Skip rule if this particular dependency is found. + * Syntax: fileAGlob->fileB where -> reads "depends on" + * @typedef {{ + * type: (string|undefined), + * filesMatching: (string|!Array|undefined), + * mustNotDependOn: (string|!Array|undefined), + * whitelist: (string|!Array|undefined), + * }} + */ +let RuleConfigDef; + +// It is often OK to add things to the whitelist, but make sure to highlight +// this in review. +exports.rules = [ + // Global rules + { + filesMatching: '**/*.js', + mustNotDependOn: 'src/video-iframe-integration.js', + whitelist: [ + // Do not extend this whitelist. + // video-iframe-integration.js is an entry point. + ], + }, + { + filesMatching: '**/*.js', + mustNotDependOn: 'src/sanitizer.js', + whitelist: [ + // DEPRECATED! Do not extend this whitelist. Use src/purifier.js instead. + // Contact @choumx for questions. + 'extensions/amp-mustache/0.1/amp-mustache.js->src/sanitizer.js', + ], + }, + { + filesMatching: '**/*.js', + mustNotDependOn: 'src/purifier.js', + whitelist: [ + 'src/sanitizer.js->src/purifier.js', + 'extensions/amp-bind/0.1/bind-impl.js->src/purifier.js', + 'extensions/amp-mustache/0.2/amp-mustache.js->src/purifier.js', + 'extensions/amp-script/0.1/amp-script.js->src/purifier.js', + ], + }, + { + filesMatching: '**/*.js', + mustNotDependOn: 'src/module.js', + whitelist: [ + 'extensions/amp-date-picker/0.1/**->src/module.js', + ], + }, + { + filesMatching: '**/*.js', + mustNotDependOn: 'third_party/**/*.js', + whitelist: [ + 'extensions/amp-crypto-polyfill/**/*.js->' + + 'third_party/closure-library/sha384-generated.js', + 'extensions/amp-mustache/**/amp-mustache.js->' + + 'third_party/mustache/mustache.js', + 'extensions/amp-ad-network-adzerk-impl/0.1/' + + 'amp-ad-network-adzerk-impl.js->third_party/mustache/mustache.js', + 'extensions/amp-timeago/0.1/amp-timeago.js->' + + 'third_party/timeagojs/timeago.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', + 'third_party/timeagojs/timeago.js->' + + 'third_party/timeagojs/timeago-locales.js', + 'extensions/amp-date-picker/**->third_party/react-dates/bundle.js', + 'extensions/amp-date-picker/**->third_party/rrule/rrule.js', + 'extensions/amp-inputmask/**->third_party/inputmask/inputmask.js', + 'extensions/amp-inputmask/**->' + + 'third_party/inputmask/inputmask.dependencyLib.js', + 'extensions/amp-subscriptions/**/*.js->' + + 'third_party/subscriptions-project/apis.js', + 'extensions/amp-subscriptions/**/*.js->' + + 'third_party/subscriptions-project/config.js', + 'extensions/amp-subscriptions-google/**/*.js->' + + 'third_party/subscriptions-project/apis.js', + 'extensions/amp-subscriptions-google/**/*.js->' + + 'third_party/subscriptions-project/config.js', + 'extensions/amp-subscriptions-google/**/*.js->' + + 'third_party/subscriptions-project/swg.js', + 'extensions/amp-recaptcha-input/**/*.js->' + + 'third_party/amp-toolbox-cache-url/dist/amp-toolbox-cache-url.esm.js', + ], + }, + // Rules for 3p + { + filesMatching: '3p/**/*.js', + mustNotDependOn: 'src/**/*.js', + whitelist: [ + '3p/**->src/utils/function.js', + '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/**->src/json.js', + '3p/**->src/3p-frame-messaging.js', + '3p/**->src/observable.js', + '3p/**->src/amp-events.js', + '3p/**->src/consent-state.js', + '3p/polyfills.js->src/polyfills/math-sign.js', + '3p/polyfills.js->src/polyfills/object-assign.js', + '3p/messaging.js->src/event-helper.js', + '3p/bodymovinanimation.js->src/event-helper.js', + '3p/iframe-messaging-client.js->src/event-helper.js', + '3p/viqeoplayer.js->src/event-helper.js', + ], + }, + { + filesMatching: '3p/**/*.js', + mustNotDependOn: 'extensions/**/*.js', + }, + // Rules for ads + { + 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/style.js', + 'ads/**->src/consent-state.js', + 'ads/google/adsense-amp-auto-ads.js->src/experiments.js', + 'ads/google/adsense-amp-auto-ads-responsive.js->src/experiments.js', + 'ads/google/doubleclick.js->src/experiments.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/consent.js', + 'ads/google/a4a/**->src/consent-state.js', + 'ads/google/a4a/**->src/dom.js', + 'ads/google/a4a/**->src/experiments.js', + 'ads/google/a4a/**->src/services.js', + 'ads/google/a4a/utils.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/_a4a-config.js->' + + 'extensions/amp-ad-network-gmossp-impl/0.1/gmossp-a4a-config.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-service/**->' + + 'src/service/video-service-interface.js', + 'extensions/amp-video/0.1/amp-video.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-video-iframe/0.1/amp-video-iframe.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-viqeo-player/0.1/amp-viqeo-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-brightcove/0.1/amp-brightcove.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-powr-player/0.1/amp-powr-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-dailymotion/0.1/amp-dailymotion.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-brid-player/0.1/amp-brid-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-gfycat/0.1/amp-gfycat.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-a4a/0.1/amp-a4a.js->src/service/variable-source.js', + 'extensions/amp-a4a/0.1/friendly-frame-util.js->' + + 'src/service/url-replacements-impl.js', + 'extensions/amp-nexxtv-player/0.1/amp-nexxtv-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-3q-player/0.1/amp-3q-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-ima-video/0.1/amp-ima-video.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-vimeo/0.1/amp-vimeo.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-wistia-player/0.1/amp-wistia-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-delight-player/0.1/amp-delight-player.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-analytics/0.1/iframe-transport.js->' + + 'src/service/extension-location.js', + 'extensions/amp-analytics/0.1/iframe-transport.js->' + + 'src/service/jank-meter.js', + 'extensions/amp-position-observer/0.1/amp-position-observer.js->' + + 'src/service/position-observer/position-observer-impl.js', + 'extensions/amp-position-observer/0.1/amp-position-observer.js->' + + 'src/service/position-observer/position-observer-worker.js', + 'extensions/amp-fx-collection/0.1/providers/fx-provider.js->' + + 'src/service/position-observer/position-observer-impl.js', + 'extensions/amp-fx-collection/0.1/providers/fx-provider.js->' + + 'src/service/position-observer/position-observer-worker.js', + 'extensions/amp-list/0.1/amp-list.js->' + + 'src/service/position-observer/position-observer-impl.js', + 'extensions/amp-list/0.1/amp-list.js->' + + 'src/service/position-observer/position-observer-worker.js', + 'src/service/video/docking.js->' + + 'src/service/position-observer/position-observer-impl.js', + 'src/service/video/docking.js->' + + 'src/service/position-observer/position-observer-worker.js', + 'extensions/amp-analytics/0.1/amp-analytics.js->' + + 'src/service/cid-impl.js', + 'extensions/amp-analytics/0.1/cookie-writer.js->' + + 'src/service/cid-impl.js', + 'extensions/amp-next-page/0.1/next-page-service.js->' + + 'src/service/position-observer/position-observer-impl.js', + 'extensions/amp-next-page/0.1/next-page-service.js->' + + 'src/service/position-observer/position-observer-worker.js', + 'extensions/amp-user-notification/0.1/amp-user-notification.js->' + + 'src/service/notification-ui-manager.js', + 'extensions/amp-consent/0.1/amp-consent.js->' + + 'src/service/notification-ui-manager.js', + // For autoplay delegation: + 'extensions/amp-story/0.1/amp-story-page.js->' + + 'src/service/video-service-sync-impl.js', + 'extensions/amp-story/1.0/amp-story-page.js->' + + 'src/service/video-service-sync-impl.js', + // Accessing USER_INTERACTED constant: + 'extensions/amp-story/1.0/media-pool.js->' + + 'src/service/video-service-interface.js', + 'extensions/amp-story/1.0/page-advancement.js->' + + 'src/service/action-impl.js', + 'extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js->' + + 'src/service/navigation.js', + 'extensions/amp-ad-network-doubleclick-impl/0.1/amp-ad-network-doubleclick-impl.js->' + + 'src/service/navigation.js', + 'extensions/amp-mowplayer/0.1/amp-mowplayer.js->' + + 'src/service/video-manager-impl.js', + 'extensions/amp-analytics/0.1/linker-manager.js->' + + 'src/service/navigation.js', + 'extensions/amp-skimlinks/0.1/link-rewriter/link-rewriter-manager.js->' + + 'src/service/navigation.js', + 'extensions/amp-list/0.1/amp-list.js->' + + 'src/service/xhr-impl.js', + 'extensions/amp-form/0.1/amp-form.js->' + + 'src/service/xhr-impl.js', + // Accessing extension-location.calculateExtensionScriptUrl(). + 'extensions/amp-script/0.1/amp-script.js->' + + 'src/service/extension-location.js', + ], + }, + { + filesMatching: 'extensions/**/*.js', + mustNotDependOn: 'src/base-element.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/fetch.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/polyfills.js->src/polyfills/array-includes.js', + 'src/polyfills.js->src/polyfills/custom-elements.js', + 'src/service/extensions-impl.js->src/polyfills/custom-elements.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. + { + filesMatching: 'src/**/*.js', + mustNotDependOn: 'extensions/**/*.js', + }, + { + filesMatching: 'src/**/*.js', + mustNotDependOn: 'ads/**/*.js', + whitelist: 'src/ad-cid.js->ads/_config.js', + }, + + // A4A + { + filesMatching: 'extensions/**/*-ad-network-*.js', + mustNotDependOn: [ + 'extensions/amp-ad/0.1/amp-ad-xorigin-iframe-handler.js', + 'src/3p-frame.js', + 'src/iframe-helper.js', + ], + whitelist: 'extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js->src/3p-frame.js', + }, + + { + mustNotDependOn: [ + 'extensions/amp-ad-network-doubleclick-impl/0.1/amp-ad-network-doubleclick-impl.js', + 'extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js', + ], + }, + + { + mustNotDependOn: [ + /** DO NOT WHITELIST ANY FILES */ + 'ads/google/deprecated_doubleclick.js', + /** DO NOT WHITELIST ANY FILES */ + ], + whitelist: [ + 'ads/google/doubleclick.js->ads/google/deprecated_doubleclick.js', + '3p/integration.js->ads/google/deprecated_doubleclick.js', + ], + }, + + // Delayed fetch for Doubleclick will be deprecated on March 29, 2018. + // Doubleclick.js will be deleted from the repository at that time. + // Please see https://github.com/ampproject/amphtml/issues/11834 + // for more information. + // Do not add any additional files to this whitelist without express + // permission from @bradfrizzell, @keithwrightbos, or @robhazan. + { + mustNotDependOn: [ + 'ads/google/doubleclick.js', + ], + whitelist: [ + /** DO NOT ADD TO WHITELIST **/ + 'ads/ix.js->ads/google/doubleclick.js', + 'ads/imonomy.js->ads/google/doubleclick.js', + 'ads/medianet.js->ads/google/doubleclick.js', + 'ads/navegg.js->ads/google/doubleclick.js', + /** DO NOT ADD TO WHITELIST **/ + 'ads/openx.js->ads/google/doubleclick.js', + 'ads/pulsepoint.js->ads/google/doubleclick.js', + 'ads/rubicon.js->ads/google/doubleclick.js', + 'ads/yieldbot.js->ads/google/doubleclick.js', + /** DO NOT ADD TO WHITELIST **/ + 'ads/criteo.js->ads/google/doubleclick.js', + /** DO NOT ADD TO WHITELIST **/ + ], + }, +]; diff --git a/build-system/enable-git-pre-push.sh b/build-system/enable-git-pre-push.sh new file mode 100755 index 000000000000..f42581c3354d --- /dev/null +++ b/build-system/enable-git-pre-push.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# +# Copyright 2018 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. +# +# This script adds a pre-push hook to .git/hooks/, which runs some basic tests +# before running "git push". +# +# To enable it, run this script: "./build-system/enable-git-pre-push.sh" + + +SCRIPT=${BASH_SOURCE[0]} +BUILD_SYSTEM_DIR=$(dirname "$SCRIPT") +AMPHTML_DIR=$(dirname "$BUILD_SYSTEM_DIR") +PRE_PUSH_SRC="build-system/default-pre-push" +GIT_HOOKS_DIR=".git/hooks" +PRE_PUSH_DEST="$GIT_HOOKS_DIR/pre-push" +PRE_PUSH_BACKUP="$GIT_HOOKS_DIR/pre-push.backup" + +GREEN() { echo -e "\033[0;32m$1\033[0m"; } +CYAN() { echo -e "\033[0;36m$1\033[0m"; } +YELLOW() { echo -e "\033[0;33m$1\033[0m"; } + +if [[ $SCRIPT != ./build-system/* ]] ; +then + echo $(YELLOW "This script must be run from the root") $(CYAN "amphtml") $(YELLOW "directory. Exiting.") + exit 1 +fi + +echo $(YELLOW "-----------------------------------------------------------------------------------------------------------------") +echo $(GREEN "Running") $(CYAN $SCRIPT) +echo $(GREEN "This script does the following:") +echo $(GREEN " 1. If already present, makes a backup of") $(CYAN "$PRE_PUSH_DEST") $(GREEN "at") $(CYAN "$PRE_PUSH_BACKUP") +echo $(GREEN " 2. Creates a new file") $(CYAN "$PRE_PUSH_DEST") $(GREEN "which calls") $(CYAN "$PRE_PUSH_SRC") +echo $(GREEN " 3. With this,") $(CYAN "git push") $(GREEN "will first run the checks in") $(CYAN "$PRE_PUSH_SRC") +echo $(GREEN " 4. You can edit") $(CYAN "$PRE_PUSH_DEST") $(GREEN "to change the pre-push hooks that are run before") $(CYAN "git push") +echo $(GREEN " 5. To skip the hook, run") $(CYAN "git push --no-verify") +echo $(GREEN " 6. To remove the hook, delete the file") $(CYAN "$PRE_PUSH_DEST") +echo $(YELLOW "-----------------------------------------------------------------------------------------------------------------") +echo -e "\n" + +read -n 1 -s -r -p "$(GREEN 'Press any key to continue...')" +echo -e "\n" + +if [ -f "$AMPHTML_DIR/$PRE_PUSH_DEST" ]; then + echo $(GREEN "Found") $(CYAN $PRE_PUSH_DEST) + mv $AMPHTML_DIR/$PRE_PUSH_DEST $AMPHTML_DIR/$PRE_PUSH_BACKUP + echo $(GREEN "Moved it to") $(CYAN $PRE_PUSH_BACKUP) +fi + +cat > $AMPHTML_DIR/$PRE_PUSH_DEST <<- EOM +#!/bin/bash +# Pre-push hook for AMPHTML +eval $AMPHTML_DIR/$PRE_PUSH_SRC +EOM +chmod 755 $AMPHTML_DIR/$PRE_PUSH_DEST + +echo $(GREEN "Successfully wrote") $(CYAN "$PRE_PUSH_DEST") diff --git a/build-system/eslint-rules/.eslintrc b/build-system/eslint-rules/.eslintrc new file mode 100644 index 000000000000..1f71c616eced --- /dev/null +++ b/build-system/eslint-rules/.eslintrc @@ -0,0 +1,17 @@ +{ + "plugins": [ + "eslint-plugin" + ], + "extends": [ + "plugin:eslint-plugin/recommended" + ], + "rules": { + "jsdoc/check-param-names": 0, + "jsdoc/check-tag-names": 0, + "jsdoc/check-types": 0, + "jsdoc/require-param": 0, + "jsdoc/require-param-name": 0, + "jsdoc/require-param-type": 0, + "jsdoc/require-returns-type": 0 + } +} diff --git a/build-system/eslint-rules/closure-type-primitives.js b/build-system/eslint-rules/closure-type-primitives.js new file mode 100644 index 000000000000..a09021ae4ed4 --- /dev/null +++ b/build-system/eslint-rules/closure-type-primitives.js @@ -0,0 +1,223 @@ +/** + * Copyright 2018 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'; + +const doctrine = require('doctrine'); +const traverse = require('traverse'); + +/** @typedef {!Object} */ +let EslintContextDef; + +/** @typedef {!Object} */ +let EslintNodeDef; + +/** + * @typedef {{ + * node: !EslintNodeDef, + * parsed: !Object + * }} + */ +let ClosureCommentDef; + +module.exports = function(context) { + const sourceCode = context.getSourceCode(); + + return { + meta: { + fixable: 'code', + }, + Program: function() { + const comments = + /** @type {!Array} */ (sourceCode.getAllComments()); + comments + .map(node => parseClosureComments(context, node)) + .forEach(comment => checkClosureComments(context, comment)); + }, + }; +}; + +/** + * Parses Closure Compiler tags into an array from the given comment. + * @param {!EslintContextDef} context + * @param {!EslintNodeDef} node + * @return {?ClosureCommentDef} + */ +function parseClosureComments(context, node) { + try { + return { + parsed: doctrine.parse(node.value, {recoverable: true, unwrap: true}), + node, + }; + } catch (e) { + reportUnparseableNode(context, node); + return null; + } +} + +/** + * Report the existence of a syntax error in a closure comment. + * @param {!EslintContextDef} context + * @param {!EslintNodeDef} node + */ +function reportUnparseableNode(context, node) { + context.report({ + node, + message: 'A Closure JSDoc syntax error was encountered.', + }); +} + +/** + * Parse a Closure Compiler comment and check if it contains a primitive + * redundantly specified as non-nullable with a ! + * e.g. {!string} + * @param {!EslintContextDef} context + * @param {?ClosureCommentDef} closureComment + */ +function checkClosureComments(context, closureComment) { + if (!closureComment) { + return; + } + + const {parsed, node} = closureComment; + traverse(parsed).forEach(astNode => { + if (!astNode) { + return; + } + + const {name} = astNode; + if (astNode.type === 'NameExpression' && isPrimitiveWrapperName(name)) { + reportPrimitiveWrapper(context, node, name); + } + if (astNode.type === 'NonNullableType') { + checkNonNullableNodes(context, node, astNode); + } + }); +} + +/** @enum {string} */ +const PRIMITIVE_WRAPPER_NAMES = [ + 'Boolean', + 'Number', + 'String', + 'Symbol', +]; + +/** + * Disallowed primitives wrappers, from + * go/es6-style#disallowed-features-wrapper-objects + * @param {string} name + * @return {boolean} + */ +function isPrimitiveWrapperName(name) { + return PRIMITIVE_WRAPPER_NAMES.includes(name); +} + +/** + * Report the existence of a primitive wrapper. If --fix is specified, + * the name will be converted to the non-wrapper primitive. + * @param {!EslintContextDef} context + * @param {!EslintNodeDef} node + * @param {string} name + */ +function reportPrimitiveWrapper(context, node, name) { + context.report({ + node, + message: 'Forbidden primitive wrapper {{ name }}.', + data: {name}, + fix(fixer) { + const badTextIndex = node.value.indexOf(name); + if (badTextIndex === -1) { + return; + } + + const start = node.range[0] + badTextIndex + 2; + const end = start + name.length; + return fixer.replaceTextRange([start, end], name.toLowerCase()); + }, + }); +} + +/** + * Check if a Closure Compiler comment contains a primitive redundantly + * specified as non-nullable with a ! + * @param {!EslintContextDef} context + * @param {!EslintNodeDef} node + * @param {!Object} astNode + */ +function checkNonNullableNodes(context, node, astNode) { + if (!astNode.expression) { + return; + } + + const {type, name} = astNode.expression; + if (type === 'FunctionType') { + reportNonNullablePrimitive(context, node, 'function'); + } else if (type === 'UndefinedLiteral') { + reportNonNullablePrimitive(context, node, 'undefined'); + } else if (type === 'NameExpression' && isNonNullablePrimitiveName(name)) { + reportNonNullablePrimitive(context, node, name); + } +} + +/** + * Default non-nullable primitives, from go/es6-style#jsdoc-nullability + * Technically `undefined` and `function()` are primitives in Closure, + * but doctrine gives their expression nodes a different type than the others, + * so we don't add them to this list. + * @enum {string} + */ +const NON_NULLABLE_PRIMITIVE_NAMES = [ + 'boolean', + 'number', + 'string', + 'symbol', +]; + +/** + * True if the given name matches a primitive type + * @param {string} name + * @return {boolean} + */ +function isNonNullablePrimitiveName(name) { + return NON_NULLABLE_PRIMITIVE_NAMES.includes(name); +} + +/** + * Report the existence of a non-nullable primitive. If --fix is specified, + * remove the offending exclamation point. + * @param {!EslintContextDef} context + * @param {!EslintNodeDef} node + * @param {string} name + */ +function reportNonNullablePrimitive(context, node, name) { + context.report({ + node, + message: 'Redundant non-nullable primitive {{ name }}.', + data: {name}, + fix(fixer) { + const badText = `!${name}`; + const badTextIndex = node.value.indexOf(badText); + if (badTextIndex === -1) { + return; + } + + const start = node.range[0] + badTextIndex + 2; + const end = start + 1; + return fixer.removeRange([start, end]); + }, + }); +} + diff --git a/build-system/eslint-rules/dict-string-keys.js b/build-system/eslint-rules/dict-string-keys.js new file mode 100644 index 000000000000..f5ee97bed987 --- /dev/null +++ b/build-system/eslint-rules/dict-string-keys.js @@ -0,0 +1,56 @@ +/** + * 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. + */ +'use strict'; + +module.exports = function(context) { + return { + CallExpression: function(node) { + if (node.callee.name === 'dict') { + if (node.arguments[0]) { + const arg1 = node.arguments[0]; + if (arg1.type !== 'ObjectExpression') { + context.report({ + node, + message: 'calls to `dict` must have an Object Literal ' + + 'Expression as the first argument', + }); + return; + } + checkNode(arg1, context); + } + } + }, + }; +}; + +function checkNode(node, context) { + if (node.type === 'ObjectExpression') { + node.properties.forEach(function(prop) { + if (!prop.key.raw && !prop.computed) { + context.report({ + node, + message: 'Found: ' + prop.key.name + '. The keys of the Object ' + + 'Literal Expression passed into `dict` must have string keys.', + }); + } + checkNode(prop.value, context); + }); + } else if (node.type === 'ArrayExpression') { + node.elements.forEach(function(elem) { + checkNode(elem, context); + }); + } +} 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..76c21c02cb08 --- /dev/null +++ b/build-system/eslint-rules/enforce-private-props.js @@ -0,0 +1,80 @@ +/** + * 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'; + +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, + message: '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, + message: 'Property marked as private but has no trailing underscore.', + }); + } + }, + }; +}; diff --git a/build-system/eslint-rules/html-template.js b/build-system/eslint-rules/html-template.js new file mode 100644 index 000000000000..83e8d9ab4b24 --- /dev/null +++ b/build-system/eslint-rules/html-template.js @@ -0,0 +1,144 @@ +/** + * Copyright 2018 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'; + +module.exports = function(context) { + function htmlCannotBeCalled(node) { + context.report({ + node, + message: 'The html helper MUST NOT be called directly. ' + + 'Instead, use it as a template literal tag: ``` html`
    ` ```', + }); + } + + function htmlForUsage(node) { + const {parent} = node; + if (parent.type === 'TaggedTemplateExpression' && + parent.tag === node) { + return htmlTagUsage(parent); + } + + if (parent.type === 'VariableDeclarator' && + parent.init === node && + parent.id.type === 'Identifier' && + parent.id.name === 'html') { + return; + } + + if (parent.type === 'AssignmentExpression' && + parent.right === node && + parent.left.type === 'Identifier' && + parent.left.name === 'html') { + return; + } + + context.report({ + node, + message: 'htmlFor result must be stored into a variable ' + + 'named "html", or used as the tag of a tagged template literal.', + }); + } + + function htmlTagUsage(node) { + const {quasi} = node; + if (quasi.expressions.length !== 0) { + context.report({ + node, + message: 'The html template tag CANNOT accept expression. ' + + 'The template MUST be static only.', + }); + } + + const template = quasi.quasis[0]; + const string = template.value.cooked; + if (!string) { + context.report({ + node: template, + message: 'Illegal escape sequence detected in template literal.', + }); + } + + if (/<(html|body|head)/i.test(string)) { + context.report({ + node: template, + message: 'It it not possible to generate HTML, BODY, or' + + ' HEAD root elements. Please do so manually with' + + ' document.createElement.', + }); + } + + const invalids = invalidVoidTag(string); + if (invalids.length) { + const sourceCode = context.getSourceCode(); + const {start} = template; + + for (let i = 0; i < invalids.length; i++) { + const {tag, offset} = invalids[i]; + context.report({ + node: template, + loc: sourceCode.getLocFromIndex(start + offset), + message: `Invalid void tag "${tag}"`, + }); + } + } + } + + function invalidVoidTag(string) { + // Void tags are defined at + // https://html.spec.whatwg.org/multipage/syntax.html#void-elements + const invalid = /<(?!area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([a-zA-Z-]+)( [^>]*)?\/>/g; + const matches = []; + + let match; + while ((match = invalid.exec(string))) { + matches.push({ + tag: match[1], + offset: match.index, + }); + } + + return matches; + } + + return { + CallExpression(node) { + if (/test-/.test(context.getFilename())) { + return; + } + + const {callee} = node; + if (callee.type !== 'Identifier') { + return; + } + + if (callee.name === 'html') { + return htmlCannotBeCalled(node); + } + if (callee.name === 'htmlFor') { + return htmlForUsage(node); + } + }, + + TaggedTemplateExpression(node) { + const {tag} = node; + if (tag.type !== 'Identifier' || tag.name !== 'html') { + return; + } + + htmlTagUsage(node); + }, + }; +}; diff --git a/build-system/eslint-rules/index.js b/build-system/eslint-rules/index.js new file mode 100644 index 000000000000..0914b90f3460 --- /dev/null +++ b/build-system/eslint-rules/index.js @@ -0,0 +1,30 @@ +/** + * Copyright 2018 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'; + +const fs = require('fs'); +const path = require('path'); + +const rules = {}; +const ruleFiles = fs.readdirSync(__dirname).filter(ruleFile => + !['index.js', 'node_modules', 'package.json', '.eslintrc'] + .includes(ruleFile)); +ruleFiles.forEach(function(ruleFile) { + const rule = ruleFile.replace(path.extname(ruleFile), ''); + rules[rule] = require(path.join(__dirname, rule)); +}); + +module.exports = {rules}; diff --git a/build-system/eslint-rules/is-experiment-on.js b/build-system/eslint-rules/is-experiment-on.js new file mode 100644 index 000000000000..3dfb978b408f --- /dev/null +++ b/build-system/eslint-rules/is-experiment-on.js @@ -0,0 +1,43 @@ +/** + * Copyright 2018 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'; + +module.exports = function(context) { + const isExperimentOn = 'CallExpression[callee.name=isExperimentOn]'; + const message = 'isExperimentOn must be passed an explicit string'; + + return { + [isExperimentOn](node) { + const arg = node.arguments[1]; + if (!arg) { + context.report({node, message}); + return; + } + + const comments = context.getCommentsBefore(arg); + const ok = comments.some(comment => comment.value === 'OK'); + if (ok) { + return; + } + + if (arg.type === 'Literal' && typeof arg.value === 'string') { + return; + } + + context.report({node, message}); + }, + }; +}; diff --git a/build-system/eslint-rules/no-array-destructuring.js b/build-system/eslint-rules/no-array-destructuring.js new file mode 100644 index 000000000000..5b27110c1143 --- /dev/null +++ b/build-system/eslint-rules/no-array-destructuring.js @@ -0,0 +1,24 @@ +/** + * 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'; + +module.exports = function(context) { + return { + ArrayPattern: function(node) { + context.report({node, message: 'No Array destructuring allowed.'}); + }, + }; +}; diff --git a/build-system/eslint-rules/no-deep-destructuring.js b/build-system/eslint-rules/no-deep-destructuring.js new file mode 100644 index 000000000000..a6e65ffed7f2 --- /dev/null +++ b/build-system/eslint-rules/no-deep-destructuring.js @@ -0,0 +1,35 @@ +/** + * Copyright 2018 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'; + +/** + * Disallows deep object destructuring, because it's complicated and confusing. + * + * Bad: + * const { x: { y } } = obj.prop; + * Good: + * const { y } = obj.prop.x; + */ +module.exports = function(context) { + return { + ObjectPattern: function(node) { + if (node.parent.type !== 'Property') { + return; + } + context.report({node, message: 'No deep object destructuring allowed.'}); + }, + }; +}; diff --git a/build-system/eslint-rules/no-duplicate-import.js b/build-system/eslint-rules/no-duplicate-import.js new file mode 100644 index 000000000000..db53da4fbd73 --- /dev/null +++ b/build-system/eslint-rules/no-duplicate-import.js @@ -0,0 +1,97 @@ +/** + * Copyright 2018 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'; + +// Enforces imports statements from a module are not duplicated +// +// Good +// import { x, y, z } from './hello'; +// +// Bad +// import { x, z } from './hello'; +// import { y } from './hello'; +module.exports = { + meta: { + fixable: 'code', + }, + + create(context) { + const imports = new Map(); + + return { + "Program:exit": function() { + imports.forEach((imports) => { + const original = imports[0]; + + for (let i = 1; i < imports.length; i++) { + const node = imports[i]; + + context.report({ + node, + message: `Duplicate import from ${node.source.value}`, + fix(fixer) { + const originalSpecifiers = original.specifiers; + const last = originalSpecifiers[originalSpecifiers.length - 1]; + + const {specifiers} = node; + let text = ''; + for (let i = 0; i < specifiers.length; i++) { + const {imported, local} = specifiers[i]; + const {name} = imported; + + if (name === local.name) { + text += `, ${name}`; + } else { + text += `, ${name} as ${local.name}`; + } + } + + return [ + fixer.remove(node), + fixer.insertTextAfter(last, text), + ]; + } + }); + } + }); + imports.clear(); + }, + + ImportDeclaration(node) { + const {specifiers} = node; + const source = node.source.value; + + if (specifiers.length === 0) { + return; + } + for (let i = 0; i < specifiers.length; i++) { + const spec = specifiers[i]; + if (specifiers[i].type === 'ImportNamespaceSpecifier') { + return; + } + } + + let nodes = imports.get(source); + if (!nodes) { + nodes = []; + imports.set(source, nodes); + } + + nodes.push(node); + } + }; + }, +}; 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..dd99660d739b --- /dev/null +++ b/build-system/eslint-rules/no-es2015-number-props.js @@ -0,0 +1,47 @@ +/** + * 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'; + +const 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, + message: '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..41790f75fbf8 --- /dev/null +++ b/build-system/eslint-rules/no-export-side-effect.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. + */ +'use strict'; + +module.exports = function(context) { + return { + ExportNamedDeclaration: function(node) { + if (node.declaration) { + const {declaration} = node; + if (declaration.type === 'VariableDeclaration') { + declaration.declarations + .map(function(declarator) { + return declarator.init; + }).filter(function(init) { + return init && /(?:Call|New)Expression/.test(init.type); + }).forEach(function(init) { + context.report({ + node: init, + message: 'Cannot export side-effect', + }); + }); + } + } else if (node.specifiers) { + context.report({ + node, + message: '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..d152de95b5f6 --- /dev/null +++ b/build-system/eslint-rules/no-for-of-statement.js @@ -0,0 +1,24 @@ +/** + * 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'; + +module.exports = function(context) { + return { + ForOfStatement: function(node) { + context.report({node, message: '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..39743202d3a9 --- /dev/null +++ b/build-system/eslint-rules/no-global.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. + */ +'use strict'; + +const astUtils = require('eslint/lib/util/ast-utils'); + +const 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) { + const {name} = node; + if (!(name in GLOBALS)) { + return; + } + if (!(/Expression/.test(node.parent.type))) { + return; + } + + if (node.parent.type === 'MemberExpression' && + node.parent.property === node) { + return; + } + + const variable = + astUtils.getVariableByName(context.getScope(), node.name); + if (variable.defs.length > 0) { + return; + } + + let message = 'Forbidden global `' + node.name + '`.'; + if (GLOBALS[name]) { + message += ' ' + GLOBALS[name]; + } + context.report({node, message}); + }, + }; +}; diff --git a/build-system/eslint-rules/no-has-own-property-method.js b/build-system/eslint-rules/no-has-own-property-method.js new file mode 100644 index 000000000000..a894f7489dcb --- /dev/null +++ b/build-system/eslint-rules/no-has-own-property-method.js @@ -0,0 +1,33 @@ +/** + * Copyright 2018 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'; + +module.exports = { + create(context) { + return { + CallExpression(node) { + if (node.callee.type == 'MemberExpression' && !node.callee.computed && + node.callee.property.name == 'hasOwnProperty') { + context.report({ + node, + message: 'Do not use hasOwnProperty directly. ' + + 'Use hasOwn from src/utils/object.js instead.', + }); + } + }, + }; + }, +}; diff --git a/build-system/eslint-rules/no-import-rename.js b/build-system/eslint-rules/no-import-rename.js new file mode 100644 index 000000000000..62f444d84df2 --- /dev/null +++ b/build-system/eslint-rules/no-import-rename.js @@ -0,0 +1,154 @@ +/** + * Copyright 2018 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'; + +const path = require('path'); + +// Forbids using these imports unless is is explicitly imported as the same +// name. This aids in writing lint rules for these imports. +// +// GOOD +// import { dict } from 'src/utils/object'; +// dict(); +// +// BAD +// import * as obj from 'src/utils/object'; +// obj.dict() +// +// Bad +// import { dict as otherName } from 'src/utils/object'; +// otherName() + +const imports = { + 'src/utils/object': ['dict'], + 'src/static-template': ['htmlFor'], + 'src/experiments': ['isExperimentOn'], + 'src/style': [ + 'assertDoesNotContainDisplay', + 'assertNotDisplay', + 'resetStyles', + 'setImportantStyles', + 'setStyle', + 'setStyles', + ], + 'src/dom': [ + 'escapeCssSelectorIdent', + 'escapeCssSelectorNth', + 'scopedQuerySelector', + 'scopedQuerySelectorAll', + ], + 'src/log': [ + 'user', + 'dev', + ] +}; + +module.exports = function(context) { + function ImportSpecifier(node, modulePath, mods) { + const {imported, local} = node; + const {name} = imported; + if (!mods.includes(name)) { + return; + } + + if (name === local.name) { + return; + } + + context.report({ + node, + message: [ + `Forbidden rename of import ${name} from ${modulePath}`, + 'This makes it easier to write lint rules for incorrect usage', + ].join('\n\t'), + }); + } + + function ImportNamespaceSpecifier(node, modulePath, mods) { + const ns = node.local.name; + const variable = context.getScope().set.get(ns); + const {references} = variable; + + for (let i = 0; i < references.length; i++) { + const ref = references[i]; + const node = context.getNodeByRangeIndex(ref.identifier.start); + const {parent} = node; + + if (parent.type !== 'MemberExpression') { + // Don't know what's going on here... + continue; + } + + if (parent.computed) { + context.report({ + node: parent, + message: 'Unable to determine what import is being used here.', + }); + continue; + } + + const {name} = parent.property; + if (mods.includes(name)) { + context.report({ + node: parent, + message: [ + `Illegal ${ns}.${name} use, must import and use as a lexical binding`, + 'This makes it easier to write lint rules for incorrect usage', + `\`import { ..., ${name}, ...} from '${modulePath}'\`;` + ].join('\n\t'), + }); + } + } + } + + return { + ImportDeclaration(node) { + const fileName = context.getFilename(); + if (/test-/.test(context.getFilename())) { + return; + } + + const {source, specifiers} = node; + const sourceValue = source.value; + const absolutePath = path.resolve(path.dirname(fileName), + sourceValue).replace(/\.js$/, ''); + + // Find out if the import matches one of the modules. + // But we don't know the repo's root directory, so do some work. + const parts = absolutePath.split('/'); + let modulePath = parts.pop(); + let mods; + while (parts.length && !mods) { + modulePath = `${parts.pop()}/${modulePath}`; + mods = imports[modulePath]; + } + + if (!mods) { + return; + } + + for (let i = 0; i < specifiers.length; i++) { + const spec = specifiers[i]; + + if (spec.type === 'ImportSpecifier') { + ImportSpecifier(spec, modulePath, mods); + } else if (spec.type === 'ImportNamespaceSpecifier') { + ImportNamespaceSpecifier(spec, sourceValue, mods); + } + } + }, + }; +}; diff --git a/build-system/eslint-rules/no-import.js b/build-system/eslint-rules/no-import.js new file mode 100644 index 000000000000..fc3ef5ab49f0 --- /dev/null +++ b/build-system/eslint-rules/no-import.js @@ -0,0 +1,27 @@ +/** + * Copyright 2018 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'; + const imports = ['sinon']; + module.exports = function(context) { + return { + ImportDeclaration(node) { + const name = node.source.value; + if (imports.includes(name)) { + context.report(node, `Importing ${name} is forbidden.`); + } + }, + }; +}; diff --git a/build-system/eslint-rules/no-is-amp-alt.js b/build-system/eslint-rules/no-is-amp-alt.js new file mode 100644 index 000000000000..36a8ed5cb643 --- /dev/null +++ b/build-system/eslint-rules/no-is-amp-alt.js @@ -0,0 +1,29 @@ +/** + * Copyright 2018 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'; + + +const selector = 'AssignmentExpression Identifier[name=IS_AMP_ALT]'; +module.exports = function(context) { + return { + [selector]: function(node) { + context.report({ + node, + message: 'No Assignment to IS_AMP_ALT global property allowed', + }); + } + }; +}; diff --git a/build-system/eslint-rules/no-mixed-operators.js b/build-system/eslint-rules/no-mixed-operators.js new file mode 100644 index 000000000000..94e41ebc641a --- /dev/null +++ b/build-system/eslint-rules/no-mixed-operators.js @@ -0,0 +1,50 @@ +/** + * Copyright 2018 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'; + +module.exports = { + meta: { + fixable: 'code', + }, + + create(context) { + const oror = 'LogicalExpression[operator="||"]'; + const andand = 'LogicalExpression[operator="&&"]'; + + return { + [`${oror} > ${andand}`](node) { + const sourceCode = context.getSourceCode(); + const before = sourceCode.getTokenBefore(node); + const after = sourceCode.getTokenAfter(node); + if (before && before.value === '(' && after && after.value === ')') { + return; + } + + context.report({ + node, + message: 'Detected mixed use of "&&" with "||" without' + + ' parenthesizing "(a && b)".', + fix(fixer) { + return [ + fixer.insertTextAfter(node, ')'), + fixer.insertTextBefore(node, '('), + ]; + }, + }); + } + }; + }, +}; diff --git a/build-system/eslint-rules/no-non-string-log-args.js b/build-system/eslint-rules/no-non-string-log-args.js new file mode 100644 index 000000000000..2bd2a3b8e648 --- /dev/null +++ b/build-system/eslint-rules/no-non-string-log-args.js @@ -0,0 +1,127 @@ +/** + * Copyright 2018 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. + * variadle: boolean, + * startPos: number + * }} + */ +let LogMethodMetadataDef; + + +/** + * @type {!Array} + */ +const transformableMethods = [ + {name: 'assert', variadic: false, startPos: 1}, + {name: 'assertString', variadic: false, startPos: 1}, + {name: 'assertNumber', variadic: false, startPos: 1}, + {name: 'assertBoolean', variadic: false, startPos: 1}, + {name: 'assertEnumValue', variadic: false, startPos: 2}, + {name: 'assertElement', variadic: false, startPos: 1}, + {name: 'createExpectedError', variadic: true, startPos: 0}, + {name: 'fine', variadic: true, startPos: 1}, + {name: 'info', variadic: true, startPos: 1}, + {name: 'warn', variadic: true, startPos: 1}, + {name: 'error', variadic: true, startPos: 1}, + {name: 'expectedError', variadic: true, startPos: 1}, + {name: 'createError', variadic: true, startPos: 0}, +]; + +/** + * @param {!Node} node + * @return {boolean} + */ +function isMessageString(node) { + if (node.type === 'Literal') { + return typeof node.value === 'string'; + } + // Allow for string concatenation operations. + if (node.type === 'BinaryExpression' && node.operator === '+') { + return isMessageString(node.left) && isMessageString(node.right); + } + return false; +} + +/** + * @param {string} name + * @return {!LogMethodMetadataDef} + */ +function getMetadata(name) { + return transformableMethods.find(cur => cur.name === name); +} + +const expressions = transformableMethods.map(method => { + return `CallExpression[callee.property.name=${method.name}]`; +}).join(','); + + +module.exports = function(context) { + return { + [expressions]: function(node) { + // Make sure that callee is a CallExpression as well. + // dev().assert() // enforce rule + // dev.assert() // ignore + const callee = node.callee; + const calleeObject = callee.object; + if (!calleeObject || + calleeObject.type !== 'CallExpression') { + return; + } + + // Make sure that the CallExpression is one of dev() or user(). + if(!['dev', 'user'].includes(calleeObject.callee.name)) { + return; + } + + const methodInvokedName = callee.property.name; + // Find the position of the argument we care about. + const metadata = getMetadata(methodInvokedName); + + // If there's no metadata, this is most likely a test file running + // private methods on log. + if (!metadata) { + return; + } + + const argToEval = node.arguments[metadata.startPos]; + + if (!argToEval) { + return; + } + + let errMsg = [ + `Must use a literal string for argument[${metadata.startPos}]`, + `on ${metadata.name} call.` + ].join(' '); + + if (metadata.variadic) { + errMsg += '\n\tIf you want to pass data to the string, use `%s` '; + errMsg += 'placeholders and pass additional arguments'; + } + + if (!isMessageString(argToEval)) { + context.report({ + node: argToEval, + message: errMsg, + }); + } + }, + }; + +}; diff --git a/build-system/eslint-rules/no-spread.js b/build-system/eslint-rules/no-spread.js new file mode 100644 index 000000000000..d0e672d72265 --- /dev/null +++ b/build-system/eslint-rules/no-spread.js @@ -0,0 +1,24 @@ +/** + * 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'; + +module.exports = function(context) { + return { + SpreadElement: function(node) { + context.report({node, message: 'No spread element allowed.'}); + }, + }; +}; diff --git a/build-system/eslint-rules/no-style-display.js b/build-system/eslint-rules/no-style-display.js new file mode 100644 index 000000000000..1e066ec62164 --- /dev/null +++ b/build-system/eslint-rules/no-style-display.js @@ -0,0 +1,145 @@ +/** + * Copyright 2018 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'; + +const path = require('path'); + +module.exports = function(context) { + const setStyleCall = 'CallExpression[callee.name=setStyle]'; + const setStylesCall = 'CallExpression[callee.name=setStyles], CallExpression[callee.name=setImportantStyles]'; + const resetStylesCall = 'CallExpression[callee.name=resetStyles]'; + + const displayMessage = [ + 'Do not set the display property using setStyle.', + 'Only the `toggle` helper in `src/style.js` is permitted to change the `display: none` style of an element.', + 'Or use `setInitialDisplay` to setup an initial `display: block`, `inline-block`, etc., if it is not possible to do so in CSS.', + ].join('\n\t'); + + return { + [setStyleCall]: function(node) { + const filePath = context.getFilename(); + if (filePath.endsWith('src/style.js')) { + return; + } + + const arg = node.arguments[1]; + if (!arg) { + return; + } + + if (arg.type !== 'Literal' || typeof arg.value !== 'string') { + if (arg.type === 'CallExpression') { + const {callee} = arg; + if (callee.type === 'Identifier' && callee.name === 'assertNotDisplay') { + return; + } + } + + return context.report({ + node: arg, + message: 'property argument (the second argument) to setStyle must be a string literal', + }); + } + + if (arg.value === 'display') { + context.report({ + node: arg, + message: displayMessage, + }); + } + }, + + [setStylesCall]: function(node) { + const callName = node.callee.name; + const arg = node.arguments[1]; + + if (!arg) { + return; + } + + if (arg.type !== 'ObjectExpression') { + if (arg.type === 'CallExpression') { + const {callee} = arg; + if (callee.type === 'Identifier' && callee.name === 'assertDoesNotContainDisplay') { + return; + } + } + + return context.report({ + node: arg, + message: `styles argument (the second argument) to ${callName} must be an object literal. You may also pass in an explicit call to assertDoesNotContainDisplay`, + }); + } + + const {properties} = arg; + for (let i = 0; i < properties.length; i++) { + const prop = properties[i]; + + if (prop.computed) { + context.report({ + node: prop, + message: 'Style names must not be computed', + }); + continue; + } + + const {key} = prop; + // `"display": "none"`, and `display: none` use two different AST keys. + if (key.value === 'display' || key.name === 'display') { + context.report({ + node: prop, + message: displayMessage, + }); + } + } + }, + + [resetStylesCall]: function(node) { + const arg = node.arguments[1]; + + if (!arg) { + return; + } + + if (arg.type !== 'ArrayExpression') { + return context.report({ + node: arg, + message: `styles argument (the second argument) to resetStyles must be an array literal`, + }); + } + + const {elements} = arg; + for (let i = 0; i < elements.length; i++) { + const el = elements[i]; + + if (el.type !== 'Literal' || typeof el.value !== 'string') { + context.report({ + node: el, + message: 'Style names must be string literals', + }); + continue; + } + + if (el.value === 'display') { + context.report({ + node: el, + message: displayMessage, + }); + } + } + }, + }; +}; diff --git a/build-system/eslint-rules/no-style-property-setting.js b/build-system/eslint-rules/no-style-property-setting.js new file mode 100644 index 000000000000..1a56aadacc97 --- /dev/null +++ b/build-system/eslint-rules/no-style-property-setting.js @@ -0,0 +1,51 @@ +/** + * Copyright 2018 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'; + +const path = require('path'); + +module.exports = function(context) { + return { + MemberExpression: function(node) { + const filePath = context.getFilename(); + const filename = path.basename(filePath); + // Ignore specific js files. + if (/^(keyframes-extractor|fixed-layer|style)\.js/ + .test(filename)) { + return; + } + // Ignore tests. + if (/^(test-(\w|-)+|(\w|-)+-testing)\.js$/.test(filename)) { + return; + } + // Ignore files in testing/ folder. + if (/\/testing\//.test(filePath)) { + return; + } + if (node.computed) { + return; + } + if (node.property.name == 'style') { + context.report({ + node, + message: 'The use of Element#style (CSSStyleDeclaration live ' + + 'object) to style elements is forbidden. Use getStyle and ' + + 'setStyle from style.js', + }); + } + }, + }; +}; diff --git a/build-system/eslint-rules/no-swallow-return-from-allow-console-error.js b/build-system/eslint-rules/no-swallow-return-from-allow-console-error.js new file mode 100644 index 000000000000..a0337f453704 --- /dev/null +++ b/build-system/eslint-rules/no-swallow-return-from-allow-console-error.js @@ -0,0 +1,65 @@ +/** + * Copyright 2018 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'; + +module.exports = { + meta: { + fixable: 'code', + }, + + create(context) { + return { + ReturnStatement(node) { + if (!/test/.test(context.getFilename())) { + return; + } + let {parent} = node; + if (parent.type !== 'BlockStatement') { + return; + } + + parent = parent.parent; + if (!parent.type.includes('Function')) { + return; + } + + parent = parent.parent; + if (parent.type !== 'CallExpression') { + return; + } + + const {callee} = parent; + if (callee.type !== 'Identifier' || + callee.name !== 'allowConsoleError') { + return; + } + + const callParent = parent.parent; + if (callParent.type === 'ReturnStatement') { + return; + } + + context.report({ + node: parent, + message: 'Must return allowConsoleError if callback contains a return', + fix(fixer) { + return fixer.insertTextBefore(parent, 'return '); + }, + }); + } + }; + }, +}; diff --git a/build-system/eslint-rules/package.json b/build-system/eslint-rules/package.json new file mode 100644 index 000000000000..1a5b0a640fb1 --- /dev/null +++ b/build-system/eslint-rules/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-amphtml-internal", + "version": "0.1.0", + "description": "ESLint rules for the AMP HTML project", + "private": true +} diff --git a/build-system/eslint-rules/prefer-deferred-promise.js b/build-system/eslint-rules/prefer-deferred-promise.js new file mode 100644 index 000000000000..4480afdcfce0 --- /dev/null +++ b/build-system/eslint-rules/prefer-deferred-promise.js @@ -0,0 +1,125 @@ +/** + * Copyright 2018 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'; + +module.exports = function(context) { + function isAssignment(node, name) { + if (node.type !== 'AssignmentExpression') { + return false; + } + + const {right} = node; + return right.type === 'Identifier' && right.name === name; + } + + return { + // Promise.resolve(CALL()) + CallExpression(node) { + if (/\btest|build-system/.test(context.getFilename())) { + return; + } + + const {callee} = node; + if (callee.type !== 'MemberExpression') { + return; + } + + const {object, property} = callee; + if (object.type !== 'Identifier' || + object.name !== 'Promise' || + property.type !== 'Identifier' || + property.name !== 'resolve') { + return; + } + + const arg = node.arguments[0]; + if (!arg || arg.type !== 'CallExpression') { + return; + } + + context.report({ + node, + message: 'Use the Promise constructor, or tryResolve in the ' + + 'src/utils/promise.js module.', + }); + }, + + // new Promise(...) + NewExpression(node) { + if (/\btest|build-system/.test(context.getFilename())) { + return; + } + + const {callee} = node; + if (callee.type !== 'Identifier' || + callee.name !== 'Promise') { + return; + } + + const comments = context.getCommentsBefore(callee); + const ok = comments.some(comment => comment.value === 'OK'); + if (ok) { + return; + } + + const resolver = node.arguments[0]; + if (!/Function/.test(resolver.type)) { + context.report({node: resolver, message: 'Must pass function'}); + return; + } + + const resolve = resolver.params[0]; + if (!resolve || resolve.type !== 'Identifier') { + context.report({node: resolver, message: 'Must have resolve param'}); + return; + } + + const {name} = resolve; + let assigned = false; + + if (resolver.type === 'ArrowFunctionExpression' && + resolver.expression === true) { + const {body} = resolver; + assigned = isAssignment(body, name); + } else { + const {body} = resolver.body; + + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement') { + continue; + } + + const {expression} = node; + assigned = isAssignment(expression, name); + if (assigned) { + break; + } + } + } + + if (!assigned) { + return; + } + + const message = [ + 'Instead of creating a pending Promise, please use ', + 'Deferred in the src/utils/promise.js module.', + ].join('\n\t'); + context.report({node: resolver, message}); + }, + }; +}; diff --git a/build-system/eslint-rules/prefer-destructuring.js b/build-system/eslint-rules/prefer-destructuring.js new file mode 100644 index 000000000000..9c723bda0f94 --- /dev/null +++ b/build-system/eslint-rules/prefer-destructuring.js @@ -0,0 +1,207 @@ +/** + * Copyright 2018 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'; + +module.exports = { + meta: { + fixable: 'code', + }, + + create(context) { + function shouldBeDestructure(node, renamable = false) { + const {id, init} = node; + + if (!init || + id.type !== 'Identifier' || + init.type !== 'MemberExpression') { + return false; + } + + const {name} = id; + const {object, property, computed} = init; + if (computed || + object.type === 'Super' || + property.leadingComments || + property.type !== 'Identifier') { + return false; + } + + return renamable || property.name === name; + } + + function shouldBeIdempotent(node) { + while (node.type === 'MemberExpression') { + node = node.object; + } + + return node.type === 'Identifier' || node.type === 'ThisExpression'; + } + + function setStruct(map, key, node, declaration) { + if (map.has(key)) { + const struct = map.get(key); + struct.nodes.add(node); + struct.declarations.add(declaration); + return map.get(key).names; + } else { + const struct = { + names: new Set(), + nodes: new Set(), + declarations: new Set(), + node, + }; + map.set(key, struct); + return struct.names; + } + } + + function processMaps(maps) { + for (let i = 0; i < maps.length; i++) { + const map = maps[i]; + map.forEach(processVariables); + map.clear(); + } + } + + function processVariables(struct, base) { + const {names, nodes, declarations, node} = struct; + + if (nodes.size === 0) { + return; + } + + context.report({ + node, + message: 'Combine repeated declarators into a destructure', + fix(fixer) { + const fixes = []; + const ids = []; + + names.forEach(name => ids.push(name)); + const replacement = `{${ids.join(', ')}} = ${base}`; + fixes.push(fixer.replaceText(node, replacement)); + + declarations.forEach(declaration => { + const {declarations} = declaration; + const all = declarations.every(decl => nodes.has(decl)); + if (!all) { + return; + } + + fixes.push(fixer.remove(declaration)); + declarations.forEach(decl => nodes.delete(decl)); + }); + + nodes.forEach(node => { + fixes.push(fixer.remove(node)); + }); + return fixes; + }, + }); + } + + return { + VariableDeclarator(node) { + if (!shouldBeDestructure(node)) { + return; + } + + const {init} = node; + if (init.leadingComments) { + return; + } + + context.report({ + node, + message: 'Use object destructuring', + fix(fixer) { + const sourceCode = context.getSourceCode(); + const {object, property} = init; + const {name} = property; + const base = sourceCode.getText(object); + return fixer.replaceText(node, `{${name}} = ${base}`); + }, + }); + }, + + 'BlockStatement, Program': function(node) { + const {body} = node; + const sourceCode = context.getSourceCode(); + const letMap = new Map(); + const constMap = new Map(); + + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'VariableDeclaration') { + processMaps([letMap, constMap]); + continue; + } + + const {declarations, kind} = node; + const variables = kind === 'let' ? letMap : constMap; + + for (let j = 0; j < declarations.length; j++) { + const decl = declarations[j]; + const {id, init} = decl; + + if (!init || init.leadingComments) { + continue; + } + + if (!shouldBeIdempotent(init)) { + continue; + } + + if (id.type === 'Identifier') { + // Allow renaming here + if (!shouldBeDestructure(decl, true)) { + continue; + } + + const base = sourceCode.getText(init.object); + const names = setStruct(variables, base, decl, node); + + // Do we need to rename? + if (shouldBeDestructure(decl)) { + names.add(id.name); + } else { + names.add(`${init.property.name}: ${id.name}`); + } + + continue; + } + + if (id.type === 'ObjectPattern') { + const base = sourceCode.getText(init); + const names = setStruct(variables, base, decl, node); + const {properties} = id; + for (let k = 0; k < properties.length; k++) { + const {key} = properties[k]; + if (key.type !== 'Identifier') { + // Deep destructuring, too complicated. + return; + } + names.add(key.name); + } + } + } + } + + processMaps([letMap, constMap]); + }, + }; + }, +}; diff --git a/build-system/eslint-rules/query-selector.js b/build-system/eslint-rules/query-selector.js new file mode 100644 index 000000000000..be5daa7692e4 --- /dev/null +++ b/build-system/eslint-rules/query-selector.js @@ -0,0 +1,194 @@ +/** + * Copyright 2018 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'; + +module.exports = function(context) { + function callQuerySelector(node) { + const {callee} = node; + + // If it's not a querySelector(All) call, I don't care about it. + const {property} = callee; + if (property.type !== 'Identifier' || + !property.name.startsWith('querySelector')) { + return; + } + + if (property.leadingComments) { + const ok = property.leadingComments.some(comment => { + return comment.value === 'OK'; + }); + if (ok) { + return; + } + } + + const selector = getSelector(node, 0); + + // What are we calling querySelector on? + let obj = callee.object; + if (obj.type === 'CallExpression') { + obj = obj.callee; + } + if (obj.type === 'MemberExpression') { + obj = obj.property; + } + + // Any query selector is allowed on document + // This check must be done after getting the selector, to ensure the + // selector adheres to escaping requirements. + if (obj.type === 'Identifier' && /[dD]oc|[rR]oot/.test(obj.name)) { + return; + } + + if (!selectorNeedsScope(selector)) { + return; + } + + context.report({ + node, + 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 querySelector("div div")). Instead, use the ' + + 'scopedQuerySelector in src/dom.js', + }); + } + + function callScopedQuerySelector(node) { + const {callee} = node; + if (!callee.name.startsWith('scopedQuerySelector')) { + return; + } + + if (callee.trailingComments) { + const ok = callee.trailingComments.some(comment => { + return comment.value === 'OK'; + }); + if (ok) { + return; + } + } + + const selector = getSelector(node, 1); + + if (selectorNeedsScope(selector)) { + return; + } + + context.report({ + node, + message: 'using scopedQuerySelector here is actually ' + + "unnecessary, since you don't use child selector semantics.", + }); + } + + function getSelector(node, argIndex) { + const arg = node.arguments[argIndex]; + let selector; + + if (!arg) { + context.report({node, message: 'no argument to query selector'}); + selector = 'dynamic value'; + } else if (arg.type === 'Literal') { + selector = arg.value; + } else if (arg.type === 'TemplateLiteral') { + + // Ensure all template variables are properly escaped. + let accumulator = ''; + const quasis = arg.quasis.map(v => v.value.raw); + for (let i = 0; i < arg.expressions.length; i++) { + const expression = arg.expressions[i]; + accumulator += quasis[i]; + + if (expression.type === 'CallExpression') { + const {callee} = expression; + if (callee.type === 'Identifier') { + const inNthChild = /:nth-(last-)?(child|of-type|col)\([^)]*$/.test( + accumulator); + + if (callee.name === 'escapeCssSelectorIdent') { + if (inNthChild) { + context.report({ + node: expression, + message: 'escapeCssSelectorIdent may not ' + + 'be used inside an :nth-X psuedo-class. Please use ' + + 'escapeCssSelectorNth instead.', + }); + } + continue; + } else if (callee.name === 'escapeCssSelectorNth') { + if (!inNthChild) { + context.report({ + node: expression, + message: 'escapeCssSelectorNth may only be ' + + 'used inside an :nth-X psuedo-class. Please use ' + + 'escapeCssSelectorIdent instead.', + }); + } + continue; + } + } + } + + context.report({ + node: expression, + message: 'Each selector value must be escaped by ' + + 'escapeCssSelectorIdent in src/dom.js', + }); + } + + selector = quasis.join(''); + } else { + if (arg.type === 'BinaryExpression') { + context.report({node: arg, message: 'Use a template literal string'}); + } + selector = 'dynamic value'; + } + + // strip out things that can't affect children selection + selector = selector.replace(/\(.*\)|\[.*\]/, function(match) { + return match[0] + match[match.length - 1]; + }); + + return selector; + } + + // Checks if the selector is using grandchild selector semantics + // `node.querySelector('child grandchild')` or `'child>grandchild'` But, + // specifically allow multi-selectors `'div, span'`. + function selectorNeedsScope(selector) { + // This regex actually verifies there is no whitespace (implicit child + // semantics) or `>` chars (direct child semantics). The one exception is + // for `,` multi-selectors, which can have whitespace. + const noChildSemantics = /^(\s*,\s*|(?!\s|>).)*$/.test(selector); + return !noChildSemantics; + } + + return { + CallExpression(node) { + if (/test-/.test(context.getFilename())) { + return; + } + + const {callee} = node; + if (callee.type === 'MemberExpression') { + callQuerySelector(node); + } else if (callee.type === 'Identifier') { + callScopedQuerySelector(node); + } + }, + }; +}; diff --git a/build-system/eslint-rules/todo-format.js b/build-system/eslint-rules/todo-format.js new file mode 100644 index 000000000000..81a7da2a45eb --- /dev/null +++ b/build-system/eslint-rules/todo-format.js @@ -0,0 +1,35 @@ +/** + * 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. + */ +'use strict'; + +module.exports = function(context) { + return { + Program: function(node) { + if (node.comments) { + node.comments.forEach(function(comment) { + if (/TODO/.test(comment.value) && + !/TODO\(@?\w+,\s*#\d{1,}\)/.test(comment.value)) { + context.report({ + node: comment, + message: 'TODOs must be in TODO(@username, #1234) format. ' + + 'Found: "' + comment.value.trim() + '"', + }); + } + }); + } + }, + }; +}; diff --git a/build-system/eslint-rules/unused-private-field.js b/build-system/eslint-rules/unused-private-field.js new file mode 100644 index 000000000000..2ce1d0949545 --- /dev/null +++ b/build-system/eslint-rules/unused-private-field.js @@ -0,0 +1,223 @@ +/** + * Copyright 2018 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'; + +module.exports = { + meta: { + fixable: 'code', + }, + + create(context) { + const stack = []; + function current() { + return stack[stack.length - 1]; + } + + function shouldIgnoreFile() { + return /\b(test|examples)\b/.test(context.getFilename()); + } + + const checkers = { + '@visibleForTesting': visibleForTestingUse, + '@restricted': restrictedUse, + '@protected': uncheckableUse, + '@override': uncheckableUse, + }; + + function checkerForAnnotation(node) { + const comments = context.getCommentsBefore(node); + + for (let i = 0; i < comments.length; i++) { + const comment = comments[i].value; + + for (const type in checkers) { + if (comment.includes(type)) { + return checkers[type]; + } + } + } + + return unannotatedUse; + } + + // Restricteds must be used in the file, but not in the class. + function restrictedUse(node, name, used) { + if (used) { + const message = [ + `Used restricted private "${name}".`.padEnd(80), + "It's marked @restricted, but it's used in the class.", + 'Please remove the @restricted annotation.', + ].join('\n\t'); + context.report({node, message}); + return; + } + + const sourceCode = context.getSourceCode(); + const {text} = sourceCode; + + let index = -1; + while (true) { + index = text.indexOf(name, index + 1); + if (index === -1) { + break; + } + + const node = sourceCode.getNodeByRangeIndex(index); + if (!node || node.type !== 'Identifier') { + continue; + } + + const {parent} = node; + if (parent.type === 'MemberExpression' && + shouldCheckMember(parent, false) && + !isAssignment(parent)) { + return; + } + } + + + const message = [ + `Unused restricted private "${name}".`.padEnd(80), + "It's marked @restricted, but it's still unused in the file.", + ].join('\n\t'); + context.report({node, message}); + } + + // VisibleForTestings must not be used in the class. + function visibleForTestingUse(node, name, used) { + if (!used) { + return; + } + + const message = [ + `Used visibleForTesting private "${name}".`.padEnd(80), + "It's marked @visibleForTesting, but it's used in the class.", + 'Please remove the @visibleForTesting annotation.', + ].join('\n\t'); + context.report({node, message}); + } + + // Protected and Override are uncheckable. Let Closure handle that. + function uncheckableUse() { + // Noop. + } + + // Unannotated fields must be used in the class + function unannotatedUse(node, name, used) { + if (used) { + return; + } + + const message = [ + `Unused private "${name}".`.padEnd(80), // Padding for alignment + 'If this is used for testing, annotate with `@visibleForTesting`.', + 'If this is a private used in the file, `@restricted`.', + 'If this is used in a subclass, `@protected`.', + 'If this is an override of a protected, `@override`.', + 'If none of these exceptions applies, please contact @jridgewell.', + ].join('\n\t'); + context.report({node, message}); + } + + + + function shouldCheckMember(node, needsThis = true) { + const {computed, object, property} = node; + if (computed || + (needsThis && object.type !== 'ThisExpression') || + property.type !== 'Identifier') { + return false; + } + + return isPrivateName(property); + } + + function isAssignment(node) { + const {parent} = node; + if (!parent) { + return false; + } + return parent.type === 'AssignmentExpression' && parent.left === node; + } + + function isPrivateName(node) { + return node.name.endsWith('_'); + } + + return { + ClassBody() { + if (shouldIgnoreFile()) { + return; + } + + stack.push({used: new Set(), declared: new Map()}); + }, + + 'ClassBody:exit': function() { + if (shouldIgnoreFile()) { + return; + } + + const {used, declared} = stack.pop(); + + declared.forEach((node, name) => { + const checker = checkerForAnnotation(node); + checker(node, name, used.has(name)); + }); + }, + + 'ClassBody > MethodDefinition': function(node) { + if (shouldIgnoreFile()) { + return; + } + + const {computed, key} = node; + if (computed || + !isPrivateName(key)) { + return; + } + + const {name} = key; + const {declared} = current(); + declared.set(name, node); + }, + + 'MethodDefinition[kind="constructor"] MemberExpression': function(node) { + if (shouldIgnoreFile() || + !shouldCheckMember(node) || + !isAssignment(node)) { + return; + } + + const {name} = node.property; + const {declared} = current(); + declared.set(name, node.parent); + }, + + 'ClassBody MemberExpression': function(node) { + if (shouldIgnoreFile() || + !shouldCheckMember(node, false) || + isAssignment(node)) { + return; + } + + const {name} = node.property; + const {used} = current(); + used.add(name); + }, + }; + }, +}; diff --git a/build-system/eslint-rules/vsync.js b/build-system/eslint-rules/vsync.js new file mode 100644 index 000000000000..525d95b41bf5 --- /dev/null +++ b/build-system/eslint-rules/vsync.js @@ -0,0 +1,55 @@ +/** + * Copyright 2018 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'; + +module.exports = function(context) { + return { + CallExpression(node) { + if (/test-/.test(context.getFilename())) { + return; + } + + const {callee} = node; + if (callee.type !== 'MemberExpression') { + return; + } + + const {property} = callee; + if (property.type !== 'Identifier' || property.name !== 'vsyncFor') { + return; + } + + if (property.leadingComments) { + const ok = property.leadingComments.some(comment => { + return comment.value === 'OK'; + }); + if (ok) { + return; + } + } + + context.report({ + node, + message: [ + 'VSync is now a privileged service.', + 'You likely want to use the `BaseElement` methods' + + ' `measureElement`, `mutateElement`, or `runElement`.', + 'In the worst case use the same methods on `Resources`.', + ].join('\n\t'), + }); + }, + }; +}; diff --git a/build-system/exec.js b/build-system/exec.js new file mode 100644 index 000000000000..d9d9a4341814 --- /dev/null +++ b/build-system/exec.js @@ -0,0 +1,107 @@ +/** + * 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. + */ +'use strict'; + +/** + * @fileoverview Provides functions for executing tasks in a child process. + */ + +const childProcess = require('child_process'); + +const shellCmd = (process.platform == 'win32') ? 'cmd' : '/bin/sh'; +const shellFlag = (process.platform == 'win32') ? '/C' : '-c'; + +/** + * Spawns the given command in a child process with the given options. + * + * @param {string} cmd + * @param {} options + * @return {} Process info. + */ +function spawnProcess(cmd, options) { + return childProcess.spawnSync(shellCmd, [shellFlag, cmd], options); +} + +/** + * Executes the provided command with the given options, returning the process + * object. + * + * @param {string} cmd Command line to execute. + * @param {} options + * @return {} Process info. + */ +exports.exec = function(cmd, options) { + options = options || {'stdio': 'inherit'}; + return spawnProcess(cmd, options); +}; + +/** + * Executes the provided shell script in an asynchronous process. + * + * @param {string} script + * @param {} options + */ +exports.execScriptAsync = function(script, options) { + return childProcess.spawn(shellCmd, [shellFlag, script], options); +}; + +/** + * Executes the provided command, and terminates the program in case of failure. + * + * @param {string} cmd Command line to execute. + * @param {} options Extra options to send to the process. + */ +exports.execOrDie = function(cmd, options) { + const p = exports.exec(cmd, options); + if (p.status != 0) { + process.exit(p.status); + } +}; + +/** + * Executes the provided command, returning the process object. + * @param {string} cmd + * @return {!Object} + */ +function getOutput(cmd) { + const p = spawnProcess( + cmd, + { + 'cwd': process.cwd(), + 'env': process.env, + 'stdio': 'pipe', + 'encoding': 'utf-8', + }); + return p; +} + +/** + * Executes the provided command, returning its stdout. + * @param {string} cmd + * @return {string} + */ +exports.getStdout = function(cmd) { + return getOutput(cmd).stdout; +}; + +/** + * Executes the provided command, returning its stderr. + * @param {string} cmd + * @return {string} + */ +exports.getStderr = function(cmd) { + return getOutput(cmd).stderr; +}; diff --git a/build-system/git.js b/build-system/git.js new file mode 100644 index 000000000000..9de45b9e6e25 --- /dev/null +++ b/build-system/git.js @@ -0,0 +1,125 @@ +/** + * Copyright 2018 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'; + +/** + * @fileoverview Provides functions for executing various git commands. + */ + +const {getStdout} = require('./exec'); + +/** + * Returns the branch point of the current branch off of master. + * @param {boolean} fromMerge true if this is a merge commit. + * @return {string} + */ +exports.gitBranchPoint = function(fromMerge = false) { + if (fromMerge) { + return getStdout('git merge-base HEAD^1 HEAD^2').trim(); + } else { + return getStdout('git merge-base master HEAD^').trim(); + } +}; + +/** + * Returns the list of files changed on the local branch relative to the branch + * point off of master, one on each line. + * @return {!Array} + */ +exports.gitDiffNameOnlyMaster = function() { + const branchPoint = exports.gitBranchPoint(); + return getStdout(`git diff --name-only ${branchPoint}`).trim().split('\n'); +}; + +/** + * Returns the list of files changed on the local branch relative to the branch + * point off of master, in diffstat format. + * @return {string} + */ +exports.gitDiffStatMaster = function() { + const branchPoint = exports.gitBranchPoint(); + return getStdout(`git -c color.ui=always diff --stat ${branchPoint}`); +}; + +/** + * Returns the list of files added by the local branch relative to the branch + * point off of master, one on each line. + * @return {!Array} + */ +exports.gitDiffAddedNameOnlyMaster = function() { + const branchPoint = exports.gitBranchPoint(); + return getStdout(`git diff --name-only --diff-filter=ARC ${branchPoint}`) + .trim().split('\n'); +}; + +/** + * Returns the full color diff of the uncommited changes on the local branch. + * @return {string} + */ +exports.gitDiffColor = function() { + return getStdout('git -c color.ui=always diff').trim(); +}; + +/** + * Returns the URL of the origin (upstream) repository. + * @return {string} + */ +exports.gitOriginUrl = function() { + return getStdout('git remote get-url origin').trim(); +}; + +/** + * Returns the name of the local branch. + * @return {string} + */ +exports.gitBranchName = function() { + return getStdout('git rev-parse --abbrev-ref HEAD').trim(); +}; + +/** + * Returns the commit hash of the latest commit. + * @return {string} + */ +exports.gitCommitHash = function() { + return getStdout('git rev-parse --verify HEAD').trim(); +}; + +/** + * Returns the email of the author of the latest commit on the local branch. + * @return {string} + */ +exports.gitCommitterEmail = function() { + return getStdout('git log -1 --pretty=format:"%ae"').trim(); +}; + +/** + * Returns the timestamp of the latest commit on the local branch. + * @return {number} + */ +exports.gitCommitFormattedTime = function() { + return getStdout( + 'TZ=UTC git log -1 --pretty="%cd" --date=format-local:%y%m%d%H%M%S') + .trim(); +}; + +/** + * Returns machine parsable list of uncommitted changed files, or an empty + * string if no files were changed. + * @return {string} + */ +exports.gitStatusPorcelain = function() { + return getStdout('git status --porcelain').trim(); +}; diff --git a/build-system/global-configs/OWNERS.yaml b/build-system/global-configs/OWNERS.yaml new file mode 100644 index 000000000000..c8b8828aeedb --- /dev/null +++ b/build-system/global-configs/OWNERS.yaml @@ -0,0 +1,3 @@ +- erwinmombay +- rsimha +- choumx 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..0a0c699dc79a --- /dev/null +++ b/build-system/global-configs/canary-config.json @@ -0,0 +1,58 @@ +{ + "allow-doc-opt-in": [ + "amp-date-picker", + "amp-next-page", + "ampdoc-shell", + "disable-amp-story-desktop", + "inabox-rov", + "url-replacement-v2", + "linker-meta-opt-in" + ], + "allow-url-opt-in": [ + "pump-early-frame", + "twitter-default-placeholder", + "twitter-default-placeholder-pulse", + "twitter-default-placeholder-fade" + ], + + "canary": 1, + "expAdsenseA4A": 0.01, + "a4aProfilingRate": 0.1, + "ad-type-custom": 1, + "amp-access-iframe": 1, + "amp-apester-media": 1, + "amp-ima-video": 1, + "amp-playbuzz": 1, + "chunked-amp": 1, + "pump-early-frame": 1, + "amp-auto-ads": 1, + "amp-auto-ads-adsense-holdout": 0.1, + "amp-auto-ads-adsense-responsive": 0.05, + "version-locking": 1, + "as-use-attr-for-format": 0.01, + "a4aFastFetchDoubleclickLaunched": 0, + "a4aFastFetchAdSenseLaunched": 0, + "no-sync-xhr-in-ads": 1, + "amp-live-list-sorting": 1, + "amp-sidebar toolbar": 1, + "amp-consent": 1, + "amp-story-hold-to-pause": 1, + "amp-story-responsive-units": 1, + "amp-story-v1": 1, + "expAdsenseUnconditionedCanonical": 0.01, + "expAdsenseCanonical": 0.01, + "faster-bind-scan": 1, + "font-display-swap": 1, + "amp-date-picker": 1, + "linker-meta-opt-in": 1, + "user-error-reporting": 1, + "no-initial-intersection": 1, + "doubleclickSraExp": 0.01, + "doubleclickSraReportExcludedBlock": 0.1, + "inabox-rov": 1, + "layers": 0, + "linker-form": 1, + "scroll-height-bounce": 0, + "scroll-height-minheight": 0, + "hidden-mutation-observer": 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..8df365eb8de7 --- /dev/null +++ b/build-system/global-configs/prod-config.json @@ -0,0 +1,59 @@ +{ + "allow-doc-opt-in": [ + "amp-date-picker", + "amp-next-page", + "ampdoc-shell", + "disable-amp-story-desktop", + "inabox-rov", + "url-replacement-v2", + "linker-meta-opt-in" + ], + "allow-url-opt-in": [ + "pump-early-frame", + "twitter-default-placeholder", + "twitter-default-placeholder-pulse", + "twitter-default-placeholder-fade" + ], + + "canary": 0, + "expAdsenseA4A": 0.01, + "a4aProfilingRate": 0.01, + "ad-type-custom": 1, + "amp-access-iframe": 1, + "amp-apester-media": 1, + "amp-ima-video": 1, + "amp-playbuzz": 1, + "chunked-amp": 1, + "amp-auto-ads": 1, + "amp-auto-ads-adsense-holdout": 0.1, + "amp-auto-ads-adsense-responsive": 0.05, + "version-locking": 1, + "as-use-attr-for-format": 0.01, + "a4aFastFetchDoubleclickLaunched": 0, + "a4aFastFetchAdSenseLaunched": 0, + "pump-early-frame": 1, + "amp-live-list-sorting": 1, + "amp-sidebar toolbar": 1, + "amp-consent": 1, + "amp-story-hold-to-pause": 1, + "amp-story-responsive-units": 1, + "amp-story-v1": 1, + "expAdsenseUnconditionedCanonical": 0.01, + "expAdsenseCanonical": 0.01, + "faster-bind-scan": 1, + "font-display-swap": 1, + "amp-date-picker": 1, + "linker-meta-opt-in": 1, + "url-replacement-v2": 1, + "user-error-reporting": 1, + "no-initial-intersection": 1, + "no-sync-xhr-in-ads": 1, + "doubleclickSraExp": 0.01, + "doubleclickSraReportExcludedBlock": 0.1, + "inabox-rov": 1, + "ampdoc-closest": 0.01, + "linker-form": 1, + "scroll-height-bounce": 0, + "scroll-height-minheight": 0, + "hidden-mutation-observer": 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 5a654ce67a1c..789626fe26a0 100644 --- a/build-system/internal-version.js +++ b/build-system/internal-version.js @@ -13,10 +13,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; -var argv = require('minimist')(process.argv.slice(2)); -var suffix = argv.type == 'canary' ? '-canary' : ''; +const argv = require('minimist')(process.argv.slice(2)); +const {gitCommitFormattedTime, gitStatusPorcelain} = require('./git'); -// Used to e.g. references the ads binary from the runtime to get -// version lock. -exports.VERSION = new Date().getTime() + suffix; +function getVersion() { + if (argv.version) { + return String(argv.version); + } else { + // Generate a consistent version number by using the commit* time of the + // latest commit on the active branch as the twelve digits, and use the + // state of the working directory as the last digit: 0 for a "clean" tree, 1 + // if there are uncommited changes in the working directory. + // + // e.g., the version number of a clean (no uncommited changes) tree that was + // commited on August 1, 2018 at 14:31:11 EDT would be `1808011831110` + // (notice that due to timezone shift, the hour value changes from EDT's 14 + // to UTC's 18. The last digit denotes that this is a clean tree.) + // + // *Commit time is different from author time! Commit time is the time that + // the PR was merged into master; author time is when the author ran the + // "git commit" command. + const lastCommitFormattedTime = gitCommitFormattedTime(); + if (gitStatusPorcelain()) { + return `${lastCommitFormattedTime}1`; + } else { + return `${lastCommitFormattedTime}0`; + } + } +} + +// Used to e.g. references the ads binary from the runtime to get version lock. +exports.VERSION = getVersion(); diff --git a/build-system/pr-check.js b/build-system/pr-check.js new file mode 100644 index 000000000000..be3f6c7b1663 --- /dev/null +++ b/build-system/pr-check.js @@ -0,0 +1,687 @@ +/** + * 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'; + +/** + * @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 argv = require('minimist')(process.argv.slice(2)); +const atob = require('atob'); +const colors = require('ansi-colors'); +const config = require('./config'); +const minimatch = require('minimatch'); +const path = require('path'); +const {execOrDie, exec, getStderr, getStdout} = require('./exec'); +const {gitDiffColor, gitDiffNameOnlyMaster, gitDiffStatMaster} = require('./git'); + +const fileLogPrefix = colors.bold(colors.yellow('pr-check.js:')); + +/** + * Starts a timer to measure the execution time of the given function. + * @param {string} functionName + * @return {DOMHighResTimeStamp} + */ +function startTimer(functionName) { + const startTime = Date.now(); + console.log( + '\n' + fileLogPrefix, 'Running', colors.cyan(functionName) + '...'); + return startTime; +} + +/** + * Stops the timer for the given function and prints the execution time. + * @param {string} functionName + * @param {DOMHighResTimeStamp} startTime + * @return {number} + */ +function stopTimer(functionName, startTime) { + const endTime = Date.now(); + const executionTime = new Date(endTime - startTime); + const mins = executionTime.getMinutes(); + const secs = executionTime.getSeconds(); + console.log( + fileLogPrefix, 'Done running', colors.cyan(functionName), + 'Total time:', colors.green(mins + 'm ' + secs + 's')); +} + +/** + * Executes the provided command and times it. Errors, if any, are printed. + * @param {string} cmd + * @return {} Process info. + */ +function timedExec(cmd) { + const startTime = startTimer(cmd); + const p = exec(cmd); + stopTimer(cmd, startTime); + return p; +} + +/** + * Executes the provided command and times it. The program terminates in case of + * failure. + * @param {string} cmd + */ +function timedExecOrDie(cmd) { + const startTime = startTimer(cmd); + execOrDie(cmd); + stopTimer(cmd, startTime); +} + +/** + * Returns a list of files in the commit range within this pull request (PR) + * after filtering out commits to master from other PRs. + * @return {!Array} + */ +function filesInPr() { + const files = gitDiffNameOnlyMaster(); + const changeSummary = gitDiffStatMaster(); + console.log(fileLogPrefix, + 'Testing the following changes at commit', + colors.cyan(process.env.TRAVIS_PULL_REQUEST_SHA)); + console.log(changeSummary); + return files; +} + +/** + * 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' && + // Exclude config files from build-system since we want it to trigger + // the flag config check. + !isFlagConfig(filePath) && + // Exclude the dev dashboard from build-system, since we want it to + // trigger the devDashboard check + !isDevDashboardFile(filePath) && + // Exclude visual diff files from build-system since we want it to trigger + // visual diff tests. + !isVisualDiffFile(filePath)) + // OWNERS.yaml files should trigger build system to run tests + || isOwnersFile(filePath); +} + +/** + * 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; + } + + // validator files for each extension + if (!filePath.startsWith('extensions/')) { + return false; + } + + const pathArray = path.dirname(filePath).split(path.sep); + if (pathArray.length < 2) { + // At least 2 with ['extensions', '{$name}'] + return false; + } + + // Validator files take the form of validator-.*\.(html|out|protoascii) + const name = path.basename(filePath); + return name.startsWith('validator-') && + (name.endsWith('.out') || name.endsWith('.html') || + name.endsWith('.protoascii')); +} + +/** + * Determines if the given path has a OWNERS.yaml basename. + * @param {string} filePath + * @return {boolean} + */ +function isOwnersFile(filePath) { + return path.basename(filePath) === 'OWNERS.yaml'; +} + +/** + * Determines if the given file is a markdown file containing documentation. + * @param {string} filePath + * @return {boolean} + */ +function isDocFile(filePath) { + return path.extname(filePath) == '.md' && !filePath.startsWith('examples/'); +} + +/** + * Determines if the given file is related to the visual diff tests. + * @param {string} filePath + * @return {boolean} + */ +function isVisualDiffFile(filePath) { + const filename = path.basename(filePath); + return (filename == 'visual-diff.js' || + filename == 'visual-tests' || + filePath.startsWith('examples/visual-tests/')); +} + +/** + * Determines if the given file is a unit test. + * @param {string} filePath + * @return {boolean} + */ +function isUnitTest(filePath) { + return config.unitTestPaths.some(pattern => { + return minimatch(filePath, pattern); + }); +} + +/** + * Determines if the given file is, + * a file concerning the dev dashboard + * Concerning the dev dashboard + * @param {string} filePath + * @return {boolean} + */ +function isDevDashboardFile(filePath) { + return (filePath === 'build-system/app.js' || + filePath.startsWith('build-system/app-index/')); +} + +/** + * Determines if the given file is an integration test. + * @param {string} filePath + * @return {boolean} + */ +function isIntegrationTest(filePath) { + return config.integrationTestPaths.some(pattern => { + return minimatch(filePath, pattern); + }); +} + +/** + * Determines if the given file contains flag configurations, by comparing it + * against the well-known json config filenames for prod and canary. + * @param {string} filePath + * @return {boolean} + */ +function isFlagConfig(filePath) { + const filename = path.basename(filePath); + return (filename == 'prod-config.json' || filename == 'canary-config.json'); +} + +/** + * Determines the targets that will be executed by the main method of + * this script. The order within this function matters. + * @param {!Array} filePaths + * @return {!Set} + */ +function determineBuildTargets(filePaths) { + if (filePaths.length == 0) { + return new Set([ + 'BUILD_SYSTEM', + 'VALIDATOR_WEBUI', + 'VALIDATOR', + 'RUNTIME', + 'UNIT_TEST', + 'DEV_DASHBOARD', + 'INTEGRATION_TEST', + 'DOCS', + 'FLAG_CONFIG', + 'VISUAL_DIFF']); + } + const targetSet = new Set(); + for (let i = 0; i < filePaths.length; i++) { + const p = filePaths[i]; + 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 if (isFlagConfig(p)) { + targetSet.add('FLAG_CONFIG'); + } else if (isUnitTest(p)) { + targetSet.add('UNIT_TEST'); + } else if (isDevDashboardFile(p)) { + targetSet.add('DEV_DASHBOARD'); + } else if (isIntegrationTest(p)) { + targetSet.add('INTEGRATION_TEST'); + } else if (isVisualDiffFile(p)) { + targetSet.add('VISUAL_DIFF'); + } else { + targetSet.add('RUNTIME'); + } + } + return targetSet; +} + +function startSauceConnect() { + process.env['SAUCE_USERNAME'] = 'amphtml'; + process.env['SAUCE_ACCESS_KEY'] = getStdout('curl --silent ' + + 'https://amphtml-sauce-token-dealer.appspot.com/getJwtToken').trim(); + const startScCmd = 'build-system/sauce_connect/start_sauce_connect.sh'; + console.log('\n' + fileLogPrefix, + 'Starting Sauce Connect Proxy:', colors.cyan(startScCmd)); + execOrDie(startScCmd); +} + +function stopSauceConnect() { + const stopScCmd = 'build-system/sauce_connect/stop_sauce_connect.sh'; + console.log('\n' + fileLogPrefix, + 'Stopping Sauce Connect Proxy:', colors.cyan(stopScCmd)); + execOrDie(stopScCmd); +} + +const command = { + testBuildSystem: function() { + timedExecOrDie('gulp ava'); + timedExecOrDie('node node_modules/jest/bin/jest.js'); + }, + testDocumentLinks: function() { + timedExecOrDie('gulp check-links'); + }, + cleanBuild: function() { + timedExecOrDie('gulp clean'); + }, + runLintCheck: function() { + timedExecOrDie('gulp lint'); + }, + runJsonCheck: function() { + timedExecOrDie('gulp caches-json'); + timedExecOrDie('gulp json-syntax'); + }, + buildCss: function() { + timedExecOrDie('gulp css'); + }, + buildRuntime: function() { + timedExecOrDie('gulp build --fortesting'); + }, + buildRuntimeMinified: function(extensions) { + let cmd = 'gulp dist --fortesting'; + if (!extensions) { + cmd = cmd + ' --noextensions'; + } + timedExecOrDie(cmd); + }, + runBundleSizeCheck: function(storeBundleSize = false) { + let cmd = 'gulp bundle-size'; + if (storeBundleSize) { + cmd += ' --store'; + } + timedExecOrDie(cmd); + }, + runDepAndTypeChecks: function() { + timedExecOrDie('gulp dep-check'); + timedExecOrDie('gulp check-types'); + }, + runUnitTests: function() { + let cmd = 'gulp test --unit --nobuild'; + if (argv.files) { + cmd = cmd + ' --files ' + argv.files; + } + // Unit tests with Travis' default chromium in coverage mode. + timedExecOrDie(cmd + ' --headless --coverage'); + if (process.env.TRAVIS) { + // A subset of unit tests on other browsers via sauce labs + cmd = cmd + ' --saucelabs_lite'; + startSauceConnect(); + timedExecOrDie(cmd); + stopSauceConnect(); + } + }, + runUnitTestsOnLocalChanges: function() { + timedExecOrDie('gulp test --nobuild --headless --local-changes'); + }, + runDevDashboardTests: function() { + timedExecOrDie('gulp test --dev_dashboard --nobuild'); + }, + runIntegrationTests: function(compiled, coverage) { + // Integration tests on chrome, or on all saucelabs browsers if set up + let cmd = 'gulp test --integration --nobuild'; + if (argv.files) { + cmd = cmd + ' --files ' + argv.files; + } + if (compiled) { + cmd += ' --compiled'; + } + if (process.env.TRAVIS) { + if (coverage) { + timedExecOrDie(cmd + ' --headless --coverage'); + } else { + startSauceConnect(); + timedExecOrDie(cmd + ' --saucelabs'); + stopSauceConnect(); + } + } else { + timedExecOrDie(cmd + ' --headless'); + } + }, + runSinglePassCompiledIntegrationTests: function() { + timedExecOrDie('rm -R dist'); + timedExecOrDie('gulp dist --fortesting --single_pass --pseudo_names'); + timedExecOrDie('gulp test --integration --nobuild --headless ' + + '--compiled --single_pass'); + timedExecOrDie('rm -R dist'); + }, + runVisualDiffTests: function(opt_mode) { + if (process.env.TRAVIS) { + process.env['PERCY_TOKEN'] = atob(process.env.PERCY_TOKEN_ENCODED); + } else if (!process.env.PERCY_PROJECT || !process.env.PERCY_TOKEN) { + console.log( + '\n' + fileLogPrefix, 'Could not find environment variables', + colors.cyan('PERCY_PROJECT'), 'and', + colors.cyan('PERCY_TOKEN') + '. Skipping visual diff tests.'); + return; + } + let cmd = 'gulp visual-diff --nobuild'; + if (opt_mode === 'empty') { + cmd += ' --empty'; + } else if (opt_mode === 'master') { + cmd += ' --master'; + } + const {status} = timedExec(cmd); + if (status != 0) { + console.error(fileLogPrefix, colors.red('ERROR:'), + 'Found errors while running', colors.cyan(cmd)); + } + }, + verifyVisualDiffTests: function() { + if (!process.env.PERCY_PROJECT || !process.env.PERCY_TOKEN) { + console.log( + '\n' + fileLogPrefix, 'Could not find environment variables', + colors.cyan('PERCY_PROJECT'), 'and', + colors.cyan('PERCY_TOKEN') + + '. Skipping verification of visual diff tests.'); + return; + } + timedExec('gulp visual-diff --verify_status'); + }, + runPresubmitTests: function() { + timedExecOrDie('gulp presubmit'); + }, + buildValidatorWebUI: function() { + timedExecOrDie('gulp validator-webui'); + }, + buildValidator: function() { + timedExecOrDie('gulp validator'); + }, + updatePackages: function() { + timedExecOrDie('gulp update-packages'); + }, +}; + +function runAllCommands() { + // Run different sets of independent tasks in parallel to reduce build time. + if (process.env.BUILD_SHARD == 'unit_tests') { + command.updatePackages(); + command.testBuildSystem(); + command.cleanBuild(); + command.buildRuntime(); + command.runVisualDiffTests(/* opt_mode */ 'master'); + command.runLintCheck(); + command.runJsonCheck(); + command.runDepAndTypeChecks(); + command.runUnitTests(); + command.runDevDashboardTests(); + command.runIntegrationTests(/* compiled */ false, /* coverage */ true); + command.verifyVisualDiffTests(); + // command.testDocumentLinks() is skipped during push builds. + command.buildValidatorWebUI(); + command.buildValidator(); + } + if (process.env.BUILD_SHARD == 'integration_tests') { + command.updatePackages(); + command.cleanBuild(); + command.buildRuntimeMinified(/* extensions */ true); + // Disable bundle-size check on release branch builds. + if (process.env['TRAVIS_BRANCH'] === 'master') { + command.runBundleSizeCheck(/* storeBundleSize */ true); + } + command.runPresubmitTests(); + command.runIntegrationTests(/* compiled */ true, /* coverage */ false); + command.runSinglePassCompiledIntegrationTests(); + } +} + +function runAllCommandsLocally() { + // These tasks don't need a build. Run them first and fail early. + command.testBuildSystem(); + command.runLintCheck(); + command.runJsonCheck(); + command.runDepAndTypeChecks(); + command.testDocumentLinks(); + + // Build if required. + if (!argv.nobuild) { + command.cleanBuild(); + command.buildRuntime(); + command.buildRuntimeMinified(/* extensions */ false); + command.runBundleSizeCheck(); + } + + // These tests need a build. + command.runPresubmitTests(); + command.runVisualDiffTests(); + command.runUnitTests(); + command.runIntegrationTests(/* compiled */ false, /* coverage */ false); + command.verifyVisualDiffTests(); + + // Validator tests. + command.buildValidatorWebUI(); + command.buildValidator(); +} + +/** + * Makes sure package.json and yarn.lock are in sync. + */ +function runYarnIntegrityCheck() { + const yarnIntegrityCheck = getStderr('yarn check --integrity').trim(); + if (yarnIntegrityCheck.includes('error')) { + console.error(fileLogPrefix, colors.red('ERROR:'), + 'Found the following', colors.cyan('yarn'), 'errors:\n' + + colors.cyan(yarnIntegrityCheck)); + console.error(fileLogPrefix, colors.red('ERROR:'), + 'Updates to', colors.cyan('package.json'), + 'must be accompanied by a corresponding update to', + colors.cyan('yarn.lock')); + console.error(fileLogPrefix, colors.yellow('NOTE:'), + 'To update', colors.cyan('yarn.lock'), 'after changing', + colors.cyan('package.json') + ',', 'run', + '"' + colors.cyan('yarn install') + '"', + 'and include the updated', colors.cyan('yarn.lock'), + 'in your PR.'); + process.exit(1); + } +} + +/** + * Makes sure that yarn.lock was properly updated. + */ +function runYarnLockfileCheck() { + const localChanges = gitDiffColor(); + if (localChanges.includes('yarn.lock')) { + console.error(fileLogPrefix, colors.red('ERROR:'), + 'This PR did not properly update', colors.cyan('yarn.lock') + '.'); + console.error(fileLogPrefix, colors.yellow('NOTE:'), + 'To fix this, sync your branch to', colors.cyan('upstream/master') + + ', run', colors.cyan('gulp update-packages') + + ', and push a new commit containing the changes.'); + console.error(fileLogPrefix, 'Expected changes:'); + console.log(localChanges); + process.exit(1); + } +} + +/** + * The main method for the script execution which much like a C main function + * receives the command line arguments and returns an exit status. + * @return {number} + */ +function main() { + const startTime = startTimer('pr-check.js'); + + // Make sure package.json and yarn.lock are in sync and up-to-date. + runYarnIntegrityCheck(); + runYarnLockfileCheck(); + + // Run the local version of all tests. + if (!process.env.TRAVIS) { + process.env['LOCAL_PR_CHECK'] = true; + console.log(fileLogPrefix, 'Running all pr-check commands locally.'); + runAllCommandsLocally(); + stopTimer('pr-check.js', startTime); + return 0; + } + + console.log( + fileLogPrefix, 'Running build shard', + colors.cyan(process.env.BUILD_SHARD), + '\n'); + + if (process.env.TRAVIS_EVENT_TYPE === 'push') { + console.log(fileLogPrefix, 'Running all commands on push build.'); + runAllCommands(); + stopTimer('pr-check.js', startTime); + return 0; + } + const files = filesInPr(); + const buildTargets = determineBuildTargets(files); + + // Exit early if flag-config files are mixed with runtime files. + if (buildTargets.has('FLAG_CONFIG') && buildTargets.has('RUNTIME')) { + console.log(fileLogPrefix, colors.red('ERROR:'), + 'Looks like your PR contains', + colors.cyan('{prod|canary}-config.json'), + 'in addition to some other files. Config and code are not kept in', + 'sync, and config needs to be backwards compatible with code for at', + 'least two weeks. See #8188'); + const nonFlagConfigFiles = files.filter(file => !isFlagConfig(file)); + console.log(fileLogPrefix, colors.red('ERROR:'), + 'Please move these files to a separate PR:', + colors.cyan(nonFlagConfigFiles.join(', '))); + stopTimer('pr-check.js', startTime); + process.exit(1); + } + + console.log( + fileLogPrefix, 'Detected build targets:', + colors.cyan(Array.from(buildTargets).sort().join(', '))); + + // Run different sets of independent tasks in parallel to reduce build time. + if (process.env.BUILD_SHARD == 'unit_tests') { + command.updatePackages(); + if (buildTargets.has('BUILD_SYSTEM') || + buildTargets.has('RUNTIME')) { + command.testBuildSystem(); + } + command.runLintCheck(); + if (buildTargets.has('DOCS')) { + command.testDocumentLinks(); + } + if (buildTargets.has('DEV_DASHBOARD')) { + command.runDevDashboardTests(); + } + if (buildTargets.has('RUNTIME') || + buildTargets.has('UNIT_TEST') || + buildTargets.has('INTEGRATION_TEST') || + buildTargets.has('BUILD_SYSTEM')) { + command.cleanBuild(); + command.buildCss(); + command.runJsonCheck(); + command.runDepAndTypeChecks(); + // Run unit tests only if the PR contains runtime or build-system changes. + if (buildTargets.has('RUNTIME') || + buildTargets.has('BUILD_SYSTEM')) { + // Before running all tests, run tests modified by the PR. (Fail early.) + command.runUnitTestsOnLocalChanges(); + command.runUnitTests(); + } else if (buildTargets.has('UNIT_TEST')) { + // PR contains only test changes. Run just the modified unit tests. + command.runUnitTestsOnLocalChanges(); + } + } + } + + if (process.env.BUILD_SHARD == 'integration_tests') { + command.updatePackages(); + if (buildTargets.has('INTEGRATION_TEST') || + buildTargets.has('RUNTIME') || + buildTargets.has('VISUAL_DIFF') || + buildTargets.has('FLAG_CONFIG') || + buildTargets.has('BUILD_SYSTEM')) { + command.cleanBuild(); + command.buildRuntime(); + command.runVisualDiffTests(); + if (buildTargets.has('RUNTIME')) { + command.buildRuntimeMinified(/* extensions */ false); + command.runBundleSizeCheck(); + } + } else { + // Generates a blank Percy build to satisfy the required Github check. + command.runVisualDiffTests(/* opt_mode */ 'empty'); + } + command.runPresubmitTests(); + if (buildTargets.has('INTEGRATION_TEST') || + buildTargets.has('RUNTIME') || + buildTargets.has('BUILD_SYSTEM')) { + command.runIntegrationTests(/* compiled */ false, /* coverage */ true); + command.runIntegrationTests(/* compiled */ false, /* coverage */ false); + } + if (buildTargets.has('INTEGRATION_TEST') || + buildTargets.has('RUNTIME') || + buildTargets.has('VISUAL_DIFF') || + buildTargets.has('FLAG_CONFIG') || + buildTargets.has('BUILD_SYSTEM')) { + command.verifyVisualDiffTests(); + } + if (buildTargets.has('VALIDATOR_WEBUI')) { + command.buildValidatorWebUI(); + } + if (buildTargets.has('VALIDATOR')) { + command.buildValidator(); + } + if (buildTargets.has('INTEGRATION_TEST') || + buildTargets.has('RUNTIME') || + buildTargets.has('BUILD_SYSTEM')) { + command.runSinglePassCompiledIntegrationTests(); + } + } + + stopTimer('pr-check.js', startTime); + return 0; +} + +process.exit(main()); diff --git a/build-system/release.sh b/build-system/release.sh new file mode 100755 index 000000000000..e7d5b1511556 --- /dev/null +++ b/build-system/release.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +readonly MERGE_FOLDER="${HOME}/.amp-release/amphtml" + +rm -rvf "$MERGE_FOLDER" + +mkdir -p "$MERGE_FOLDER" + +git clone https://github.com/ampproject/amphtml.git "$MERGE_FOLDER" + +cd "$MERGE_FOLDER" +echo "====================" +echo "in folder $(pwd)" +echo "====================" + +git checkout -B canary master + +echo "done with resetting canary" +echo "====================" + +git checkout release + +readonly last_release_tag=$(git describe --abbrev=0 --first-parent --tags canary) + +echo "trying to merging ${last_release_tag} into release" + +git merge "$last_release_tag" + +echo "done with merging ${last_release_tag} into canary" +echo "====================" + +git checkout master + +echo " +==================== +Please manually push the branches if merges were successful. +If canary was not ahead of release, you might not need to push release +and merge would have exited with \"Already-up-to-date.\" + +cd ${MERGE_FOLDER} + +https push (needs auth login entry): +git push https://github.com/ampproject/amphtml.git canary +git push https://github.com/ampproject/amphtml.git release + +ssh push: +git push git@github.com:ampproject/amphtml.git canary +git push git@github.com:ampproject/amphtml.git release + + +single line ssh push (if both branches can be pushed): +cd ${MERGE_FOLDER} && git push git@github.com:ampproject/amphtml.git canary && git push git@github.com:ampproject/amphtml.git release +" diff --git a/build-system/runner/.classpath b/build-system/runner/.classpath new file mode 100644 index 000000000000..200440ff0ffb --- /dev/null +++ b/build-system/runner/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/build-system/runner/.project b/build-system/runner/.project new file mode 100644 index 000000000000..7e664b4d2693 --- /dev/null +++ b/build-system/runner/.project @@ -0,0 +1,17 @@ + + + runner + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/build-system/runner/build.xml b/build-system/runner/build.xml new file mode 100644 index 000000000000..222321467779 --- /dev/null +++ b/build-system/runner/build.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build-system/runner/dist/runner.jar b/build-system/runner/dist/runner.jar new file mode 100644 index 000000000000..2f40603b5f32 Binary files /dev/null and b/build-system/runner/dist/runner.jar differ diff --git a/build-system/runner/lib/jar-in-jar-loader.zip b/build-system/runner/lib/jar-in-jar-loader.zip new file mode 100644 index 000000000000..6ee121769d72 Binary files /dev/null and b/build-system/runner/lib/jar-in-jar-loader.zip differ diff --git a/build-system/runner/runner.iml b/build-system/runner/runner.iml new file mode 100644 index 000000000000..06922e896d42 --- /dev/null +++ b/build-system/runner/runner.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/build-system/runner/src/org/ampproject/AmpCodingConvention.java b/build-system/runner/src/org/ampproject/AmpCodingConvention.java new file mode 100644 index 000000000000..639da3e29299 --- /dev/null +++ b/build-system/runner/src/org/ampproject/AmpCodingConvention.java @@ -0,0 +1,106 @@ +/** + * 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. + */ + +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; +import com.google.javascript.jscomp.ClosureCodingConvention; +import com.google.javascript.jscomp.newtypes.JSType; +import com.google.javascript.rhino.jstype.JSTypeNative; + +import java.util.ArrayList; +import java.util.Collection; + + +/** + * A coding convention for AMP. + */ +public final class AmpCodingConvention extends CodingConventions.Proxy { + /** By default, decorate the ClosureCodingConvention. */ + public AmpCodingConvention() { + this(new ClosureCodingConvention()); + } + + /** Decorates a wrapped CodingConvention. */ + public AmpCodingConvention(CodingConvention convention) { + super(convention); + } + + @Override public Collection getAssertionFunctions() { + return ImmutableList.of( + new AssertionFunctionSpec("user.assert", JSTypeNative.TRUTHY), + new AssertionFunctionSpec("dev.assert", JSTypeNative.TRUTHY), + new AssertionFunctionSpec("Log$$module$src$log.prototype.assert", JSTypeNative.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") + ); + } + + /** + * {@inheritDoc} + * Because AMP objects can travel between compilation units, we consider + * non-private methods exported. + * Should we decide to do full-program compilation (for version bound JS + * 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; + } + // Bad hack, but we should really not try to inline CSS as these strings can + // be very long. + // See https://github.com/ampproject/amphtml/issues/10118 + if (name.equals("cssText$$module$build$css")) { + 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; + } + // Starting with _ explicitly exports a name. + if (name.startsWith("_")) { + return true; + } + return !name.endsWith("_") && !name.endsWith("ForTesting"); + } + + /** + * {@inheritDoc} + * We cannot rename properties we treat as exported, because they may travel + * between compilation unit. + */ + @Override public boolean blockRenamingForProperty(String name) { + return isExported(name, false); + } +} diff --git a/build-system/runner/src/org/ampproject/AmpCommandLineRunner.java b/build-system/runner/src/org/ampproject/AmpCommandLineRunner.java new file mode 100644 index 000000000000..96d4b95af1b9 --- /dev/null +++ b/build-system/runner/src/org/ampproject/AmpCommandLineRunner.java @@ -0,0 +1,146 @@ +/** + * 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. + */ +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; + + +/** + * Adds a custom pass for Tree shaking `dev.fine` and `dev.assert` calls. + */ +public class AmpCommandLineRunner extends CommandLineRunner { + + /** + * Identifies if the runner only needs to do type checking. + */ + private boolean typecheck_only = false; + + private boolean pseudo_names = false; + + private boolean is_production_env = true; + + private boolean single_file_compilation = false; + + /** + * List of string suffixes to eliminate from the AST. + */ + 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); + } + + @Override protected CompilerOptions createOptions() { + if (typecheck_only) { + return createTypeCheckingOptions(); + } + CompilerOptions options = super.createOptions(); + options.setCollapseProperties(true); + AmpPass ampPass = new AmpPass(getCompiler(), is_production_env, suffixTypes, + assignmentReplacements, prodAssignmentReplacements); + options.addCustomPass(CustomPassExecutionTime.BEFORE_OPTIMIZATIONS, ampPass); + options.setDevirtualizePrototypeMethods(true); + options.setExtractPrototypeMemberDeclarations(true); + options.setSmartNameRemoval(true); + options.optimizeCalls = true; + if (single_file_compilation) { + options.renamePrefixNamespace = "_"; + } else { + // Have to turn this off because we cannot know whether sub classes + // might override a method. In the future this might be doable + // with using a more complete extern file instead. + options.setRemoveUnusedPrototypeProperties(false); + options.setInlineProperties(false); + options.setComputeFunctionSideEffects(false); + // Since we are not computing function side effects, at least let the + // compiler remove calls to functions with `@nosideeffects`. + options.setMarkNoSideEffectCalls(true); + // Property renaming. Relies on AmpCodingConvention to be safe. + options.setRenamingPolicy(VariableRenamingPolicy.ALL, + PropertyRenamingPolicy.ALL_UNQUOTED); + } + options.setDisambiguatePrivateProperties(true); + options.setGeneratePseudoNames(pseudo_names); + return options; + } + + @Override protected void setRunOptions(CompilerOptions options) + throws IOException, FlagUsageException { + super.setRunOptions(options); + if (!single_file_compilation) { + options.setCodingConvention(new AmpCodingConvention()); + } + } + + /** + * Create the most basic CompilerOptions instance with type checking turned on. + */ + protected CompilerOptions createTypeCheckingOptions() { + CompilerOptions options = super.createOptions(); + options.setCheckTypes(true); + options.setInferTypes(true); + return options; + } + + 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.typecheck_only = true; + } else if (arg.contains("FORTESTING=true")) { + runner.is_production_env = false; + } else if (arg.contains("PSEUDO_NAMES=true")) { + runner.pseudo_names = true; + } else if (arg.contains("SINGLE_FILE_COMPILATION=true")) { + runner.single_file_compilation = true; + } + } + + if (runner.shouldRunCompiler()) { + runner.run(); + } + if (runner.hasErrors()) { + System.exit(-1); + } + } +} diff --git a/build-system/runner/src/org/ampproject/AmpPass.java b/build-system/runner/src/org/ampproject/AmpPass.java new file mode 100644 index 000000000000..530f6717736e --- /dev/null +++ b/build-system/runner/src/org/ampproject/AmpPass.java @@ -0,0 +1,336 @@ +/** + * 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. + */ +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; + +/** + * Does a `stripTypeSuffix` which currently can't be done through + * the normal `strip` mechanisms provided by closure compiler. + * Some of the known mechanisms we tried before writing our own compiler pass + * are setStripTypes, setStripTypePrefixes, setStripNameSuffixes, setStripNamePrefixes. + * The normal mechanisms found in closure compiler can't strip the expressions we want because + * they are either prefix based and/or operate on the es6 translated code which would mean they + * operate on a qualifier string name that looks like + * "module$__$__$__$extensions$amp_test$0_1$log.dev.fine". + * + * Other custom pass examples found inside closure compiler src: + * https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PolymerPass.java + * https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/AngularPass.java + */ +class AmpPass extends AbstractPostOrderCallback implements HotSwapCompilerPass { + + final AbstractCompiler compiler; + private final Map> stripTypeSuffixes; + private final Map assignmentReplacements; + private final Map prodAssignmentReplacements; + final boolean isProd; + + 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) { + hotSwapScript(root, null); + } + + @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { + NodeTraversal.traverseEs6(compiler, scriptRoot, this); + } + + @Override public void visit(NodeTraversal t, Node n, Node parent) { + if (isCallRemovable(n)) { + maybeEliminateCallExceptFirstParam(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); + } + } + + /** + * 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(); + + // 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.reportChangeToEnclosingScope(expr); + } + + 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; + } + + 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.reportChangeToEnclosingScope(varNode); + return; + } + } + } + } + } + + /** + * Predicate for any fnQualifiedName.props call. + * example: + * isFunctionInvokeAndPropAccess(n, "getMode", "test"); // matches `getMode().test` + */ + 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.isCall()) { + return false; + } + Node fullQualifiedFnName = call.getFirstChild(); + if (fullQualifiedFnName == null) { + return false; + } + + 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; + } + } + } + } + + return false; + } + + private void replaceWithBooleanExpression(boolean bool, Node n, Node parent) { + Node booleanNode = bool ? IR.trueNode() : IR.falseNode(); + booleanNode.useSourceInfoIfMissingFrom(n); + parent.replaceChild(n, booleanNode); + compiler.reportChangeToEnclosingScope(parent); + } + + private void removeExpression(Node n, Node parent) { + Node scope = parent; + if (parent.isExprResult()) { + Node grandparent = parent.getParent(); + grandparent.removeChild(parent); + scope = grandparent; + } else { + parent.removeChild(n); + } + compiler.reportChangeToEnclosingScope(scope); + } + + 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) { + removeExpression(call, parent); + return; + } + + firstArg.detachFromParent(); + parent.replaceChild(call, firstArg); + compiler.reportChangeToEnclosingScope(parent); + } + + /** + * Checks the nodes qualified name if it ends with one of the items in + * stripTypeSuffixes + */ + boolean qualifiedNameEndsWithStripType(Node n, Set suffixes) { + String name = n.getQualifiedName(); + return qualifiedNameEndsWithStripType(name, suffixes); + } + + /** + * Checks if the string ends with one of the items in stripTypeSuffixes + */ + boolean qualifiedNameEndsWithStripType(String name, Set suffixes) { + if (name != null) { + for (String suffix : suffixes) { + if (name.endsWith(suffix)) { + return true; + } + } + } + return false; + } +} diff --git a/build-system/runner/test/org/ampproject/AmpPassTest.java b/build-system/runner/test/org/ampproject/AmpPassTest.java new file mode 100644 index 000000000000..4bb2812f451d --- /dev/null +++ b/build-system/runner/test/org/ampproject/AmpPassTest.java @@ -0,0 +1,358 @@ + +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.CompilerTestCase; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.Node; + + +/** + * Tests {@link AmpPass}. + */ +public class AmpPassTest extends CompilerTestCase { + + 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, /* isProd */ true, suffixTypes, assignmentReplacements, + prodAssignmentReplacements); + } + + @Override protected int getNumRepetitions() { + // This pass only runs once. + return 1; + } + + public void testDevFineRemoval() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + " 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 module$src$log = { dev: function() { return { fine: function() {}} } };", + " 'hello world';", + " console.log('this is preserved');", + "})()")); + test( + LINE_JOINER.join( + "(function() {", + " var module$src$log = { dev: function() { return { fine: function() {}} } };", + " module$src$log.dev().fine();", + " console.log('this is preserved');", + "})()"), + LINE_JOINER.join( + "(function() {", + " var module$src$log = { dev: function() { return { fine: function() {}} } };", + " console.log('this is preserved');", + "})()")); + } + + public void testDevErrorPreserve() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + " var log = { dev: { error: function() {} } };", + " log.dev.error('hello world');", + " console.log('this is preserved');", + "})()"), + LINE_JOINER.join( + "(function() {", + " var log = { dev: { error: function() {} } };", + " log.dev.error('hello world');", + " console.log('this is preserved');", + "})()")); + } + + public void testDevAssertExpressionRemoval() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + " 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 module$src$log = { dev: function() { return { assert: function() {}} } };", + " \"hello world\";", + " console.log('this is preserved');", + "})()")); + test( + LINE_JOINER.join( + "(function() {", + " 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 module$src$log = { dev: function() { return { assert: function() {}} } };", + " var someValue;", + " console.log('this is preserved', someValue);", + "})()")); + } + + public void testDevAssertPreserveFirstArg() throws Exception { + test( + LINE_JOINER.join( + "(function() {", + " 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 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 { + test( + // Does reliasing + LINE_JOINER.join( + "(function() {", + " 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 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 { + test( + 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 { + test( + 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 { + test( + 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..2c8ead369d40 --- /dev/null +++ b/build-system/runner/test/org/ampproject/AmpPassTestEnvTest.java @@ -0,0 +1,113 @@ +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.javascript.jscomp.Compiler; +import com.google.javascript.jscomp.CompilerPass; +import com.google.javascript.jscomp.CompilerTestCase; + + +/** + * Tests {@link AmpPass}. + */ +public class AmpPassTestEnvTest extends CompilerTestCase { + + 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 { + test( + 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/sauce_connect/start_sauce_connect.sh b/build-system/sauce_connect/start_sauce_connect.sh new file mode 100755 index 000000000000..1eea3279b6bd --- /dev/null +++ b/build-system/sauce_connect/start_sauce_connect.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# +# Copyright 2018 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. +# +# This script starts the sauce connect proxy, and waits for a successful +# connection. + +CYAN() { echo -e "\033[0;36m$1\033[0m"; } +YELLOW() { echo -e "\033[1;33m$1\033[0m"; } + +SC_VERSION="sc-4.5.1-linux" +DOWNLOAD_URL="https://saucelabs.com/downloads/$SC_VERSION.tar.gz" +DOWNLOAD_DIR="sauce_connect" +TAR_FILE="$DOWNLOAD_DIR/$SC_VERSION.tar.gz" +BINARY_FILE="$SC_VERSION/bin/sc" +PID_FILE="sauce_connect_pid" +LOG_FILE="sauce_connect_log" +READY_FILE="sauce_connect_ready" +READY_DELAY_SECS=60 +LOG_PREFIX=$(YELLOW "start_sauce_connect.sh") + +# Download the sauce connect proxy binary (if needed) and unpack it. +if [[ -f $TAR_FILE ]]; then + echo "$LOG_PREFIX Using cached Sauce Connect binary $(CYAN "$TAR_FILE")" +else + echo "$LOG_PREFIX Downloading $(CYAN "$DOWNLOAD_URL")" + wget -q "$DOWNLOAD_URL" -P "$DOWNLOAD_DIR" +fi +echo "$LOG_PREFIX Unpacking $(CYAN "$TAR_FILE")" +tar -xzf "$TAR_FILE" + +# Clean up old log files, if any. +if [[ -f "$LOG_FILE" ]]; then + echo "$LOG_PREFIX Deleting old log file $(CYAN "$LOG_FILE")" + rm "$LOG_FILE" +fi +if [[ -f $PID_FILE ]]; then + echo "$LOG_PREFIX Deleting old pid file $(CYAN "$PID_FILE")" + rm "$PID_FILE" +fi + +# Establish the tunnel identifier (job number on Travis / username during local dev). +if [[ -z "$TRAVIS_JOB_NUMBER" ]]; then + TUNNEL_IDENTIFIER="$(git log -1 --pretty=format:"%ae")" +else + TUNNEL_IDENTIFIER="$TRAVIS_JOB_NUMBER" +fi + +# Launch proxy and wait for a tunnel to be created. +echo "$LOG_PREFIX Launching $(CYAN "$BINARY_FILE")" +"$BINARY_FILE" --tunnel-identifier "$TUNNEL_IDENTIFIER" --readyfile "$READY_FILE" --pidfile "$PID_FILE" 1>"$LOG_FILE" 2>&1 & +count=0 +while [ $count -lt $READY_DELAY_SECS ] +do + if [ -e "$READY_FILE" ] + then + # Print confirmation. + PID="$(cat "$PID_FILE")" + TUNNEL_ID="$(grep -oP "Tunnel ID: \K.*$" "$LOG_FILE")" + echo "$LOG_PREFIX Sauce Connect Proxy with tunnel ID $(CYAN "$TUNNEL_ID") and identifier $(CYAN "$TUNNEL_IDENTIFIER") is now running as pid $(CYAN "$PID")" + break + else + # Continue waiting. + sleep 1 + (( count++ )) + if [ $count -eq $READY_DELAY_SECS ] + then + # Print the error logs. + echo "$LOG_PREFIX Sauce Connect Proxy has not started after $(CYAN "$READY_DELAY_SECS") seconds." + echo "$LOG_PREFIX Log file contents:" + cat "$LOG_FILE" + exit 1 + fi + fi +done diff --git a/build-system/sauce_connect/stop_sauce_connect.sh b/build-system/sauce_connect/stop_sauce_connect.sh new file mode 100755 index 000000000000..345894b55fc0 --- /dev/null +++ b/build-system/sauce_connect/stop_sauce_connect.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Copyright 2018 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. +# +# This script stops the sauce connect proxy, and waits for a clean exit. + +CYAN() { echo -e "\033[0;36m$1\033[0m"; } +YELLOW() { echo -e "\033[1;33m$1\033[0m"; } + +PID_FILE="sauce_connect_pid" +LOG_FILE="sauce_connect_log" +LOG_PREFIX=$(YELLOW "stop_sauce_connect.sh") + +# Early exit if there's no proxy running. +if [[ ! -f "$PID_FILE" ]]; then + echo "$LOG_PREFIX Sauce Connect Proxy is not running" + exit 0 +fi + +# Stop the sauce connect proxy. +PID="$(cat "$PID_FILE")" +echo "$LOG_PREFIX Stopping Sauce Connect Proxy pid $(CYAN "$PID")" +kill "$PID" + +# Clean up files. +if [[ -f "$LOG_FILE" ]]; then + echo "$LOG_PREFIX Cleaning up log file $(CYAN "$LOG_FILE")" + rm "$LOG_FILE" +fi +if [[ -f "$PID_FILE" ]]; then + echo "$LOG_PREFIX Cleaning up pid file $(CYAN "$PID_FILE")" + rm "$PID_FILE" +fi + +# Done. +echo "$LOG_PREFIX Successfully stopped Sauce Connect Proxy" diff --git a/build-system/scope-require.js b/build-system/scope-require.js new file mode 100644 index 000000000000..a19cabd75d48 --- /dev/null +++ b/build-system/scope-require.js @@ -0,0 +1,136 @@ +/** + * 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. + */ +'use strict'; + +const astReplace = require('ast-replace'); +const detectGlobals = require('acorn-globals'); +const escodegen = require('escodegen'); +const rocambole = require('rocambole'); + +const colors = require('ansi-colors'); +const es = require('event-stream'); +const fs = require('fs'); +const program = require('commander'); + +/** + * Changes global `require` calls to be referenced from a given global + * namespace. e.g. if scopeName is `AMP`, calls will be transformed to + * `AMP.require`. + * @param {string} src The contents of a JavaScript source file. + * @param {string} scopeName The name to prepend to `require` calls. + * @return {string} The transformed source code + */ +function scopeRequire(src, scopeName) { + const ast = rocambole.parse(src); + const globals = detectGlobals(ast); + const flatGlobals = globals.reduce((acc, g) => acc.concat(g.nodes), []); + + flatGlobals + .filter(node => isIdentifier(node) && isRequire(node)) + .forEach(node => + replaceIdentifier(node.parent, test => test === node, scopeName)); + + return escodegen.generate(ast, {format: {compact: true}}); +} + +/** + * True if the node is an Identifier node + * @param {!Object} node An AST node + * @return {boolean} + */ +function isIdentifier(node) { + return node.type === 'Identifier'; +} + +/** + * True if the node name is `require` + * @param {!Object} node An AST node + * @return {boolean} + */ +function isRequire(node) { + return node.name === 'require'; +} + +/** + * Replaces an Identifer node in the AST with a Member node. + * @param {!Object} ast The AST subtree we are currently mutating. + * @param {function(!Object):boolean} test Tests if the visitor is on the desired node + * @param {string} scopeName The name to reference `require` calls from. + */ +function replaceIdentifier(ast, test, scopeName) { + scopeName = scopeName || 'window'; + const replacement = { + 'Identifier': { + replace: node => { + if (node.name !== scopeName) { + return createMemberNode(node, scopeName); + } + }, + test, + }, + }; + astReplace(ast, replacement); +} + +/** + * Convert the given Identifier node to be referenced from the scope name + * @param {!Object} identifierNode + * @param {string} scopeName + * @return {!Object} + */ +function createMemberNode(identifierNode, scopeName) { + return { + 'type': 'MemberExpression', + 'object': { + 'type': 'Identifier', + 'name': scopeName, + }, + 'property': identifierNode, + }; +} + + +program + .description('Scope global `require` calls to an object.') + .option('-i, --infile [filename]', 'The path of the input file.' + + ' Reads from stdin if unspecified') + .option('-o, --outfile [filename]', 'The path for the output file.' + + ' Writes to stdout if unspecified.') + .option('-n --name [name]', 'The name to reference `require` calls from.' + + ' The default is `AMP`', 'AMP') + .parse(process.argv); + +const inputStream = (program.infile && program.infile !== '-' ? + fs.createReadStream(program.infile) : + process.stdin); +inputStream.on('error', err => { + console./*OK*/error(colors.red('\nError reading file: ' + err.path)); +}); + +const outputStream = (program.outfile && program.outfile !== '-' ? + fs.createWriteStream(program.outfile) : + process.stdout); +outputStream.on('error', err => { + console./*OK*/error(colors.red('\nError writing file: ' + err.path)); +}); + +const scopeRequireStream = es.map((inputFile, cb) => + cb(null, scopeRequire(inputFile.toString('utf8'), program.name))); + +inputStream + .pipe(es.wait()) + .pipe(scopeRequireStream) + .pipe(outputStream); diff --git a/build-system/server-a4a-template.html b/build-system/server-a4a-template.html new file mode 100644 index 000000000000..d19141c7f97d --- /dev/null +++ b/build-system/server-a4a-template.html @@ -0,0 +1,30 @@ + + + + + A4A Envelope + + + + + + +

    A4A Envelope

    +
    3p: FORCE3P
    +
    url: AD_URL
    +
    size: AD_WIDTHxAD_HEIGHT
    + +
    scroll down to see the ad
    + + + + + + diff --git a/build-system/server-inabox-template.html b/build-system/server-inabox-template.html new file mode 100644 index 000000000000..c3621d05e37d --- /dev/null +++ b/build-system/server-inabox-template.html @@ -0,0 +1,36 @@ + + + + + In-a-box Envelope + + + + + +

    In-a-box Envelope

    +
    url: AD_URL
    +
    size: AD_WIDTHxAD_HEIGHT
    + +
    scroll down to see the ad
    + + + + + + diff --git a/build-system/server.js b/build-system/server.js index 947bc0c708f6..dc96368ae9c2 100644 --- a/build-system/server.js +++ b/build-system/server.js @@ -13,64 +13,81 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; /** * @fileoverview Creates an http server to handle static * files and list directories for use with the gulp live server */ -var app = require('connect')(); -var bodyParser = require('body-parser'); -var clr = require('connect-livereload'); -var finalhandler = require('finalhandler'); -var path = require('path'); -var serveIndex = require('serve-index'); -var serveStatic = require('serve-static'); +const app = require(require.resolve('./app.js')); +const colors = require('ansi-colors'); +const gulp = require('gulp-help')(require('gulp')); +const isRunning = require('is-running'); +const log = require('fancy-log'); +const morgan = require('morgan'); +const webserver = require('gulp-webserver'); -var args = Array.prototype.slice.call(process.argv, 2, 4); -var paths = args[0]; -var port = args[1]; +const { + SERVE_HOST: host, + SERVE_PORT: port, + SERVE_PROCESS_ID: gulpProcess, +} = process.env; -app.use(bodyParser.json()); +const useHttps = process.env.SERVE_USEHTTPS == 'true'; +const quiet = process.env.SERVE_QUIET == 'true'; +const sendCachingHeaders = process.env.SERVE_CACHING_HEADERS == 'true'; +const noCachingExtensions = process.env.SERVE_EXTENSIONS_WITHOUT_CACHING == + 'true'; +const header = require('connect-header'); -app.use('/api/show', function(req, res) { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ - showNotification: true - })); -}); - -app.use('/api/dont-show', function(req, res) { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ - showNotification: false - })); +// Exit if the port is in use. +process.on('uncaughtException', function(err) { + if (err.errno === 'EADDRINUSE') { + log(colors.red('Port', port, 'in use, shutting down server')); + } else { + log(colors.red(err)); + } + process.kill(gulpProcess, 'SIGINT'); + process.exit(1); }); +// Exit in the event of a crash in the parent gulp process. +setInterval(function() { + if (!isRunning(gulpProcess)) { + process.exit(1); + } +}, 1000); -app.use('/api/echo/post', function(req, res) { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(req.body, null, 2)); -}); - -app.use(clr()); +const middleware = []; +if (!quiet) { + middleware.push(morgan('dev')); +} +middleware.push(app.middleware); +if (sendCachingHeaders) { + middleware.push(header({ + 'cache-control': ' max-age=600', + })); +} -paths.split(',').forEach(function(pth) { - // Serve static files that exist - app.use(serveStatic(path.join(process.cwd(), pth))); - // Serve directory listings - app.use(serveIndex(path.join(process.cwd(), pth), - {'icons':true,'view':'details'})); -}); +if (noCachingExtensions) { + middleware.push(function(req, res, next) { + if (req.url.startsWith('/dist/v0/amp-')) { + log('Skipping caching for ', req.url); + res.header('Cache-Control', 'no-store'); + } + next(); + }); +} -// 404 everything else -app.use(function notFound(req, res) { - var done = finalhandler(req,res); - var err = new Error('File Not Found'); - err.status = 404; - done(err); -}); - -// Start up the server -app.listen(port, function () { - console./*OK*/log('serving %s at http://localhost:%s', paths, port); -}); +// Start gulp webserver +(async() => { + await app.beforeServeTasks(); + gulp.src(process.cwd()) + .pipe(webserver({ + port, + host, + directoryListing: true, + https: useHttps, + middleware, + })); +})(); diff --git a/build-system/shorten-license.js b/build-system/shorten-license.js new file mode 100644 index 000000000000..189eb4a7ceca --- /dev/null +++ b/build-system/shorten-license.js @@ -0,0 +1,82 @@ +/** + * 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. + */ +'use strict'; + +const escape = require('regexp.escape'); +const pumpify = require('pumpify'); +const replace = require('gulp-regexp-sourcemaps'); + +/* eslint-disable */ +const MIT_FULL = [ +'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.' +].join('\n'); + +const MIT_SHORT = [ +'Use of this source code is governed by a MIT-style', +'license that can be found in the LICENSE file or at', +'https://opensource.org/licenses/MIT.' +].join('\n'); + +const POLYMER_BSD_FULL = [ +'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', +].join('\n '); + +const BSD_SHORT = [ +'Use of this source code is governed by a BSD-style', +'license that can be found in the LICENSE file or at', +'https://developers.google.com/open-source/licenses/bsd', +].join('\n '); + +/* eslint-enable */ + +const LICENSES = [ + [MIT_FULL, MIT_SHORT], + [POLYMER_BSD_FULL, BSD_SHORT], +]; + +/* + * We can replace full-text of standard licenses with a pre-approved shorten + * version. + */ +module.exports = function() { + const streams = LICENSES.map(tuple => { + const regex = new RegExp(escape(tuple[0]), 'g'); + return replace(regex, tuple[1], 'shorten-license'); + }); + + return pumpify.obj(streams); +}; + diff --git a/build-system/single-pass.js b/build-system/single-pass.js new file mode 100644 index 000000000000..c7ef763fbd81 --- /dev/null +++ b/build-system/single-pass.js @@ -0,0 +1,609 @@ +/** + * Copyright 2018 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. + */ + +const babel = require('@babel/core'); +const babelify = require('babelify'); +const browserify = require('browserify'); +const ClosureCompiler = require('google-closure-compiler').compiler; +const colors = require('ansi-colors'); +const conf = require('./build.conf'); +const devnull = require('dev-null'); +const fs = require('fs-extra'); +const log = require('fancy-log'); +const minimist = require('minimist'); +const move = require('glob-move'); +const path = require('path'); +const Promise = require('bluebird'); +const relativePath = require('path').relative; +const tempy = require('tempy'); +const through = require('through2'); +const {extensionBundles, altMainBundles, TYPES} = require('../bundles.config'); +const {TopologicalSort} = require('topological-sort'); +const TYPES_VALUES = Object.keys(TYPES).map(x => TYPES[x]); +const wrappers = require('./compile-wrappers'); + +const argv = minimist(process.argv.slice(2)); +let singlePassDest = typeof argv.single_pass_dest === 'string' ? + argv.single_pass_dest : './dist/'; + +if (!singlePassDest.endsWith('/')) { + singlePassDest = `${singlePassDest}/`; +} + +const SPLIT_MARKER = `/** SPLIT${Math.floor(Math.random() * 10000)} */`; + +// Since we no longer pass the process_common_js_modules flag to closure +// compiler, we must now tranform these common JS node_modules to ESM before +// passing them to closure. +// TODO(rsimha, erwinmombay): Derive this list programmatically if possible. +const commonJsModules = [ + 'node_modules/dompurify/', + 'node_modules/promise-pjs/', + 'node_modules/set-dom/', +]; + +// Override to local closure compiler JAR +ClosureCompiler.JAR_PATH = require.resolve('./runner/dist/runner.jar'); + +const mainBundle = 'src/amp.js'; +const extensionsInfo = {}; +let extensions = extensionBundles.concat(altMainBundles) + .filter(unsupportedExtensions).map(ext => { + const path = buildFullPathFromConfig(ext); + if (Array.isArray(path)) { + path.forEach((p, index) => { + extensionsInfo[p] = Object.create(ext); + extensionsInfo[p].filename = ext.name + '-' + ext.version[index]; + }); + } else { + extensionsInfo[path] = Object.create(ext); + if (isAltMainBundle(ext.name) && ext.path) { + extensionsInfo[path].filename = ext.name; + } else { + extensionsInfo[path].filename = ext.name + '-' + ext.version; + } + } + return path; + }); +// Flatten nested arrays to support multiple versions +extensions = [].concat.apply([], extensions); + +const jsFilesToWrap = []; + +exports.getFlags = function(config) { + config.define.push('SINGLE_FILE_COMPILATION=true'); + /* eslint "google-camelcase/google-camelcase": 0 */ + // Reasonable defaults. + const flags = { + compilation_level: 'ADVANCED', + rewrite_polyfills: false, + create_source_map: '%outname%.map', + parse_inline_source_maps: true, + apply_input_source_maps: true, + source_map_location_mapping: [ + '|/', + ], + //new_type_inf: true, + language_in: 'ES6', + language_out: config.language_out || 'ES5', + module_output_path_prefix: config.writeTo || 'out/', + module_resolution: 'NODE', + externs: config.externs, + define: config.define, + // Turn off warning for "Unknown @define" since we use define to pass + // args such as FORTESTING to our runner. + jscomp_off: ['unknownDefines'], + // checkVars: Demote "variable foo is undeclared" errors. + // moduleLoad: Demote "module not found" errors to ignore missing files + // in type declarations in the swg.js bundle. + jscomp_warning: ['checkVars', 'moduleLoad'], + jscomp_error: [ + 'checkTypes', + 'accessControls', + 'const', + 'constantProperty', + 'globalThis', + ], + hide_warnings_for: config.hideWarningsFor, + }; + + // Turn object into deterministically sorted array. + const flagsArray = []; + Object.keys(flags).sort().forEach(function(flag) { + const val = flags[flag]; + if (val instanceof Array) { + val.forEach(function(item) { + flagsArray.push('--' + flag, item); + }); + } else { + if (val != null) { + flagsArray.push('--' + flag, val); + } else { + flagsArray.push('--' + flag); + } + } + }); + + return exports.getGraph(config.modules, config).then(function(g) { + return flagsArray.concat( + exports.getBundleFlags(g, flagsArray)); + }); +}; + +exports.getBundleFlags = function(g) { + const flagsArray = []; + + // Write all the packages (directories with a package.json) as --js + // inputs to the flags. Closure compiler reads the packages to resolve + // non-relative module names. + Object.keys(g.packages).sort().forEach(function(pkg) { + flagsArray.push('--js', pkg); + }); + + // Build up the weird flag structure that closure compiler calls + // modules and we call bundles. + const bundleKeys = Object.keys(g.bundles).sort(); + // TODO(erwinm): special case src/amp.js for now. add a propert sort + // comparator here. + const indexOfAmp = bundleKeys.indexOf(mainBundle); + bundleKeys.splice(indexOfAmp, 1); + bundleKeys.splice(0, 0, mainBundle); + const indexOfIntermediate = bundleKeys.indexOf('_base_i'); + bundleKeys.splice(indexOfIntermediate, 1); + bundleKeys.splice(1, 0, '_base_i'); + bundleKeys.forEach(function(originalName) { + const isMain = originalName == mainBundle; + // TODO(erwinm): This access will break + const bundle = g.bundles[originalName]; + bundle.modules.forEach(function(js) { + flagsArray.push('--js', `${g.tmp}/${js}`); + }); + let name; + let info = extensionsInfo[bundle.name]; + if (info) { + name = info.filename; + if (!name) { + throw new Error('Expected filename ' + JSON.stringify(info)); + } + } else if (bundle.name == mainBundle) { + name = 'v0'; + info = { + name, + }; + } else { + // TODO(@cramforce): Remove special case. + if (!/_base/.test(bundle.name)) { + throw new Error('Unexpected missing extension info ' + bundle.name + + ',' + JSON.stringify(bundle)); + } + name = bundle.name; + info = { + name, + }; + } + // And now build --module $name:$numberOfJsFiles:$bundleDeps + let cmd = name + ':' + (bundle.modules.length); + const bundleDeps = []; + if (!isMain) { + const configEntry = getExtensionBundleConfig(originalName); + if (configEntry) { + cmd += `:${configEntry.type}`; + bundleDeps.push('_base_i', configEntry.type); + } else { + // All lower tier bundles depend on _base_i + if (TYPES_VALUES.includes(name)) { + cmd += ':_base_i'; + bundleDeps.push('_base_i'); + } else { + cmd += ':v0'; + } + } + } + flagsArray.push('--module', cmd); + if (bundleKeys.length > 1) { + function massageWrapper(w) { + return (w.replace('<%= contents %>', '%s') + /*+ '\n//# sourceMappingURL=%basename%.map\n'*/); + } + // We need to post wrap the main bundles. We can't wrap v0.js either + // since it would have the wrapper already when we read it and prepend + // it. + if (isMain || isAltMainBundle(name)) { + jsFilesToWrap.push(name); + } else { + const configEntry = getExtensionBundleConfig(originalName); + const marker = configEntry && Array.isArray(configEntry.postPrepend) ? + SPLIT_MARKER : ''; + flagsArray.push('--module_wrapper', name + ':' + + massageWrapper(wrappers.extension( + info.name, info.loadPriority, bundleDeps, marker))); + } + } else { + throw new Error('Expect to build more than one bundle.'); + } + }); + flagsArray.push('--js_module_root', `${g.tmp}/node_modules/`); + flagsArray.push('--js_module_root', `${g.tmp}/`); + return flagsArray; +}; + +exports.getGraph = function(entryModules, config) { + let resolve; + let reject; + const promise = new Promise(function(res, rej) { + resolve = res; + reject = rej; + }); + const nodes = new Map(); + const topo = new TopologicalSort(nodes); + const graph = { + entryModules, + // Lookup whether a module is a dep of a given entry module + depOf: {}, + // Map of module id to its deps array. + deps: {}, + // Topological sorted array of all deps. + sorted: undefined, + // Generated bundles + bundles: { + _base_i: { + isBase: true, + name: '_base_i', + // The modules in the bundle. + modules: [], + }, + }, + packages: {}, + tmp: tempy.directory(), + }; + + TYPES_VALUES.forEach(type => { + graph.bundles[type] = { + isBase: true, + name: type, + modules: [], + }; + }); + + config.babel = config.babel || {}; + + // Use browserify with babel to learn about deps. + const b = browserify(entryModules, { + debug: true, + deps: true, + detectGlobals: false, + }) + // The second stage are transforms that closure compiler supports + // directly and which we don't want to apply during deps finding. + .transform(babelify, { + compact: false, + plugins: [ + require.resolve('babel-plugin-transform-es2015-modules-commonjs'), + ], + }); + // This gets us the actual deps. We collect them in an array, so + // we can sort them prior to building the dep tree. Otherwise the tree + // will not be stable. + const depEntries = []; + b.pipeline.get('deps').push(through.obj(function(row, enc, next) { + row.source = null; // Release memory + depEntries.push(row); + next(); + })); + + b.bundle().on('end', function() { + const edges = {}; + depEntries.sort(function(a, b) { + return a.id < b.id; + }).forEach(function(row) { + const id = unifyPath(exports.maybeAddDotJs( + relativePath(process.cwd(), row.id))); + topo.addNode(id, id); + const deps = edges[id] = Object.keys(row.deps).sort().map(function(dep) { + return unifyPath(relativePath(process.cwd(), + row.deps[dep])); + }); + graph.deps[id] = deps; + if (row.entry) { + graph.depOf[id] = {}; + graph.depOf[id][id] = true; // Self edge. + deps.forEach(function(dep) { + graph.depOf[id][dep] = true; + }); + } + }); + Object.keys(edges).sort().forEach(function(id) { + edges[id].forEach(function(dep) { + topo.addEdge(id, dep); + }); + }); + graph.sorted = Array.from(topo.sort().keys()).reverse(); + + setupBundles(graph); + transformPathsToTempDir(graph, config); + resolve(graph); + fs.writeFileSync('deps.txt', JSON.stringify(graph, null, 2)); + }).on('error', reject).pipe(devnull()); + return promise; +}; + +function setupBundles(graph) { + // For each module, mark them as to whether any of the entry + // modules depends on them (transitively). + Array.from(graph.sorted).reverse().forEach(function(id) { + graph.deps[id].forEach(function(dep) { + Object.keys(graph.depOf).sort().forEach(function(entry) { + if (graph.depOf[entry][id]) { + graph.depOf[entry][dep] = true; + } + }); + }); + }); + + // Create the bundles. + graph.sorted.forEach(function(id) { + let inBundleCount = 0; + // The bundle a module should go into. + let dest; + // Bundles that this item must be available to. + const bundleDestCandidates = []; + // Count in how many bundles a modules wants to be. + Object.keys(graph.depOf).sort().forEach(function(entry) { + if (graph.depOf[entry][id]) { + inBundleCount++; + dest = entry; + const configEntry = getExtensionBundleConfig(entry); + const type = configEntry ? configEntry.type : mainBundle; + bundleDestCandidates.push(type); + } + }); + console/*OK*/.assert(inBundleCount >= 1, + 'Should be in at least 1 bundle', id, 'Bundle count', + inBundleCount, graph.depOf); + // If a module is in more than 1 bundle, it must go into _base. + if (bundleDestCandidates.length > 1) { + const first = bundleDestCandidates[0]; + const allTheSame = !bundleDestCandidates.some(c => c != first); + const needsBase = bundleDestCandidates.some(c => c == mainBundle); + dest = mainBundle; + // If all requested bundles are the same, then that is the right + // place. + if (allTheSame) { + dest = first; + } else if (!needsBase) { + // If multiple type-bundles want the file, but it doesn't have to be + // in base, move the file into the intermediate bundle. + dest = '_base_i'; + } + } + if (!graph.bundles[dest]) { + graph.bundles[dest] = { + isBase: false, + name: dest, + modules: [], + }; + } + graph.bundles[dest].modules.push(id); + }); +} + +/** + * Returns true if the file is known to be a common JS module. + * @param {string} file + */ +function isCommonJsModule(file) { + return commonJsModules.some(function(module) { + return file.startsWith(module); + }); +} + +/** + * Takes all of the nodes in the dependency graph and transfers them + * to a temporary directory where we can run babel transformations. + * + * @param {!Object} graph + * @param {!Object} config + */ +function transformPathsToTempDir(graph, config) { + if (!process.env.TRAVIS) { + log('Writing transforms to', colors.cyan(graph.tmp)); + } + // `sorted` will always have the files that we need. + graph.sorted.forEach(f => { + // For now, just copy node_module files instead of transforming them. The + // exceptions are common JS modules that need to be transformed to ESM + // because we now no longer use the process_common_js_modules flag for + // closure compiler. + if (f.startsWith('node_modules/') && !isCommonJsModule(f)) { + fs.copySync(f, `${graph.tmp}/${f}`); + } else { + const {code} = babel.transformFileSync(f, { + plugins: conf.plugins( + /* isEsmBuild */ config.define.indexOf['ESM_BUILD=true'] !== -1, + /* isCommonJsModule */ isCommonJsModule(f)), + retainLines: true, + }); + fs.outputFileSync(`${graph.tmp}/${f}`, code); + } + }); +} + +// Returns the extension bundle config for the given filename or null. +function getExtensionBundleConfig(filename) { + const basename = path.basename(filename, '.js'); + return extensionBundles.filter(x => x.name == basename)[0]; +} + +const knownExtensions = { + mjs: true, + js: true, + es: true, + es6: true, + json: true, +}; + +exports.maybeAddDotJs = function(id) { + const extensionMatch = id.match(/\.([a-zA-Z0-9]+)$/); + const extension = extensionMatch ? extensionMatch[1].toLowerCase() : null; + if (!knownExtensions[extension]) { + id += '.js'; + } + return id; +}; + +function unifyPath(id) { + return id.split(path.sep).join('/'); +} + +function buildFullPathFromConfig(ext) { + function getPath(version) { + return `extensions/${ext.name}/${version}/${ext.name}.js`; + } + + // Allow alternate bundles to declare their own source location path. + if (isAltMainBundle(ext.name) && ext.path) { + return ext.path; + } + + if (Array.isArray(ext.version)) { + return ext.version.map(ver => getPath(ver)); + } + + return getPath(ext.version); +} + +function unsupportedExtensions(name) { + return name; +} + +/** + * Predicate to identify if a given extension name is an alternate main bundle + * like amp-shadow, amp-inabox etc. + * + * @param {string} name + * @return {boolean} + */ +function isAltMainBundle(name) { + return altMainBundles.some(altMainBundle => { + return altMainBundle.name === name; + }); +} + +exports.singlePassCompile = function(entryModule, options) { + return exports.getFlags({ + modules: [entryModule].concat(extensions), + writeTo: singlePassDest, + define: options.define, + externs: options.externs, + hideWarningsFor: options.hideWarningsFor, + }).then(compile).then(function() { + // Move things into place as AMP expects them. + fs.ensureDirSync(`${singlePassDest}/v0`); + return Promise.all([ + // Move all files that need to live in /v0/. ex. _base files + // all extensions. + move(`${singlePassDest}/amp*`, `${singlePassDest}/v0`).then(() => { + return move('dist/v0/amp4ads*', 'dist'); + }), + move(`${singlePassDest}/_base*`, `${singlePassDest}/v0`), + ]); + }).then(wrapMainBinaries).then(postProcessConcat).catch(e => { + // NOTE: passing the message here to colors.red breaks the output. + console./*OK*/error(e.message); + process.exit(1); + }); +}; + +/** + * Wrap AMPs main binaries with the compiler wrappers. We are not able to + * use closures wrapper mechanism for this since theres some concatenation + * we need to do to build the alternative binaries such as shadow-v0 and + * amp4ads-v0. + * TODO(#18811, erwinm): this breaks source maps and we need a way to fix this. + * magic-string might be part of the solution here so explore that (pre or post + * process) + */ +function wrapMainBinaries() { + const pair = wrappers.mainBinary.split('<%= contents %>'); + const prefix = pair[0]; + const suffix = pair[1]; + // Cache the v0 file so we can prepend it to alternative binaries. + const mainFile = fs.readFileSync('dist/v0.js', 'utf8'); + jsFilesToWrap.forEach(x => { + const path = `dist/${x}.js`; + const bootstrapCode = path === 'dist/v0.js' ? '' : mainFile; + const isAmpAltstring = path === 'dist/v0.js' ? '' : 'self.IS_AMP_ALT=1;'; + fs.writeFileSync(path, `${isAmpAltstring}${prefix}${bootstrapCode}` + + `${fs.readFileSync(path).toString()}${suffix}`); + }); +} + +/** + * Appends the listed file to the built js binary. + * TODO(erwinm, #18811): This operation is needed but straight out breaks + * source maps. + */ +function postProcessConcat() { + const extensions = extensionBundles.filter( + x => Array.isArray(x.postPrepend)); + extensions.forEach(extension => { + const isAltMainBundle = altMainBundles.some(x => { + return x.name === extension.name; + }); + // We assume its in v0 unless its an alternative main binary. + const srcTargetDir = isAltMainBundle ? 'dist/' : 'dist/v0/'; + + function createFullPath(version) { + return `${srcTargetDir}${extension.name}-${version}.js`; + } + + let targets = []; + if (Array.isArray(extension.version)) { + targets = extension.version.map(createFullPath); + } else { + targets.push(createFullPath(extension.version)); + } + targets.forEach(path => { + const prependContent = extension.postPrepend.map(x => { + return ';' + fs.readFileSync(x, 'utf8').toString(); + }).join(''); + const content = fs.readFileSync(path, 'utf8').toString() + .split(SPLIT_MARKER); + const prefix = content[0]; + const suffix = content[1]; + fs.writeFileSync(path, prefix + prependContent + suffix, 'utf8'); + }); + }); +} + + +function compile(flagsArray) { + fs.writeFileSync('flags-array.txt', JSON.stringify(flagsArray, null, 2)); + return new Promise(function(resolve, reject) { + new ClosureCompiler(flagsArray).run(function(exitCode, stdOut, stdErr) { + if (exitCode == 0) { + resolve({ + warnings: null, + }); + } else { + reject( + new Error('Closure compiler compilation of bundles failed.\n' + + stdOut + '\n' + + stdErr)); + } + }); + }); +} diff --git a/build-system/tasks/ava.js b/build-system/tasks/ava.js new file mode 100644 index 000000000000..8056247e946a --- /dev/null +++ b/build-system/tasks/ava.js @@ -0,0 +1,33 @@ +/** + * 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. + */ +'use strict'; + +const ava = require('gulp-ava'); +const gulp = require('gulp-help')(require('gulp')); + +/** + * Runs ava tests. + */ +function runAvaTests() { + return gulp.src([ + 'csvify-size/test.js', + 'get-zindex/test.js', + 'prepend-global/test.js', + ]) + .pipe(ava({silent: !!process.env.TRAVIS})); +} + +gulp.task('ava', 'Runs ava tests for gulp tasks', runAvaTests); diff --git a/build-system/tasks/babel-helpers.js b/build-system/tasks/babel-helpers.js deleted file mode 100644 index 580ed8a541ce..000000000000 --- a/build-system/tasks/babel-helpers.js +++ /dev/null @@ -1,85 +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. - */ - -/** - * @fileoverview gulp task that generates a lightweight version of - * babel helpers based on the features we actually use in the source code. - */ - -var babel = require('babel-core'); -var fs = require('fs'); -var gulp = require('gulp-help')(require('gulp')); -var through = require('through2'); -var util = require('gulp-util'); - -var options = JSON.parse(fs.readFileSync('.babelrc', 'utf8').toString()); - - -/** - * @param {!Array>} helpers - * @param {!File} file vinyl object. - * @param {string} enc - * @param {function} cb - */ -function onFileThrough(helpers, file, enc, cb) { - if (file.isNull()) { - cb(null, file); - return; - } - - if (file.isStream()) { - cb(new util.PluginError('babel-helpers', 'Stream not supported')); - return; - } - - var usedHelpers = babel.transform(file.contents, options) - .metadata.usedHelpers; - helpers.push(usedHelpers); - cb(null, file); -} - -/** - * @param {!Array>} usedHelpers - * @param {function} cb - */ -function onFileThroughEnd(usedHelpers, cb) { - var helpers = [].concat.apply([], usedHelpers); - var content = babel.buildExternalHelpers(helpers); - fs.writeFileSync('third_party/babel/custom-babel-helpers.js', content +'\n'); - cb(); -} - -/** - * @return {!Stream} - */ -function babelHelpers() { - var helpers = []; - return through.obj( - onFileThrough.bind(null, helpers), - onFileThroughEnd.bind(null, helpers) - ); -} - -/** - * @return {!Stream} - */ -function buildBabelHelpers(cb) { - return gulp.src('./{src,3p,extensions,builtins}/**/*.js') - .pipe(babelHelpers()); -} - -gulp.task('babel-helpers', 'Builds custom-babel-helpers.js', - buildBabelHelpers); diff --git a/build-system/tasks/bundle-size.js b/build-system/tasks/bundle-size.js new file mode 100644 index 000000000000..4f4c93613805 --- /dev/null +++ b/build-system/tasks/bundle-size.js @@ -0,0 +1,275 @@ +/** + * Copyright 2018 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'; + +const argv = require('minimist')(process.argv.slice(2)); +const colors = require('ansi-colors'); +const fs = require('fs-extra'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const octokit = require('@octokit/rest')(); +const path = require('path'); +const {getStdout} = require('../exec'); +const {gitBranchPoint, gitCommitHash, gitOriginUrl} = require('../git'); + +const runtimeFile = './dist/v0.js'; + +const buildArtifactsRepoOptions = { + owner: 'ampproject', + repo: 'amphtml-build-artifacts', +}; +const expectedGitHubProject = 'ampproject/amphtml'; + +const {green, red, cyan, yellow} = colors; + +// Status values returned from running `npx bundlesize` +const STATUS_PASS = 0; +const STATUS_FAIL = 1; +const STATUS_ERROR = 2; + +/** + * Get the max bundle size from the build artifacts repository. + * + * @return {string} the max allowed bundle size. + */ +async function getMaxBundleSize() { + if (process.env.GITHUB_ARTIFACTS_RO_TOKEN) { + octokit.authenticate({ + type: 'token', + token: process.env.GITHUB_ARTIFACTS_RO_TOKEN, + }); + } + + return await octokit.repos.getContent( + Object.assign(buildArtifactsRepoOptions, { + path: path.join('bundle-size', '.max_size'), + }) + ).then(result => { + const maxSize = + Buffer.from(result.data.content, 'base64').toString().trim(); + log('Max bundle size from GitHub is', cyan(maxSize)); + return maxSize; + }).catch(error => { + log(red('ERROR: Failed to retrieve the max allowed bundle size from' + + ' GitHub.')); + throw error; + }); +} + +/** + * Get the bundle size of the ancenstor commit from when this branch was split + * off from the `master` branch. + * + * @return {string} the `master` ancestor's bundle size. + */ +async function getAncestorBundleSize() { + const fromMerge = + process.env.TRAVIS && process.env.TRAVIS_EVENT_TYPE === 'pull_request'; + const gitBranchPointSha = gitBranchPoint(fromMerge); + const gitBranchPointShortSha = gitBranchPointSha.substring(0, 7); + log('Branch point from master is', cyan(gitBranchPointShortSha)); + return await octokit.repos.getContent( + Object.assign(buildArtifactsRepoOptions, { + path: path.join('bundle-size', gitBranchPointSha), + }) + ).then(result => { + const ancestorBundleSize = + Buffer.from(result.data.content, 'base64').toString().trim(); + log('Bundle size of', cyan(gitBranchPointShortSha), 'is', + cyan(ancestorBundleSize)); + return ancestorBundleSize; + }).catch(() => { + log(yellow('WARNING: Failed to retrieve bundle size of'), + cyan(gitBranchPointShortSha)); + log(yellow('Falling back to comparing to the max bundle size only')); + return null; + }); +} + +/** + * Store the bundle size of a commit hash in the build artifacts storage + * repository to the passed value. + * + * @param {string} bundleSize the new bundle size in 99.99KB format. + * @return {!Promise} + */ +function storeBundleSize(bundleSize) { + if (!process.env.TRAVIS || process.env.TRAVIS_EVENT_TYPE !== 'push') { + log(yellow('Skipping'), cyan('--store') + ':', + 'this action can only be performed on `push` builds on Travis'); + return; + } + + const gitOriginUrlValue = gitOriginUrl(); + if (!gitOriginUrlValue.includes(expectedGitHubProject)) { + log('Git origin URL is', cyan(gitOriginUrlValue)); + log('Skipping storing the bundle size in the artifacts repository on', + 'GitHub...'); + return; + } + + if (!process.env.GITHUB_ARTIFACTS_RW_TOKEN) { + log(red('ERROR: Missing GITHUB_ARTIFACTS_RW_TOKEN, cannot store the ' + + 'bundle size in the artifacts repository on GitHub!')); + process.exitCode = 1; + return; + } + + const commitHash = gitCommitHash(); + const githubApiCallOptions = Object.assign(buildArtifactsRepoOptions, { + path: path.join('bundle-size', commitHash), + }); + + octokit.authenticate({ + type: 'token', + token: process.env.GITHUB_ARTIFACTS_RW_TOKEN, + }); + + return octokit.repos.getContent(githubApiCallOptions).then(() => { + log('The file', cyan(`bundle-size/${commitHash}`), 'already exists in the', + 'build artifacts repository on GitHub. Skipping...'); + }).catch(() => { + return octokit.repos.createFile(Object.assign(githubApiCallOptions, { + message: `bundle-size: ${commitHash} (${bundleSize})`, + content: Buffer.from(bundleSize).toString('base64'), + })).then(() => { + log('Stored the new bundle size of', cyan(bundleSize), 'in the artifacts', + 'repository on GitHub'); + }).catch(error => { + log(red(`ERROR: Failed to create the bundle-size/${commitHash} file in`), + red('the build artifacts repository on GitHub!')); + log(red('Error message was:'), error.message); + process.exitCode = 1; + }); + }); +} + +function compareBundleSize(maxBundleSize) { + const cmd = `npx bundlesize -f "${runtimeFile}" -s "${maxBundleSize}"`; + log('Running ' + cyan(cmd) + '...'); + const output = getStdout(cmd).trim(); + + const error = output.match(/ERROR .*/); + if (error || output.length == 0) { + return { + output: error || '[no output from npx command]', + status: STATUS_ERROR, + newBundleSize: '', + }; + } + + const bundleSizeOutputMatches = output.match(/(PASS|FAIL) .*: (\d+.?\d*KB) .*/); + if (bundleSizeOutputMatches) { + return { + output: bundleSizeOutputMatches[0], + status: bundleSizeOutputMatches[1] == 'PASS' ? STATUS_PASS : STATUS_FAIL, + newBundleSize: bundleSizeOutputMatches[2], + }; + } + log(red('ERROR:'), 'could not infer bundle size from output.'); + return { + output, + status: STATUS_ERROR, + newBundleSize: '', + }; +} + +/** + * Checks gzipped size of existing v0.js (amp.js) against `maxSize`. + * Does _not_ rebuild: run `gulp dist --fortesting --noextensions` first. + */ +async function checkBundleSize() { + if (!fs.existsSync(runtimeFile)) { + log(yellow('Could not find'), cyan(runtimeFile) + + yellow('. Skipping bundlesize check.')); + log(yellow('To include this check, run'), + cyan('gulp dist --fortesting [--noextensions]'), + yellow('before'), cyan('gulp bundle-size') + yellow('.')); + return; + } + + const maxSize = await getMaxBundleSize(); + const ancestorBundleSize = await getAncestorBundleSize(); + + let compareAgainstMaxSize = true; + let output, status, newBundleSize; + if (ancestorBundleSize) { + ({output, status, newBundleSize} = compareBundleSize(ancestorBundleSize)); + switch (status) { + case STATUS_ERROR: + log(red(output)); + process.exitCode = 1; + return; + case STATUS_FAIL: + const sizeDelta = + (parseFloat(newBundleSize) - parseFloat(ancestorBundleSize)) + .toFixed(2); + log(yellow('New bundle size of'), cyan(newBundleSize), + yellow('is larger than the ancestor\'s bundle size of'), + cyan(ancestorBundleSize), + yellow('(Δ +') + cyan(sizeDelta) + yellow('KB)')); + log('Continuing to compare to max bundle size...'); + compareAgainstMaxSize = true; + break; + case STATUS_PASS: + log(green(output)); + compareAgainstMaxSize = false; + break; + } + } + + if (compareAgainstMaxSize) { + ({output, status, newBundleSize} = compareBundleSize(maxSize)); + switch (status) { + case STATUS_ERROR: + log(red(output)); + process.exitCode = 1; + return; + case STATUS_FAIL: + const sizeDelta = + (parseFloat(newBundleSize) - parseFloat(maxSize)) + .toFixed(2); + log(red(output)); + log(red('ERROR:'), cyan('bundlesize'), red('found that'), + cyan(runtimeFile), red('has exceeded its size cap of'), + cyan(maxSize), red('(Δ +') + cyan(sizeDelta) + red('KB)')); + log(red('This is part of a new effort to reduce AMP\'s binary size ' + + '(#14392).')); + log(red('Please contact @choumx or @jridgewell for assistance.')); + process.exitCode = 1; + return; + case STATUS_PASS: + log(green(output)); + break; + } + } + + if (argv.store) { + return storeBundleSize(newBundleSize); + } +} + + +gulp.task( + 'bundle-size', + 'Checks if the minified AMP binary has exceeded its size cap', + checkBundleSize, + { + options: { + 'store': ' Store bundle size in AMP build artifacts repo (used only ' + + 'for `master` builds)', + }, + }); diff --git a/build-system/tasks/changelog.js b/build-system/tasks/changelog.js index ff769d43d938..40f089460ed7 100644 --- a/build-system/tasks/changelog.js +++ b/build-system/tasks/changelog.js @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; /** * @fileoverview Creates a gulp task that fetches the titles and files @@ -21,73 +22,171 @@ * json, and yaml changes. */ -var BBPromise = require('bluebird'); -var argv = require('minimist')(process.argv.slice(2)); -var assert = require('assert'); -var child_process = require('child_process'); -var config = require('../config'); -var extend = require('util')._extend; -var git = require('gulp-git'); -var gulp = require('gulp-help')(require('gulp')); -var request = BBPromise.promisify(require('request')); -var util = require('gulp-util'); +const argv = require('minimist')(process.argv.slice(2)); +const assert = require('assert'); +const BBPromise = require('bluebird'); +const childProcess = require('child_process'); +const colors = require('ansi-colors'); +const config = require('../config'); +const extend = require('util')._extend; +const git = require('gulp-git'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const request = BBPromise.promisify(require('request')); -var GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN; -var exec = BBPromise.promisify(child_process.exec); -var gitExec = BBPromise.promisify(git.exec); +const {GITHUB_ACCESS_TOKEN} = process.env; +const exec = BBPromise.promisify(childProcess.exec); +const gitExec = BBPromise.promisify(git.exec); -var isCanary = argv.type == 'canary'; -var suffix = isCanary ? '-canary' : ''; -var branch = isCanary ? 'canary' : 'release'; -var isDryrun = argv.dryrun; +const branch = argv.branch || 'canary'; +const isDryrun = argv.dryrun; +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 = { + 'access_token': GITHUB_ACCESS_TOKEN, + }; +} + +if (GITHUB_ACCESS_TOKEN) { + latestReleaseOptions.qs = { + 'access_token': GITHUB_ACCESS_TOKEN, + }; +} + +/** + * @typedef {{ + * logs: !Array, + * tag: (string|undefined), + * changelog: (string|undefined) + * baseTag: (string|undefined) + * }} + */ +let GitMetadataDef; + +/** + * @typedef {{ + * title: string, + * sha: string, + * pr: (PrMetadata|undefined) + * }} + */ +let LogMetadataDef; + +/** + * @typedef {{ + * id: number, + * title: string, + * body: string, + * merge_commit_sha: string, + * url: string, + * filenames: !Array + * }} + */ +let PrMetadataDef; function changelog() { if (!GITHUB_ACCESS_TOKEN) { - util.log(util.colors.red('Warning! You have not set the ' + - 'GITHUB_ACCESS_TOKEN env var. This task might hit the default ' + - 'rate limit set by github (60).')); - util.log(util.colors.green('See https://help.github.com/articles/' + + log(colors.red('Warning! You have not set the ' + + 'GITHUB_ACCESS_TOKEN env var. Aborting "changelog" task.')); + log(colors.green('See https://help.github.com/articles/' + 'creating-an-access-token-for-command-line-use/ ' + 'for instructions on how to create a github access token. We only ' + 'need `public_repo` scope.')); + return; } return getGitMetadata(); } +/** + * @return {!Promise} + */ function getGitMetadata() { - if (!argv.version) { - throw new Error('no version value passed in. See --version flag option.'); + if (!argv.tag) { + throw new Error('no tag value passed in. See --tag flag option.'); } - var gitMetadata = {}; - return getLastGitTag() - .then(onGitTagSuccess.bind(null, gitMetadata)) + const gitMetadata = {logs: [], tag: undefined, baseTag: undefined}; + return getLastGitTag(gitMetadata) .then(getGitLog) - .then(onGitLogSuccess.bind(null, gitMetadata)) - .then(fetchGithubMetadata) - .then(buildChangelog.bind(null, gitMetadata)) + .then(getGithubPullRequestsMetadata) + .then(getGithubFilesMetadata) + .then(getBaseCanaryVersion) + .then(buildChangelog) .then(function(gitMetadata) { - util.log(util.colors.blue('\n' + gitMetadata.changelog)); + log(colors.blue('\n' + gitMetadata.changelog)); if (isDryrun) { return; } return getCurrentSha().then( - submitReleaseNotes.bind(null, argv.version, gitMetadata.changelog) + submitReleaseNotes.bind(null, argv.tag, gitMetadata.changelog) ); }) .catch(errHandler); } + +/** + * When creating a special `amp-release-*` branch always find its root + * canary version so we can add it to the changelog. We do this by + * cross referencing the refs/remotes/origin/canary sha to all the + * tags' sha and look for the matching once. This should only be done on + * none canary branches since this assumes our baseTag should be whatever + * is currently in canary. + * + * To be more accurate we need to query github for list of current + * pre-release tags, but this is a cheaper operation than that and will + * only be wrong if somebody pushes new changes to canary and had not + * tagged it yet during the build/release process. + * + * @param {!GitMetadataDef} gitMetadata + * @return {!GitMetadataDef} + */ +function getBaseCanaryVersion(gitMetadata) { + const command = 'git show-ref --tags | ' + + 'grep $(git show-ref refs/remotes/origin/canary | cut -d \' \' -f 1) | ' + + 'cut -d \'/\' -f 3'; + + if (isAmpRelease(argv.branch)) { + return exec(command).then(baseCanaryVersion => { + if (baseCanaryVersion) { + gitMetadata.baseTag = baseCanaryVersion.trim(); + } + return gitMetadata; + }); + } + return gitMetadata; +} + +/** + * @param {string} version + * @param {string} changelog + * @param {string} sha + * @return {!Promise} + */ function submitReleaseNotes(version, changelog, sha) { - var name = String(version); - var options = { + const name = String(version); + const options = { url: 'https://api.github.com/repos/ampproject/amphtml/releases', method: 'POST', headers: { 'User-Agent': 'amp-changelog-gulp-task', - 'Accept': 'application/vnd.github.v3+json' + 'Accept': 'application/vnd.github.v3+json', }, json: true, body: { @@ -96,173 +195,435 @@ function submitReleaseNotes(version, changelog, sha) { 'name': name, 'body': changelog, 'draft': true, - 'prerelease': isCanary - } + 'prerelease': true, + }, }; if (GITHUB_ACCESS_TOKEN) { options.qs = { - access_token: GITHUB_ACCESS_TOKEN - } + 'access_token': GITHUB_ACCESS_TOKEN, + }; } return request(options).then(function() { - util.log(util.colors.green('Release Notes submitted')); + log(colors.green('Release Notes submitted')); }); } +/** + * @return {!Promise} + */ function getCurrentSha() { - return gitExec({ args: 'rev-parse HEAD' }).then(function(sha) { + return gitExec({args: 'rev-parse HEAD'}).then(function(sha) { return sha.trim(); }); } -function buildChangelog(gitMetadata, githubMetadata) { - var titles = githubMetadata - .filter(function(data) { - return !data.filenames.every(function(filename) { - return config.changelogIgnoreFileTypes.test(filename); - }); - }) - .map(function(data) { - return ' - ' + data.title.trim(); +/** + * @param {!GitMetadataDef} gitMetadata + * @return {!GitMetadataDef} + */ +function buildChangelog(gitMetadata) { + let changelog = `## Version: ${argv.tag}\n\n`; + + if (gitMetadata.baseTag && isAmpRelease(argv.branch)) { + changelog += `## Based on original release: [${gitMetadata.baseTag}]` + + '(https://github.com/ampproject/amphtml/releases/' + + `tag/${gitMetadata.baseTag})\n\n`; + } + + // Append all titles + changelog += gitMetadata.logs.filter(function(log) { + const {pr} = log; + if (!pr) { + return true; + } + // Ignore PRs that are just all docs changes. + return !pr.filenames.every(function(filename) { + return config.changelogIgnoreFileTypes.test(filename); + }); + }) + .map(function(log) { + const {pr} = log; + if (!pr) { + return ' - ' + log.title; + } + return ` - ${pr.title.trim()} (#${pr.id})`; }).join('\n'); - gitMetadata.changelog = titles; + changelog += '\n\n## Breakdown by component\n\n'; + const sections = buildSections(gitMetadata); + + Object.keys(sections).sort().forEach(function(section) { + changelog += `
    \n${section}\n`; + const uniqueItems = sections[section].filter(function(title, idx) { + return sections[section].indexOf(title) == idx; + }); + changelog += uniqueItems.join(''); + changelog += '
    \n'; + }); + + gitMetadata.changelog = changelog; return gitMetadata; } +/** + * @param {!GitMetadata} gitMetadata + * @return {!Object} + */ +function buildSections(gitMetadata) { + const sections = {}; + gitMetadata.logs.forEach(function(log) { + const {pr} = log; + if (!pr) { + return; + } + const hasNonDocChange = !pr.filenames.every(function(filename) { + return config.changelogIgnoreFileTypes.test(filename); + }); + const listItem = `${pr.title.trim()} (#${pr.id})\n`; + if (hasNonDocChange) { + changelog += listItem; + } + + pr.filenames.forEach(function(filename) { + let section; + let body = ''; + const path = filename.split('/'); + const isExtensionChange = path[0] == 'extensions'; + const isBuiltinChange = path[0] == 'builtins'; + const isAdsChange = path[0] == 'ads'; + // TODO: figure out how to break down validator changes since + // it is usually a big PR with a number of commits, and the commit + // message is what is useful for a changelog. + const isValidatorChange = path[0] == 'validator'; + + if (isExtensionChange) { + section = path[1]; + } else if (isBuiltinChange && isJs(path[1])) { + // builtins files dont have a nested per component folder. + section = path[1].replace(/\.js$/, ''); + } else if (isAdsChange && isJs(path[1])) { + section = 'ads'; + } else if (isValidatorChange) { + section = 'validator'; + } + + if (section) { + if (!sections[section]) { + sections[section] = []; + } + // if its the validator section, read the body of the PR + // and format it correctly under the bullet list. + if (section == 'validator') { + body = `${pr.body}\n`; + } + sections[section].push(listItem + body); + } + }); + }); + return sections; +} + /** * Get the latest git tag from either a normal release or from a canary release. - * @return {!Promise} + * @param {!GitMetadataDef} gitMetadata + * @return {!Promise} */ -function getLastGitTag() { - var options = { - args: 'describe --abbrev=0 --tags' - }; - var canaryGrep = isCanary ? 'grep canary$' : 'grep -v canary$'; - return exec('git tag | ' + canaryGrep + ' | ' + - 'xargs -I@ git log --format=format:"%ai @%n" -1 @ | ' + - 'sort -r | awk \'{print $4}\' | head -1').then(function(tag) { - return tag.replace('\n', ''); - }); +function getLastGitTag(gitMetadata) { + return request(latestReleaseOptions).then(res => { + const body = JSON.parse(res.body); + if (!body.tag_name) { + throw new Error('getLastGitTag: ' + body.message); + } + gitMetadata.tag = body.tag_name; + return gitMetadata; + }); } /** - * @param {string} tag - * @return {!Promise} + * Runs `git log ${branch}...{tag} --pretty=oneline --first-parent` + * @param {!GitMetadataDef} gitMetadata + * @return {!Promise} */ -function getGitLog(tag) { - var options = { - args: 'log ' + branch + '...' + tag + ' --pretty=format:%s --merges' +function getGitLog(gitMetadata) { + const options = { + args: `log ${branch}...${gitMetadata.tag} --pretty=oneline --first-parent`, }; - return gitExec(options).then(function(log) { - if (!log) { - throw new Error('No log found "git log ' + branch + - '...' + tag + '".\nIs it possible that there is no delta?\n' + + return gitExec(options).then(function(logs) { + if (!logs) { + throw new Error('No logs found "git log ' + branch + '...' + + gitMetadata.tag + '".\nIs it possible that there is no delta?\n' + 'Make sure to fetch and rebase (or reset --hard) the latest ' + 'from remote upstream.'); } - return log; + const commits = logs.split('\n').filter(log => !!log.length); + gitMetadata.logs = commits.map(log => { + const words = log.split(' '); + return {sha: words.shift(), title: words.join(' ')}; + }); + return gitMetadata; }); } -function fetchGithubMetadata(ids) { - var options = { - url: 'https://api.github.com/repos/ampproject/amphtml/pulls/', - headers: { - 'User-Agent': 'amp-changelog-gulp-task' - } - }; +/** + * @param {!GitMetadataDef} gitMetadata + * @return {!Promise} + */ +function getGithubPullRequestsMetadata(gitMetadata) { + // (erwinm): Github seems to only return data for the first 3 pages + // from my manual testing. + return BBPromise.all([ + getClosedPullRequests(1), + getClosedPullRequests(2), + getClosedPullRequests(3), + ]) + .then(requests => [].concat.apply([], requests)) + .then(prs => { + gitMetadata.prs = prs; + const githubPrRequest = gitMetadata.logs.map(log => { + const pr = prs.filter(pr => pr.merge_commit_sha == log.sha)[0]; + if (pr) { + log.pr = buildPrMetadata(pr); + } else if (isPrIdInTitle(log.title)) { + const id = getPrIdFromCommit(log.title); + const prOptions = extend({}, pullOptions); + prOptions.url += `/${id}`; + const fileOptions = extend({}, prOptions); + fileOptions.url += '/files'; + // If we couldn't find the matching pull request from 3 pages + // of closed pull request try and fetch it through the id + // if we can retrieve it from the commit message (only available + // through github merge). + return getPullRequest(prOptions, log); + } + return BBPromise.resolve(); + }); + return BBPromise.all(githubPrRequest).then(() => { + return gitMetadata; + }); + }); +} - if (GITHUB_ACCESS_TOKEN) { - options.qs = { - access_token: GITHUB_ACCESS_TOKEN +/** + * We either fetch the pulls/${id}/files but if we have no PrMetadata yet, + * we will try and also fetch pulls/${id} first before fetching + * pulls/${id}/files. + * + * @param {!GitMetadataDef} gitMetadata + * @return {!Promise} + */ +function getGithubFilesMetadata(gitMetadata) { + const githubFileRequests = gitMetadata.logs.map(log => { + if (log.pr) { + const fileOptions = extend({}, pullOptions); + fileOptions.url = `${log.pr.url}/files`; + return getPullRequestFiles(fileOptions, log.pr); } - } - - // NOTE: (erwinm) not sure if theres a better way to do this, we're - // doing n + 1 fetches here since we can't batch things up. - var requests = ids.map(function(id) { - var prOption = extend({}, options); - prOption.url += id; - - return getPullRequestTitle(prOption).then(function(title) { - var filesOption = extend({}, prOption); - filesOption.url += '/files'; - return getPullRequestFiles(title, filesOption); - }); + return BBPromise.resolve(); + }); + return BBPromise.all(githubFileRequests).then(() => { + return gitMetadata; }); +} - return BBPromise/*OK*/.all(requests); +/** + * Fetches pulls?page=${opt_page} + * + * @param {number=} opt_page + * @return {!Promise} + */ +function getClosedPullRequests(opt_page) { + opt_page = opt_page || 1; + const options = extend({}, pullOptions); + options.qs = { + state: 'closed', + page: opt_page, + 'access_token': GITHUB_ACCESS_TOKEN, + }; + return request(options).then(res => { + const prs = JSON.parse(res.body); + assert(Array.isArray(prs), 'prs must be an array.'); + return prs; + }); } -function getPullRequestTitle(prOption) { +/** + * @param {Object} prOption + * @param {!LogMetadataDef} log + * @return {!Promise} + */ +function getPullRequest(prOption, log) { return request(prOption).then(function(res) { - var body = JSON.parse(res.body); - assert(typeof body.url == 'string', 'should have url string. ' + res.body); - var url = body.url.split('/'); - var pr = url[url.length - 1]; - return body.title + ' (#' + pr + ')'; + const pr = JSON.parse(res.body); + assert(typeof pr === 'object', 'Pull Requests Metadata must be an object'); + log.pr = buildPrMetadata(pr); + return log.pr; }); } -function getPullRequestFiles(title, filesOption) { +/** + * @param {!Object} filesOption + * @param {!PrMetadataDef} pr + * @return {!Promise} + */ +function getPullRequestFiles(filesOption, pr) { return request(filesOption).then(function(res) { - var body = JSON.parse(res.body); + const body = JSON.parse(res.body); assert(Array.isArray(body) && body.length > 0, 'Pull request response must not be empty. ' + res.body); - var filenames = body.map(function(file) { + const filenames = body.map(function(file) { return file.filename; }); - return { - title: title, - filenames: filenames - }; + pr.filenames = filenames; + return pr; }); } -function onGitTagSuccess(gitMetadata, tag) { - if (!tag) { - throw new Error('Could not find latest ' + branch + ' tag.'); +function errHandler(err) { + let msg = err; + if (err.message) { + msg = err.message; } + log(colors.red(msg)); +} - gitMetadata.tag = tag; - util.log(util.colors.green('Current latest tag: ' + tag)); - return tag; +/** + * Check if the string starts with "Merge pull request #" + * @param {string} str + * @return {boolean} + */ +function isPrIdInTitle(str) { + return str./* OK*/indexOf('Merge pull request #') == 0; } -function onGitLogSuccess(gitMetadata, logs) { - var commits = logs.split('\n'); - assert(typeof logs == 'string', 'git log should be a string.\n' + logs); - return commits - .filter(function(commit) { - // filter non Pull request merges - return commit.indexOf('Merge pull') == 0; - }) - .map(function(commit) { - // We only need the PR id - var id = commit.split(' ')[3].slice(1); - var value = parseInt(id, 10); - assert(value > 0, 'Should be an integer greater than 0. ' + value); - return id; - }); +/** + * @param {string} commit + * @return {number} + */ +function getPrIdFromCommit(commit) { + // We only need the PR id + const id = commit.split(' ')[3].slice(1); + const value = parseInt(id, 10); + assert(value > 0, 'Should be an integer greater than 0. ' + value); + return id; } -function errHandler(err) { - var msg = err; - if (err.message) { - msg = err.message; +/** + * Checks if string ends with ".js" + * @param {string} str + * @return {boolean} + */ +function isJs(str) { + return str./* OK*/endsWith('.js'); +} + +/** + * Checks if amp-release-* branch + * @param {?string|undefined} str + * @return {boolean} + */ +function isAmpRelease(str) { + return !!(str && str./* OK*/indexOf('amp-release') == 0); +} + +/** + * @param {!JSONValue} pr + * @return {!PrMetadata} + */ +function buildPrMetadata(pr) { + return { + 'id': pr.number, + 'title': pr.title, + 'body': pr.body, + 'merge_commit_sha': pr.merge_commit_sha, + 'url': pr._links.self.href, + }; +} + +function changelogUpdate() { + if (!GITHUB_ACCESS_TOKEN) { + log(colors.red('Warning! You have not set the ' + + 'GITHUB_ACCESS_TOKEN env var. Aborting "changelog" task.')); + log(colors.green('See https://help.github.com/articles/' + + 'creating-an-access-token-for-command-line-use/ ' + + 'for instructions on how to create a github access token. We only ' + + 'need `public_repo` scope.')); + return; + } + if (!argv.message) { + log(colors.red('--message flag must be set.')); + } + return update(); +} + +function update() { + const url = 'https://api.github.com/repos/ampproject/amphtml/releases/tags/' + + `${argv.tag}`; + const tagsOptions = { + url, + method: 'GET', + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + }; + + const releasesOptions = { + url: 'https://api.github.com/repos/ampproject/amphtml/releases/', + method: 'PATCH', + body: {}, + json: true, + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + }; + + if (GITHUB_ACCESS_TOKEN) { + tagsOptions.qs = { + 'access_token': GITHUB_ACCESS_TOKEN, + }; + releasesOptions.qs = { + 'access_token': GITHUB_ACCESS_TOKEN, + }; } - util.log(util.colors.red(msg)); - return err; + + return request(tagsOptions).then(res => { + const release = JSON.parse(res.body); + if (!release.body) { + return; + } + const {id} = release; + releasesOptions.url += id; + if (argv.suffix) { + releasesOptions.body.body = release.body + argv.message; + } else { + releasesOptions.body.body = argv.message + release.body; + } + return request(releasesOptions).then(() => { + log(colors.green('Update Successful.')); + }) + .catch(e => { + log(colors.red('Update Failed. ' + e.message)); + }); + }); } gulp.task('changelog', 'Create github release draft', changelog, { options: { dryrun: ' Generate changelog but dont push it out', type: ' Pass in "canary" to generate a canary changelog', - version: ' The git tag and github release label', - } + tag: ' The git tag and github release label', + }, +}); + +const updateMessage = 'Update github release. Ex. prepend ' + + 'canary percentage changes to release'; +gulp.task('changelog:update', updateMessage, changelogUpdate, { + options: { + dryrun: ' Generate changelog but dont push it out', + tag: ' The git tag and github release label', + }, }); diff --git a/build-system/tasks/check-links.js b/build-system/tasks/check-links.js new file mode 100644 index 000000000000..93bfe8c85c55 --- /dev/null +++ b/build-system/tasks/check-links.js @@ -0,0 +1,186 @@ +/** + * 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. + */ +'use strict'; + +const argv = require('minimist')(process.argv.slice(2)); +const BBPromise = require('bluebird'); +const colors = require('ansi-colors'); +const fs = require('fs-extra'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const markdownLinkCheck = BBPromise.promisify(require('markdown-link-check')); +const path = require('path'); +const {gitDiffAddedNameOnlyMaster, gitDiffNameOnlyMaster} = require('../git'); + +const maybeUpdatePackages = process.env.TRAVIS ? [] : ['update-packages']; + +/** + * Parses the list of files in argv, or extracts it from the commit log. + * + * @return {!Array} + */ +function getMarkdownFiles() { + if (!!argv.files) { + return argv.files.split(','); + } + return gitDiffNameOnlyMaster().filter(function(file) { + return path.extname(file) == '.md' && !file.startsWith('examples/'); + }); +} + +/** + * Parses the list of files in argv and checks for dead links. + * + * @return {Promise} Used to wait until all async link checkers finish. + */ +function checkLinks() { + const markdownFiles = getMarkdownFiles(); + const linkCheckers = markdownFiles.map(function(markdownFile) { + return runLinkChecker(markdownFile); + }); + return BBPromise.all(linkCheckers) + .then(function(allResults) { + let deadLinksFound = false; + const filesWithDeadLinks = []; + allResults.map(function(results, index) { + // Skip files that were deleted by the PR. + if (!fs.existsSync(markdownFiles[index])) { + return; + } + let deadLinksFoundInFile = false; + results.forEach(function(result) { + // Skip links to files that were introduced by the PR. + if (isLinkToFileIntroducedByPR(result.link)) { + return; + } + if (result.status === 'dead') { + deadLinksFound = true; + deadLinksFoundInFile = true; + log('[%s] %s', colors.red('✖'), result.link); + } else if (!process.env.TRAVIS) { + log('[%s] %s', colors.green('✔'), result.link); + } + }); + if (deadLinksFoundInFile) { + filesWithDeadLinks.push(markdownFiles[index]); + log( + colors.red('ERROR'), + 'Possible dead link(s) found in', + colors.magenta(markdownFiles[index])); + } else { + log( + colors.green('SUCCESS'), + 'All links in', + colors.magenta(markdownFiles[index]), 'are alive.'); + } + }); + if (deadLinksFound) { + log( + colors.red('ERROR'), + 'Please update dead link(s) in', + colors.magenta(filesWithDeadLinks.join(',')), + 'or whitelist them in build-system/tasks/check-links.js'); + log( + colors.yellow('NOTE'), + 'If the link(s) above are not meant to resolve to a real webpage', + 'surrounding them with backticks will exempt them from the link', + 'checker.'); + process.exit(1); + } else { + log( + colors.green('SUCCESS'), + 'All links in all markdown files in this branch are alive.'); + } + }); +} + +/** + * Determines if a link points to a file added, copied, or renamed in the PR. + * + * @param {string} link Link being tested. + * @return {boolean} True if the link points to a file introduced by the PR. + */ +function isLinkToFileIntroducedByPR(link) { + return gitDiffAddedNameOnlyMaster().some(function(file) { + return (file.length > 0 && link.includes(path.parse(file).base)); + }); +} + +/** + * Filters out whitelisted links before running the link checker. + * + * @param {string} markdown Original markdown. + * @return {string} Markdown after filtering out whitelisted links. + */ +function filterWhitelistedLinks(markdown) { + let filteredMarkdown = markdown; + + // localhost links optionally preceded by ( or [ (not served on Travis) + filteredMarkdown = + filteredMarkdown.replace(/(\(|\[)?http:\/\/localhost:8000/g, ''); + + // Links in script tags (illustrative, and not always valid) + filteredMarkdown = filteredMarkdown.replace(/src="http.*?"/g, ''); + + // Links inside a block (illustrative, and not always valid) + filteredMarkdown = filteredMarkdown.replace(/([^]*?)<\/code>/g, ''); + + // Links inside a
     block (illustrative, and not always valid)
    +  filteredMarkdown = filteredMarkdown.replace(/
    ([^]*?)<\/pre>/g, '');
    +
    +  // The heroku nightly build page is not always acccessible by the checker.
    +  filteredMarkdown = filteredMarkdown.replace(
    +      /\(http:\/\/amphtml-nightly.herokuapp.com\/\)/g, '');
    +
    +  // After all whitelisting is done, clean up any remaining empty blocks bounded
    +  // by backticks. Otherwise, `` will be treated as the start of a code block
    +  // and confuse the link extractor.
    +  filteredMarkdown = filteredMarkdown.replace(/\ \`\`\ /g, '');
    +
    +  return filteredMarkdown;
    +}
    +
    +/**
    + * Reads the raw contents in the given markdown file, filters out localhost
    + * links (because they do not resolve on Travis), and checks for dead links.
    + *
    + * @param {string} markdownFile Path of markdown file, relative to src root.
    + * @return {Promise} Used to wait until the async link checker is done.
    + */
    +function runLinkChecker(markdownFile) {
    +  // Skip files that were deleted by the PR.
    +  if (!fs.existsSync(markdownFile)) {
    +    return Promise.resolve();
    +  }
    +  const markdown = fs.readFileSync(markdownFile).toString();
    +  const filteredMarkdown = filterWhitelistedLinks(markdown);
    +  const opts = {
    +    baseUrl: 'file://' + path.dirname(path.resolve((markdownFile))),
    +  };
    +  return markdownLinkCheck(filteredMarkdown, opts);
    +}
    +
    +gulp.task(
    +    'check-links',
    +    'Detects dead links in markdown files',
    +    maybeUpdatePackages,
    +    checkLinks,
    +    {
    +      options: {
    +        'files': '  CSV list of files in which to check links',
    +      },
    +    }
    +);
    diff --git a/build-system/tasks/clean.js b/build-system/tasks/clean.js
    index 1a7d7ac2122b..d84845359da4 100644
    --- a/build-system/tasks/clean.js
    +++ b/build-system/tasks/clean.js
    @@ -13,18 +13,23 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +'use strict';
     
    -var del = require('del');
    -var gulp = require('gulp-help')(require('gulp'));
    +const del = require('del');
    +const gulp = require('gulp-help')(require('gulp'));
     
     
     /**
      * Clean up the build artifacts
    - *
    - * @param {function} done callback
      */
     function clean() {
    -  return del(['dist', 'dist.3p', 'dist.tools', 'build', 'examples.build']);
    +  return del([
    +    'dist',
    +    'dist.3p',
    +    'dist.tools',
    +    'build',
    +    '.amp-build',
    +  ]);
     }
     
     
    diff --git a/build-system/tasks/compile-access-expr.js b/build-system/tasks/compile-access-expr.js
    deleted file mode 100644
    index 674366eca9cf..000000000000
    --- a/build-system/tasks/compile-access-expr.js
    +++ /dev/null
    @@ -1,35 +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.
    - */
    -
    -var jison = require('jison');
    -var gulp = require('gulp');
    -var fs = require('fs-extra');
    -
    -gulp.task('compile-access-expr', function() {
    -  var path = 'extensions/amp-access/0.1/';
    -
    -  var bnf = fs.readFileSync(path + 'access-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 jsExports = 'exports.parser = parser;';
    -
    -  var out = license + '\n\n' + jsModule + '\n\n' + jsExports + '\n';
    -  fs.writeFileSync(path + 'access-expr-impl.js', out);
    -});
    diff --git a/build-system/tasks/compile-expr.js b/build-system/tasks/compile-expr.js
    new file mode 100644
    index 000000000000..d41b21325d28
    --- /dev/null
    +++ b/build-system/tasks/compile-expr.js
    @@ -0,0 +1,89 @@
    +/**
    + * Copyright 2018 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';
    +
    +const fs = require('fs-extra');
    +const gulp = require('gulp');
    +const jison = require('jison');
    +
    +/**
    + * Helper function that uses jison to generate a parser for the input file.
    + * @param {string} path
    + * @param {string} jisonFilename
    + * @param {string} imports
    + * @param {string} parserName
    + * @param {string} jsFilename
    + */
    +function compileExpr(path, jisonFilename, imports, parserName, jsFilename) {
    +  const bnf = fs.readFileSync(path + jisonFilename, 'utf8');
    +  const settings = {
    +    type: 'lalr',
    +    debug: false,
    +    moduleType: 'js',
    +  };
    +  const generator = new jison.Generator(bnf, settings);
    +  const jsModule = generator.generate(settings);
    +
    +  const license = fs.readFileSync(
    +      'build-system/tasks/js-license.txt', 'utf8');
    +  const suppressCheckTypes = '/** @fileoverview ' +
    +      '@suppress {checkTypes, suspiciousCode, uselessCode} */';
    +  const jsExports = 'export const ' + parserName + ' = parser;';
    +
    +  const out = [
    +    license,
    +    suppressCheckTypes,
    +    imports,
    +    jsModule,
    +    jsExports]
    +      .join('\n\n')
    +      // Required in order to support babel 7, since 'token-stack: true' will
    +      // adversely affect lexer performance.
    +      // See https://github.com/ampproject/amphtml/pull/18574#discussion_r223506153.
    +      .replace(/[ \t]*_token_stack:[ \t]*/, '') + '\n';
    +  fs.writeFileSync(path + jsFilename, out);
    +}
    +
    +function compileAccessExpr() {
    +  const path = 'extensions/amp-access/0.1/';
    +  const jisonFilename = 'access-expr-impl.jison';
    +  const imports = '';
    +  const parserName = 'accessParser';
    +  const jsFilename = 'access-expr-impl.js';
    +  compileExpr(path, jisonFilename, imports, parserName, jsFilename);
    +}
    +
    +function compileBindExpr() {
    +  const path = 'extensions/amp-bind/0.1/';
    +  const jisonFilename = 'bind-expr-impl.jison';
    +  const imports = 'import {AstNode, AstNodeType} from \'./bind-expr-defines\';';
    +  const parserName = 'bindParser';
    +  const jsFilename = 'bind-expr-impl.js';
    +  compileExpr(path, jisonFilename, imports, parserName, jsFilename);
    +}
    +
    +function compileCssExpr() {
    +  const path = 'extensions/amp-animation/0.1/';
    +  const jisonFilename = 'css-expr-impl.jison';
    +  const imports = 'import * as ast from \'./css-expr-ast\';';
    +  const parserName = 'cssParser';
    +  const jsFilename = 'css-expr-impl.js';
    +  compileExpr(path, jisonFilename, imports, parserName, jsFilename);
    +}
    +
    +gulp.task('compile-access-expr', compileAccessExpr);
    +gulp.task('compile-bind-expr', compileBindExpr);
    +gulp.task('compile-css-expr', compileCssExpr);
    diff --git a/build-system/tasks/compile.js b/build-system/tasks/compile.js
    index 70d81a4e11d6..7018dc28126a 100644
    --- a/build-system/tasks/compile.js
    +++ b/build-system/tasks/compile.js
    @@ -13,23 +13,32 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +'use strict';
     
    -var fs = require('fs-extra');
    -var closureCompiler = require('gulp-closure-compiler');
    -var gulp = require('gulp');
    -var rename = require('gulp-rename');
    -var replace = require('gulp-replace');
    -var internalRuntimeVersion = require('../internal-version').VERSION;
    +const argv = require('minimist')(process.argv.slice(2));
    +const closureCompiler = require('gulp-closure-compiler');
    +const colors = require('ansi-colors');
    +const fs = require('fs-extra');
    +const gulp = require('gulp');
    +const {VERSION: internalRuntimeVersion} = require('../internal-version') ;
     
    -var queue = [];
    -var inProgress = 0;
    -var MAX_PARALLEL_CLOSURE_INVOCATIONS = 4;
    +const rename = require('gulp-rename');
    +const replace = require('gulp-regexp-sourcemaps');
    +const rimraf = require('rimraf');
    +const shortenLicense = require('../shorten-license');
    +const {highlight} = require('cli-highlight');
    +const {singlePassCompile} = require('../single-pass');
    +
    +const isProdBuild = !!argv.type;
    +const queue = [];
    +let inProgress = 0;
    +const MAX_PARALLEL_CLOSURE_INVOCATIONS = 4;
     
     // Compiles AMP with the closure compiler. This is intended only for
    -// production use. During development we intent to continue using
    +// production use. During development we intend to continue using
     // babel, as it has much faster incremental compilation.
     exports.closureCompile = function(entryModuleFilename, outputDir,
    -    outputFilename, options) {
    +  outputFilename, options) {
       // Rate limit closure compilation to MAX_PARALLEL_CLOSURE_INVOCATIONS
       // concurrent processes.
       return new Promise(function(resolve) {
    @@ -37,11 +46,15 @@ exports.closureCompile = function(entryModuleFilename, outputDir,
           inProgress++;
           compile(entryModuleFilename, outputDir, outputFilename, options)
               .then(function() {
    +            if (process.env.TRAVIS) {
    +              // Print a progress dot after each task to avoid Travis timeouts.
    +              process.stdout.write('.');
    +            }
                 inProgress--;
                 next();
                 resolve();
               }, function(e) {
    -            console./*OK*/error('Compilation error', e.message);
    +            console./*OK*/error(colors.red('Compilation error:'), e.message);
                 process.exit(1);
               });
         }
    @@ -58,106 +71,368 @@ exports.closureCompile = function(entryModuleFilename, outputDir,
       });
     };
     
    -function compile(entryModuleFilename, outputDir,
    -    outputFilename, options) {
    -  return new Promise(function(resolve, reject) {
    -    var intermediateFilename = 'build/cc/' +
    +function cleanupBuildDir() {
    +  fs.mkdirsSync('build/cc');
    +  rimraf.sync('build/fake-module');
    +  rimraf.sync('build/patched-module');
    +  fs.mkdirsSync('build/patched-module/document-register-element/build');
    +  fs.mkdirsSync('build/fake-module/third_party/babel');
    +  fs.mkdirsSync('build/fake-module/src/polyfills/');
    +  fs.mkdirsSync('build/fake-polyfills/src/polyfills');
    +}
    +exports.cleanupBuildDir = cleanupBuildDir;
    +
    +// Formats a closure compiler error message into a more readable form by
    +// dropping the lengthy java invocation line...
    +//     Command failed: java -jar ... --js_output_file=""
    +// ...and then syntax highlighting the error text.
    +function formatClosureCompilerError(message) {
    +  const javaInvocationLine = /Command failed:[^]*--js_output_file=\".*?\"\n/;
    +  message = message.replace(javaInvocationLine, '');
    +  message = highlight(message, {ignoreIllegals: true});
    +  message = message.replace(/WARNING/g, colors.yellow('WARNING'));
    +  message = message.replace(/ERROR/g, colors.red('ERROR'));
    +  return message;
    +}
    +
    +function compile(entryModuleFilenames, outputDir, outputFilename, options) {
    +  const hideWarningsFor = [
    +    'third_party/caja/',
    +    'third_party/closure-library/sha384-generated.js',
    +    'third_party/subscriptions-project/',
    +    'third_party/d3/',
    +    'third_party/mustache/',
    +    'third_party/vega/',
    +    'third_party/webcomponentsjs/',
    +    'third_party/rrule/',
    +    'third_party/react-dates/',
    +    'third_party/amp-toolbox-cache-url/',
    +    'third_party/inputmask/',
    +    'node_modules/',
    +    'build/patched-module/',
    +    // Can't seem to suppress `(0, win.eval)` suspicious code warning
    +    '3p/environment.js',
    +    // Generated code.
    +    'extensions/amp-access/0.1/access-expr-impl.js',
    +  ];
    +  const baseExterns = [
    +    'build-system/amp.extern.js',
    +    'third_party/closure-compiler/externs/web_animations.js',
    +    'third_party/moment/moment.extern.js',
    +    'third_party/react-externs/externs.js',
    +  ];
    +  const define = [];
    +  if (argv.pseudo_names) {
    +    define.push('PSEUDO_NAMES=true');
    +  }
    +  if (argv.fortesting) {
    +    define.push('FORTESTING=true');
    +  }
    +  if (options.singlePassCompilation) {
    +    const compilationOptions = {
    +      define,
    +      externs: baseExterns,
    +      hideWarningsFor,
    +    };
    +
    +    // Add babel plugin to remove unwanted polyfills in esm build
    +    if (options.esmPassCompilation) {
    +      compilationOptions['dest'] = './dist/esm/';
    +      define.push('ESM_BUILD=true');
    +    }
    +
    +    console/*OK*/.assert(typeof entryModuleFilenames == 'string');
    +    const entryModule = entryModuleFilenames;
    +    // TODO(@cramforce): Run the post processing step
    +    return singlePassCompile(
    +        entryModule,
    +        compilationOptions
    +    ).then(() => {
    +      return new Promise((resolve, reject) => {
    +        const stream = gulp.src(outputDir + '/**/*.js');
    +        stream.on('end', resolve);
    +        stream.on('error', reject);
    +        stream.pipe(
    +            replace(/\$internalRuntimeVersion\$/g, internalRuntimeVersion, 'runtime-version'))
    +            .pipe(shortenLicense())
    +            .pipe(gulp.dest(outputDir));
    +      });
    +    });
    +  }
    +
    +  return new Promise(function(resolve) {
    +    let entryModuleFilename;
    +    if (entryModuleFilenames instanceof Array) {
    +      entryModuleFilename = entryModuleFilenames[0];
    +    } else {
    +      entryModuleFilename = entryModuleFilenames;
    +      entryModuleFilenames = [entryModuleFilename];
    +    }
    +    const checkTypes =
    +        options.checkTypes || options.typeCheckOnly || argv.typecheck_only;
    +    const intermediateFilename = 'build/cc/' +
             entryModuleFilename.replace(/\//g, '_').replace(/^\./, '');
    -    console./*OK*/log('Starting closure compiler for ', entryModuleFilename);
    -    fs.mkdirsSync('build/cc');
    -    fs.mkdirsSync('build/fake-module/third_party/babel');
    -    fs.mkdirsSync('build/fake-module/src');
    -    fs.writeFileSync(
    -        'build/fake-module/third_party/babel/custom-babel-helpers.js',
    -        '// Not needed in closure compiler\n');
    -    fs.writeFileSync(
    -        'build/fake-module/src/polyfills.js',
    -        '// Not needed in closure compiler\n');
    -    var wrapper = '(function(){var process={env:{}};%output%})();';
    +    // If undefined/null or false then we're ok executing the deletions
    +    // and mkdir.
    +    if (!options.preventRemoveAndMakeDir) {
    +      cleanupBuildDir();
    +    }
    +    const unneededFiles = [
    +      'build/fake-module/third_party/babel/custom-babel-helpers.js',
    +    ];
    +    let wrapper = '(function(){%output%})();';
         if (options.wrapper) {
    -      wrapper = options.wrapper.replace('<%= contents %>',
    -          'var process={env:{}};%output%');
    +      wrapper = options.wrapper.replace('<%= contents %>', '%output%');
         }
    -    wrapper += '\n//# sourceMappingURL=' +
    -        outputFilename + '.map\n';
    +    wrapper += '\n//# sourceMappingURL=' + outputFilename + '.map\n';
         if (fs.existsSync(intermediateFilename)) {
           fs.unlinkSync(intermediateFilename);
         }
    +    let sourceMapBase = 'http://localhost:8000/';
    +    if (isProdBuild) {
    +      // Point sourcemap to fetch files from correct GitHub tag.
    +      sourceMapBase = 'https://raw.githubusercontent.com/ampproject/amphtml/' +
    +            internalRuntimeVersion + '/';
    +    }
         const srcs = [
    -      '3p/**/*.js',
    -      'ads/**/*.js',
    -      'extensions/**/*.js',
    -      'build/**/*.js',
    -      '!build/cc/**',
    -      '!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',
    +      // A4A has these cross extension deps.
    +      'extensions/amp-ad-network*/**/*-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',
    +      // Needed for WebAnimationService
    +      'extensions/amp-animation/**/*.js',
    +      // For amp-bind in the web worker (ww.js).
    +      'extensions/amp-bind/**/*.js',
    +      // Needed to access form impl from other extensions
    +      'extensions/amp-form/**/*.js',
    +      // Needed to access inputmask impl from other extensions
    +      'extensions/amp-inputmask/**/*.js',
    +      // Needed for AccessService
    +      'extensions/amp-access/**/*.js',
    +      // Needed for AmpStoryVariableService
    +      'extensions/amp-story/**/*.js',
    +      // Needed for SubscriptionsService
    +      'extensions/amp-subscriptions/**/*.js',
    +      // Needed to access UserNotificationManager from other extensions
    +      'extensions/amp-user-notification/**/*.js',
    +      // Needed for VideoService
    +      'extensions/amp-video-service/**/*.js',
    +      // Needed to access ConsentPolicyManager from other extensions
    +      'extensions/amp-consent/**/*.js',
    +      // Needed to access AmpGeo type for service locator
    +      'extensions/amp-geo/**/*.js',
    +      // Needed for AmpViewerIntegrationVariableService
    +      'extensions/amp-viewer-integration/**/*.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/timeagojs/**/*.js',
    +      'third_party/vega/**/*.js',
    +      'third_party/d3/**/*.js',
    +      'third_party/subscriptions-project/*.js',
    +      'third_party/webcomponentsjs/ShadowCSS.js',
    +      'third_party/rrule/rrule.js',
    +      'third_party/react-dates/bundle.js',
    +      'third_party/amp-toolbox-cache-url/**/*.js',
    +      'third_party/inputmask/**/*.js',
    +      'node_modules/dompurify/dist/purify.es.js',
    +      'node_modules/promise-pjs/promise.js',
    +      'node_modules/set-dom/src/**/*.js',
    +      'node_modules/web-animations-js/web-animations.install.js',
    +      'node_modules/web-activities/activity-ports.js',
    +      'node_modules/@ampproject/animations/dist/animations.mjs',
    +      'node_modules/@ampproject/worker-dom/dist/' +
    +          'unminified.index.safe.mjs.patched.js',
           'node_modules/document-register-element/build/' +
    -          'document-register-element.max.js',
    -      'node_modules/core-js/modules/**.js',
    +          'document-register-element.patched.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 rollup configs
    +      '!**/rollup.config.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.includes('extensions/')) {
    +        return;
    +      }
    +      const 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.
    -    if (options.includePolyfills) {
    -      srcs.push('!build/fake-module/src/polyfills.js');
    +    if (options.includeOnlyESMLevelPolyfills) {
    +      const polyfills = fs.readdirSync('src/polyfills');
    +      const polyfillsShadowList = polyfills.filter(p => {
    +        // custom-elements polyfill must be included.
    +        return p !== 'custom-elements.js';
    +      });
    +      srcs.push(
    +          '!build/fake-module/src/polyfills.js',
    +          '!build/fake-module/src/polyfills/**/*.js',
    +          '!build/fake-polyfills/src/polyfills.js',
    +          'src/polyfills/custom-elements.js',
    +          'build/fake-polyfills/**/*.js');
    +      polyfillsShadowList.forEach(polyfillFile => {
    +        srcs.push(`!src/polyfills/${polyfillFile}`);
    +        fs.writeFileSync('build/fake-polyfills/src/polyfills/' + polyfillFile,
    +            'export function install() {}');
    +      });
    +    } else if (options.includePolyfills) {
    +      srcs.push(
    +          '!build/fake-module/src/polyfills.js',
    +          '!build/fake-module/src/polyfills/**/*.js',
    +          '!build/fake-polyfills/**/*.js',
    +      );
         } else {
    -      srcs.push('!src/polyfills.js');
    +      srcs.push('!src/polyfills.js', '!build/fake-polyfills/**/*.js',);
    +      unneededFiles.push('build/fake-module/src/polyfills.js');
    +    }
    +    unneededFiles.forEach(function(fake) {
    +      if (!fs.existsSync(fake)) {
    +        fs.writeFileSync(fake,
    +            '// Not needed in closure compiler\n' +
    +            'export function deadCode() {}');
    +      }
    +    });
    +
    +    let externs = baseExterns;
    +    if (options.externs) {
    +      externs = externs.concat(options.externs);
         }
    -    /*eslint "google-camelcase/google-camelcase": 0*/
    -    return gulp.src(srcs)
    -    .pipe(closureCompiler({
    +
    +    /* eslint "google-camelcase/google-camelcase": 0*/
    +    const compilerOptions = {
           // Temporary shipping with our own compiler that has a single patch
           // applied
    -      compilerPath: 'third_party/closure-compiler/compiler.jar',
    +      compilerPath: 'build-system/runner/dist/runner.jar',
           fileName: intermediateFilename,
    -      continueWithWarnings: true,
    -      tieredCompilation: true,  // Magic speed up.
    +      continueWithWarnings: false,
    +      tieredCompilation: true, // Magic speed up.
           compilerFlags: {
    -        // Custom compilation level. Trying to land this in the core
    -        // compiler.
    -        compilation_level: 'SIMPLE_PLUS_OPTIMIZATIONS',
    +        compilation_level: options.compilationLevel || 'SIMPLE_OPTIMIZATIONS',
    +        // Turns on more optimizations.
    +        assume_function_wrapper: true,
             // Transpile from ES6 to ES5.
             language_in: 'ECMASCRIPT6',
             language_out: 'ECMASCRIPT5',
    -        js_module_root: ['node_modules/', 'build/fake-module/'],
    -        common_js_entry_module: entryModuleFilename,
    +        // 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,
    +        js_module_root: [
    +          // Do _not_ include 'node_modules/' in js_module_root with 'NODE'
    +          // resolution or bad things will happen (#18600).
    +          'build/patched-module/',
    +          'build/fake-module/',
    +          'build/fake-polyfills/',
    +        ],
    +        entry_point: entryModuleFilenames,
    +        module_resolution: 'NODE',
             process_common_js_modules: true,
             // This strips all files from the input set that aren't explicitly
             // required.
             only_closure_dependencies: true,
             output_wrapper: wrapper,
             create_source_map: intermediateFilename + '.map',
    -        source_map_location_mapping: '|http://localhost:8000/',
    -        warning_level: process.env.TRAVIS ? 'QUIET' : 'DEFAULT',
    -      }
    -    }))
    -    .on('error', function(err) {
    -      console./*OK*/error(err.message);
    -      process.exit(1);
    -    })
    -    .pipe(rename(outputFilename))
    -    .pipe(replace(/\$internalRuntimeVersion\$/g, internalRuntimeVersion))
    -    .pipe(gulp.dest(outputDir))
    -    .on('end', function() {
    -      console./*OK*/log('Compiled ', entryModuleFilename, 'to',
    -          outputDir + '/' + outputFilename, 'via', intermediateFilename);
    -      gulp.src(intermediateFilename + '.map')
    -          .pipe(rename(outputFilename + '.map'))
    +        source_map_location_mapping:
    +            '|' + sourceMapBase,
    +        warning_level: 'DEFAULT',
    +        jscomp_error: [],
    +        // moduleLoad: Demote "module not found" errors to ignore missing files
    +        //     in type declarations in the swg.js bundle.
    +        jscomp_warning: ['moduleLoad'],
    +        // 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: hideWarningsFor,
    +      },
    +    };
    +
    +    // For now do type check separately
    +    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.push('TYPECHECK_ONLY=true');
    +      compilerOptions.compilerFlags.jscomp_error.push(
    +          'checkTypes',
    +          'accessControls',
    +          'const',
    +          'constantProperty',
    +          'globalThis');
    +      compilerOptions.compilerFlags.conformance_configs =
    +          'build-system/conformance-config.textproto';
    +    }
    +
    +    if (compilerOptions.compilerFlags.define.length == 0) {
    +      delete compilerOptions.compilerFlags.define;
    +    }
    +
    +    let stream = gulp.src(srcs)
    +        .pipe(closureCompiler(compilerOptions))
    +        .on('error', function(err) {
    +          const {message} = err;
    +          console./*OK*/error(colors.red(
    +              'Compiler issues for ' + outputFilename + ':\n') +
    +              formatClosureCompilerError(message));
    +          process.exit(1);
    +        });
    +    // If we're only doing type checking, no need to output the files.
    +    if (!argv.typecheck_only && !options.typeCheckOnly) {
    +      stream = stream
    +          .pipe(rename(outputFilename))
    +          .pipe(replace(/\$internalRuntimeVersion\$/g, internalRuntimeVersion, 'runtime-version'))
    +          .pipe(shortenLicense())
               .pipe(gulp.dest(outputDir))
    -          .on('end', resolve);
    -    });
    +          .on('end', function() {
    +            gulp.src(intermediateFilename + '.map')
    +                .pipe(rename(outputFilename + '.map'))
    +                .pipe(gulp.dest(outputDir))
    +                .on('end', resolve);
    +          });
    +    } else {
    +      stream = stream.on('end', resolve);
    +    }
    +    return stream;
       });
    -};
    +}
    diff --git a/build-system/tasks/create-golden-css/css/main.css b/build-system/tasks/create-golden-css/css/main.css
    new file mode 100644
    index 000000000000..a43034acbbdf
    --- /dev/null
    +++ b/build-system/tasks/create-golden-css/css/main.css
    @@ -0,0 +1,849 @@
    +/**
    + * 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.
    + */
    +
    +/**
    + * NOTE: This file exists to compare outputs of css file transformation
    + * that passes through postcss from version to version.
    + *
    + * built from sha: 0ca74e671
    + */
    +
    +/**
    + * 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.
    + */
    +html {
    +  overflow-x: hidden !important;
    +}
    +html, body {
    +  height: auto !important;
    +}
    +
    +html.i-amphtml-fie {
    +  height: 100% !important;
    +  width: 100% !important;
    +}
    +
    +/**
    + * Margin:0 is currently needed for iOS viewer embeds.
    + * See:
    + * https://github.com/ampproject/amphtml/blob/master/spec/amp-html-layout.md
    + */
    +body {
    +  margin: 0 !important;
    +}
    +
    +/** These properties can be overriden by user stylesheets. */
    +body {
    +  /* Text adjust is set to 100% to avoid iOS Safari bugs where the font-size is
    +     sometimes not restored during orientation. See #449. */
    +  text-size-adjust: 100%;
    +}
    +
    +[hidden] {
    +  display: none !important;
    +}
    +
    +/**
    + * 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;
    +}
    +
    +/**
    + * Override a user-supplied `body{overflow: visible; position:relative}`. This
    + * style is set in runtime vs css to avoid conflicts with ios-embedded mode
    + * and fixed transfer layer.
    + */
    +html.i-amphtml-singledoc > body,
    +html.i-amphtml-fie > body {
    +  overflow: visible !important;
    +  position: relative !important;
    +}
    +
    +/**
    + * Set `body {overflow-x: hidden}` for iOS WebView. This is b/c iOS WebView
    + * does NOT respect `html {overflow-x: hidden}`.
    + * Note! For all other natural cases body's style is `body {overflow: visible}`
    + * to avoid visibility issues with iframes.
    + */
    +html.i-amphtml-webview > body {
    +  overflow-x: hidden !important;
    +  overflow-y: visible !important;
    +}
    +
    +
    +/**
    + * iOS-Embed mode (iOS <= 8). The `body` itself is scrollable.
    + */
    +html.i-amphtml-ios-embed-legacy > body {
    +  overflow-x: hidden !important;
    +  overflow-y: auto !important;
    +  position: absolute !important;
    +}
    +
    +/**
    + * iOS-Embed Wrapper mode. The `body` is wrapped into `#i-amphtml-wrapper`
    + * element.
    + */
    +html.i-amphtml-ios-embed {
    +  overflow-y: auto !important;
    +  position: static;
    +}
    +
    +/** Wrapper for iOS Embed Wrapper mode. */
    +#i-amphtml-wrapper {
    +  overflow-x: hidden !important;
    +  overflow-y: auto !important;
    +  position: absolute !important;
    +  top: 0 !important;
    +  left: 0 !important;
    +  right: 0 !important;
    +  bottom: 0 !important;
    +  margin: 0 !important;
    +  display: block !important;
    +}
    +
    +/**
    + * Overflow scrolling is set separately on iOS, after DOMReady due to various
    + * rendering bugs. See #8798 for details.
    + */
    +html.i-amphtml-ios-embed.i-amphtml-ios-overscroll,
    +html.i-amphtml-ios-embed.i-amphtml-ios-overscroll > #i-amphtml-wrapper {
    +  -webkit-overflow-scrolling: touch !important;
    +}
    +
    +#i-amphtml-wrapper > body {
    +  /* Make sure position:absolute elements are positioned relative to the body,
    +     not i-amphtml-wrapper */
    +  position: relative !important;
    +  /* `body` must have a 1px transparent border for two purposes:
    +      (1) to cancel out margin collapse in body's children so that position
    +          absolute element is positioned correctly
    +      (2) to offset scroll adjustment to 1 to avoid scroll freeze problem. */
    +  border-top: 1px solid transparent !important;
    +}
    +
    +
    +.i-amphtml-element {
    +  display: inline-block;
    +}
    +
    +.i-amphtml-layout-nodisplay,
    +[layout=nodisplay]:not(.i-amphtml-layout-nodisplay)
    +{
    +  /* Display is set/reset in JS */
    +}
    +
    +.i-amphtml-layout-fixed,
    +[layout=fixed][width][height]:not(.i-amphtml-layout-fixed)
    +{
    +  display: inline-block;
    +  position: relative;
    +}
    +
    +.i-amphtml-layout-responsive,
    +[layout=responsive][width][height]:not(.i-amphtml-layout-responsive),
    +[width][height][sizes]:not(.i-amphtml-layout-responsive)
    +{
    +  display: block;
    +  position: relative;
    +}
    +
    +.i-amphtml-layout-fixed-height,
    +[layout=fixed-height][height]
    +{
    +  display: block;
    +  position: relative;
    +}
    +
    +.i-amphtml-layout-container,
    +[layout=container]
    +{
    +  display: block;
    +  position: relative;
    +}
    +
    +.i-amphtml-layout-fill,
    +[layout=fill]:not(.i-amphtml-layout-fill)
    +{
    +  display: block;
    +  overflow: hidden !important;
    +  position: absolute;
    +  top: 0;
    +  left: 0;
    +  bottom: 0;
    +  right: 0;
    +}
    +
    +.i-amphtml-layout-flex-item,
    +[layout=flex-item]:not(.i-amphtml-layout-flex-item)
    +{
    +  display: block;
    +  position: relative;
    +  flex: 1 1 auto;
    +}
    +
    +.i-amphtml-layout-size-defined {
    +  overflow: hidden !important;
    +}
    +
    +.i-amphtml-layout-awaiting-size {
    +  position: absolute !important;
    +  top: auto !important;
    +  bottom: auto !important;
    +}
    +
    +i-amphtml-sizer {
    +  display: block !important;
    +}
    +
    +.i-amphtml-fill-content {
    +  display: block;
    +  /* These lines are a work around to this issue in iOS:     */
    +  /* https://bugs.webkit.org/show_bug.cgi?id=155198          */
    +  /* And: https://github.com/ampproject/amphtml/issues/11133 */
    +  height: 0;
    +  max-height: 100%;
    +  max-width: 100%;
    +  min-height: 100%;
    +  min-width: 100%;
    +  width: 0;
    +  margin: auto;
    +}
    +
    +.i-amphtml-layout-size-defined .i-amphtml-fill-content {
    +  position: absolute;
    +  top: 0;
    +  left: 0;
    +  bottom: 0;
    +  right: 0;
    +}
    +
    +.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.
    + */
    +.i-amphtml-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 {
    +}
    +
    +.i-amphtml-unresolved {
    +  position: relative;
    +  overflow: hidden !important;
    +}
    +
    +.i-amphtml-scroll-disabled {
    +  overflow-x: hidden !important;
    +  overflow-y: hidden !important;
    +}
    +
    +#i-amphtml-wrapper.i-amphtml-scroll-disabled {
    +  overflow-x: hidden !important;
    +  overflow-y: hidden !important;
    +}
    +
    +/* "notbuild" classes are set as soon as an element is created and removed
    +   as soon as the element is built. */
    +
    +.amp-notbuilt {
    +  /* For author styling. */
    +}
    +
    +.i-amphtml-notbuilt,
    +[layout]:not(.i-amphtml-element)
    +{
    +  position: relative;
    +  overflow: hidden !important;
    +  color: transparent !important;
    +}
    +
    +.i-amphtml-notbuilt:not(.i-amphtml-layout-container) > * ,
    +[layout]:not([layout=container]):not(.i-amphtml-element) > *
    +{
    +  display: none;
    +}
    +
    +.i-amphtml-ghost {
    +  visibility: hidden !important;
    +}
    +
    +.i-amphtml-layout {
    +  /* Just state. */
    +}
    +
    +.i-amphtml-error {
    +  /* Just state. */
    +}
    +
    +/* layout=nodisplay are automatically hidden until displayed directly. */
    +[layout=nodisplay]:not(.i-amphtml-display) {
    +  display: none !important;
    +}
    +
    +.i-amphtml-element > [placeholder],
    +[layout]:not(.i-amphtml-element) > [placeholder] {
    +  display: block;
    +}
    +
    +.i-amphtml-element > [placeholder].hidden,
    +.i-amphtml-element > [placeholder].amp-hidden {
    +  visibility: hidden;
    +}
    +
    +.i-amphtml-element:not(.amp-notsupported) > [fallback] {
    +  display: none;
    +}
    +
    +.i-amphtml-layout-size-defined > [placeholder],
    +.i-amphtml-layout-size-defined > [fallback] {
    +  position: absolute !important;
    +  top: 0 !important;
    +  left: 0 !important;
    +  right: 0 !important;
    +  bottom: 0 !important;
    +  z-index: 1;
    +}
    +
    +.i-amphtml-notbuilt > [placeholder] {
    +  display: block !important;
    +}
    +
    +.i-amphtml-hidden-by-media-query {
    +  display: none !important;
    +}
    +
    +.i-amphtml-element-error {
    +  background: red !important;
    +  color: white !important;
    +  position: relative !important;
    +}
    +
    +.i-amphtml-element-error::before {
    +  content: attr(error-message);
    +}
    +
    +/**
    + * Wraps an element to make the contents scrollable. This is
    + * used to wrap iframes which in iOS always behave as if they had the
    + * seamless attribute.
    + * TODO(dvoytenko, #8464): cleanup old `i-amp-scroll-container`.
    + */
    +i-amphtml-scroll-container, i-amp-scroll-container {
    +  position: absolute;
    +  top: 0;
    +  left: 0;
    +  right: 0;
    +  bottom: 0;
    +  display: block;
    +}
    +
    +/**
    + * Overflow:auto will be set with delay due to iOS bug where iframe is not
    + * rendered.
    + * TODO(dvoytenko, #8464): cleanup old `i-amp-scroll-container`.
    + */
    +i-amphtml-scroll-container.amp-active, i-amp-scroll-container.amp-active {
    +  overflow: auto;
    +  -webkit-overflow-scrolling: touch;
    +}
    +
    +.i-amphtml-loading-container {
    +  display: block !important;
    +  z-index: 1;
    +}
    +
    +.i-amphtml-notbuilt > .i-amphtml-loading-container {
    +  display: block !important;
    +}
    +
    +/**
    + * `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.
    + */
    +.i-amphtml-loading-container.amp-hidden {
    +  visibility: hidden;
    +}
    +
    +.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(151, 151, 151, 0.65);
    +  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 4s ease infinite;
    +}
    +
    +.i-amphtml-loader {
    +  position: absolute;
    +  display: block;
    +  height: 10px;
    +  top: 50%;
    +  left: 50%;
    +  transform: translateX(-50%) translateY(-50%);
    +  transform-origin: 50% 50%;
    +  white-space: nowrap;
    +}
    +
    +.i-amphtml-loader.amp-active .i-amphtml-loader-dot {
    +  animation: i-amphtml-loader-dots 2s infinite;
    +}
    +
    +.i-amphtml-loader-dot {
    +  position: relative;
    +  display: inline-block;
    +
    +  height: 10px;
    +  width: 10px;
    +  margin: 2px;
    +  border-radius: 100%;
    +  background-color: rgba(0, 0, 0, .3);
    +
    +  box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, .2);
    +  will-change: transform;
    +}
    +
    +.i-amphtml-loader .i-amphtml-loader-dot:nth-child(1) {
    +  animation-delay: 0s;
    +}
    +
    +.i-amphtml-loader .i-amphtml-loader-dot:nth-child(2) {
    +  animation-delay: .1s;
    +}
    +
    +.i-amphtml-loader .i-amphtml-loader-dot:nth-child(3) {
    +  animation-delay: .2s;
    +}
    +
    +@keyframes i-amphtml-loader-dots {
    +  0%, 100% {
    +    transform: scale(.7);
    +    background-color: rgba(0, 0, 0, .3);
    +  }
    +
    +  50% {
    +    transform: scale(.8);
    +    background-color: rgba(0, 0, 0, .5);
    +  }
    +}
    +
    +
    +/**
    + * "overflow" element is an element shown when more content is available but
    + * not currently visible. Typically tapping on this element shows the full
    + * content.
    + */
    +.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;
    +}
    +
    +.i-amphtml-element > [overflow].amp-visible {
    +  visibility: visible;
    +}
    +
    +
    +/* Polyfill for IE and any other browser that don't understand templates. */
    +template {
    +  display: none !important;
    +}
    +
    +/**
    + * Authors can set this class on their html tags to provide `border-box` box-sizing
    + * to every element in their document. Individual elements can override as necessary.
    + */
    +.amp-border-box,
    +.amp-border-box *,
    +.amp-border-box *:before,
    +.amp-border-box *:after {
    +  box-sizing: border-box;
    +}
    +
    +/* amp-pixel is always non-displayed. */
    +amp-pixel {
    +  display: none !important;
    +}
    +
    +/**
    + * Instagram wraps the standard image into a fixed size container.
    + * With these offsets, users can simply specify the the size of the
    + * instagram images and things have the right size.
    + * In particular the effect of adding padding to this container is
    + * that with responsive layouts the responsiveness is based on the
    + * asset while the padding stays constant.
    + * This information is here instead of living with the CSS of the
    + * component, so that the runtime can reserve the correct space
    + * before the instagram implementation loads.
    + */
    +amp-instagram {
    +  padding: 64px 0px 0px 0px !important;
    +  background-color: white;
    +}
    +
    +
    +/**
    + * Analytics tags should never be visible. keep them hidden.
    + */
    +amp-analytics {
    +  /* Fixed to make position independent of page other elements. */
    +  position: fixed !important;
    +  top: 0 !important;
    +  width: 1px !important;
    +  height: 1px !important;
    +  overflow: hidden !important;
    +  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
    + * can be provided before AMP Access JS has been loaded.
    + */
    +[amp-access][amp-access-hide] {
    +  display: none;
    +}
    +
    +
    +/**
    + * Forms error/success messaging containers should be hidden at first.
    + */
    +form [submitting],
    +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;
    +}
    +
    +.i-amphtml-jank-meter {
    +  position: fixed;
    +  background-color: rgba(232,72,95,.5);
    +  bottom: 0;
    +  right: 0;
    +  color: #fff;
    +  font-size: 16px;
    +  z-index: 1000;
    +  padding: 5px;
    +}
    +
    +/**
    + * Animated equalizer icon for muted autoplaying videos.
    + */
    +i-amphtml-video-mask, i-amp-video-mask {
    +  z-index: 1;
    +}
    +.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);}
    +}
    +
    +
    +/**
    + * Video Docking
    + */
    +
    +.i-amphtml-dockable-video {
    +  padding: 0px;
    +  margin:0px;
    +  transition: background-color 1s;
    +}
    +
    +.i-amphtml-dockable-video > video.i-amphtml-dockable-video-minimizing,
    +.i-amphtml-dockable-video > iframe.i-amphtml-dockable-video-minimizing {
    +  position: fixed;
    +  height: auto;
    +  overflow: hidden;
    +  z-index: 16;
    +  will-change: transform;
    +  transform: scale(0.6) translateX(20px) translateY(20px);
    +  border-radius: 6px;
    +  box-shadow: 0px 3px 9px 3px rgba(0, 0, 0, 0.1);
    +  transition: box-shadow 1s, border-radius 1s;
    +  /* Override fill-content */
    +  min-width: initial !important;
    +  min-height: initial !important;
    +  margin: initial !important;
    +}
    +
    +/**
    + * amp-accordion to avoid FOUC.
    + */
    +
    +/* Non-overridable properties */
    +amp-accordion {
    +  display: block !important;
    +}
    +
    +/* Make sections non-floatable */
    +amp-accordion > section {
    +  float: none !important;
    +}
    +
    +/*  Display the first 2 elements (heading and content) */
    +amp-accordion > section > * {
    +  float: none !important;
    +  display: block !important;
    +  overflow: hidden !important; /* clearfix */
    +  position: relative !important;
    +}
    +
    +amp-accordion,
    +amp-accordion > section,
    +.i-amphtml-accordion-header,
    +.i-amphtml-accordion-content {
    +  margin: 0;
    +}
    +
    +/* heading element*/
    +.i-amphtml-accordion-header {
    +  cursor: pointer;
    +  background-color: #efefef;
    +  padding-right: 20px;
    +  border: solid 1px #dfdfdf;
    +}
    +
    +/* Collapse content by default. */
    +amp-accordion > section > :last-child {
    +  display: none !important;
    +}
    +
    +/* Expand content when needed. */
    +amp-accordion > section[expanded] > :last-child {
    +  display: block !important;
    +}
    +
    +/**
    + * amp-story
    + */
    + amp-story, amp-story-page {
    +  display: block;
    +  height: 100vh;
    +  margin: 0;
    +  padding: 0;
    +  overflow: hidden;
    +  width: 100%;
    +}
    +
    +amp-story-page {
    +  background-color: #757575;
    +}
    +
    +amp-date-picker {
    +  display: block;
    +}
    +
    +amp-date-picker:not([i-amphtml-date-picker-attached])  {
    +  display: flex;
    +  width: 200px;
    +  height: 50px;
    +  border: 1px solid #dbdbdb;
    +}
    +
    +amp-date-picker[type="range"]:not([i-amphtml-date-picker-attached]) {
    +  width: 400px;
    +}
    +
    +amp-date-picker[i-amphtml-date-picker-attached] [amp-date-placeholder],
    +amp-date-picker[type="range"][i-amphtml-date-picker-attached] [amp-date-placeholder-start],
    +amp-date-picker[type="range"][i-amphtml-date-picker-attached] [amp-date-placeholder-end] {
    +  display: none;
    +}
    +
    +amp-date-picker [amp-date-placeholder],
    +amp-date-picker[type="range"] [amp-date-placeholder-start],
    +amp-date-picker[type="range"] [amp-date-placeholder-end] {
    +  border: 0;
    +  font-weight: 200;
    +  font-size: 18px;
    +  line-height: 24px;
    +  color: #333;
    +  font-family: serif;
    +  padding: 4px 8px;
    +  width: 100%;
    +  height: 100%;
    +  box-sizing: border-box;
    +  white-space: nowrap;
    +  overflow: hidden;
    +}
    +
    +amp-date [amp-date-placeholder]::placeholder,
    +amp-date-picker[type="range"] [amp-date-placeholder-start]::placeholder,
    +amp-date-picker[type="range"] [amp-date-placeholder-end]::placeholder {
    +  color: #757575;
    +}
    diff --git a/build-system/tasks/create-golden-css/index.js b/build-system/tasks/create-golden-css/index.js
    new file mode 100644
    index 000000000000..b1ebc3619198
    --- /dev/null
    +++ b/build-system/tasks/create-golden-css/index.js
    @@ -0,0 +1,31 @@
    +/**
    + * 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.
    + */
    +'use strict';
    +const fs = require('fs-extra');
    +const gulp = require('gulp-help')(require('gulp'));
    +const {transformCss} = require('../jsify-css');
    +
    +function main() {
    +  return transformCss('./build-system/tasks/create-golden-css/css/main.css', {
    +    normalizeWhitespace: false,
    +    discardComments: false,
    +  }).then(function(result) {
    +    fs.writeFileSync('./test/golden-files/main.css', result);
    +  });
    +}
    +
    +gulp.task('create-golden-css', 'Creates a golden file for untransformed css',
    +    main);
    diff --git a/build-system/tasks/create-module-compatible-es5-bundle.js b/build-system/tasks/create-module-compatible-es5-bundle.js
    new file mode 100644
    index 000000000000..ce222873d327
    --- /dev/null
    +++ b/build-system/tasks/create-module-compatible-es5-bundle.js
    @@ -0,0 +1,37 @@
    +/**
    + * Copyright 2018 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.
    + */
    +
    +const $$ = require('gulp-load-plugins')();
    +const gulp = $$.help(require('gulp'));
    +
    +/* Copy source to source-nomodule.js and
    + * make it compatible with `
    +  
    +
    +
    +  <${name} layout="responsive" width="150" height="80">
    +
    +
    +`;
    +}
    +
    +function makeExtension() {
    +  if (!argv.name) {
    +    log(colors.red(
    +        'Error! Please pass in the "--name" flag with a value'));
    +  }
    +  const {name} = argv;
    +  const examplesFile = getExamplesFile(name);
    +
    +  fs.mkdirpSync(`extensions/${name}/0.1/test`);
    +  fs.writeFileSync(`extensions/${name}/${name}.md`,
    +      getMarkdownExtensionFile(name));
    +  fs.writeFileSync(`extensions/${name}/validator-${name}.protoascii`,
    +      getValidatorFile(name));
    +  fs.writeFileSync(`extensions/${name}/0.1/${name}.js`,
    +      getJsExtensionFile(name));
    +  fs.writeFileSync(`extensions/${name}/0.1/test/test-${name}.js`,
    +      getJsTestExtensionFile(name));
    +  fs.writeFileSync(`extensions/${name}/0.1/test/validator-${name}.html`,
    +      examplesFile);
    +
    +  const examplesFileValidatorOut = examplesFile.trim().split('\n')
    +      .map(line => `|  ${line}`)
    +      .join('\n');
    +
    +  fs.writeFileSync(`extensions/${name}/0.1/test/validator-${name}.out`,
    +      ['PASS', examplesFileValidatorOut].join('\n'));
    +
    +  fs.writeFileSync(`examples/${name}.amp.html`,
    +      examplesFile);
    +}
    +
    +gulp.task('make-extension', 'Create an extension skeleton', makeExtension, {
    +  options: {
    +    name: '  The name of the extension. Preferable prefixed with `amp-*`',
    +  },
    +});
    diff --git a/build-system/tasks/firebase.js b/build-system/tasks/firebase.js
    new file mode 100644
    index 000000000000..405b47108de1
    --- /dev/null
    +++ b/build-system/tasks/firebase.js
    @@ -0,0 +1,110 @@
    +/**
    + * Copyright 2018 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.
    + */
    +const argv = require('minimist')(process.argv.slice(2));
    +const colors = require('ansi-colors');
    +const fs = require('fs-extra');
    +const gulp = require('gulp-help')(require('gulp'));
    +const log = require('fancy-log');
    +const path = require('path');
    +
    +async function walk(dest) {
    +  const filelist = [];
    +  const files = await fs.readdir(dest);
    +
    +  for (let i = 0; i < files.length; i++) {
    +    const file = `${dest}/${files[i]}`;
    +
    +    fs.statSync(file).isDirectory() ?
    +      Array.prototype.push.apply(filelist, await walk(file)) :
    +      filelist.push(file);
    +  }
    +
    +  return filelist;
    +}
    +
    +async function copyAndReplaceUrls(src, dest) {
    +  await fs.copy(src, dest, {overwrite: true});
    +  // Recursively gets all the files within the directory and its children.
    +  const files = await walk(dest);
    +  const promises = files.filter(fileName => path.extname(fileName) == '.html')
    +      .map(file => replaceUrls(file));
    +  await Promise.all(promises);
    +}
    +
    +async function modifyThirdPartyUrl() {
    +  const filePath = 'firebase/dist/amp.js';
    +  const data = await fs.readFile('firebase/dist/amp.js', 'utf8');
    +  const result = data.replace(
    +      'self.AMP_CONFIG={',
    +      'self.AMP_CONFIG={"thirdPartyUrl":location.origin,');
    +  await fs.writeFile(filePath, result, 'utf8');
    +}
    +
    +async function generateFirebaseFolder() {
    +  await fs.mkdirp('firebase');
    +  if (argv.file) {
    +    log(colors.green(`Processing file: ${argv.file}.`));
    +    log(colors.green('Writing file to firebase.index.html.'));
    +    await fs.copyFile(/*src*/ argv.file, 'firebase/index.html',
    +        {overwrite: true});
    +    await replaceUrls('firebase/index.html');
    +  } else {
    +    await Promise.all([
    +      copyAndReplaceUrls('test/manual', 'firebase/manual'),
    +      copyAndReplaceUrls('examples', 'firebase/examples'),
    +    ]);
    +  }
    +  log(colors.green('Copying local amp files from dist folder.'));
    +  await Promise.all([
    +    fs.copy('dist', 'firebase/dist', {overwrite: true}),
    +    fs.copy('dist.3p/current', 'firebase/dist.3p/current', {overwrite: true}),
    +  ]);
    +  await Promise.all([
    +    modifyThirdPartyUrl(),
    +    fs.copyFile('firebase/dist/ww.max.js', 'firebase/dist/ww.js',
    +        {overwrite: true}),
    +  ]);
    +}
    +
    +async function replaceUrls(filePath) {
    +  const data = await fs.readFile(filePath, 'utf8');
    +  let result = data.replace(/https:\/\/cdn\.ampproject\.org\/v0\.js/g, '/dist/amp.js');
    +  if (argv.min) {
    +    result = result.replace(/https:\/\/cdn\.ampproject\.org\/v0\/(.+?).js/g, '/dist/v0/$1.js');
    +  } else {
    +    result = result.replace(/https:\/\/cdn\.ampproject\.org\/v0\/(.+?).js/g, '/dist/v0/$1.max.js');
    +  }
    +  await fs.writeFile(filePath, result, 'utf8');
    +}
    +
    +const tasks = [];
    +
    +if (!argv.nobuild) {
    +  tasks.push(argv.min ? 'dist' : 'build');
    +}
    +
    +gulp.task(
    +    'firebase',
    +    'Generates firebase folder for deployment',
    +    tasks,
    +    generateFirebaseFolder,
    +    {
    +      options: {
    +        'file': 'File to deploy to firebase as index.html',
    +        'min': 'Source from minified files',
    +        'nobuild': 'Skips the gulp build|dist step.',
    +      },
    +    });
    diff --git a/build-system/tasks/gen-codeowners/index.js b/build-system/tasks/gen-codeowners/index.js
    new file mode 100644
    index 000000000000..217e1ef199af
    --- /dev/null
    +++ b/build-system/tasks/gen-codeowners/index.js
    @@ -0,0 +1,110 @@
    +/**
    + * 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.
    + */
    +'use strict';
    +const BBPromise = require('bluebird');
    +const fs = BBPromise.promisifyAll(require('fs-extra'));
    +const gulp = require('gulp-help')(require('gulp'));
    +const intercept = require('gulp-intercept');
    +const log = require('fancy-log');
    +const minimist = require('minimist');
    +const path = require('path');
    +const yaml = require('yamljs');
    +
    +const argv = minimist(process.argv.slice(2));
    +
    +/**
    + * @param {!Object>} dirs
    + * @return {string}
    + */
    +function buildCodeownersFile(dirs) {
    +  const dirpaths = Object.keys(dirs);
    +  let codeowners = '';
    +  dirpaths.forEach(function(dirpath) {
    +    codeowners += `${dirpath === '*' ? dirpath : `${dirpath}/`} `;
    +    dirs[dirpath].forEach(function(item, i, arr) {
    +      if (typeof item === 'string') {
    +        // Allow leading `@` to be optional
    +        codeowners += item.indexOf('@') !== 0 ? `@${item}` : item;
    +        const nextItem = arr[i + 1];
    +        // Look ahead if we need to add a space
    +        if (nextItem && typeof nextItem === 'string') {
    +          codeowners += ' ';
    +        }
    +      } else {
    +        codeowners += '\n';
    +        // The entry is going to be an object where the key is
    +        // the username and the array values are paths to file
    +        // rooted to the current directory (`dirpath`).
    +        // ex.
    +        // ```yaml
    +        // - ampproject/somegroup
    +        //   - some.js
    +        // ```
    +        //
    +        // gets turned into:
    +        //
    +        // ```js
    +        // {'ampproject/somegroup': ['some.js']}
    +        // ```
    +        const subItemUsername = Object.keys(item)[0];
    +        const username = subItemUsername.indexOf('@') !== 0 ?
    +          `@${subItemUsername}` : subItemUsername;
    +        item[subItemUsername].forEach(function(pattern) {
    +          codeowners += `${dirpath === '*' ? pattern :
    +            `${dirpath}/${pattern}`} ${username}`;
    +        });
    +      }
    +    });
    +    codeowners += '\n';
    +  });
    +  return codeowners;
    +}
    +
    +/**
    + * @param {string} root
    + * @param {string} target
    + * @param {boolean} writeToDisk
    + * @return {!Stream}
    + */
    +function generate(root, target, writeToDisk) {
    +  // Allow flags to override values.
    +  root = argv.root || root;
    +  target = argv.target || target;
    +  writeToDisk = argv.writeToDisk || writeToDisk;
    +
    +  const dirs = Object.create(null);
    +  return gulp.src(`${root}/**/OWNERS.yaml`)
    +      .pipe(intercept(function(file) {
    +        const dirname = path.relative(process.cwd(), path.dirname(file.path));
    +        dirs[dirname || '*'] = yaml.parse(file.contents.toString());
    +      }))
    +      .on('end', function() {
    +        if (writeToDisk) {
    +          fs.removeSync(target);
    +          const codeowners = buildCodeownersFile(dirs);
    +          fs.writeFileSync(target, codeowners);
    +        } else {
    +          const codeowners = buildCodeownersFile(dirs);
    +          log(codeowners);
    +        }
    +      });
    +}
    +
    +gulp.task('gen-codeowners', 'Create CODEOWNERS file from OWNERS.yaml files',
    +    generate.bind(null, process.cwd(), './CODEOWNERS', true));
    +
    +exports.generate = generate;
    +exports.buildCodeownersFile = buildCodeownersFile;
    diff --git a/build-system/tasks/gen-codeowners/test.js b/build-system/tasks/gen-codeowners/test.js
    new file mode 100644
    index 000000000000..703db842accf
    --- /dev/null
    +++ b/build-system/tasks/gen-codeowners/test.js
    @@ -0,0 +1,67 @@
    +/**
    + * 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.
    + */
    +'use strict';
    +
    +const BBPromise = require('bluebird');
    +const colors = require('ansi-colors');
    +const exec = require('child_process').execSync;
    +const fs = BBPromise.promisifyAll(require('fs-extra'));
    +const log = require('fancy-log');
    +const m = require('./');
    +const test = require('ava');
    +
    +test('sync - build out correct CODEOWNERS', t => {
    +  const owners = {
    +    '*': [
    +      'username1',
    +      '@username2',
    +      {'ampproject/group': ['*.protoascii']},
    +    ],
    +    ads: ['username3', '@username1'],
    +    'some/deeply/nested/dir': ['username5', {'ampproject/group2': ['some.js']}],
    +  };
    +  t.plan(1);
    +  const result = m.buildCodeownersFile(owners);
    +  const expected = `* @username1 @username2
    +*.protoascii @ampproject/group
    +ads/ @username3 @username1
    +some/deeply/nested/dir/ @username5
    +some/deeply/nested/dir/some.js @ampproject/group2
    +`;
    +  t.is(expected, result);
    +});
    +
    +// TODO(erwinm, #11042): remove skip when we need to enforce sync
    +test.skip('CODEOWNERS must be in sync with OWNERS.yaml', t => {
    +  t.plan(1);
    +  const tmppath = '/tmp/amphtml/CODEOWNERS';
    +  fs.ensureDirSync('/tmp/amphtml');
    +  // Run through exec instead of directly invoking `generate` through
    +  // the module import since this changes the `process.cwd` to be
    +  // ../gen-codeowners
    +  exec(`gulp gen-codeowners --target ${tmppath}`);
    +  const realFile = fs.readFileSync(`${process.cwd()}/../../../CODEOWNERS`);
    +  const testFile = fs.readFileSync(tmppath);
    +  const isInSync = testFile.toString() === realFile.toString();
    +  t.true(isInSync,
    +      'CODEOWNERS is out of sync. Please re-generate CODEOWNERS by ' +
    +      'running `gulp gen-codeowners`');
    +  if (!isInSync) {
    +    log(colors.red('CODEOWNERS is out of sync. Please re-generate ' +
    +        'CODEOWNERS by running `gulp gen-codeowners`'));
    +  }
    +  fs.removeSync(tmppath);
    +});
    diff --git a/build-system/tasks/get-zindex/index.js b/build-system/tasks/get-zindex/index.js
    new file mode 100644
    index 000000000000..a0fcb186be1a
    --- /dev/null
    +++ b/build-system/tasks/get-zindex/index.js
    @@ -0,0 +1,139 @@
    +/**
    + * 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';
    +
    +
    +const fs = require('fs');
    +const gulp = require('gulp-help')(require('gulp'));
    +const PluginError = require('plugin-error');
    +const postcss = require('postcss');
    +const table = require('text-table');
    +const through = require('through2');
    +
    +const tableHeaders = [
    +  ['selector', 'z-index', 'file'],
    +  ['---', '---', '---'],
    +];
    +
    +const 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
    +      let 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 PluginError('size', 'Stream not supported'));
    +    return;
    +  }
    +
    +  const selectors = Object.create(null);
    +
    +  postcss([zIndexCollector.bind(null, selectors)])
    +      .process(file.contents.toString(), {
    +        from: file.relative,
    +      }).then(() => {
    +        cb(null, {name: file.relative, selectors});
    +      });
    +}
    +
    +/**
    + * @param {!Object} filesData
    + *    accumulation of files and the rules and z index values.
    + * @return {!Array>}
    + */
    +function createTable(filesData) {
    +  const rows = [];
    +  Object.keys(filesData).sort().forEach(fileName => {
    +    const selectors = filesData[fileName];
    +    Object.keys(selectors).sort().forEach(selectorName => {
    +      const zIndex = selectors[selectorName];
    +      const row = [selectorName, zIndex, fileName];
    +      rows.push(row);
    +    });
    +  });
    +  rows.sort((a, b) => {
    +    const aZIndex = parseInt(a[1], 10);
    +    const bZIndex = parseInt(b[1], 10);
    +    return aZIndex - bZIndex;
    +  });
    +  return rows;
    +}
    +
    +
    +/**
    + * @param {string} glob
    + * @return {!Stream}
    + */
    +function getZindex(glob) {
    +  return gulp.src(glob).pipe(through.obj(onFileThrough));
    +}
    +
    +/**
    + * @param {function()} cb
    + */
    +function getZindexForAmp(cb) {
    +  const 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', () => {
    +        const rows = createTable(filesData);
    +        rows.unshift.apply(rows, tableHeaders);
    +        const 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..72678cb28dbc
    --- /dev/null
    +++ b/build-system/tasks/get-zindex/test.js
    @@ -0,0 +1,56 @@
    +/**
    + * 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';
    +
    +
    +const m = require('./');
    +const test = require('ava');
    +
    +const result = {
    +  'test.css': {
    +    '.selector-1': '1',
    +    '.selector-2': '0',
    +    '.selector-3': '99',
    +  },
    +  'test-2.css': {
    +    '.selector-4': '80',
    +  },
    +};
    +
    +test.cb('collects selectors', t => {
    +  const data = Object.create(null);
    +  const testFiles = `${__dirname}/*.css`;
    +  m.getZindex(testFiles)
    +      .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);
    +  const table = m.createTable(result);
    +  const 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 278e9f08ea2b..4c0e703641b1 100644
    --- a/build-system/tasks/index.js
    +++ b/build-system/tasks/index.js
    @@ -13,16 +13,34 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +'use strict';
     
    -require('./babel-helpers');
    +require('./ava');
    +require('./bundle-size');
     require('./changelog');
    +require('./check-links');
     require('./clean');
     require('./compile');
    -require('./compile-access-expr');
    +require('./compile-expr');
    +require('./create-golden-css');
    +require('./csvify-size');
    +require('./dep-check');
    +require('./firebase');
    +require('./get-zindex');
    +require('./gen-codeowners');
    +require('./extension-generator');
    +require('./pr-check');
    +require('./process-github-issues');
    +require('./process-3p-github-pr');
    +require('./json-check');
     require('./lint');
    -require('./make-golden');
    +require('./prepend-global');
     require('./presubmit-checks');
    +require('./release-tagging');
    +require('./runtime-test');
     require('./serve');
     require('./size');
    -require('./test');
    +require('./todos');
    +require('./update-packages');
     require('./validator');
    +require('./visual-diff');
    diff --git a/build-system/tasks/jsify-css.js b/build-system/tasks/jsify-css.js
    new file mode 100644
    index 000000000000..113d0820c0dd
    --- /dev/null
    +++ b/build-system/tasks/jsify-css.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.
    + */
    +'use strict';
    +
    +
    +const autoprefixer = require('autoprefixer');
    +const colors = require('ansi-colors');
    +const cssnano = require('cssnano');
    +const fs = require('fs-extra');
    +const log = require('fancy-log');
    +const postcss = require('postcss');
    +const postcssImport = require('postcss-import');
    +
    +// NOTE: see https://github.com/ai/browserslist#queries for `browsers` list
    +const cssprefixer = autoprefixer({
    +  browsers: [
    +    'last 5 ChromeAndroid versions',
    +    'last 5 iOS versions',
    +    'last 3 FirefoxAndroid versions',
    +    'last 5 Android versions',
    +    'last 2 ExplorerMobile versions',
    +    'last 2 OperaMobile versions',
    +    'last 2 OperaMini versions',
    +  ],
    +});
    +
    +const cssNanoDefaultOptions = {
    +  autoprefixer: false,
    +  convertValues: false,
    +  discardUnused: false,
    +  cssDeclarationSorter: false,
    +  // `mergeIdents` this is only unsafe if you rely on those animation names in
    +  // JavaScript.
    +  mergeIdents: true,
    +  reduceIdents: false,
    +  reduceInitial: false,
    +  zindex: false,
    +  svgo: {
    +    encode: true,
    +  },
    +};
    +
    +/**
    + * Css transformations to target file using postcss.
    +
    + * @param {string} filename css file
    + * @param {!Object=} opt_cssnano cssnano options
    + * @return {!Promise} that resolves with the css content after
    + *    processing
    + */
    +const transformCss = exports.transformCss = function(filename, opt_cssnano) {
    +  opt_cssnano = opt_cssnano || Object.create(null);
    +  // See http://cssnano.co/optimisations/ for full list.
    +  // We try and turn off any optimization that is marked unsafe.
    +  const cssnanoOptions = Object.assign(Object.create(null),
    +      cssNanoDefaultOptions, opt_cssnano);
    +  const cssnanoTransformer = cssnano({preset: ['default', cssnanoOptions]});
    +
    +  const css = fs.readFileSync(filename, 'utf8');
    +  const transformers = [postcssImport, cssprefixer, cssnanoTransformer];
    +  return postcss(transformers).process(css.toString(), {
    +    'from': filename,
    +  });
    +};
    +
    +/**
    + * 'Jsify' a CSS file - Adds vendor specific css prefixes to the css file,
    + * compresses the file, removes the copyright comment, and adds the sourceURL
    + * to the stylesheet
    + *
    + * @param {string} filename css file
    + * @return {!Promise} that resolves with the css content after
    + *    processing
    + */
    +exports.jsifyCssAsync = function(filename) {
    +  return transformCss(filename).then(function(result) {
    +    result.warnings().forEach(function(warn) {
    +      log(colors.red(warn.toString()));
    +    });
    +    const {css} = result;
    +    return css + '\n/*# sourceURL=/' + filename + '*/';
    +  });
    +};
    diff --git a/build-system/tasks/json-check.js b/build-system/tasks/json-check.js
    new file mode 100644
    index 000000000000..ae996aaa9773
    --- /dev/null
    +++ b/build-system/tasks/json-check.js
    @@ -0,0 +1,81 @@
    +/**
    + * 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.
    + */
    +'use strict';
    +
    +const colors = require('ansi-colors');
    +const gulp = require('gulp-help')(require('gulp'));
    +const log = require('fancy-log');
    +const through2 = require('through2');
    +const {jsonGlobs} = require('../config');
    +
    +const expectedCaches = ['cloudflare', 'google'];
    +
    +/**
    + * Fail if caches.json is missing some expected caches.
    + */
    +function checkCachesJson() {
    +  return gulp.src(['caches.json'])
    +      .pipe(through2.obj(function(file) {
    +        let obj;
    +        try {
    +          obj = JSON.parse(file.contents.toString());
    +        } catch (e) {
    +          log(colors.yellow('Could not parse caches.json. '
    +                + 'This is most likely a fatal error that '
    +                + 'will be found by checkValidJson'));
    +          return;
    +        }
    +        const foundCaches = [];
    +        for (const foundCache of obj.caches) {
    +          foundCaches.push(foundCache.id);
    +        }
    +        for (const cache of expectedCaches) {
    +          if (!foundCaches.includes(cache)) {
    +            log(colors.red('Missing expected cache "'
    +                  + cache + '" in caches.json'));
    +            process.exitCode = 1;
    +          }
    +        }
    +      }));
    +}
    +
    +/**
    + * Fail if JSON files are valid.
    + */
    +function checkValidJson() {
    +  let hasError = false;
    +  return gulp.src(jsonGlobs)
    +      .pipe(through2.obj(function(file) {
    +        try {
    +          JSON.parse(file.contents.toString());
    +        } catch (e) {
    +          log(colors.red('Invalid JSON in '
    +              + file.relative + ': ' + e.message));
    +          hasError = true;
    +        }
    +      }))
    +      .on('end', function() {
    +        if (hasError) {
    +          process.exit(1);
    +        }
    +      });
    +}
    +
    +gulp.task('caches-json', 'Check that some expected caches are included.',
    +    checkCachesJson);
    +
    +gulp.task(
    +    'json-syntax', 'Check that JSON files are valid JSON.', checkValidJson);
    diff --git a/build-system/tasks/karma.conf.js b/build-system/tasks/karma.conf.js
    new file mode 100644
    index 000000000000..ce6a873b317c
    --- /dev/null
    +++ b/build-system/tasks/karma.conf.js
    @@ -0,0 +1,270 @@
    +/**
    + * 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.
    + */
    +'use strict';
    +
    +const COMMON_CHROME_FLAGS = [
    +  // Dramatically speeds up iframe creation time.
    +  '--disable-extensions',
    +  // Allows simulating user actions (e.g unmute) which otherwise will be denied.
    +  '--autoplay-policy=no-user-gesture-required',
    +];
    +
    +// Reduces the odds of Sauce labs timing out during tests. See #16135.
    +// Reference: https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts
    +const SAUCE_TIMEOUT_CONFIG = {
    +  maxDuration: 10 * 60,
    +  commandTimeout: 10 * 60,
    +  idleTimeout: 5 * 60,
    +};
    +
    +const preprocessors = ['browserify'];
    +
    +/**
    + * @param {!Object} config
    + */
    +module.exports = {
    +  frameworks: [
    +    'fixture',
    +    'browserify',
    +    'mocha',
    +    'sinon-chai',
    +    'chai',
    +    'source-map-support',
    +  ],
    +
    +  preprocessors: {
    +    './test/fixtures/*.html': ['html2js'],
    +    './test/**/*.js': preprocessors,
    +    './ads/**/test/test-*.js': preprocessors,
    +    './extensions/**/test/**/*.js': preprocessors,
    +    './testing/**/*.js': preprocessors,
    +  },
    +
    +  // TODO(rsimha, #15510): Sauce labs on Safari doesn't reliably support
    +  // 'localhost' addresses. See #14848 for more info.
    +  // Details: https://support.saucelabs.com/hc/en-us/articles/115010079868
    +  hostname: 'localhost',
    +
    +  browserify: {
    +    watch: true,
    +    debug: true,
    +    basedir: __dirname + '/../../',
    +    transform: [
    +      ['babelify', {'sourceMapsAbsolute': true}],
    +    ],
    +    // Prevent "cannot find module" errors on Travis. See #14166.
    +    bundleDelay: process.env.TRAVIS ? 5000 : 1200,
    +  },
    +
    +  reporters: ['super-dots', 'karmaSimpleReporter'],
    +
    +  superDotsReporter: {
    +    nbDotsPerLine: 100000,
    +    color: {
    +      success: 'green',
    +      failure: 'red',
    +      ignore: 'yellow',
    +    },
    +    icon: {
    +      success: '●',
    +      failure: '●',
    +      ignore: '○',
    +    },
    +  },
    +
    +  specReporter: {
    +    suppressPassed: true,
    +    suppressSkipped: true,
    +    suppressFailed: false,
    +    suppressErrorSummary: true,
    +    maxLogLines: 20,
    +  },
    +
    +  mochaReporter: {
    +    output: 'full',
    +    colors: {
    +      success: 'green',
    +      error: 'red',
    +      info: 'yellow',
    +    },
    +    symbols: {
    +      success: '●',
    +      error: '●',
    +      info: '○',
    +    },
    +  },
    +
    +  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 the Karma constant config.LOG_ERROR, so we hard code it here.
    +  // Hopefully it'll never change.
    +  logLevel: 'ERROR',
    +
    +  autoWatch: true,
    +
    +  browsers: [
    +    process.env.TRAVIS ? 'Chrome_travis_ci' : 'Chrome_no_extensions',
    +  ],
    +
    +  // Number of sauce platforms to start in parallel
    +  concurrency: 4,
    +
    +  customLaunchers: {
    +    /* eslint "google-camelcase/google-camelcase": 0*/
    +    Chrome_travis_ci: {
    +      base: 'Chrome',
    +      flags: ['--no-sandbox'].concat(COMMON_CHROME_FLAGS),
    +    },
    +    Chrome_no_extensions: {
    +      base: 'Chrome',
    +      flags: COMMON_CHROME_FLAGS,
    +    },
    +    Chrome_no_extensions_headless: {
    +      base: 'ChromeHeadless',
    +      flags: ['--no-sandbox'].concat(COMMON_CHROME_FLAGS),
    +    },
    +    // SauceLabs configurations.
    +    // New configurations can be created here:
    +    // https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/
    +    SL_Chrome_67: Object.assign({
    +      base: 'SauceLabs',
    +      browserName: 'chrome',
    +      platform: 'Windows 10',
    +      version: '67.0',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_Chrome_Android_7: Object.assign({
    +      base: 'SauceLabs',
    +      appiumVersion: '1.8.1',
    +      deviceName: 'Android GoogleAPI Emulator',
    +      browserName: 'Chrome',
    +      platformName: 'Android',
    +      platformVersion: '7.1',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_Chrome_45: Object.assign({
    +      base: 'SauceLabs',
    +      browserName: 'chrome',
    +      platform: 'Windows 8',
    +      version: '45.0',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_Android_6: Object.assign({
    +      base: 'SauceLabs',
    +      appiumVersion: '1.8.1',
    +      deviceName: 'Android Emulator',
    +      browserName: 'Chrome',
    +      platformName: 'Android',
    +      platformVersion: '6.0',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_iOS_11: Object.assign({
    +      base: 'SauceLabs',
    +      appiumVersion: '1.8.1',
    +      deviceName: 'iPhone X Simulator',
    +      browserName: 'Safari',
    +      platformName: 'iOS',
    +      platformVersion: '11.3',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_Firefox_61: Object.assign({
    +      base: 'SauceLabs',
    +      browserName: 'firefox',
    +      platform: 'Windows 10',
    +      version: '61.0',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_Safari_11: Object.assign({
    +      base: 'SauceLabs',
    +      browserName: 'safari',
    +      platform: 'macOS 10.13',
    +      version: '11.1',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_Edge_17: Object.assign({
    +      base: 'SauceLabs',
    +      browserName: 'MicrosoftEdge',
    +      platform: 'Windows 10',
    +      version: '17.17134',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +    SL_IE_11: Object.assign({
    +      base: 'SauceLabs',
    +      browserName: 'internet explorer',
    +      platform: 'Windows 10',
    +      version: '11.103',
    +    }, SAUCE_TIMEOUT_CONFIG),
    +  },
    +
    +  sauceLabs: {
    +    testName: 'AMP HTML on Sauce',
    +    tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER,
    +    startConnect: false,
    +    connectOptions: {
    +      noSslBumpDomains: 'all',
    +    },
    +  },
    +
    +  client: {
    +    mocha: {
    +      reporter: 'html',
    +      // Longer timeout on Travis; fail quickly at local.
    +      timeout: process.env.TRAVIS ? 10000 : 2000,
    +    },
    +    captureConsole: true,
    +    verboseLogging: false,
    +  },
    +
    +  singleRun: true,
    +  browserDisconnectTimeout: 10000,
    +  browserNoActivityTimeout: 4 * 60 * 1000,
    +  captureTimeout: 4 * 60 * 1000,
    +  failOnEmptyTestSuite: false,
    +
    +  // IF YOU CHANGE THIS, DEBUGGING WILL RANDOMLY KILL THE BROWSER
    +  browserDisconnectTolerance: process.env.TRAVIS ? 2 : 0,
    +
    +  // Import our gulp webserver as a Karma server middleware
    +  // So we instantly have all the custom server endpoints available
    +  beforeMiddleware: ['custom'],
    +  plugins: [
    +    'karma-browserify',
    +    'karma-chai',
    +    'karma-source-map-support',
    +    'karma-chrome-launcher',
    +    'karma-edge-launcher',
    +    'karma-firefox-launcher',
    +    'karma-fixture',
    +    'karma-html2js-preprocessor',
    +    'karma-ie-launcher',
    +    'karma-mocha',
    +    'karma-mocha-reporter',
    +    'karma-safari-launcher',
    +    'karma-sauce-launcher',
    +    'karma-simple-reporter',
    +    'karma-sinon-chai',
    +    'karma-super-dots-reporter',
    +    {
    +      'middleware:custom': ['factory', function() {
    +        return require(require.resolve('../app.js')).middleware;
    +      }],
    +    },
    +  ],
    +};
    diff --git a/build-system/tasks/lint.js b/build-system/tasks/lint.js
    index 8ff996c92497..933b4a0c0740 100644
    --- a/build-system/tasks/lint.js
    +++ b/build-system/tasks/lint.js
    @@ -13,81 +13,212 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +'use strict';
     
     
    -var argv = require('minimist')(process.argv.slice(2));
    -var config = require('../config');
    -var eslint = require('gulp-eslint');
    -var exec = require('child_process').exec;
    -var gulp = require('gulp-help')(require('gulp'));
    -var gulpIf = require('gulp-if');
    -var lazypipe = require('lazypipe');
    -var util = require('gulp-util');
    -var watch = require('gulp-watch');
    +const argv = require('minimist')(process.argv.slice(2));
    +const colors = require('ansi-colors');
    +const config = require('../config');
    +const eslint = require('gulp-eslint');
    +const eslintIfFixed = require('gulp-eslint-if-fixed');
    +const gulp = require('gulp-help')(require('gulp'));
    +const lazypipe = require('lazypipe');
    +const log = require('fancy-log');
    +const path = require('path');
    +const watch = require('gulp-watch');
    +const {gitDiffNameOnlyMaster} = require('../git');
     
    -var isWatching = (argv.watch || argv.w) || false;
    -
    -var options = {
    +const isWatching = (argv.watch || argv.w) || false;
    +const options = {
       fix: false,
    -  plugins: ['eslint-plugin-google-camelcase'],
    -  "ecmaFeatures": {
    -    "modules": true,
    -    "arrowFunctions": true,
    -    "blockBindings": true,
    -    "forOf": false,
    -    "destructuring": false
    -  },
    +  quiet: argv.quiet || false,
     };
    +let collapseLintResults = !!process.env.TRAVIS;
     
    -var watcher = lazypipe().pipe(watch, config.lintGlobs);
    +const maybeUpdatePackages = process.env.TRAVIS ? [] : ['update-packages'];
    +const rootDir = path.dirname(path.dirname(__dirname));
     
     /**
    - * Checks if current Vinyl file has been fixed by eslint.
    - * @param {!Vinyl} file
    - * @return {boolean}
    + * Initializes the linter stream based on globs
    + * @param {!Object} globs
    + * @param {!Object} streamOptions
    + * @return {!ReadableStream}
      */
    -function isFixed(file) {
    -  // Has ESLint fixed the file contents?
    -  return file.eslint != null && file.eslint.fixed;
    +function initializeStream(globs, streamOptions) {
    +  let stream = gulp.src(globs, streamOptions);
    +  if (isWatching) {
    +    const watcher = lazypipe().pipe(watch, globs);
    +    stream = stream.pipe(watcher());
    +  }
    +  return stream;
     }
     
     /**
    - * Run the eslinter on the src javascript and log the output
    - * @return {!Stream} Readable stream
    + * Logs a message on the same line to indicate progress
    + * @param {string} message
      */
    -function lint() {
    -  var errorsFound = false;
    -  var stream = gulp.src(config.lintGlobs);
    +function logOnSameLine(message) {
    +  if (!process.env.TRAVIS && process.stdout.isTTY) {
    +    process.stdout.moveCursor(0, -1);
    +    process.stdout.cursorTo(0);
    +    process.stdout.clearLine();
    +  }
    +  log(message);
    +}
     
    -  if (isWatching) {
    -    stream = stream.pipe(watcher());
    +/**
    + * Runs the linter on the given stream using the given options.
    + * @param {string} filePath
    + * @param {!ReadableStream} stream
    + * @param {!Object} options
    + * @return {boolean}
    + */
    +function runLinter(filePath, stream, options) {
    +  if (!process.env.TRAVIS) {
    +    log(colors.green('Starting linter...'));
       }
    +  if (collapseLintResults) {
    +    // TODO(#15255, #14761): Remove log folding after warnings are fixed.
    +    log(colors.bold(colors.yellow('Lint results: ')) + 'Expand this section');
    +    console./* OK*/log('travis_fold:start:lint_results\n');
    +  }
    +  const fixedFiles = {};
    +  return stream.pipe(eslint(options))
    +      .pipe(eslint.formatEach('stylish', function(msg) {
    +        logOnSameLine(msg.trim() + '\n');
    +      }))
    +      .pipe(eslintIfFixed(filePath))
    +      .pipe(eslint.result(function(result) {
    +        if (!process.env.TRAVIS) {
    +          logOnSameLine(colors.green('Linted: ') + result.filePath);
    +        }
    +        if (options.fix && result.fixed) {
    +          const relativePath = path.relative(rootDir, result.filePath);
    +          const status = result.errorCount == 0 ?
    +            colors.green('Fixed: ') : colors.yellow('Partially fixed: ');
    +          logOnSameLine(status + colors.cyan(relativePath));
    +          fixedFiles[relativePath] = status;
    +        }
    +      }))
    +      .pipe(eslint.results(function(results) {
    +        // TODO(#15255, #14761): Remove log folding after warnings are fixed.
    +        if (collapseLintResults) {
    +          console./* OK*/log('travis_fold:end:lint_results');
    +        }
    +        if (results.errorCount == 0 && results.warningCount == 0) {
    +          if (!process.env.TRAVIS) {
    +            logOnSameLine(colors.green('SUCCESS: ') +
    +                'No linter warnings or errors.');
    +          }
    +        } else {
    +          const prefix = results.errorCount == 0 ?
    +            colors.yellow('WARNING: ') : colors.red('ERROR: ');
    +          logOnSameLine(prefix + 'Found ' +
    +              results.errorCount + ' error(s) and ' +
    +              results.warningCount + ' warning(s).');
    +          if (!options.fix) {
    +            log(colors.yellow('NOTE 1:'),
    +                'You may be able to automatically fix some of these warnings ' +
    +                '/ errors by running',
    +                colors.cyan('gulp lint --local-changes --fix'),
    +                'from your local branch.');
    +            log(colors.yellow('NOTE 2:'),
    +                'Since this is a destructive operation (that edits your files',
    +                'in-place), make sure you commit before running the command.');
    +          }
    +        }
    +        if (options.fix && Object.keys(fixedFiles).length > 0) {
    +          log(colors.green('INFO: ') + 'Summary of fixes:');
    +          Object.keys(fixedFiles).forEach(file => {
    +            log(fixedFiles[file] + colors.cyan(file));
    +          });
    +        }
    +      }))
    +      .pipe(eslint.failAfterError());
    +}
     
    -  if (argv.fix) {
    -    options.fix = true;
    +/**
    + * Extracts the list of JS files in this PR from the commit log.
    + *
    + * @return {!Array}
    + */
    +function jsFilesChanged() {
    +  return gitDiffNameOnlyMaster().filter(function(file) {
    +    return path.extname(file) == '.js';
    +  });
    +}
    +
    +/**
    + * Checks if there are eslint rule changes, in which case we must lint all
    + * files.
    + *
    + * @return {boolean}
    + */
    +function eslintRulesChanged() {
    +  if (process.env.TRAVIS_EVENT_TYPE === 'push') {
    +    return false;
       }
    +  return gitDiffNameOnlyMaster().filter(function(file) {
    +    return path.basename(file).includes('.eslintrc') ||
    +        path.dirname(file) === 'build-system/eslint-rules';
    +  }).length > 0;
    +}
     
    -  return stream.pipe(eslint(options))
    -    .pipe(eslint.formatEach('stylish', function(msg) {
    -      errorsFound = true;
    -      util.log(util.colors.red(msg));
    -    }))
    -    .pipe(gulpIf(isFixed, gulp.dest('.')))
    -    .on('end', function() {
    -      if (errorsFound && !options.fix) {
    -        util.log(util.colors.blue('Run `gulp lint --fix` to automatically ' +
    -            'fix some of these lint warnings/errors. This is a destructive ' +
    -            'operation (operates on the file system) so please make sure ' +
    -            'you commit before running.'));
    -        process.exit(1);
    -      }
    +/**
    + * Sets the list of files to be linted.
    + *
    + * @param {!Array} files
    + */
    +function setFilesToLint(files) {
    +  config.lintGlobs =
    +      config.lintGlobs.filter(e => e !== '**/*.js').concat(files);
    +  if (!process.env.TRAVIS) {
    +    log(colors.green('INFO: ') + 'Running lint on the following files:');
    +    files.forEach(file => {
    +      log(colors.cyan(file));
         });
    +  }
    +  collapseLintResults = false;
     }
     
    -gulp.task('lint', 'Validates against Google Closure Linter', lint,
    -{
    -  options: {
    -    'watch': '  Watches for changes in files, validates against the linter',
    -    'fix': '  Fixes simple lint errors (spacing etc).'
    +/**
    + * Run the eslinter on the src javascript and log the output
    + * @return {!Stream} Readable stream
    + */
    +function lint() {
    +  if (argv.fix) {
    +    options.fix = true;
       }
    -});
    +  if (argv.files) {
    +    setFilesToLint(argv.files.split(','));
    +  } else if (!eslintRulesChanged() &&
    +      (process.env.TRAVIS_EVENT_TYPE === 'pull_request' ||
    +       process.env.LOCAL_PR_CHECK ||
    +       argv['local-changes'])) {
    +    const jsFiles = jsFilesChanged();
    +    if (jsFiles.length == 0) {
    +      log(colors.green('INFO: ') + 'No JS files in this PR');
    +      return Promise.resolve();
    +    }
    +    setFilesToLint(jsFiles);
    +  }
    +  const basePath = '.';
    +  const stream = initializeStream(config.lintGlobs, {base: basePath});
    +  return runLinter(basePath, stream, options);
    +}
    +
    +
    +gulp.task(
    +    'lint',
    +    'Validates against Google Closure Linter',
    +    maybeUpdatePackages,
    +    lint,
    +    {
    +      options: {
    +        'watch': '  Watches for changes in files, validates against the linter',
    +        'fix': '  Fixes simple lint errors (spacing etc)',
    +        'local-changes':
    +            '  Lints just the changes commited to the local branch',
    +        'quiet': '  Suppress warnings from outputting',
    +      },
    +    });
    diff --git a/build-system/tasks/make-golden.js b/build-system/tasks/make-golden.js
    deleted file mode 100644
    index dbab2244bd88..000000000000
    --- a/build-system/tasks/make-golden.js
    +++ /dev/null
    @@ -1,307 +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.
    - */
    -
    -var argv = require('minimist')(process.argv.slice(2));
    -var dirname = require('path').dirname;
    -var exec = require('child_process').exec;
    -var fs = require('fs-extra');
    -var gulp = require('gulp');
    -var imageDiff = require('gulp-image-diff');
    -var util = require('gulp-util');
    -
    -
    -/**
    - * Use phantomjs to take a screenshot of a page at the given url and save it
    - * to the output path.
    - *
    - * @param {string} host hostname
    - * @param {string} path url path for page to screenshot (minus hostname)
    - * @param {string} output output file path
    - * @param {string} device device type e.g. 'iPhone6+' for screenshot
    - * @param {boolean} verbose use verbose logging
    - * @param {function} cb callback
    - */
    -function doScreenshot(host, path, output, device, verbose, cb) {
    -  fs.mkdirpSync(dirname(output));
    -  if (verbose) {
    -    util.log('Output to: ', output);
    -  }
    -  exec('phantomjs --ssl-protocol=any --ignore-ssl-errors=true ' +
    -       '--load-images=true ' +
    -      'testing/screenshots/make-screenshot.js ' +
    -      '"' + host + '" ' +
    -      '"' + path + '" ' +
    -      '"' + output + '" ' +
    -      '"' + device + '" ',
    -      function(err, stdout, stderr) {
    -        if (verbose) {
    -          util.log(util.colors.gray('stdout: ', stdout));
    -          if (stderr.length) {
    -            util.log(util.colors.red('stderr: ', stderr));
    -          }
    -        }
    -        if (err != null) {
    -          util.log(util.colors.red('exec error: ', err));
    -        }
    -        cb();
    -      });
    -}
    -
    -
    -/**
    - * Make a golden image of the url.
    - * Ex:
    - * `gulp make-golden --path=examples.build/everything.amp.max.html \
    - *     --host=http://localhost:8000`
    - *  @param {function} cb callback function
    - */
    -function makeGolden(cb) {
    -  var path = argv.path;
    -  var host = argv.host || 'http://localhost:8000';
    -  var output = argv.output;
    -  var device = argv.device || 'iPhone6+';
    -  var verbose = (argv.verbose || argv.v);
    -
    -  if (!output) {
    -    output = 'screenshots' + (path && path[0] != '/' ? '/' : '') +
    -        path + '.png';
    -  }
    -
    -  doScreenshot(host, path, output, device, verbose, cb);
    -}
    -
    -
    -/**
    - * Test if screenshots match the golden images, and generate a report to
    - * compare the differences
    - * Ex:
    - * `gulp diff-screenshots --host=http://localhost:8000`
    - * @param {function} cb callback function
    - */
    -function testScreenshots(cb) {
    -  var host = argv.host || 'http://localhost:8000';
    -  var name = argv.name || 'screenshots';
    -  var dir = 'build/' + name;
    -  var verbose = (argv.verbose || argv.v);
    -
    -  fs.mkdirpSync(dir);
    -  fs.emptyDirSync(dir);
    -
    -  var reportFile = dir + '/report.html';
    -  reportPreambule(reportFile);
    -  var errorCount = 0;
    -
    -  /**
    -   * Check if file ends with suffix specified
    -   * @param {string} s string to check
    -   * @param {string} suffix suffix to look for
    -   * @return {boolean} true if suffix matches, false otherwise
    -   */
    -  function endsWith(s, suffix) {
    -    return s.indexOf(suffix, s.length - suffix.length) != -1;
    -  }
    -
    -  var goldenFiles = [];
    -
    -  /**
    -   * Recursively scan a directory for png files collecting the
    -   * names of them. Add the resulting filenames to the
    -   * goldenFiles collection.
    -   * @param {string} dir path to directory
    -   */
    -  function scanDir(dir) {
    -    fs.readdirSync(dir).forEach(function(file) {
    -      var path = dir + '/' + file;
    -      if (endsWith(file, '.png')) {
    -        goldenFiles.push(path.replace('screenshots/', ''));
    -      } else if (fs.statSync(path).isDirectory()) {
    -        scanDir(path);
    -      }
    -    });
    -  }
    -  scanDir('screenshots');
    -
    -  var todo = goldenFiles.length;
    -  if (verbose) {
    -    util.log('Diffs to be done: ', todo, goldenFiles);
    -  }
    -  goldenFiles.forEach(function(file) {
    -    diffScreenshot_(file, dir, host, verbose, function(res) {
    -      reportRecord(reportFile, file, dir, res);
    -      if (res.error || res.disparity > 0) {
    -        errorCount++;
    -        util.log(util.colors.red('Screenshot diff failed: ', file,
    -            JSON.stringify(res)));
    -      } else if (verbose) {
    -        util.log(util.colors.green('Screenshot diff successful: ', file));
    -      }
    -
    -      todo--;
    -      if (todo == 0) {
    -        reportPostambule(reportFile);
    -        if (errorCount == 0) {
    -          util.log(util.colors.green('Screenshots tests successful'));
    -        } else {
    -          util.log(util.colors.red('Screenshots tests failed: ', errorCount,
    -              reportFile));
    -          process.exit(1);
    -        }
    -        cb();
    -      }
    -    });
    -  });
    -}
    -
    -/**
    - * Take a screenshot of the page and diff the result against the golden image.
    - *
    - * @param {string} file filename for file to test
    - * @param {string} dir directory path to test images
    - * @param {string} host hostname
    - * @param {boolean} verbose use verbose logging
    - * @param {function} cb callback function
    - */
    -function diffScreenshot_(file, dir, host, verbose, cb) {
    -  if (verbose) {
    -    util.log('Screenshot diff for ', file);
    -  }
    -
    -  var goldenFile = 'screenshots/' + file;
    -  var goldenCopyFile = dir + '/' + file;
    -  var htmlPath = file.replace('.png', '');
    -  var tmpFile = dir + '/' + file.replace('.png', '.tmp.png');
    -  var diffFile = dir + '/' + file.replace('.png', '.diff.png');
    -
    -  fs.copySync(goldenFile, goldenCopyFile, {clobber: true});
    -
    -  doScreenshot(host, htmlPath, tmpFile, 'iPhone6+', verbose, function() {
    -    // TODO: pixelColorTolerance: 0.10
    -    gulp.src([tmpFile])
    -        .pipe(imageDiff({
    -          referenceImage: goldenCopyFile,
    -          differenceMapImage: diffFile,
    -          logProgress: verbose
    -        }))
    -        .pipe(imageDiff.jsonReporter())
    -        .pipe(gulp.dest(diffFile + '.json'))
    -        .on('error', function(error) {
    -          util.log(util.colors.red('Screenshot diff failed: ', file, error));
    -          cb({error: error});
    -        })
    -        .on('end', function(res) {
    -          var contents = fs.readFileSync(diffFile + '.json', 'utf8');
    -          var json = JSON.parse(contents);
    -          cb(json[0]);
    -        });
    -  });
    -}
    -
    -/**
    - * Write preambule html into the report file
    - * @param {string} reportFile filepath to report file
    - */
    -function reportPreambule(reportFile) {
    -  fs.writeFileSync(reportFile,
    -      '' +
    -      '' +
    -      '

    Screenshot Diffs

    ' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '', - 'utf8'); -} - -/** - * Append the postambule html to the report file - * @param {string} reportFile filepath to report file - */ -function reportPostambule(reportFile) { - fs.appendFileSync(reportFile, - '
    PathResultGoldenWorkDiff
    ', - 'utf8'); -} - -/** - * Create an html report record for an image diff and add it to the report file - * @param {string} reportFile report file path - * @param {string} file screenshot file path - * @param {string} dir screenshot directory path - * @param {!Object} record screenshot diff results record - */ -function reportRecord(reportFile, file, dir, record) { - - /** - * Create html for a thumbnail link - * @param {string} file file path to a thumbnail image - * @return {string} html anchor tag string - */ - function thumb(file) { - file = file.replace(dir + '/', ''); - return '' + - '' + - ''; - } - - fs.appendFileSync(reportFile, - '' + - '' + file + '' + - '
    ' + - record.disparity + '
    ' + - '' + thumb(record.referenceImage) + '' + - '' + thumb(record.compareImage) + '' + - '' + thumb(record.differenceMap) + '' + - '', - 'utf8'); -} - - -gulp.task('make-golden', 'Creates a "golden" screenshot', makeGolden, { - options: { - 'host': ' The host. Defaults to "http://localhost:8000".', - 'path': ' The path of the page URL on the host.' + - ' E.g. "/test/manual/amp-img.amp.html"', - 'output': ' The file where to output the screenshot.' + - ' Defaults to "screenshots/{path}.png"', - 'device': ' The name of the device which parameters to be used for' + - ' screenshotting. Defaults to "iPhone6+".', - 'verbose': ' Verbose logging. Default is false. Shorthand is "-v"' - } -}); - -gulp.task('test-screenshots', 'Tests screenshots against "golden" images', - testScreenshots, { - options: { - 'host': ' The host. Defaults to "http://localhost:8000".', - 'name': ' The name of the run. Defaults to "screenshots".' + - ' The run files are placed in the "build/{name}" dir.', - 'verbose': ' Verbose logging. Default is false. Shorthand is "-v"' - } -}); diff --git a/build-system/tasks/pr-check.js b/build-system/tasks/pr-check.js new file mode 100644 index 000000000000..d255e9c0a9dc --- /dev/null +++ b/build-system/tasks/pr-check.js @@ -0,0 +1,47 @@ +/** + * Copyright 2018 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'; + +const argv = require('minimist')(process.argv.slice(2)); +const gulp = require('gulp-help')(require('gulp')); +const {execOrDie} = require('../exec'); + + +/** + * Simple wrapper around pr-check.js. + */ +function prCheck() { + let cmd = 'node build-system/pr-check.js'; + if (argv.files) { + cmd = cmd + ' --files ' + argv.files; + } + if (argv.nobuild) { + cmd = cmd + ' --nobuild'; + } + execOrDie(cmd); +} + +gulp.task( + 'pr-check', + 'Locally runs the PR checks that are run by Travis CI.', + prCheck, + { + options: { + 'files': ' Restricts unit / integration tests to just these files', + 'nobuild': ' Skips building the runtime via `gulp build`.', + }, + } +); diff --git a/build-system/tasks/prepend-global/index.js b/build-system/tasks/prepend-global/index.js new file mode 100644 index 000000000000..0e4eb300f40a --- /dev/null +++ b/build-system/tasks/prepend-global/index.js @@ -0,0 +1,281 @@ +/** + * 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'; + +const argv = require('minimist')(process.argv.slice(2)); +const BBPromise = require('bluebird'); +const childProcess = require('child_process'); +const exec = BBPromise.promisify(childProcess.exec); +const colors = require('ansi-colors'); +const fs = BBPromise.promisifyAll(require('fs')); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); + +const {red, cyan} = colors; + +/** + * Returns the number of AMP_CONFIG matches in the given config string. + * + * @param {string} str + * @return {number} + */ +function numConfigs(str) { + const re = /\/\*AMP_CONFIG\*\//g; + const matches = str.match(re); + return matches == null ? 0 : matches.length; +} + +/** + * Checks that only 1 AMP_CONFIG should exist after append. + * + * @param {string} str + */ +function sanityCheck(str) { + const numMatches = numConfigs(str); + if (numMatches != 1) { + throw new Error( + 'Found ' + numMatches + ' AMP_CONFIG(s) before write. Aborting!'); + } +} + +/** + * @param {string} filename File containing the config + * @param {string=} opt_localBranch Whether to use the local branch version + * @param {string=} opt_branch If not the local branch, which branch to use + * @return {!Promise} + */ +function checkoutBranchConfigs(filename, opt_localBranch, opt_branch) { + if (opt_localBranch) { + return Promise.resolve(); + } + const 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 String containing the AMP config + * @param {string} fileString String containing the AMP runtime + * @return {string} + */ +function prependConfig(configString, fileString) { + return `self.AMP_CONFIG||(self.AMP_CONFIG=${configString});` + + `/*AMP_CONFIG*/${fileString}`; +} + +/** + * @param {string} filename Destination filename + * @param {string} fileString String to write + * @param {boolean=} opt_dryrun If true, print the contents without writing them + * @return {!Promise} + */ +function writeTarget(filename, fileString, opt_dryrun) { + if (opt_dryrun) { + log(cyan(`overwriting: ${filename}`)); + log(fileString); + return Promise.resolve(); + } + return fs.writeFileAsync(filename, fileString); +} + +/** + * @param {string|boolean} value + * @param {string} defaultValue + * @return {string} + */ +function valueOrDefault(value, defaultValue) { + if (typeof value === 'string') { + return value; + } + return defaultValue; +} + +/** + * @param {string} config Prod or canary + * @param {string} target File containing the AMP runtime (amp.js or v0.js) + * @param {string} filename File containing the (prod or canary) config + * @param {boolean=} opt_localDev Whether to enable local development + * @param {boolean=} opt_localBranch Whether to use the local branch version + * @param {string=} opt_branch If not the local branch, which branch to use + * @param {boolean=} opt_fortesting Whether to force getMode().test to be true + * @return {!Promise} + */ +function applyConfig( + config, target, filename, opt_localDev, opt_localBranch, opt_branch, + opt_fortesting) { + return checkoutBranchConfigs(filename, opt_localBranch, opt_branch) + .then(() => { + return Promise.all([ + fs.readFileAsync(filename), + fs.readFileAsync(target), + ]); + }) + .then(files => { + let configJson; + try { + configJson = JSON.parse(files[0].toString()); + } catch (e) { + log(red(`Error parsing config file: ${filename}`)); + throw e; + } + if (opt_localDev) { + configJson = enableLocalDev(config, target, configJson); + } + if (opt_fortesting) { + configJson = Object.assign({test: true}, configJson); + } + const targetString = files[1].toString(); + const configString = JSON.stringify(configJson); + return prependConfig(configString, targetString); + }) + .then(fileString => { + sanityCheck(fileString); + return writeTarget(target, fileString, argv.dryrun); + }) + .then(() => { + if (!process.env.TRAVIS) { + log('Wrote', cyan(config), 'AMP config to', cyan(target)); + } + }); +} + +/** + * @param {string} config Prod or canary + * @param {string} target File containing the AMP runtime (amp.js or v0.js) + * @param {string} configJson The json object in which to enable local dev + * @return {string} + */ +function enableLocalDev(config, target, configJson) { + let LOCAL_DEV_AMP_CONFIG = {localDev: true}; + if (!process.env.TRAVIS) { + log('Enabled local development mode in', cyan(target)); + } + const TESTING_HOST = process.env.AMP_TESTING_HOST; + if (typeof TESTING_HOST == 'string') { + const TESTING_HOST_FULL_URL = TESTING_HOST.match(/^https?:\/\//) ? + TESTING_HOST : 'http://' + TESTING_HOST; + const TESTING_HOST_NO_PROTOCOL = + TESTING_HOST.replace(/^https?:\/\//, ''); + + LOCAL_DEV_AMP_CONFIG = Object.assign(LOCAL_DEV_AMP_CONFIG, { + thirdPartyUrl: TESTING_HOST_FULL_URL, + thirdPartyFrameHost: TESTING_HOST_NO_PROTOCOL, + thirdPartyFrameRegex: TESTING_HOST_NO_PROTOCOL, + }); + if (!process.env.TRAVIS) { + log('Set', cyan('TESTING_HOST'), 'to', cyan(TESTING_HOST), + 'in', cyan(target)); + } + } + return Object.assign(LOCAL_DEV_AMP_CONFIG, configJson); +} + +/** + * @param {string} target Target file from which to remove the AMP config + * @return {!Promise} + */ +function removeConfig(target) { + return fs.readFileAsync(target) + .then(file => { + let contents = file.toString(); + if (numConfigs(contents) == 0) { + if (!process.env.TRAVIS) { + log('No configs found in', cyan(target)); + } + return Promise.resolve(); + } + sanityCheck(contents); + const config = + /self\.AMP_CONFIG\|\|\(self\.AMP_CONFIG=.*?\/\*AMP_CONFIG\*\//; + contents = contents.replace(config, ''); + return writeTarget(target, contents, argv.dryrun).then(() => { + if (!process.env.TRAVIS) { + log('Removed existing config from', cyan(target)); + } + }); + }); +} + +function main() { + const TESTING_HOST = process.env.AMP_TESTING_HOST; + const target = argv.target || TESTING_HOST; + + if (!target) { + log(red('Missing --target.')); + return; + } + + if (argv.remove) { + return removeConfig(target); + } + + if (!(argv.prod || argv.canary)) { + log(red('One of --prod or --canary should be provided.')); + return; + } + + let filename = ''; + + // Prod by default. + const config = argv.canary ? 'canary' : 'prod'; + 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 removeConfig(target).then(() => { + return applyConfig( + config, target, filename, + argv.local_dev, argv.local_branch, argv.branch, argv.fortesting); + }); +} + +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.', + 'local_dev': ' Enables runtime to be used for local development.', + 'branch': ' Switch to a git branch to get config source from. ' + + 'Uses master by default.', + 'local_branch': ' Don\'t switch branches and use the config from the ' + + 'local branch.', + 'fortesting': ' Force the config to return true for getMode().test', + 'remove': ' Removes previously prepended json config from the target ' + + 'file (if present).', + }, +}); + +exports.checkoutBranchConfigs = checkoutBranchConfigs; +exports.prependConfig = prependConfig; +exports.writeTarget = writeTarget; +exports.valueOrDefault = valueOrDefault; +exports.sanityCheck = sanityCheck; +exports.numConfigs = numConfigs; +exports.removeConfig = removeConfig; +exports.applyConfig = applyConfig; diff --git a/build-system/tasks/prepend-global/test.js b/build-system/tasks/prepend-global/test.js new file mode 100644 index 000000000000..60b049f6831d --- /dev/null +++ b/build-system/tasks/prepend-global/test.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. + */ +'use strict'; + + +const m = require('./'); +const test = require('ava'); + + +test('sync - prepends global config', t => { + t.plan(1); + const 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); + let 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); + const 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;'; + const badStr2 = 'var x = 1 + 1;'; + const goodStr = 'self.AMP_CONFIG||(self.AMP_CONFIG={"hello":"world"})' + + '/*AMP_CONFIG*/' + + 'var x = 1 + 1;'; + t.false(m.numConfigs(badStr) == 1); + t.true(m.numConfigs(goodStr) == 1); + t.false(m.numConfigs(badStr2) == 1); +}); diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js index 303adbfa3970..b22f7e599a83 100644 --- a/build-system/tasks/presubmit-checks.js +++ b/build-system/tasks/presubmit-checks.js @@ -13,60 +13,151 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; -var gulp = require('gulp-help')(require('gulp')); -var path = require('path'); -var srcGlobs = require('../config').presubmitGlobs; -var util = require('gulp-util'); +const colors = require('ansi-colors'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const path = require('path'); +const srcGlobs = require('../config').presubmitGlobs; +const through2 = require('through2'); -var dedicatedCopyrightNoteSources = /(\.js|\.css|\.go)$/; +const dedicatedCopyrightNoteSources = /(\.js|\.css|\.go)$/; -var es6polyfill = 'Not available because we do not currently' + - ' ship with a needed ES6 polyfill.'; - -var requiresReviewPrivacy = +const requiresReviewPrivacy = 'Usage of this API requires dedicated review due to ' + 'being privacy sensitive. Please file an issue asking for permission' + ' to use if you have not yet done so.'; -var privateServiceFactory = 'This service should only be installed in ' + +const privateServiceFactory = 'This service should only be installed in ' + 'the whitelisted files. Other modules should use a public function ' + 'typically called serviceNameFor.'; -var shouldNeverBeUsed = +const shouldNeverBeUsed = 'Usage of this API is not allowed - only for internal purposes.'; +const backwardCompat = 'This method must not be called. It is only retained ' + + 'for backward compatibility during rollout.'; + +const 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 = { +const forbiddenTerms = { 'DO NOT SUBMIT': '', + // TODO(dvoytenko, #8464): cleanup whitelist. + '(^-amp-|\\W-amp-)': { + message: 'Switch to new internal class form', + whitelist: [ + 'build-system/amp4test.js', + 'build-system/app-index/boilerplate.js', + 'build-system/tasks/extension-generator/index.js', + 'css/amp.css', + 'extensions/amp-pinterest/0.1/amp-pinterest.css', + 'extensions/amp-pinterest/0.1/follow-button.js', + 'extensions/amp-pinterest/0.1/pin-widget.js', + 'extensions/amp-pinterest/0.1/pinit-button.js', + ], + }, + '(^i-amp-|\\Wi-amp-)': { + message: 'Switch to new internal ID form', + whitelist: [ + 'build-system/tasks/extension-generator/index.js', + 'build-system/tasks/create-golden-css/css/main.css', + 'css/amp.css', + ], + }, 'describe\\.only': '', + 'describes.*\\.only': '', 'it\\.only': '', - 'sinon\\.(spy|stub|mock)\\(\\w[^)]*\\)': { - message: 'Use a sandbox instead to avoid repeated `#restore` calls' + 'Math\.random[^;()]*=': 'Use Sinon to stub!!!', + 'gulp-util': { + message: '`gulp-util` will be deprecated soon. See ' + + 'https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5 ' + + 'for a list of alternatives.', + }, + 'document-register-element.node': { + message: 'Use `document-register-element.patched` instead', + whitelist: ['build-system/tasks/update-packages.js'], + }, + 'sinon\\.(spy|stub|mock)\\(': { + 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', }, - 'sinon\\.useFakeTimers': { - 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 ' + + message: 'If you run against this, use console/*OK*/.[log|error] to ' + 'whitelist a legit case.', - // TODO: temporary, remove when validator is up to date whitelist: [ - 'validator/validator.js', - 'validator/parse-css.js', - 'validator/validator-in-browser.js', - ] + 'build-system/pr-check.js', + 'build-system/app.js', + 'build-system/amp4test.js', + 'build-system/check-package-manager.js', + 'validator/nodejs/index.js', // NodeJs only. + 'validator/engine/parse-css.js', + 'validator/engine/validator-in-browser.js', + 'validator/engine/validator.js', + 'gulpfile.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/iframe-attributes.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.', + }, + 'iframePing': { + message: 'This is only available in vendor config for ' + + 'temporary workarounds.', + whitelist: [ + 'extensions/amp-analytics/0.1/config.js', + 'extensions/amp-analytics/0.1/requests.js', + ], }, // Service factories that should only be installed once. - 'installActionService': { + 'installActionServiceForDoc': { message: privateServiceFactory, whitelist: [ + 'src/inabox/amp-inabox-lite.js', 'src/service/action-impl.js', 'src/service/standard-actions-impl.js', - 'src/amp-core-service.js', + 'src/runtime.js', ], }, 'installActionHandler': { @@ -74,77 +165,230 @@ var forbiddenTerms = { whitelist: [ 'src/service/action-impl.js', 'extensions/amp-access/0.1/amp-access.js', + 'extensions/amp-form/0.1/amp-form.js', ], }, - 'installCidService': { + 'installActivityService': { message: privateServiceFactory, whitelist: [ - 'src/service/cid-impl.js', - 'extensions/amp-access/0.1/amp-access.js', + 'extensions/amp-analytics/0.1/activity-impl.js', 'extensions/amp-analytics/0.1/amp-analytics.js', ], }, - 'installStorageService': { + 'cidServiceForDocForTesting': { message: privateServiceFactory, whitelist: [ - 'extensions/amp-analytics/0.1/amp-analytics.js', + 'src/service/cid-impl.js', + ], + }, + 'installCryptoService': { + message: privateServiceFactory, + whitelist: [ + 'src/service/crypto-impl.js', + 'src/runtime.js', + ], + }, + 'installDocumentStateService': { + message: privateServiceFactory, + whitelist: [ + 'src/service/document-state.js', + 'src/runtime.js', + ], + }, + 'installDocService': { + message: privateServiceFactory, + whitelist: [ + 'src/amp.js', + 'src/amp-shadow.js', + 'src/inabox/amp-inabox.js', + 'src/inabox/amp-inabox-lite.js', + 'src/service/ampdoc-impl.js', + 'testing/describes.js', + 'testing/iframe.js', + ], + }, + 'installPerformanceService': { + message: privateServiceFactory, + whitelist: [ + 'src/amp.js', + 'src/amp-shadow.js', + 'src/inabox/amp-inabox.js', + 'src/inabox/amp-inabox-lite.js', + 'src/service/performance-impl.js', + ], + }, + 'installStorageServiceForDoc': { + message: privateServiceFactory, + whitelist: [ + 'src/inabox/amp-inabox-lite.js', + 'src/runtime.js', 'src/service/storage-impl.js', ], }, - 'installViewerService': { + 'installTemplatesService': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', - 'src/service/history-impl.js', - 'src/service/resources-impl.js', + 'src/runtime.js', + 'src/service/template-impl.js', + ], + }, + 'installUrlReplacementsServiceForDoc': { + message: privateServiceFactory, + whitelist: [ + 'src/inabox/amp-inabox-lite.js', + 'src/runtime.js', + 'src/service/url-replacements-impl.js', + ], + }, + 'installViewerServiceForDoc': { + message: privateServiceFactory, + whitelist: [ + 'src/runtime.js', + 'src/inabox/amp-inabox.js', + 'src/inabox/amp-inabox-lite.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/service/viewport-impl.js', + 'src/runtime.js', + 'src/service/viewer-impl.js', + ], + }, + 'installViewportServiceForDoc': { + message: privateServiceFactory, + whitelist: [ + 'src/runtime.js', + 'src/service/viewport/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/viewport/viewport-impl.js', 'src/service/vsync-impl.js', ], }, - 'installResourcesService': { + 'installResourcesServiceForDoc': { message: privateServiceFactory, whitelist: [ - 'src/amp-core-service.js', + 'src/inabox/amp-inabox-lite.js', + 'src/runtime.js', 'src/service/resources-impl.js', 'src/service/standard-actions-impl.js', ], }, - 'sendMessage': { + 'installXhrService': { + message: privateServiceFactory, + whitelist: [ + 'src/runtime.js', + 'src/service/xhr-impl.js', + ], + }, + 'installPositionObserverServiceForDoc': { message: privateServiceFactory, whitelist: [ + 'src/service/position-observer/position-observer-impl.js', + 'extensions/amp-list/0.1/amp-list.js', + 'extensions/amp-position-observer/0.1/amp-position-observer.js', + 'extensions/amp-next-page/0.1/next-page-service.js', + 'extensions/amp-fx-collection/0.1/providers/fx-provider.js', + 'src/service/video-manager-impl.js', + 'src/service/video/docking.js', + 'src/service/video/autoplay.js', + ], + }, + 'getServiceForDocDeprecated': { + message: 'Use getServiceForDoc() instead.', + whitelist: [ + 'src/chunk.js', + 'src/service.js', + 'src/services.js', + ], + }, + 'initLogConstructor|setReportError': { + message: 'Should only be called from JS binary entry files.', + whitelist: [ + '3p/integration.js', + '3p/ampcontext-lib.js', + '3p/iframe-transport-client-lib.js', + '3p/recaptcha.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', + 'extensions/amp-viewer-integration/0.1/examples/amp-viewer-host.js', + 'src/runtime.js', + 'src/log.js', + 'src/web-worker/web-worker.js', + 'tools/experiments/experiments.js', + ], + }, + 'parseUrlWithA': { + message: 'Use parseUrl instead.', + whitelist: [ + 'src/url.js', + 'src/service/navigation.js', + 'src/service/url-impl.js', + 'dist.3p/current/integration.js', + ], + }, + '\\.sendMessage\\(': { + message: 'Usages must be reviewed.', + whitelist: [ + // viewer-impl.sendMessage + 'src/error.js', + 'src/service/navigation.js', 'src/service/viewer-impl.js', + 'src/service/viewport/viewport-impl.js', + 'src/service/performance-impl.js', + 'src/service/resources-impl.js', + 'extensions/amp-bind/0.1/bind-impl.js', + 'extensions/amp-app-banner/0.1/amp-app-banner.js', + 'extensions/amp-subscriptions/0.1/viewer-subscription-platform.js', + 'extensions/amp-viewer-integration/0.1/highlight-handler.js', + + // iframe-messaging-client.sendMessage + '3p/iframe-messaging-client.js', + '3p/ampcontext.js', + '3p/ampcontext-integration.js', + '3p/recaptcha.js', + 'dist.3p/current/integration.js', // includes previous + ], + }, + '\\.sendMessageAwaitResponse\\(': { + message: 'Usages must be reviewed.', + whitelist: [ + 'extensions/amp-access/0.1/login-dialog.js', + 'extensions/amp-access/0.1/signin.js', + 'extensions/amp-subscriptions/0.1/viewer-subscription-platform.js', + 'src/impression.js', + 'src/service/cid-impl.js', + 'src/service/history-impl.js', 'src/service/storage-impl.js', - 'examples/viewer-integr-messaging.js', + 'src/ssr-template-helper.js', + 'src/service/viewer-impl.js', + 'src/service/viewer-cid-api.js', + 'src/utils/xhr-utils.js', ], }, // Privacy sensitive - 'cidFor': { + 'cidForDoc|cidForDocOrNull': { message: requiresReviewPrivacy, whitelist: [ - 'builtins/amp-ad.js', - 'src/cid.js', + 'src/ad-cid.js', + 'src/services.js', 'src/service/cid-impl.js', - 'src/url-replacements.js', + 'src/service/standard-actions-impl.js', + 'src/service/url-replacements-impl.js', 'extensions/amp-access/0.1/amp-access.js', + 'extensions/amp-subscriptions/0.1/amp-subscriptions.js', + 'extensions/amp-experiment/0.1/variant.js', 'extensions/amp-user-notification/0.1/amp-user-notification.js', + 'extensions/amp-consent/0.1/consent-state-manager.js', ], }, 'getBaseCid': { @@ -154,126 +398,263 @@ var forbiddenTerms = { 'src/service/viewer-impl.js', ], }, - 'cookie\\W': { + 'isTrustedViewer': { message: requiresReviewPrivacy, whitelist: [ - 'src/cookies.js', + 'src/error.js', + 'src/utils/xhr-utils.js', + 'src/service/viewer-impl.js', + 'src/service/viewer-cid-api.js', + 'src/inabox/inabox-viewer.js', 'src/service/cid-impl.js', + 'src/impression.js', ], }, - 'getCookie\\W': { - message: requiresReviewPrivacy, + 'eval\\(': { + message: shouldNeverBeUsed, whitelist: [ - 'src/service/cid-impl.js', - 'src/cookies.js', - 'src/experiments.js', - 'tools/experiments/experiments.js', - ] + 'extension/amp-bind/0.1/test/test-bind-expr.js', + ], }, - 'setCookie\\W': { + 'storageForDoc': { message: requiresReviewPrivacy, whitelist: [ + 'src/services.js', 'src/service/cid-impl.js', - 'src/cookies.js', - 'src/experiments.js', - 'tools/experiments/experiments.js', - ] + 'extensions/amp-user-notification/0.1/amp-user-notification.js', + 'extensions/amp-app-banner/0.1/amp-app-banner.js', + 'extensions/amp-consent/0.1/consent-state-manager.js', + ], }, - 'isDevChannel\\W': { + 'localStorage': { message: requiresReviewPrivacy, whitelist: [ - 'extensions/amp-access/0.1/amp-access.js', - 'extensions/amp-user-notification/0.1/amp-user-notification.js', - 'src/experiments.js', + 'src/service/cid-impl.js', 'src/service/storage-impl.js', - 'tools/experiments/experiments.js', - ] + 'testing/fake-dom.js', + 'extensions/amp-access/0.1/amp-access-iframe.js', + 'extensions/amp-web-push/0.1/amp-web-push-helper-frame.js', + 'extensions/amp-web-push/0.1/amp-web-push-permission-dialog.js', + ], }, - 'isDevChannelVersionDoNotUse_\\W': { - message: shouldNeverBeUsed, + 'sessionStorage': { + message: requiresReviewPrivacy, whitelist: [ - 'src/experiments.js', - ] + 'extensions/amp-access/0.1/amp-access-iframe.js', + 'extensions/amp-accordion/0.1/amp-accordion.js', + ], }, - 'isTrusted': { + 'indexedDB': { message: requiresReviewPrivacy, - whitelist: [ - 'src/service/viewer-impl.js', - ] }, - 'eval\\(': '', - 'storageFor': { + 'openDatabase': requiresReviewPrivacy, + 'requestFileSystem': requiresReviewPrivacy, + 'webkitRequestFileSystem': requiresReviewPrivacy, + 'getAccessReaderId': { message: requiresReviewPrivacy, whitelist: [ - 'src/storage.js', - 'extensions/amp-user-notification/0.1/amp-user-notification.js', + 'build-system/amp.extern.js', + 'extensions/amp-access/0.1/amp-access.js', + 'extensions/amp-access/0.1/access-vars.js', + 'extensions/amp-access-scroll/0.1/scroll-impl.js', + 'extensions/amp-subscriptions/0.1/amp-subscriptions.js', + 'src/service/url-replacements-impl.js', ], }, - 'localStorage': { + 'getAuthdataField': { message: requiresReviewPrivacy, whitelist: [ - 'src/service/cid-impl.js', - 'src/service/storage-impl.js', + 'build-system/amp.extern.js', + 'extensions/amp-access/0.1/amp-access.js', + 'extensions/amp-access/0.1/access-vars.js', + 'extensions/amp-subscriptions/0.1/amp-subscriptions.js', + 'src/service/url-replacements-impl.js', ], }, - 'sessionStorage': requiresReviewPrivacy, - 'indexedDB': requiresReviewPrivacy, - 'openDatabase': requiresReviewPrivacy, - 'requestFileSystem': requiresReviewPrivacy, - 'webkitRequestFileSystem': requiresReviewPrivacy, '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. - 'Promise\\.all': es6polyfill, - 'Promise\\.race': es6polyfill, - '\\.startsWith': { - message: es6polyfill, - whitelist: [ - 'validator/tokenize-css.js', - 'validator/validator.js' - ] - }, - '\\.endsWith': es6polyfill, - // 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, + // Overridden APIs. + '(doc.*)\\.referrer': { + message: 'Use Viewer.getReferrerUrl() instead.', whitelist: [ - 'extensions/amp-access/0.1/access-expr-impl.js', + '3p/integration.js', + 'ads/google/a4a/utils.js', + 'dist.3p/current/integration.js', + 'src/service/viewer-impl.js', + 'src/error.js', + 'src/window-interface.js', ], }, - - // Overridden APIs. - '(doc.*)\\.referrer': { + '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', + ], + }, + 'internalListenImplementation': { + message: 'Use `listen()` in either `event-helper` or `3p-frame-messaging`' + + ', depending on your use case.', + whitelist: [ + 'src/3p-frame-messaging.js', + 'src/event-helper.js', + 'src/event-helper-listen.js', + 'dist.3p/current/integration.js', // includes previous + ], + }, + 'setTimeout.*throw': { + message: 'Use dev.error or user.error instead.', + whitelist: [ + 'src/log.js', + ], + }, + '(dev|user)\\(\\)\\.(fine|info|warn|error)\\((?!\\s*([A-Z0-9-]+|[\'"`][A-Z0-9-]+[\'"`]))[^,)\n]*': { // eslint-disable-line max-len + message: 'Logging message require explicitly `TAG`, or an all uppercase' + + ' string as the first parameter', + }, + '\\.schedulePass\\(': { + message: 'schedulePass is heavy, think twice before using it', + whitelist: [ + 'src/service/resources-impl.js', + ], + }, + '\\.requireLayout\\(': { + message: 'requireLayout is restricted b/c it affects non-contained elements', // eslint-disable-line max-len + whitelist: [ + 'extensions/amp-animation/0.1/web-animations.js', + 'extensions/amp-lightbox-gallery/0.1/amp-lightbox-gallery.js', + 'src/service/resources-impl.js', + ], + }, + '\\.updateLayoutPriority\\(': { + message: 'updateLayoutPriority 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. + 'src/polyfills/custom-elements.js', + ], + }, + 'startupChunk\\(': { + message: 'startupChunk( should only be used during startup', + whitelist: [ + 'src/amp.js', + 'src/chunk.js', + 'src/inabox/amp-inabox.js', + 'src/runtime.js', + ], + }, + 'AMP_CONFIG': { + message: 'Do not access AMP_CONFIG directly. Use isExperimentOn() ' + + 'and getMode() to access config', + whitelist: [ + 'build-system/amp.extern.js', + 'build-system/app.js', + 'build-system/tasks/firebase.js', + 'build-system/tasks/prepend-global/index.js', + 'build-system/tasks/prepend-global/test.js', + 'build-system/tasks/visual-diff/index.js', + 'dist.3p/current/integration.js', + 'src/config.js', + 'src/experiments.js', + 'src/mode.js', + 'src/web-worker/web-worker.js', // Web worker custom error reporter. + 'tools/experiments/experiments.js', + 'build-system/amp4test.js', + 'gulpfile.js', + ], + }, + 'data:image/svg(?!\\+xml;charset=utf-8,)[^,]*,': { + message: 'SVG data images must use charset=utf-8: ' + + '"data:image/svg+xml;charset=utf-8,..."', + }, + 'new CustomEvent\\(': { + message: 'Use createCustomEvent() helper instead.', + whitelist: [ + 'src/event-helper.js', + ], + }, + 'new FormData\\(': { + message: 'Use createFormDataWrapper() instead and call ' + + 'formDataWrapper.getFormData() to get the native FormData object.', + whitelist: [ + 'src/form-data-wrapper.js', + ], + }, + '([eE]xit|[eE]nter|[cC]ancel|[rR]equest)Full[Ss]creen\\(': { + message: 'Use fullscreenEnter() and fullscreenExit() from dom.js instead.', + whitelist: [ + 'ads/google/imaVideo.js', + 'dist.3p/current/integration.js', + 'src/video-iframe-integration.js', + ], + }, + '\\.defer\\(\\)': { + message: 'Promise.defer() is deprecated and should not be used.', + }, + '(dev|user)\\(\\)\\.assert(Element|String|Number)?\\(\\s*([A-Z][A-Z0-9-]*,)': { // eslint-disable-line max-len + message: 'TAG is not an argument to assert(). Will cause false positives.', + }, + 'eslint no-unused-vars': { + message: 'Use a line-level "no-unused-vars" rule instead.', + whitelist: [ + 'extensions/amp-access/0.1/iframe-api/access-controller.js', + ], + }, + 'this\\.skip\\(\\)': { + message: 'Use of `this.skip()` is forbidden in test files. Use ' + + '`this.skipTest()` from within a `before()` block instead. See #17245.', + checkInTestFolder: true, + whitelist: [ + 'test/_init_tests.js', + ], + }, + '[^\\.]makeBodyVisible\\(': { + message: 'This is a protected function. If you are calling this to show ' + + 'body after an error please use `makeBodyVisibleRecovery`', + whitelist: [ + 'src/amp.js', + 'src/amp-shadow.js', + 'src/style-installer.js', + 'src/inabox/amp-inabox.js', + 'src/inabox/amp-inabox-lite.js', ], }, }; -var ThreePTermsMessage = 'The 3p bootstrap iframe has no polyfills loaded and' + - ' can thus not use most modern web APIs.'; +const ThreePTermsMessage = 'The 3p bootstrap iframe has no polyfills loaded' + + ' and can thus not use most modern web APIs.'; -var forbidden3pTerms = { +const forbidden3pTerms = { // We need to forbid promise usage because we don't have our own polyfill // available. This whitelisting of callNext is a major hack to allow one // 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 ' + - 'or mark with `/*OK*/` or `/*REVIEW*/` and consult the AMP team. ' + +const bannedTermsHelpString = 'Please review viewport service for helper ' + + 'methods or mark with `/*OK*/` or `/*REVIEW*/` and consult the AMP team. ' + 'Most of the forbidden property/method access banned on the ' + '`forbiddenTermsSrcInclusive` object can be found in ' + '[What forces layout / reflow gist by Paul Irish]' + @@ -286,10 +667,9 @@ var bannedTermsHelpString = 'Please review viewport.js for a helper method ' + 'forbidden property/method or mark it with `object./*REVIEW*/property` ' + 'if you are unsure and so that it stands out in code reviews.'; -var forbiddenTermsSrcInclusive = { +const forbiddenTermsSrcInclusive = { '\\.innerHTML(?!_)': bannedTermsHelpString, '\\.outerHTML(?!_)': bannedTermsHelpString, - '\\.postMessage(?!_)': bannedTermsHelpString, '\\.offsetLeft(?!_)': bannedTermsHelpString, '\\.offsetTop(?!_)': bannedTermsHelpString, '\\.offsetWidth(?!_)': bannedTermsHelpString, @@ -299,39 +679,246 @@ 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, + // Functions + '\\.changeHeight\\(': bannedTermsHelpString, + '\\.changeSize\\(': bannedTermsHelpString, + '\\.attemptChangeHeight\\(0\\)': 'please consider using `attemptCollapse()`', + '\\.collapse\\(': bannedTermsHelpString, + '\\.expand\\(': bannedTermsHelpString, + '\\.focus\\(': bannedTermsHelpString, + '\\.getBBox\\(': bannedTermsHelpString, + '\\.getBoundingClientRect\\(': bannedTermsHelpString, + '\\.getClientRects\\(': bannedTermsHelpString, + '\\.getMatchedCSSRules\\(': bannedTermsHelpString, + '\\.scrollBy\\(': bannedTermsHelpString, + '\\.scrollIntoView\\(': bannedTermsHelpString, + '\\.scrollIntoViewIfNeeded\\(': bannedTermsHelpString, + '\\.scrollTo\\(': bannedTermsHelpString, + '\\.webkitConvertPointFromNodeToPage\\(': bannedTermsHelpString, + '\\.webkitConvertPointFromPageToNode\\(': bannedTermsHelpString, + '\\.scheduleUnlayout\\(': bannedTermsHelpString, + '\\.postMessage\\(': { + message: bannedTermsHelpString, + whitelist: [ + 'extensions/amp-install-serviceworker/0.1/amp-install-serviceworker.js', + ], + }, + 'getComputedStyle\\(': { + message: 'Due to various bugs in Firefox, you must use the computedStyle ' + + 'helper in style.js.', + whitelist: [ + 'src/style.js', + 'dist.3p/current/integration.js', + ], + }, + 'decodeURIComponent\\(': { + message: 'decodeURIComponent throws for malformed URL components. Please ' + + 'use tryDecodeUriComponent from src/url.js', + whitelist: [ + '3p/integration.js', + 'dist.3p/current/integration.js', + 'examples/pwa/pwa.js', + 'validator/engine/parse-url.js', + 'validator/engine/validator.js', + 'validator/webui/webui.js', + 'extensions/amp-pinterest/0.1/util.js', + 'src/url.js', + 'src/url-try-decode-uri-component.js', + 'src/utils/bytes.js', + ], + }, + 'Text(Encoder|Decoder)\\(': { + message: 'TextEncoder/TextDecoder is not supported in all browsers.' + + ' Please use UTF8 utilities from src/bytes.js', + whitelist: [ + 'ads/google/a4a/line-delimited-response-handler.js', + 'examples/pwa/pwa.js', + 'src/utils/bytes.js', + ], + }, + 'contentHeightChanged': { + message: bannedTermsHelpString, + whitelist: [ + 'src/inabox/inabox-viewport.js', + 'src/service/resources-impl.js', + 'src/service/viewport/viewport-binding-def.js', + 'src/service/viewport/viewport-binding-ios-embed-sd.js', + 'src/service/viewport/viewport-binding-ios-embed-wrapper.js', + 'src/service/viewport/viewport-binding-natural.js', + 'src/service/viewport/viewport-impl.js', + ], + }, + 'preloadExtension': { + message: bannedTermsHelpString, + whitelist: [ + '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', + 'src/analytics.js', + 'src/extension-analytics.js', + 'src/services.js', + 'extensions/amp-ad/0.1/amp-ad.js', + 'extensions/amp-a4a/0.1/amp-a4a.js', + 'extensions/amp-a4a/0.1/template-validator.js', + 'extensions/amp-ad-network-adsense-impl/0.1/amp-ad-network-adsense-impl.js', // eslint-disable-line max-len + 'extensions/amp-ad-network-doubleclick-impl/0.1/amp-ad-network-doubleclick-impl.js', // eslint-disable-line max-len + 'extensions/amp-lightbox-gallery/0.1/amp-lightbox-gallery.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', + 'extensions/amp-auto-ads/0.1/amp-auto-ads.js', + 'extensions/amp-auto-ads/0.1/anchor-ad-strategy.js', + ], + }, + 'reject\\(\\)': { + message: 'Always supply a reason in rejections. ' + + 'error.cancellation() may be applicable.', + whitelist: [ + 'extensions/amp-access/0.1/access-expr-impl.js', + 'extensions/amp-animation/0.1/css-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/resources-impl.js', + 'src/service/url-replacements-impl.js', + 'src/service/variable-source.js', + 'src/validator-integration.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', + 'extensions/amp-web-push/0.1/iframehost.js', + 'extensions/amp-recaptcha-input/0.1/amp-recaptcha-service.js', + 'dist.3p/current/integration.js', + ], + }, + '\\.getTime\\(\\)': { + message: 'Unless you do weird date math (whitelist), use Date.now().', + whitelist: [ + 'extensions/amp-timeago/0.1/amp-timeago.js', + ], + }, + '\\.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', + 'extensions/amp-analytics/0.1/cookie-writer.js', + 'extensions/amp-analytics/0.1/requests.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', + ], + }, + '\\.setNonBoolean\\(': { + message: requiresReviewPrivacy, + whitelist: [ + 'src/service/storage-impl.js', + ], + }, + '(cdn|3p)\\.ampproject\\.': { + message: 'The CDN domain should typically not be hardcoded in source ' + + 'code. Use a property of urls from src/config.js instead.', + whitelist: [ + 'ads/_a4a-config.js', + 'build-system/app.js', + 'build-system/app-index/template.js', + 'dist.3p/current/integration.js', + 'extensions/amp-iframe/0.1/amp-iframe.js', + 'src/config.js', + 'testing/local-amp-chrome-extension/background.js', + 'tools/errortracker/errortracker.go', + 'validator/engine/validator-in-browser.js', + 'validator/nodejs/index.js', + 'validator/webui/serve-standalone.go', + 'build-system/app-video-testbench.js', + 'build-system/tasks/check-links.js', + 'build-system/tasks/extension-generator/index.js', + 'gulpfile.js', + ], + }, + '\\<\\<\\<\\<\\<\\<': { + message: 'Unresolved merge conflict.', + }, + '\\>\\>\\>\\>\\>\\>': { + message: 'Unresolved merge conflict.', + }, + '\\.indexOf\\([\'"][^)]+\\)\\s*===?\\s*0\\b': { + message: 'use startsWith helper in src/string.js', + whitelist: [ + 'dist.3p/current/integration.js', + ], + }, + '\\.indexOf\\(.*===?.*\\.length': 'use endsWith helper in src/string.js', + '/url-parse-query-string': { + message: 'Import parseQueryString from `src/url.js`', + whitelist: [ + 'src/url.js', + 'src/mode.js', + 'dist.3p/current/integration.js', + ], + }, + '\\.trim(Left|Right)\\(\\)': { + message: 'Unsupported on IE; use trim() or a helper instead.', + whitelist: [ + 'validator/engine/validator.js', + ], + }, + '\\.matches\\(': 'Please use matches() helper in src/dom.js', }; // Terms that must appear in a source file. -var requiredTerms = { - 'Copyright 20(15|16) The AMP HTML Authors\\.': +const requiredTerms = { + 'Copyright 20(15|16|17|18) The AMP HTML Authors\\.': dedicatedCopyrightNoteSources, 'Licensed under the Apache License, Version 2\\.0': dedicatedCopyrightNoteSources, @@ -346,9 +933,40 @@ var requiredTerms = { * @return {boolean} */ function isInTestFolder(path) { - var dirs = path.split('/'); - var folder = dirs[dirs.length - 2]; - return path.startsWith('test/') || folder == 'test'; + const dirs = path.split('/'); + return dirs.indexOf('test') >= 0; +} + +/** + * Check if file is inside the build-system/babel-plugins test/fixture folder. + * @param {string} filePath + * @return {boolean} + */ +function isInBuildSystemFixtureFolder(filePath) { + const folder = path.dirname(filePath); + return folder.startsWith('build-system/babel-plugins') && + folder.includes('test/fixtures'); +} + +/** + * Strip Comments + * @param {string} contents + */ +function stripComments(contents) { + // Multi-line comments + contents = contents.replace(/\/\*(?!.*\*\/)(.|\n)*?\*\//g, function(match) { + // Preserve the newlines + const newlines = []; + for (let 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'); } /** @@ -363,16 +981,16 @@ function isInTestFolder(path) { * false otherwise */ function matchTerms(file, terms) { - var pathname = file.path; - var contents = file.contents.toString(); - var relative = file.relative; + const contents = stripComments(file.contents.toString()); + const {relative} = file; return Object.keys(terms).map(function(term) { - var fix; - var whitelist = terms[term].whitelist; + let fix; + const {whitelist, checkInTestFolder} = terms[term]; // 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))) { + if (isInBuildSystemFixtureFolder(relative) || (Array.isArray(whitelist) && + (whitelist.indexOf(relative) != -1 || + (isInTestFolder(relative) && !checkInTestFolder)))) { return false; } // we can't optimize building the `RegExp` objects early unless we build @@ -380,12 +998,27 @@ 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')); + const regex = new RegExp(term, 'gm'); + let index = 0; + let line = 1; + let column = 0; + let match; + let 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)); - if (typeof terms[term] == 'string') { + log(colors.red('Found forbidden: "' + match[0] + + '" in ' + relative + ':' + line + ':' + column)); + if (typeof terms[term] === 'string') { fix = terms[term]; } else { fix = terms[term].message; @@ -393,12 +1026,12 @@ function matchTerms(file, terms) { // log the possible fix information if provided for the term. if (fix) { - util.log(util.colors.blue(fix)); + log(colors.blue(fix)); } - util.log(util.colors.blue('==========')); - return true; + log(colors.blue('==========')); } - return false; + + return hasTerm; }).some(function(hasAnyTerm) { return hasAnyTerm; }); @@ -414,23 +1047,27 @@ function matchTerms(file, terms) { * false otherwise */ function hasAnyTerms(file) { - var pathname = file.path; - var basename = path.basename(pathname); - var hasTerms = false; - var hasSrcInclusiveTerms = false; - var has3pTerms = false; + const pathname = file.path; + const basename = path.basename(pathname); + let hasTerms = false; + let hasSrcInclusiveTerms = false; + let has3pTerms = false; hasTerms = matchTerms(file, forbiddenTerms); - var isTestFile = /^test-/.test(basename) || /^_init_tests/.test(basename); + const isTestFile = /^test-/.test(basename) || /^_init_tests/.test(basename) + || /_test\.js$/.test(basename); if (!isTestFile) { hasSrcInclusiveTerms = matchTerms(file, forbiddenTermsSrcInclusive); } - var is3pFile = /3p|ads/.test(pathname) || + const is3pFile = /\/(3p|ads)\//.test(pathname) || basename == '3p.js' || basename == 'style.js'; - if (is3pFile && !isTestFile) { + // Yet another reason to move ads/google/a4a somewhere else + const isA4A = /\/a4a\//.test(pathname); + const isRecaptcha = basename == 'recaptcha.js'; + if (is3pFile && !isRecaptcha && !isTestFile && !isA4A) { has3pTerms = matchTerms(file, forbidden3pTerms); } @@ -446,18 +1083,18 @@ function hasAnyTerms(file) { * content, false otherwise */ function isMissingTerms(file) { - var contents = file.contents.toString(); + const contents = file.contents.toString(); return Object.keys(requiredTerms).map(function(term) { - var filter = requiredTerms[term]; + const filter = requiredTerms[term]; if (!filter.test(file.path)) { return false; } - var matches = contents.match(new RegExp(term)); + const matches = contents.match(new RegExp(term)); if (!matches) { - util.log(util.colors.red('Did not find required: "' + term + + log(colors.red('Did not find required: "' + term + '" in ' + file.relative)); - util.log(util.colors.blue('==========')); + log(colors.blue('==========')); return true; } return false; @@ -471,32 +1108,28 @@ function isMissingTerms(file) { * any forbidden terms and log any errors found. */ function checkForbiddenAndRequiredTerms() { - var forbiddenFound = false; - var missingRequirements = false; + let forbiddenFound = false; + let 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; - }); - })) - .on('end', function() { - if (forbiddenFound) { - util.log(util.colors.blue( - 'Please remove these usages or consult with the AMP team.')); - } - if (missingRequirements) { - util.log(util.colors.blue( - 'Adding these terms (e.g. by adding a required LICENSE ' + + .pipe(through2.obj(function(file, enc, cb) { + forbiddenFound = hasAnyTerms(file) || forbiddenFound; + missingRequirements = isMissingTerms(file) || missingRequirements; + cb(); + })) + .on('end', function() { + if (forbiddenFound) { + log(colors.blue( + 'Please remove these usages or consult with the AMP team.')); + } + if (missingRequirements) { + log(colors.blue( + 'Adding these terms (e.g. by adding a required LICENSE ' + 'to the file)')); - } - if (forbiddenFound || missingRequirements) { - process.exit(1); - } - }); + } + if (forbiddenFound || missingRequirements) { + process.exit(1); + } + }); } gulp.task('presubmit', 'Run validation against files to check for forbidden ' + diff --git a/build-system/tasks/process-3p-github-pr.js b/build-system/tasks/process-3p-github-pr.js new file mode 100644 index 000000000000..5895c2552e6c --- /dev/null +++ b/build-system/tasks/process-3p-github-pr.js @@ -0,0 +1,331 @@ +/** + * Copyright 2018 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. + */ + +/** + * This script auto triage pull requests from 3P. + * It supports: + * 1. Triaging and 3P ad service integration PR. + */ + +'use strict'; +const argv = require('minimist')(process.argv.slice(2)); +const assert = require('assert'); +const BBPromise = require('bluebird'); +const colors = require('ansi-colors'); +const extend = require('util')._extend; +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const request = BBPromise.promisify(require('request')); + +const {GITHUB_ACCESS_TOKEN} = process.env; + +const isDryrun = argv.dryrun; + +let reviewer = ''; + +const REGEX_3P_INTEGRATION = new RegExp('3p/integration.js'); +const REGEX_3P_AD_JS = new RegExp('ads/[^/]+.js'); +const REGEX_3P_AD_MD = new RegExp('ads/[^/]+.md'); +const REGEX_3P_AD_CONFIG = new RegExp('ads/_config.js'); +const REGEX_3P_AD_EXAMPLE = new RegExp('examples/ads.amp.html'); +const REGEX_AD_MD = new RegExp('extensions/amp-ad/amp-ad.md'); + +const adIntegrationFileList = [ + REGEX_3P_INTEGRATION, + REGEX_3P_AD_JS, + REGEX_3P_AD_MD, + REGEX_3P_AD_CONFIG, + REGEX_3P_AD_EXAMPLE, + REGEX_AD_MD, +]; + +const internalContributors = [ + // Feel free to add your name here + // if you don't want your PR to be auto triaged. + 'bradfrizzell', + 'calebcordry', + 'glevitzky', + 'keithwrightbos', + 'lannka', + 'jasti', + 'rudygalfi', + 'zhouyx', +]; + +const reviewers = [ + // In rotation order + 'calebcordry', + 'zhouyx', + 'lannka', +]; + +const REF_DATE = new Date('May 13, 2018 00:00:00'); +const WEEK_DIFF = 604800000; + +const ANALYZE_OUTCOME = { + AD: 1, // Ad integration PR, ping the ad onduty person +}; + +const AD_COMMENT = 'Dear contributor! Thank you for the pull request. ' + + 'It looks like this PR is trying to add support to an ad network. \n \n' + + 'If this is your first time adding support for ' + + 'a new third-party ad service, please make sure your follow our ' + + '[developer guideline](https://github.com/ampproject/amphtml/blob/master/' + + 'ads/README.md#developer-guidelines-for-a-pull-request). \n \n' + + 'If you have not implemented it, we also highly recommend implementing ' + + 'the [renderStart API](https://github.com/ampproject/amphtml/blob/master/' + + 'ads/README.md#available-apis) to provide better user experience. ' + + 'Please let us know if there is any question. \n \n'; + +const defaultOption = { + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + qs: { + 'access_token': GITHUB_ACCESS_TOKEN, + }, +}; + +// we need around 14 batches to get more than 1k issues +const NUM_BATCHES = 14; + +/** + * Calculate the reviewer this week, based on rotation calendar + */ +function calculateReviewer() { + const now = Date.now(); + const diff = now - REF_DATE; + const week = diff / WEEK_DIFF; + const turn = Math.floor(week % 3); + return reviewers[turn]; +} + +/** + * Main function for auto triaging + */ +function processPRs() { + if (!GITHUB_ACCESS_TOKEN) { + log(colors.red('You have not set the ' + + 'GITHUB_ACCESS_TOKEN env var.')); + log(colors.green('See https://help.github.com/articles/' + + 'creating-an-access-token-for-command-line-use/ ' + + 'for instructions on how to create a github access token. We only ' + + 'need `public_repo` scope.')); + return; + } + + reviewer = calculateReviewer(); + + const arrayPromises = []; + // we need to pull issues in batches + for (let batch = 1; batch < NUM_BATCHES; batch++) { + arrayPromises.push(getIssues(batch)); + } + return BBPromise.all(arrayPromises) + .then(requests => [].concat.apply([], requests)) + .then(issues => { + const allIssues = issues; + const allTasks = []; + allIssues.forEach(function(issue) { + allTasks.push(handleIssue(issue)); + }); + return Promise.all(allTasks); + }).then(() => { + log(colors.blue('auto triaging succeed!')); + }); +} + +function handleIssue(issue) { + return isQualifiedPR(issue).then(outcome => { + return replyToPR(issue, outcome); + }); +} + +/** + * Fetches issues?page=${opt_page} + * + * @param {number=} opt_page + * @return {!Promise { + const issues = JSON.parse(res.body); + assert(Array.isArray(issues), 'issues must be an array.'); + return issues; + }); +} + +/** + * API call to get all changed files of a pull request. + * @param {!Object} pr + */ +function getPullRequestFiles(pr) { + const options = extend({}, defaultOption); + const {number} = pr; + options.url = 'https://api.github.com/repos/ampproject/amphtml/pulls/' + + `${number}/files`; + return request(options).then(res => { + const files = JSON.parse(res.body); + if (!Array.isArray(files)) { + return null; + } + return files; + }); +} + +/** + * Determine the type of a give pull request + * @param {?Array} files + */ +function analyzeChangedFiles(files) { + if (!files) { + return; + } + // Only support 3p ads integration files + const fileCount = files.length; + if (fileCount == 0) { + return null; + } + let matchFileCount = 0; + for (let i = 0; i < fileCount; i++) { + const fileName = files[i].filename; + for (let j = 0; j < adIntegrationFileList.length; j++) { + const regex = adIntegrationFileList[j]; + if (regex.test(fileName)) { + matchFileCount++; + continue; + } + } + } + const percentage = matchFileCount / fileCount; + if (percentage > 0.75 || matchFileCount >= 3) { + // Still need to check the matchFileCount because of incorrect rebase. + return ANALYZE_OUTCOME.AD; + } + return null; +} + +/** + * Determine if we need to reply to an issue + * @param {!Object} issue + */ +function isQualifiedPR(issue) { + // All issues are opened has no assignee + if (!issue.pull_request) { + // Is not a pull request + return Promise.resolve(null); + } + + const author = issue.user.login; + if (internalContributors.indexOf(author) > -1) { + // If it is a pull request from internal contributor + return Promise.resolve(null); + } + // get pull request reviewer API is not working as expected. Skip + + // Get changed files of this PR + return getPullRequestFiles(issue).then(files => { + return analyzeChangedFiles(files); + }); +} + +/** + * Auto reply + * @param {!Object} pr + * @param {ANALYZE_OUTCOME} outcome + */ +function replyToPR(pr, outcome) { + let promise = Promise.resolve(); + if (outcome == ANALYZE_OUTCOME.AD) { + promise = promise.then(() => { + // We should be good with rate limit given the number of + // 3p integration PRs today. + const comment = AD_COMMENT + `Thank you! Ping @${reviewer} for review`; + return applyComment(pr, comment); + }).then(() => { + return assignIssue(pr, [reviewer]); + }); + } + return promise; +} + +/** + * API call to comment on a give issue. + * @param {!Object} issue + * @param {string} comment + */ +function applyComment(issue, comment) { + const {number} = issue; + const options = extend({ + url: 'https://api.github.com/repos/ampproject/amphtml/issues/' + + `${number}/comments`, + method: 'POST', + body: JSON.stringify({ + 'body': comment, + }), + }, defaultOption); + if (isDryrun) { + log(colors.blue(`apply comment to PR #${number}, ` + + `comment is ${comment}`)); + return Promise.resolve(); + } + return request(options); +} + +/** + * API call to assign an issue with a list of assignees + * @param {!Object} issue + * @param {!Array} assignees + */ +function assignIssue(issue, assignees) { + const {number} = issue; + const options = extend({ + url: 'https://api.github.com/repos/ampproject/amphtml/issues/' + + `${number}/assignees`, + method: 'POST', + body: JSON.stringify({ + 'assignees': assignees, + }), + }, defaultOption); + if (isDryrun) { + log(colors.blue(`assign PR #${number}, ` + + `to ${assignees}`)); + return Promise.resolve(); + } + return request(options); +} + +gulp.task( + 'process-3p-github-pr', + 'Automatically triage 3P integration PRs', + processPRs, + { + options: { + dryrun: ' Generate process but don\'t push it out', + }, + } +); diff --git a/build-system/tasks/process-github-issues.js b/build-system/tasks/process-github-issues.js new file mode 100644 index 000000000000..610eac1cbc9c --- /dev/null +++ b/build-system/tasks/process-github-issues.js @@ -0,0 +1,416 @@ +/** + * 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. + */ +'use strict'; +const argv = require('minimist')(process.argv.slice(2)); +const assert = require('assert'); +const BBPromise = require('bluebird'); +const colors = require('ansi-colors'); +const extend = require('util')._extend; +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const request = BBPromise.promisify(require('request')); + +const {GITHUB_ACCESS_TOKEN} = process.env; + +const isDryrun = argv.dryrun; + +const issuesOptions = { + url: 'https://api.github.com/repos/ampproject/amphtml/issues', + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + qs: { + 'access_token': GITHUB_ACCESS_TOKEN, + }, +}; + +const milestoneOptions = { + url: 'https://api.github.com/repos/ampproject/amphtml/milestones', + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + qs: { + 'access_token': GITHUB_ACCESS_TOKEN, + }, +}; + +// 4 is the number for Milestone 'Backlog Bugs' +const MILESTONE_BACKLOG_BUGS = 4; +// 11 is the number for Milestone '3P Implementation' +const MILESTONE_3P_IMPLEMENTATION = 11; +// 12 is the number for Milestone 'Docs Updates' +const MILESTONE_DOCS_UPDATES = 12; +// By default we will assign 'Pending Triage' milestone, number 20 +const MILESTONE_PENDING_TRIAGE = 20; +// 22 is the number for Milestone 'Prioritized FRs' +const MILESTONE_PRIORITIZED_FRS = 22; +// 23 is the number for Milestone 'New FRs' +const MILESTONE_NEW_FRS = 23; +// 25 is the number for Milestone 'Good First Issues (GFI)' +const MILESTONE_GREAT_ISSUES = 25; +// days for biweekly updates +const BIWEEKLY_DAYS = 14; +// days for quarterly updates +const QUARTERLY_DAYS = 89; +// we need around 14 batches to get more than 1k issues +const NUM_BATCHES = 14; + +// We start processing the issues by checking token first +function processIssues() { + if (!GITHUB_ACCESS_TOKEN) { + log(colors.red('You have not set the ' + + 'GITHUB_ACCESS_TOKEN env var.')); + log(colors.green('See https://help.github.com/articles/' + + 'creating-an-access-token-for-command-line-use/ ' + + 'for instructions on how to create a github access token. We only ' + + 'need `public_repo` scope.')); + return; + } + return updateGitHubIssues().then(function() { + log(colors.blue('automation applied')); + }); +} +/** + * Fetches issues?page=${opt_page} + * + * @param {number=} opt_page + * @return {!Promise { + const issues = JSON.parse(res.body); + assert(Array.isArray(issues), 'issues must be an array.'); + return issues; + }); +} +/** + * Function goes through all the gitHub issues, + * gets all the Labels we are interested in, + * depending if missing milestone or label, + * tasks applied as per design go/ampgithubautomation + */ +function updateGitHubIssues() { + let promise = Promise.resolve(); + const arrayPromises = []; + // we need to pull issues in batches + for (let batch = 1; batch < NUM_BATCHES; batch++) { + arrayPromises.push(getIssues(batch)); + } + return BBPromise.all(arrayPromises) + .then(requests => [].concat.apply([], requests)) + .then(issues => { + const allIssues = issues; + allIssues.forEach(function(issue) { + const { + labels, + milestone, + assignee, + 'pull_request': pullRequest, + 'updated_at': issueLastUpdate, + } = issue; + let issueType; + let milestoneTitle; + let milestoneState; + let hasPriority = false; + let hasCategory = false; + let issueNewMilestone = MILESTONE_PENDING_TRIAGE; + let assigneeName = ''; + let biweeklyUpdate = true; + let quartelyUpdate = true; + // if an issue is a pull request, we'll skip it + if (pullRequest) { + if (isDryrun) { + log(colors.red(issue.number + ' is a pull request')); + } + return; + } + if (getLastUpdate(issueLastUpdate) > QUARTERLY_DAYS) { + quartelyUpdate = false; + biweeklyUpdate = false; + } else if (getLastUpdate(issueLastUpdate) > BIWEEKLY_DAYS) { + biweeklyUpdate = false; + } + // Get the assignee + if (assignee) { + assigneeName = '@' + assignee.login; + } + // Get the title and state of the milestone + if (milestone) { + milestoneTitle = milestone.title; + milestoneState = milestone.state; + issueNewMilestone = milestone.number; + } + // promise starts + promise = promise.then(function() { + log('Update ' + issue.number); + const updates = []; + // Get the labels we want to check + labels.forEach(function(label) { + if (label) { + // Check if the issues has type + if (label.name.startsWith('Type') || + label.name.startsWith('Related')) { + issueType = label.name; + } + // Check if the issues has Priority + if (label.name.startsWith('P0') || + label.name.startsWith('P1') || + label.name.startsWith('P2') || + label.name.startsWith('P3')) { + hasPriority = true; + if (label.name.startsWith('P0') || + label.name.startsWith('P1')) { + if (biweeklyUpdate == false) { + biweeklyUpdate = true; + updates.push(applyComment(issue, 'This is a high priority' + + ' issue but it hasn\'t been updated in awhile. ' + + assigneeName + ' Do you have any updates?')); + } + } else if (label.name.startsWith('P2') && + quartelyUpdate == false) { + quartelyUpdate = true; + updates.push(applyComment(issue, 'This issue hasn\'t been ' + + ' updated in awhile. ' + + assigneeName + ' Do you have any updates?')); + } + } + if (label.name.startsWith('Category') || + label.name.startsWith('Related to') || + label.name.startsWith('GFI') || + label.name.startsWith('good first issue')) { + hasCategory = true; + } + } + }); + // Milestone task: move issues from closed milestone + if (milestone) { + if (milestoneState === 'closed') { + issueNewMilestone = MILESTONE_BACKLOG_BUGS; + updates.push(applyMilestone(issue, issueNewMilestone)); + } + } + if (issueNewMilestone === MILESTONE_PENDING_TRIAGE) { + if (quartelyUpdate == false) { + quartelyUpdate = true; + updates.push(applyComment(issue, 'This issue seems to be in ' + + ' Pending Triage for awhile. ' + + assigneeName + ' Please triage this to ' + + 'an appropriate milestone.')); + } + } + // if issueType is not null, add correct milestones + if (issueType != null) { + if (issueNewMilestone === MILESTONE_PENDING_TRIAGE || + milestone == null) { + if (issueType === 'Type: Feature Request') { + issueNewMilestone = MILESTONE_NEW_FRS; + updates.push(applyMilestone(issue, issueNewMilestone)); + } else if (issueType === 'Related to: Documentation' || + issueType === 'Type: Design Review' || + issueType === 'Type: Status Update') { + issueNewMilestone = MILESTONE_DOCS_UPDATES; + updates.push(applyMilestone(issue, issueNewMilestone)); + } else if (issueType === 'Type: Bug' || + issueType === 'Related to: Flaky Tests') { + issueNewMilestone = MILESTONE_BACKLOG_BUGS; + updates.push(applyMilestone(issue, issueNewMilestone)); + } else if (milestone == null) { + updates.push(applyMilestone(issue, issueNewMilestone)); + } + } + } else if (milestone == null) { + updates.push(applyMilestone(issue, issueNewMilestone)); + } else if (issueNewMilestone === MILESTONE_PRIORITIZED_FRS || + issueNewMilestone === MILESTONE_NEW_FRS) { + updates.push(applyLabel(issue, 'Type: Feature Request')); + } else if (issueNewMilestone === MILESTONE_BACKLOG_BUGS || + milestoneTitle.startsWith('Sprint')) { + updates.push(applyLabel(issue, 'Type: Bug')); + } + // Apply default priority if no priority + if (hasPriority == false && + issueNewMilestone != MILESTONE_NEW_FRS && + issueNewMilestone !== MILESTONE_3P_IMPLEMENTATION && + issueNewMilestone !== MILESTONE_PENDING_TRIAGE && + milestone != null) { + updates.push(applyLabel(issue, 'P2: Soon')); + } + // Add comment with missing Category + if (hasCategory == false) { + if (issueNewMilestone === MILESTONE_PENDING_TRIAGE || + issueNewMilestone === MILESTONE_DOCS_UPDATES || + issueNewMilestone == null || + issueNewMilestone === MILESTONE_GREAT_ISSUES) { + if (isDryrun) { + log(colors.green('No comment needed ' + + ' for #' + issue.number)); + } + } else { + updates.push(applyComment(issue, + 'This issue doesn\'t have a category' + + ' which makes it harder for us to keep track of it. ' + + assigneeName + ' Please add an appropriate category.')); + } + } + return Promise.all(updates); + }); + }); + return promise; + }); +} + +/** + * @param {string} issue + * @param {number} milestoneNumber + * @return {!Promise<*>} + */ +function applyMilestone(issue, milestoneNumber) { + const options = extend({}, milestoneOptions); + options.qs = { + 'state': 'open', + 'per_page': 100, + 'access_token': GITHUB_ACCESS_TOKEN, + }; + + issue.milestone = milestoneNumber; + if (isDryrun) { + log(colors.green('Milestone applied ' + milestoneNumber + + ' for #' + issue.number)); + return; + } else { + return createGithubRequest('/issues/' + issue.number,'PATCH', + issue.milestone, 'milestone'); + } +} + +/** + * @param {string} issue + * @param {string} label + * @return {!Promise<*>} + */ +function applyLabel(issue, label) { + const options = extend({}, issuesOptions); + options.qs = { + 'state': 'open', + 'per_page': 100, + 'access_token': GITHUB_ACCESS_TOKEN, + }; + if (isDryrun) { + log(colors.green('Label applied ' + + label + ' for #' + issue.number)); + return; + } else { + return createGithubRequest('/issues/' + issue.number + '/labels','POST', + [label], 'label'); + } + +} + +/** + * @param {string} issue + * @param {string} comment + * @return {!Promise<*>} + */ +function applyComment(issue, comment) { + const options = extend({}, issuesOptions); + options.qs = { + 'state': 'open', + 'per_page': 100, + 'access_token': GITHUB_ACCESS_TOKEN, + }; + // delay the comment request so we don't reach github rate limits requests + const promise = new Promise(resolve => setTimeout(resolve, 120000)); + return promise.then(function() { + if (isDryrun) { + log(colors.blue('waited 2 minutes to avoid gh rate limits')); + log(colors.green('Comment applied after ' + + 'waiting 2 minutes to avoid github rate limits: ' + comment + + ' for #' + issue.number)); + return; + } else { + createGithubRequest('/issues/' + issue.number + '/comments','POST', + comment, 'comment'); + } + }); +} +// calculate number of days since the latest update +function getLastUpdate(issueLastUpdate) { + const t = new Date(); + const splits = issueLastUpdate.split('-', 3); + const exactDay = splits[2].split('T', 1); + const firstDate = Date.UTC(splits[0],splits[1],exactDay[0]); + const secondDate = Date.UTC(t.getFullYear(),t.getMonth() + 1,t.getDate()); + const diff = Math.abs((firstDate.valueOf() - + secondDate.valueOf()) / (24 * 60 * 60 * 1000)); + return diff; +} + +/** + * Function pushes the updates requested based on the path received + * @param {string} path + * @param {string=} opt_method + * @param {*} opt_data + * @param {string} typeRequest + * @return {!Promise<*>} + */ +function createGithubRequest(path, opt_method, opt_data, typeRequest) { + const options = { + url: 'https://api.github.com/repos/ampproject/amphtml' + path, + body: {}, + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + qs: { + 'access_token': GITHUB_ACCESS_TOKEN, + }, + }; + if (opt_method) { + options.method = opt_method; + } + if (opt_data) { + options.json = true; + if (typeRequest === 'milestone') { + options.body.milestone = opt_data; + } else if (typeRequest === 'comment') { + options.body.body = opt_data; + } else { + options.body = opt_data; + } + } + return request(options); +} + +gulp.task( + 'process-github-issues', + 'Automatically updates the labels ' + + 'and milestones of all open issues at github.com/ampproject/amphtml.', + processIssues, + { + options: { + dryrun: ' Generate process but don\'t push it out', + }, + } +); diff --git a/build-system/tasks/release-tagging.js b/build-system/tasks/release-tagging.js new file mode 100644 index 000000000000..e02e68533abb --- /dev/null +++ b/build-system/tasks/release-tagging.js @@ -0,0 +1,222 @@ +/** + * 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. + */ +'use strict'; + +const argv = require('minimist')(process.argv.slice(2)); +const BBPromise = require('bluebird'); +const colors = require('ansi-colors'); +const fs = require('fs-extra'); +const git = require('gulp-git'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const request = BBPromise.promisify(require('request')); + +const {GITHUB_ACCESS_TOKEN} = process.env; +const gitExec = BBPromise.promisify(git.exec); + +const isDryrun = argv.dryrun; +const verbose = (argv.verbose || argv.v); + +const LABELS = { + 'canary': 'PR use: In Canary', + 'prod': 'PR use: In Production', +}; + + +/** + * @param {string} type Either of "canary" or "prod". + * @param {string} dir Working dir. + * @return {!Promise} + */ +function releaseTagFor(type, dir) { + log('Tag release for: ', type); + let promise = Promise.resolve(); + const ampDir = dir + '/amphtml'; + + // Fetch tag. + let tag; + promise = promise.then(function() { + return githubRequest('/releases'); + }).then(res => { + const array = JSON.parse(res.body); + for (let i = 0; i < array.length; i++) { + const release = array[i]; + const releaseType = release.prerelease ? 'canary' : 'prod'; + if (releaseType == type) { + tag = release.tag_name; + break; + } + } + }); + + // Checkout tag. + promise = promise.then(function() { + log('Git tag: ', tag); + return gitExec({ + cwd: ampDir, + args: 'checkout ' + tag, + }); + }); + + // Log. + const pullRequests = []; + promise = promise.then(function() { + const date = new Date(); + date.setDate(date.getDate() - 15); + const dateIso = date.toISOString().split('T')[0]; + return gitExec({ + cwd: ampDir, + args: 'log --pretty=oneline --since=' + dateIso, + }); + }).then(function(output) { + const lines = output.split('\n'); + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + const paren = line.lastIndexOf('('); + line = paren != -1 ? line.substring(paren) : ''; + if (!line) { + continue; + } + const match = line.match(/\(\#(\d+)\)/); + if (match && match[1]) { + pullRequests.push(match[1]); + } + } + }); + + // Update. + const label = LABELS[type]; + promise = promise.then(function() { + log('Update ' + pullRequests.length + ' pull requests'); + const updates = []; + pullRequests.forEach(function(pullRequest) { + updates.push(applyLabel(pullRequest, label)); + }); + return Promise.all(updates); + }); + + return promise.then(function() { + log(colors.green('Tag release for ' + type + ' done.')); + }); +} + +/** + * @param {string} pullRequest + * @param {string} label + * @return {!Promise} + */ +function applyLabel(pullRequest, label) { + if (verbose && isDryrun) { + log('Apply label ' + label + ' for #' + pullRequest); + } + if (isDryrun) { + return Promise.resolve(); + } + return githubRequest( + '/issues/' + pullRequest + '/labels', + 'POST', + [label]).then(function() { + if (verbose) { + log(colors.green( + 'Label applied ' + label + ' for #' + pullRequest)); + } + }); +} + +/** + * @param {string} dir Working dir. + * @return {!Promise} + */ +function gitFetch(dir) { + const ampDir = dir + '/amphtml'; + let clonePromise; + if (fs.existsSync(ampDir)) { + clonePromise = Promise.resolve(); + } else { + clonePromise = gitExec({ + cwd: dir, + args: 'clone https://github.com/ampproject/amphtml.git', + }); + } + return clonePromise.then(function() { + return gitExec({ + cwd: ampDir, + args: 'fetch --tags', + }); + }); +} + +/** + * @param {string} path + * @param {string=} opt_method + * @param {*} opt_data + * @return {!Promise<*>} + */ +function githubRequest(path, opt_method, opt_data) { + const options = { + url: 'https://api.github.com/repos/ampproject/amphtml' + path, + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + qs: { + 'access_token': GITHUB_ACCESS_TOKEN, + }, + }; + if (opt_method) { + options.method = opt_method; + } + if (opt_data) { + options.json = true; + options.body = opt_data; + } + return request(options); +} + +/** + * @return {!Promise} + */ +function releaseTag() { + let promise = Promise.resolve(); + + const dir = 'build/tagging'; + log('Work dir: ', dir); + fs.mkdirpSync(dir); + promise = promise.then(function() { + return gitFetch(dir); + }); + + const type = argv.type || 'all'; + if (type == 'all' || type == 'canary') { + promise = promise.then(function() { + return releaseTagFor('canary', dir); + }); + } + if (type == 'all' || type == 'prod') { + promise = promise.then(function() { + return releaseTagFor('prod', dir); + }); + } + return promise; +} + + +gulp.task('release:tag', 'Tag the releases in pull requests', releaseTag, { + options: { + dryrun: ' Generate update log but dont push it out', + type: ' Either of "canary", "prod" or "all". Default is "all".', + }, +}); diff --git a/build-system/tasks/runtime-test.js b/build-system/tasks/runtime-test.js new file mode 100644 index 000000000000..0640cb44b066 --- /dev/null +++ b/build-system/tasks/runtime-test.js @@ -0,0 +1,674 @@ +/** + * 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. + */ +'use strict'; + +const argv = require('minimist')(process.argv.slice(2)); +const colors = require('ansi-colors'); +const config = require('../config'); +const deglob = require('globs-to-files'); +const findImports = require('find-imports'); +const fs = require('fs'); +const gulp = require('gulp-help')(require('gulp')); +const Karma = require('karma').Server; +const karmaDefault = require('./karma.conf'); +const log = require('fancy-log'); +const minimatch = require('minimatch'); +const Mocha = require('mocha'); +const opn = require('opn'); +const path = require('path'); +const webserver = require('gulp-webserver'); +const {app} = require('../test-server'); +const {createCtrlcHandler, exitCtrlcHandler} = require('../ctrlcHandler'); +const {exec, getStdout} = require('../exec'); +const {gitDiffNameOnlyMaster} = require('../git'); + +const {green, yellow, cyan, red, bold} = colors; + +const preTestTasks = argv.nobuild ? [] : ( + (argv.unit || argv.a4a || argv['local-changes']) ? ['css'] : ['build']); +const extensionsCssMapPath = 'EXTENSIONS_CSS_MAP'; + +/** + * Read in and process the configuration settings for karma + * @return {!Object} Karma configuration + */ +function getConfig() { + if (argv.safari) { + return Object.assign({}, karmaDefault, {browsers: ['Safari']}); + } + if (argv.firefox) { + return Object.assign({}, karmaDefault, {browsers: ['Firefox']}); + } + if (argv.edge) { + return Object.assign({}, karmaDefault, {browsers: ['Edge']}); + } + if (argv.ie) { + return Object.assign({}, karmaDefault, {browsers: ['IE']}); + } + if (argv.headless) { + return Object.assign({}, karmaDefault, + {browsers: ['Chrome_no_extensions_headless']}); + } + if (argv.saucelabs || argv.saucelabs_lite) { + if (!process.env.SAUCE_USERNAME) { + throw new Error('Missing SAUCE_USERNAME Env variable'); + } + if (!process.env.SAUCE_ACCESS_KEY) { + throw new Error('Missing SAUCE_ACCESS_KEY Env variable'); + } + return Object.assign({}, karmaDefault, { + reporters: ['super-dots', 'saucelabs', 'karmaSimpleReporter'], + browsers: argv.saucelabs ? [ + // With --saucelabs, integration tests are run on this set of browsers. + 'SL_Chrome_67', + 'SL_Firefox_61', + 'SL_Safari_11', + // TODO(rsimha, #16687): Enable after Sauce disconnects are resolved. + // 'SL_Chrome_Android_7', + // 'SL_Chrome_45', + // 'SL_Android_6', + // 'SL_iOS_11', + // 'SL_Edge_17', + // 'SL_IE_11', + ] : [ + // With --saucelabs_lite, a subset of the unit tests are run. + // Only browsers that support chai-as-promised may be included below. + // TODO(rsimha-amp): Add more browsers to this list. #6039. + 'SL_Safari_11', + ], + }); + } + return karmaDefault; +} + +/** + * Returns an array of ad types. + * @return {!Array} + */ +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']; + + // Add all other ad types + const files = fs.readdirSync('./ads/'); + for (let 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 (let j = 0; j < expanded.length; j++) { + adTypes.push(expanded[j]); + } + } else { + adTypes.push(adType); + } + } + } + return adTypes; +} + +/** + * Mitigates https://github.com/karma-runner/karma-sauce-launcher/issues/117 + * by refreshing the wd cache so that Karma can launch without an error. + */ +function refreshKarmaWdCache() { + exec('node ./node_modules/wd/scripts/build-browser-scripts.js'); +} + +/** + * Prints help messages for args if tests are being run for local development. + */ +function printArgvMessages() { + const argvMessages = { + safari: 'Running tests on Safari.', + firefox: 'Running tests on Firefox.', + ie: 'Running tests on IE.', + edge: 'Running tests on Edge.', + saucelabs: 'Running integration tests on Sauce Labs browsers.', + saucelabs_lite: 'Running tests on a subset of Sauce Labs browsers.', // eslint-disable-line google-camelcase/google-camelcase + nobuild: 'Skipping build.', + watch: 'Enabling watch mode. Editing and saving a file will cause the' + + ' tests for that file to be re-run in the same browser instance.', + verbose: 'Enabling verbose mode. Expect lots of output!', + testnames: 'Listing the names of all tests being run.', + files: 'Running tests in the file(s): ' + cyan(argv.files), + integration: 'Running only the integration tests. Prerequisite: ' + + cyan('gulp build'), + 'dev_dashboard': 'Only running tests for the Dev Dashboard.', + unit: 'Running only the unit tests. Prerequisite: ' + cyan('gulp css'), + a4a: 'Running only A4A tests.', + compiled: 'Running tests against minified code.', + grep: 'Only running tests that match the pattern "' + + cyan(argv.grep) + '".', + coverage: 'Running tests in code coverage mode.', + headless: 'Running tests in a headless Chrome window.', + 'local-changes': 'Running unit tests directly affected by the files' + + ' changed in the local branch.', + }; + if (!process.env.TRAVIS) { + log(green('Run'), cyan('gulp help'), + green('to see a list of all test flags.')); + log(green('⤷ Use'), cyan('--nohelp'), + green('to silence these messages.')); + if (!argv.unit && !argv.integration && !argv.files && !argv.a4a && + !argv['local-changes'] && !argv.dev_dashboard) { + log(green('Running all tests.')); + log(green('⤷ Use'), cyan('--unit'), green('or'), cyan('--integration'), + green('to run just the unit tests or integration tests.')); + log(green('⤷ Use'), cyan('--local-changes'), + green('to run unit tests from files commited to the local branch.')); + } + if (!argv.testnames && !argv.files && !argv['local-changes']) { + log(green('⤷ Use'), cyan('--testnames'), + green('to see the names of all tests being run.')); + } + if (!argv.headless) { + log(green('⤷ Use'), cyan('--headless'), + green('to run tests in a headless Chrome window.')); + } + if (!argv.compiled) { + log(green('Running tests against unminified code.')); + } + Object.keys(argv).forEach(arg => { + const message = argvMessages[arg]; + if (message) { + log(yellow('--' + arg + ':'), green(message)); + } + }); + } +} + +/** + * Returns true if the given file is a unit test. + * + * @param {string} file + * @return {boolean} + */ +function isUnitTest(file) { + return config.unitTestPaths.some(pattern => { + return minimatch(file, pattern); + }); +} + +/** + * Returns the list of files imported by a JS file + * + * @param {string} jsFile + * @return {!Array} + */ +function getImports(jsFile) { + const imports = findImports([jsFile], { + flatten: true, + packageImports: false, + absoluteImports: true, + relativeImports: true, + }); + const files = []; + const rootDir = path.dirname(path.dirname(__dirname)); + const jsFileDir = path.dirname(jsFile); + imports.forEach(function(file) { + const fullPath = path.resolve(jsFileDir, file) + '.js'; + if (fs.existsSync(fullPath)) { + const relativePath = path.relative(rootDir, fullPath); + files.push(relativePath); + } + }); + return files; +} + +/** + * Returns true if the test file should be run for any one of the source files. + * + * @param {string} testFile + * @param {!Array} srcFiles + * @return {boolean} + */ +function shouldRunTest(testFile, srcFiles) { + const filesImported = getImports(testFile); + return filesImported.filter(function(file) { + return srcFiles.includes(file); + }).length > 0; +} + +/** + * Retrieves the set of unit tests that should be run for a set of source files. + * + * @param {!Array} srcFiles + * @return {!Array} + */ +function getTestsFor(srcFiles) { + const rootDir = path.dirname(path.dirname(__dirname)); + const allUnitTests = deglob.sync(config.unitTestPaths); + return allUnitTests.filter(testFile => { + return shouldRunTest(testFile, srcFiles); + }).map(fullPath => path.relative(rootDir, fullPath)); +} + +/** + * Adds an entry that maps a CSS file to a JS file + * + * @param {!Object} cssData + * @param {string} cssBinaryName + * @param {!Object} cssJsFileMap + */ +function addCssJsEntry(cssData, cssBinaryName, cssJsFileMap) { + const cssFilePath = 'extensions/' + cssData['name'] + '/' + + cssData['version'] + '/' + cssBinaryName + '.css'; + const jsFilePath = 'build/' + cssBinaryName + '-' + + cssData['version'] + '.css.js'; + cssJsFileMap[cssFilePath] = jsFilePath; +} + +/** + * Extracts a mapping from CSS files to JS files from a well known file + * generated during `gulp css`. + * + * @return {!Object} + */ +function extractCssJsFileMap() { + if (!fs.existsSync(extensionsCssMapPath)) { + log(red('ERROR:'), 'Could not find the file', + cyan(extensionsCssMapPath) + '.'); + log('Make sure', cyan('gulp css'), 'was run prior to this.'); + process.exit(); + } + const extensionsCssMap = fs.readFileSync(extensionsCssMapPath, 'utf8'); + const extensionsCssMapJson = JSON.parse(extensionsCssMap); + const extensions = Object.keys(extensionsCssMapJson); + const cssJsFileMap = {}; + extensions.forEach(extension => { + const cssData = extensionsCssMapJson[extension]; + if (cssData['hasCss']) { + addCssJsEntry(cssData, cssData['name'], cssJsFileMap); + if (cssData.hasOwnProperty('cssBinaries')) { + const cssBinaries = cssData['cssBinaries']; + cssBinaries.forEach(cssBinary => { + addCssJsEntry(cssData, cssBinary, cssJsFileMap); + }); + } + } + }); + return cssJsFileMap; +} + +/** + * Retrieves the set of JS source files that import the given CSS file. + * + * @param {string} cssFile + * @param {!Object} cssJsFileMap + * @return {!Array} + */ +function getJsFilesFor(cssFile, cssJsFileMap) { + const jsFiles = []; + if (cssJsFileMap.hasOwnProperty(cssFile)) { + const cssFileDir = path.dirname(cssFile); + const jsFilesInDir = fs.readdirSync(cssFileDir).filter(file => { + return path.extname(file) == '.js'; + }); + jsFilesInDir.forEach(jsFile => { + const jsFilePath = cssFileDir + '/' + jsFile; + if (getImports(jsFilePath).includes(cssJsFileMap[cssFile])) { + jsFiles.push(jsFilePath); + } + }); + } + return jsFiles; +} + +/** + * Extracts the list of unit tests to run based on the changes in the local + * branch. + * + * @return {!Array} + */ +function unitTestsToRun() { + const cssJsFileMap = extractCssJsFileMap(); + const filesChanged = gitDiffNameOnlyMaster(); + const testsToRun = []; + let srcFiles = []; + filesChanged.forEach(file => { + if (isUnitTest(file)) { + testsToRun.push(file); + } else if (path.extname(file) == '.js') { + srcFiles = srcFiles.concat([file]); + } else if (path.extname(file) == '.css') { + srcFiles = srcFiles.concat(getJsFilesFor(file, cssJsFileMap)); + } + }); + if (srcFiles.length > 0) { + log(green('INFO: ') + 'Determining which unit tests to run...'); + const moreTestsToRun = getTestsFor(srcFiles); + moreTestsToRun.forEach(test => { + if (!testsToRun.includes(test)) { + testsToRun.push(test); + } + }); + } + return testsToRun; +} + +/** + * Runs all the tests. + */ +function runTests() { + + if (argv.dev_dashboard) { + + const mocha = new Mocha(); + + // Add our files + const allDevDashboardTests = deglob.sync(config.devDashboardTestPaths); + allDevDashboardTests.forEach(file => { + mocha.addFile(file); + }); + + // Create our deffered + let resolver; + const deferred = new Promise(resolverIn => {resolver = resolverIn;}); + + // Listen for Ctrl + C to cancel testing + const handlerProcess = createCtrlcHandler('test'); + + // Run the tests. + mocha.run(function(failures) { + if (failures) { + process.exit(1); + } + resolver(); + }); + return deferred.then(() => exitCtrlcHandler(handlerProcess)); + } + + if (!argv.integration && process.env.AMPSAUCE_REPO) { + console./* OK*/info('Deactivated for ampsauce repo'); + } + + if (argv.saucelabs && !argv.integration) { + log(red('ERROR:'), 'Only integration tests may be run on the full set of ' + + 'Sauce Labs browsers'); + log('Use', cyan('--saucelabs'), 'with', cyan('--integration')); + process.exit(); + } + + const c = getConfig(); + + if (argv.watch || argv.w) { + c.singleRun = false; + } + + if (argv.verbose || argv.v) { + c.client.captureConsole = true; + c.client.verboseLogging = true; + } + + if (!process.env.TRAVIS && (argv.testnames || argv['local-changes'])) { + c.reporters = ['mocha']; + } + + // Exclude chai-as-promised from runs on the full set of sauce labs browsers. + // See test/chai-as-promised/chai-as-promised.js for why this is necessary. + c.files = argv.saucelabs ? [] : config.chaiAsPromised; + + if (argv.files) { + c.files = c.files.concat(config.commonIntegrationTestPaths, argv.files); + if (!argv.saucelabs && !argv.saucelabs_lite) { + c.reporters = ['mocha']; + } + } else if (argv['local-changes']) { + const testsToRun = unitTestsToRun(); + if (testsToRun.length == 0) { + log(green('INFO: ') + + 'No unit tests were directly affected by local changes.'); + return Promise.resolve(); + } else { + log(green('INFO: ') + 'Running the following unit tests:'); + testsToRun.forEach(test => { + log(cyan(test)); + }); + } + c.files = c.files.concat(config.commonUnitTestPaths, testsToRun); + } else if (argv.integration) { + c.files = c.files.concat( + config.commonIntegrationTestPaths, config.integrationTestPaths); + } else if (argv.unit) { + if (argv.saucelabs_lite) { + c.files = c.files.concat( + config.commonUnitTestPaths, config.unitTestOnSaucePaths); + } else { + c.files = c.files.concat( + config.commonUnitTestPaths, config.unitTestPaths); + } + } else if (argv.a4a) { + c.files = c.files.concat(config.a4aTestPaths); + } else { + c.files = c.files.concat(config.testPaths); + } + + // c.client is available in test browser via window.parent.karma.config + c.client.amp = { + useCompiledJs: !!argv.compiled, + saucelabs: (!!argv.saucelabs) || (!!argv.saucelabs_lite), + singlePass: !!argv.single_pass, + adTypes: getAdTypes(), + mochaTimeout: c.client.mocha.timeout, + propertiesObfuscated: !!argv.single_pass, + }; + + if (argv.compiled) { + process.env.SERVE_MODE = 'compiled'; + } else { + process.env.SERVE_MODE = 'default'; + } + + if (argv.grep) { + c.client.mocha = { + 'grep': argv.grep, + }; + } + + if (argv.coverage) { + c.client.captureConsole = false; + c.browserify.transform = [ + ['babelify', { + plugins: [ + ['babel-plugin-istanbul', { + exclude: [ + './ads/**/*.js', + './third_party/**/*.js', + './test/**/*.js', + './extensions/**/test/**/*.js', + './testing/**/*.js', + ], + }], + ], + }], + ]; + c.plugins.push('karma-coverage-istanbul-reporter'); + c.reporters = c.reporters.concat(['coverage-istanbul']); + c.coverageIstanbulReporter = { + dir: 'test/coverage', + reports: process.env.TRAVIS ? ['lcov'] : ['html', 'text', 'text-summary'], + }; + } + + // Run fake-server to test XHR responses. + process.env.AMP_TEST = 'true'; + const server = gulp.src(process.cwd(), {base: '.'}).pipe(webserver({ + port: 31862, + host: 'localhost', + directoryListing: true, + middleware: [app], + }).on('kill', function() { + log(yellow('Shutting down test responses server on localhost:31862')); + })); + log(yellow( + 'Started test responses server on localhost:31862')); + + // Listen for Ctrl + C to cancel testing + const handlerProcess = createCtrlcHandler('test'); + + // Avoid Karma startup errors + refreshKarmaWdCache(); + + // On Travis, collapse the summary printed by the 'karmaSimpleReporter' + // reporter for full unit test runs, since it likely contains copious amounts + // of logs. + const shouldCollapseSummary = process.env.TRAVIS && c.client.captureConsole && + c.reporters.includes('karmaSimpleReporter') && !argv['local-changes']; + const sectionMarker = + (argv.saucelabs || argv.saucelabs_lite) ? 'saucelabs' : 'local'; + + let resolver; + const deferred = new Promise(resolverIn => {resolver = resolverIn;}); + new Karma(c, function(exitCode) { + if (shouldCollapseSummary) { + console./* OK*/log('travis_fold:end:console_errors_' + sectionMarker); + } + server.emit('kill'); + if (exitCode) { + log( + red('ERROR:'), + yellow('Karma test failed with exit code ' + exitCode)); + } + if (argv.coverage) { + if (process.env.TRAVIS) { + const codecovCmd = + './node_modules/.bin/codecov --file=test/coverage/lcov.info'; + let flags = ''; + if (argv.unit) { + flags = ' --flags=unit_tests'; + } else if (argv.integration) { + flags = ' --flags=integration_tests'; + } + log(green('INFO: ') + 'Uploading code coverage report to ' + + cyan('https://codecov.io/gh/ampproject/amphtml') + ' by running ' + + cyan(codecovCmd + flags) + '...'); + const output = getStdout(codecovCmd + flags); + const viewReportPrefix = 'View report at: '; + const viewReport = output.match(viewReportPrefix + '.*'); + if (viewReport && viewReport.length > 0) { + log(green('INFO: ') + viewReportPrefix + + cyan(viewReport[0].replace(viewReportPrefix, ''))); + } else { + log(yellow('WARNING: ') + + 'Code coverage report upload may have failed:\n' + + yellow(output)); + } + } else { + const coverageReportUrl = + 'file://' + path.resolve('test/coverage/index.html'); + log(green('INFO: ') + 'Generated code coverage report at ' + + cyan(coverageReportUrl)); + opn(coverageReportUrl, {wait: false}); + } + } + // TODO(rsimha, 14814): Remove after Karma / Sauce ticket is resolved. + if (process.env.TRAVIS) { + setTimeout(() => { + process.exit(exitCode); + }, 5000); + } else { + process.exitCode = exitCode; + } + resolver(); + }).on('run_start', function() { + if (argv.saucelabs || argv.saucelabs_lite) { + log(green('Running tests on ' + c.browsers.length + + ' Sauce Labs browser(s)...')); + } else { + log(green('Running tests locally...')); + } + }).on('run_complete', function() { + if (shouldCollapseSummary) { + console./* OK*/log(bold(red('Console errors:')), + 'Expand this section and fix all errors printed by your tests.'); + console./* OK*/log('travis_fold:start:console_errors_' + sectionMarker); + } + }).on('browser_complete', function(browser) { + const result = browser.lastResult; + // Prevent cases where Karma detects zero tests and still passes. #16851. + if (result.total == 0) { + log(red('ERROR: Zero tests detected by Karma. Something went wrong.')); + if (!argv.watch) { + process.exit(1); + } + } + if (shouldCollapseSummary) { + let message = browser.name + ': '; + message += 'Executed ' + (result.success + result.failed) + + ' of ' + result.total + ' (Skipped ' + result.skipped + ') '; + if (result.failed === 0) { + message += green('SUCCESS'); + } else { + message += red(result.failed + ' FAILED'); + } + message += '\n'; + console./* OK*/log('\n'); + log(message); + } + }).start(); + return deferred.then(() => exitCtrlcHandler(handlerProcess)); +} + +/** + * Run tests after applying the prod / canary AMP config to the runtime. + */ +gulp.task('test', 'Runs tests', preTestTasks, function() { + // TODO(alanorozco): Come up with a more elegant check? + global.AMP_TESTING = true; + + if (!argv.nohelp) { + printArgvMessages(); + } + return runTests(); +}, { + options: { + 'verbose': ' With logging enabled', + 'testnames': ' Lists the name of each test being run', + 'watch': ' Watches for changes in files, runs corresponding test(s)', + 'saucelabs': ' Runs integration tests on saucelabs (requires setup)', + 'saucelabs_lite': ' Runs tests on a subset of saucelabs browsers ' + + '(requires setup)', + 'safari': ' Runs tests on Safari', + 'firefox': ' Runs tests on Firefox', + 'edge': ' Runs tests on Edge', + 'ie': ' Runs tests on IE', + 'unit': ' Run only unit tests.', + 'integration': ' Run only integration tests.', + 'dev_dashboard': ' Run only the dev dashboard tests. ' + + 'Reccomend using with --nobuild', + 'compiled': ' Changes integration tests to use production JS ' + + 'binaries for execution', + 'grep': ' Runs tests that match the pattern', + 'files': ' Runs tests for specific files', + 'nohelp': ' Silence help messages that are printed prior to test run', + 'a4a': ' Runs all A4A tests', + 'coverage': ' Run tests in code coverage mode', + 'headless': ' Run tests in a headless Chrome window', + 'local-changes': ' Run unit tests directly affected by the files ' + + 'changed in the local branch', + }, +}); diff --git a/build-system/tasks/serve.js b/build-system/tasks/serve.js index f864b35d3a6c..43f9b0b10a63 100644 --- a/build-system/tasks/serve.js +++ b/build-system/tasks/serve.js @@ -13,31 +13,95 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; -var argv = require('minimist')(process.argv.slice(2)); -var gulp = require('gulp-help')(require('gulp')); -var gls = require('gulp-live-server'); -var path = require('path'); -var util = require('gulp-util'); +const argv = require('minimist')(process.argv.slice(2)); +const colors = require('ansi-colors'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const nodemon = require('nodemon'); -var port = argv.port || process.env.PORT || 8000; +const host = argv.host || 'localhost'; +const port = argv.port || process.env.PORT || 8000; +const useHttps = argv.https != undefined; +const quiet = argv.quiet != undefined; +const sendCachingHeaders = argv.cache != undefined; +const noCachingExtensions = argv.noCachingExtensions != undefined; +const disableDevDashboardCache = argv.disable_dev_dashboard_cache || false; /** * Starts a simple http server at the repository root */ function serve() { - var serverScript = path.join(__dirname, '../server.js') - var server = gls.new([serverScript, (argv.path || '/'), port]); - server.start(); - util.log(util.colors.yellow( - 'Run `gulp build` then go to http://localhost:' + port + '/examples.build/article.amp.max.html' - )); + // Get the serve mode + if (argv.compiled) { + process.env.SERVE_MODE = 'compiled'; + log(colors.green('Serving minified js')); + } else if (argv.cdn) { + process.env.SERVE_MODE = 'cdn'; + log(colors.green('Serving current prod js')); + } else { + process.env.SERVE_MODE = 'default'; + log(colors.green('Serving unminified js')); + } + + const config = { + script: require.resolve('../server.js'), + watch: [ + require.resolve('../app.js'), + require.resolve('../server.js'), + ], + env: { + 'NODE_ENV': 'development', + 'SERVE_PORT': port, + 'SERVE_HOST': host, + 'SERVE_USEHTTPS': useHttps, + 'SERVE_PROCESS_ID': process.pid, + 'SERVE_QUIET': quiet, + 'SERVE_CACHING_HEADERS': sendCachingHeaders, + 'SERVE_EXTENSIONS_WITHOUT_CACHING': noCachingExtensions, + 'DISABLE_DEV_DASHBOARD_CACHE': disableDevDashboardCache, + }, + stdout: !quiet, + }; + + if (argv.inspect) { + Object.assign(config, { + execMap: { + js: 'node --inspect', + }, + }); + } + + nodemon(config).once('quit', function() { + log(colors.green('Shutting down server')); + }); } -gulp.task('serve', 'Serves content in root dir over http://localhost:' + - port + '/', serve, { - options: { - 'port': ' Specifies alternative port to use instead of default (8000)' +process.on('SIGINT', function() { + process.exit(); +}); + +gulp.task( + 'serve', + 'Serves content in root dir over ' + getHost() + '/', + serve, + { + options: { + 'host': ' Hostname or IP address to bind to (default: localhost)', + 'port': ' Specifies alternative port (default: 8000)', + 'https': ' Use HTTPS server (default: false)', + 'quiet': ' Do not log HTTP requests (default: false)', + 'cache': ' Make local resources cacheable by the browser ' + + '(default: false)', + 'inspect': ' Run nodemon in `inspect` mode', + 'disable_dev_dashboard_cache': 'Disables the dev dashboard cache', + }, } - } ); + +function getHost() { + return (useHttps ? 'https' : 'http') + '://' + host + ':' + port; +} + +exports.serve = serve; diff --git a/build-system/tasks/size.js b/build-system/tasks/size.js index 09ac2f7826fa..1c5cac0ed870 100644 --- a/build-system/tasks/size.js +++ b/build-system/tasks/size.js @@ -13,30 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; -var del = require('del'); -var fs = require('fs'); -var gulp = require('gulp-help')(require('gulp')); -var gzipSize = require('gzip-size'); -var prettyBytes = require('pretty-bytes'); -var table = require('text-table'); -var through = require('through2'); -var util = require('gulp-util'); +const del = require('del'); +const fs = require('fs'); +const gulp = require('gulp-help')(require('gulp')); +const gzipSize = require('gzip-size'); +const PluginError = require('plugin-error'); +const prettyBytes = require('pretty-bytes'); +const table = require('text-table'); +const through = require('through2'); -var tempFolderName = '__size-temp'; +const tempFolderName = '__size-temp'; -var MAX_FILE_SIZE_POS = 0; -var MIN_FILE_SIZE_POS = 1; -var FILENAME_POS = 2; +const MIN_FILE_SIZE_POS = 0; +const FILENAME_POS = 2; // normalized table headers -var tableHeaders = [ +const tableHeaders = [ ['max', 'min', 'gzip', 'file'], ['---', '---', '---', '---'], ]; -var tableOptions = { +const tableOptions = { align: ['r', 'r', 'r', 'l'], hsep: ' | ', }; @@ -50,9 +50,9 @@ var tableOptions = { * @return {number} */ function findMaxIndexByFilename(rows, predicate) { - for (var i = 0; i < rows.length; i++) { - var curRow = rows[i]; - var curFilename = curRow[2]; + for (let i = 0; i < rows.length; i++) { + const curRow = rows[i]; + const curFilename = curRow[FILENAME_POS]; if (predicate(curFilename)) { return i; } @@ -69,10 +69,10 @@ function findMaxIndexByFilename(rows, predicate) { * @param {boolean} mergeNames */ function normalizeRow(rows, minFilename, maxFilename, mergeNames) { - var minIndex = findMaxIndexByFilename(rows, function(filename) { + const minIndex = findMaxIndexByFilename(rows, function(filename) { return filename == minFilename; }); - var maxIndex = findMaxIndexByFilename(rows, function(filename) { + const maxIndex = findMaxIndexByFilename(rows, function(filename) { return filename == maxFilename; }); @@ -80,7 +80,7 @@ function normalizeRow(rows, minFilename, maxFilename, mergeNames) { if (mergeNames) { rows[minIndex][FILENAME_POS] += ' / ' + rows[maxIndex][FILENAME_POS]; } - rows[minIndex].unshift(rows[maxIndex][MAX_FILE_SIZE_POS]); + rows[minIndex].unshift(rows[maxIndex][MIN_FILE_SIZE_POS]); rows.splice(maxIndex, 1); } } @@ -97,9 +97,33 @@ 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); + + normalizeRow(rows, 'current-min/iframe-transport-client-v0.js', + 'current/iframe-transport-client-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); + + normalizeRow(rows, 'examiner.js', 'examiner.max.js', true); + + normalizeRow(rows, 'ww.js', 'ww.max.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; + let curName = null; + let i = rows.length; // we are mutating in place... kind of icky but this will do fow now. while (i--) { curName = rows[i][FILENAME_POS]; @@ -117,8 +141,8 @@ function normalizeRows(rows) { * @param {string} filename */ function normalizeExtension(rows, filename) { - var isMax = /\.max\.js$/.test(filename); - var counterpartName = filename.replace(/(v0\/.*?)(\.max)?(\.js)$/, + const isMax = /\.max\.js$/.test(filename); + const counterpartName = filename.replace(/(v0\/.*?)(\.max)?(\.js)$/, function(full, grp1, grp2, grp3) { if (isMax) { return grp1 + grp3; @@ -149,7 +173,7 @@ function onFileThrough(rows, file, enc, cb) { } if (file.isStream()) { - cb(new util.PluginError('size', 'Stream not supported')); + cb(new PluginError('size', 'Stream not supported')); return; } @@ -173,8 +197,8 @@ function onFileThrough(rows, file, enc, cb) { function onFileThroughEnd(rows, cb) { rows = normalizeRows(rows); rows.unshift.apply(rows, tableHeaders); - var tbl = table(rows, tableOptions); - console/*OK*/.log(tbl); + const tbl = table(rows, tableOptions); + console/* OK*/.log(tbl); fs.writeFileSync('test/size.txt', tbl); cb(); } @@ -185,7 +209,7 @@ function onFileThroughEnd(rows, cb) { * @return {!Stream} a Writable Stream */ function sizer() { - var rows = []; + const rows = []; return through.obj( onFileThrough.bind(null, rows), onFileThroughEnd.bind(null, rows) @@ -199,13 +223,15 @@ function sizer() { */ function sizeTask() { gulp.src([ - 'dist/**/*.js', - '!dist/**/*-latest.js', - 'dist.3p/{current,current-min}/**/*.js', - ]) - .pipe(sizer()) - .pipe(gulp.dest(tempFolderName)) - .on('end', del.bind(null, [tempFolderName])); + 'dist/**/*.js', + '!dist/**/*-latest.js', + '!dist/**/*check-types.js', + '!dist/**/amp-viewer-host.max.js', + 'dist.3p/{current,current-min}/**/*.js', + ]) + .pipe(sizer()) + .pipe(gulp.dest(tempFolderName)) + .on('end', del.bind(null, [tempFolderName])); } gulp.task('size', 'Runs a report on artifact size', sizeTask); diff --git a/build-system/tasks/test.js b/build-system/tasks/test.js deleted file mode 100644 index a0a93fb6f5c5..000000000000 --- a/build-system/tasks/test.js +++ /dev/null @@ -1,111 +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. - */ - -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; - -/** - * 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); - } - - if (argv.firefox) { - return extend(obj, karmaConfig.firefox); - } - - if (argv.saucelabs) { - if (!process.env.SAUCE_USERNAME) { - throw new Error('Missing SAUCE_USERNAME Env variable'); - } - 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 extend(obj, karmaConfig.default); -} - -var prerequisites = ['build']; -if (process.env.TRAVIS) { - // No need to do this because we are guaranteed to have done - // it. - prerequisites = []; -} - -/** - * Run tests. - */ -gulp.task('test', 'Runs tests', prerequisites, function(done) { - if (argv.saucelabs && process.env.MAIN_REPO && - // Sauce Labs does not work on Pull Requests directly. - // The @ampsauce bot builds these. - process.env.TRAVIS_PULL_REQUEST) { - console./*OK*/info('Deactivated for main repo'); - return; - } - - if (!argv.integration && process.env.AMPSAUCE_REPO) { - console./*OK*/info('Deactivated for ampsauce repo') - } - - var c = getConfig(); - var browsers = []; - - if (argv.watch || argv.w) { - c.singleRun = false; - } - - if (argv.verbose || argv.v) { - c.client.captureConsole = true; - } - - if (argv.integration) { - c.files = config.integrationTestPaths; - } else { - c.files = config.testPaths; - } - - c.client.amp = { - useCompiledJs: !!argv.compiled - }; - - karma.start(c, done); -}, { - options: { - 'verbose': ' With logging enabled', - 'watch': ' Watches for changes in files, runs corresponding test(s)', - 'saucelabs': ' Runs test on saucelabs (requires setup)', - 'safari': ' Runs tests in Safari', - 'firefox': ' Runs tests in Firefox', - 'integration': 'Run only integration tests.', - 'compiled': 'Changes integration tests to use production JS ' + - 'binaries for execution', - 'oldchrome': 'Runs test with an old chrome. Saucelabs only.', - } -}); diff --git a/build-system/tasks/todos.js b/build-system/tasks/todos.js new file mode 100644 index 000000000000..ce24fff6d0bf --- /dev/null +++ b/build-system/tasks/todos.js @@ -0,0 +1,141 @@ +/** + * 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. + */ +'use strict'; + +const BBPromise = require('bluebird'); +const colors = require('ansi-colors'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const request = BBPromise.promisify(require('request')); +const srcGlobs = require('../config').presubmitGlobs; +const through2 = require('through2'); + +const {GITHUB_ACCESS_TOKEN} = process.env; + +/** @type {!Object>} */ +const issueCache = Object.create(null); + + +/** + * Test if a file's contents contains closed TODOs. + * + * @param {!File} file file is a vinyl file object + * @return {Promise} Number of found closed TODOs. + */ +function findClosedTodosInFile(file) { + const contents = file.contents.toString(); + const todos = contents.match(/TODO\([^\)]*\)/g); + if (!todos || todos.length == 0) { + return Promise.resolve(0); + } + + const promises = []; + for (let i = 0; i < todos.length; i++) { + const todo = todos[i]; + const parts = /TODO\([^\)]*\#(\d*)\)/.exec(todo); + const issueId = parts ? parts[1] : null; + if (!issueId) { + continue; + } + promises.push(reportClosedIssue(file, issueId, todo)); + } + + if (promises.length == 0) { + return Promise.resolve(0); + } + return Promise.all(promises).then(results => { + return results.reduce(function(acc, v) { + return acc + v; + }, 0); + }).catch(function(error) { + log(colors.red('Failed in', file.path, error, error.stack)); + return 0; + }); +} + + +/** + * @param {!File} file file is a vinyl file object + * @param {string} issueId + * @param {string} todo + * @return {!Promise} + */ +function reportClosedIssue(file, issueId, todo) { + if (issueCache[issueId] !== undefined) { + return issueCache[issueId]; + } + return issueCache[issueId] = githubRequest('/issues/' + issueId) + .then(response => { + const issue = JSON.parse(response.body); + const value = issue.state == 'closed' ? 1 : 0; + if (value) { + log(colors.red(todo, 'in', file.path)); + } + return value; + }); +} + + +/** + * @param {string} path + * @param {string=} opt_method + * @param {*} opt_data + * @return {!Promise<*>} + */ +function githubRequest(path, opt_method, opt_data) { + const options = { + url: 'https://api.github.com/repos/ampproject/amphtml' + path, + headers: { + 'User-Agent': 'amp-changelog-gulp-task', + 'Accept': 'application/vnd.github.v3+json', + }, + qs: { + 'access_token': GITHUB_ACCESS_TOKEN, + }, + }; + if (opt_method) { + options.method = opt_method; + } + if (opt_data) { + options.json = true; + options.body = opt_data; + } + return request(options); +} + + +/** + * todos:find-closed task. + */ +function findClosedTodosTask() { + let foundCount = 0; + return gulp.src(srcGlobs) + .pipe(through2.obj(function(file, enc, cb) { + findClosedTodosInFile(file).then(function(count) { + foundCount += count; + cb(); + }); + })) + .on('end', function() { + if (foundCount > 0) { + log(colors.red('Found closed TODOs: ', foundCount)); + process.exit(1); + } + }); +} + + +gulp.task('todos:find-closed', 'Find closed TODOs', findClosedTodosTask); diff --git a/build-system/tasks/update-packages.js b/build-system/tasks/update-packages.js new file mode 100644 index 000000000000..248af63a0bb8 --- /dev/null +++ b/build-system/tasks/update-packages.js @@ -0,0 +1,204 @@ +/** + * 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. + */ +'use strict'; + +const colors = require('ansi-colors'); +const fs = require('fs-extra'); +const gulp = require('gulp-help')(require('gulp')); +const log = require('fancy-log'); +const {exec, execOrDie, getStderr} = require('../exec'); + +const yarnExecutable = 'npx yarn'; + +/** + * Writes the given contents to the patched file if updated + * @param {string} patchedName Name of patched file + * @param {string} file Contents to write + */ +function writeIfUpdated(patchedName, file) { + if (!fs.existsSync(patchedName) || + fs.readFileSync(patchedName) != file) { + fs.writeFileSync(patchedName, file); + if (!process.env.TRAVIS) { + log(colors.green('Patched'), colors.cyan(patchedName)); + } + } +} + +/** + * @param {string} filePath + * @param {string} newFilePath + * @param {...any} args Search and replace string pairs. + */ +function replaceInFile(filePath, newFilePath, ...args) { + let file = fs.readFileSync(filePath, 'utf8'); + for (let i = 0; i < args.length; i += 2) { + const searchValue = args[i]; + const replaceValue = args[i + 1]; + if (!file.includes(searchValue)) { + throw new Error(`Expected "${searchValue}" to appear in ${filePath}.`); + } + file = file.replace(searchValue, replaceValue); + } + writeIfUpdated(newFilePath, file); +} + +/** + * Patches Web Animations API by wrapping its body into `install` function. + * This gives us an option to call polyfill directly on the main window + * or a friendly iframe. + */ +function patchWebAnimations() { + // Copies web-animations-js into a new file that has an export. + const patchedName = 'node_modules/web-animations-js/' + + 'web-animations.install.js'; + let file = fs.readFileSync( + 'node_modules/web-animations-js/' + + 'web-animations.min.js').toString(); + // Replace |requestAnimationFrame| with |window|. + file = file.replace(/requestAnimationFrame/g, function(a, b) { + if (file.charAt(b - 1) == '.') { + return a; + } + return 'window.' + a; + }); + // Fix web-animations-js code that violates strict mode. + // See https://github.com/ampproject/amphtml/issues/18612 and + // https://github.com/web-animations/web-animations-js/issues/46 + file = file.replace(/b.true=a/g, 'b?b.true=a:true'); + // Wrap the contents inside the install function. + file = 'export function installWebAnimations(window) {\n' + + 'var document = window.document;\n' + + file + + '\n' + + '}\n'; + writeIfUpdated(patchedName, file); +} + +/** + * Creates a version of document-register-element that can be installed + * without side effects. + */ +function patchRegisterElement() { + // 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: https://github.com/google/closure-compiler/issues/1831 + const dir = 'node_modules/document-register-element/build/'; + replaceInFile( + dir + 'document-register-element.node.js', + dir + 'document-register-element.patched.js', + // Elimate the immediate side effect. + 'installCustomElements(global);', + '', + // Replace CJS export with ES6 export. + 'module.exports = installCustomElements;', + 'export {installCustomElements};'); +} + +/** + * Closure Compiler doesn't recognize .mjs extension yet, so copy the file to + * have a .js file extension. + */ +function patchWorkerDom() { + const dir = 'node_modules/@ampproject/worker-dom/dist/'; + fs.copyFileSync( + dir + 'unminified.index.safe.mjs', + dir + 'unminified.index.safe.mjs.patched.js'); +} + +/** + * Makes sure ES6 packages in node_modules that are used by the runtime will be + * transformed by babelify. The list of packages is dynamically generated by + * reading the `dependencies` section of the package.json in the project root. + * This is a no-op if transforms are already enabled for a package. + * See https://github.com/babel/babelify#why-arent-files-in-node_modules-being-transformed + */ +function transformEs6Packages() { + const rootPackageJsonFile = 'package.json'; + const rootPackageJsonContents = fs.readFileSync(rootPackageJsonFile, 'utf8'); + const rootPackageJson = JSON.parse(rootPackageJsonContents); + const es6Packages = Object.keys(rootPackageJson['dependencies']); + es6Packages.forEach(es6Package => { + const packageJsonFile = 'node_modules/' + es6Package + '/package.json'; + const packageJsonContents = fs.readFileSync(packageJsonFile, 'utf8'); + const packageJson = JSON.parse(packageJsonContents); + if (!packageJson['browserify']) { + packageJson['browserify'] = {'transform': ['babelify']}; + const updatedPackageJson = JSON.stringify(packageJson, null, 2); + fs.writeFileSync(packageJsonFile, updatedPackageJson, 'utf8'); + if (!process.env.TRAVIS) { + log(colors.green('Enabled ES6 transforms for runtime dependency'), + colors.cyan(es6Package)); + } + } + }); +} + +/** + * Installs custom lint rules from build-system/eslint-rules to node_modules. + */ +function installCustomEslintRules() { + const customRuleDir = 'build-system/eslint-rules'; + const customRuleName = 'eslint-plugin-amphtml-internal'; + exec(yarnExecutable + ' unlink', {'stdio': 'ignore', 'cwd': customRuleDir}); + exec(yarnExecutable + ' link', {'stdio': 'ignore', 'cwd': customRuleDir}); + exec(yarnExecutable + ' unlink ' + customRuleName, {'stdio': 'ignore'}); + exec(yarnExecutable + ' link ' + customRuleName, {'stdio': 'ignore'}); + if (!process.env.TRAVIS) { + log(colors.green('Installed lint rules from'), colors.cyan(customRuleDir)); + } +} + +/** + * Does a yarn check on node_modules, and if it is outdated, runs yarn. + */ +function runYarnCheck() { + const integrityCmd = yarnExecutable + ' check --integrity'; + if (getStderr(integrityCmd).trim() != '') { + log(colors.yellow('WARNING:'), 'The packages in', + colors.cyan('node_modules'), 'do not match', + colors.cyan('package.json.')); + const verifyTreeCmd = yarnExecutable + ' check --verify-tree'; + exec(verifyTreeCmd); + log('Running', colors.cyan('yarn'), 'to update packages...'); + execOrDie(yarnExecutable); // Stop execution when Ctrl + C is detected. + } else { + log(colors.green('All packages in'), + colors.cyan('node_modules'), colors.green('are up to date.')); + } +} + +/** + * Installs custom lint rules, updates node_modules (for local dev), and patches + * web-animations-js and document-register-element if necessary. + */ +function updatePackages() { + installCustomEslintRules(); + if (!process.env.TRAVIS) { + runYarnCheck(); + } + patchWebAnimations(); + patchRegisterElement(); + patchWorkerDom(); + transformEs6Packages(); +} + +gulp.task( + 'update-packages', + 'Runs yarn if node_modules is out of date, and patches web-animations-js', + updatePackages +); diff --git a/build-system/tasks/validator.js b/build-system/tasks/validator.js index f08ae53facce..bbe95009f3a6 100644 --- a/build-system/tasks/validator.js +++ b/build-system/tasks/validator.js @@ -13,16 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +'use strict'; -var gulp = require('gulp-help')(require('gulp')); -var execSync = require('child_process').execSync; +const argv = require('minimist')(process.argv.slice(2)); +const gulp = require('gulp-help')(require('gulp')); +const {execOrDie} = require('../exec'); +let validatorArgs = ''; +if (argv.update_tests) { + validatorArgs += ' --update_tests'; +} /** * Simple wrapper around the python based validator build. */ function validator() { - execSync('cd validator && python build.py') + execOrDie('cd validator && python build.py' + validatorArgs); +} + +/** + * Simple wrapper around the python based validator webui build. + */ +function validatorWebui() { + execOrDie('cd validator/webui && python build.py' + validatorArgs); } gulp.task('validator', 'Builds and tests the AMP validator.', validator); +gulp.task('validator-webui', 'Builds and tests the AMP validator web UI.', + validatorWebui); diff --git a/build-system/tasks/visual-diff/helpers.js b/build-system/tasks/visual-diff/helpers.js new file mode 100644 index 000000000000..8360f562c948 --- /dev/null +++ b/build-system/tasks/visual-diff/helpers.js @@ -0,0 +1,205 @@ +/** + * Copyright 2018 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'; + +const colors = require('ansi-colors'); +const fancyLog = require('fancy-log'); +const sleep = require('sleep-promise'); + +const CSS_SELECTOR_RETRY_MS = 100; +const CSS_SELECTOR_RETRY_ATTEMPTS = 50; +const CSS_SELECTOR_TIMEOUT_MS = + CSS_SELECTOR_RETRY_MS * CSS_SELECTOR_RETRY_ATTEMPTS; + +/** + * Logs a message to the console. + * + * @param {string} mode + * @param {!Array} messages + */ +function log(mode, ...messages) { + switch (mode) { + case 'verbose': + if (process.env.TRAVIS) { + return; + } + fancyLog.info(colors.green('VERBOSE:'), ...messages); + break; + case 'info': + fancyLog.info(colors.green('INFO:'), ...messages); + break; + case 'warning': + fancyLog.warn(colors.yellow('WARNING:'), ...messages); + break; + case 'error': + fancyLog.error(colors.red('ERROR:'), ...messages); + break; + case 'fatal': + process.exitCode = 1; + fancyLog.error(colors.red('FATAL:'), ...messages); + throw new Error(messages.join(' ')); + case 'travis': + if (process.env['TRAVIS']) { + messages.forEach(message => process.stdout.write(message)); + } + break; + } +} + +/** + * Verifies that all CSS elements are as expected before taking a snapshot. + * + * @param {!puppeteer.Page} page a Puppeteer control browser tab/page. + * @param {string} testName the full name of the test. + * @param {!Array} forbiddenCss Array of CSS elements that must not be + * found in the page. + * @param {!Array} loadingIncompleteCss Array of CSS elements that must + * eventually be removed from the page. + * @param {!Array} loadingCompleteCss Array of CSS elements that must + * eventually appear on the page. + * @throws {string} an encountered error. + */ +async function verifyCssElements(page, testName, forbiddenCss, + loadingIncompleteCss, loadingCompleteCss) { + // Begin by waiting for all loader dots to disappear. + if (!(await waitForLoaderDot(page))) { + throw new Error(`${colors.cyan(testName)} still has the AMP loader dot ` + + `after ${CSS_SELECTOR_TIMEOUT_MS} ms`); + } + + if (forbiddenCss) { + for (const css of forbiddenCss) { + if ((await page.$(css)) !== null) { + throw new Error(`${colors.cyan(testName)} | The forbidden CSS ` + + `element ${colors.cyan(css)} exists in the page`); + } + } + } + + if (loadingIncompleteCss) { + log('verbose', 'Waiting for invisibility of all:', + colors.cyan(loadingIncompleteCss.join(', '))); + for (const css of loadingIncompleteCss) { + if (!(await waitForElementVisibility(page, css, {hidden: true}))) { + throw new Error(`${colors.cyan(testName)} | An element with the CSS ` + + `selector ${colors.cyan(css)} is still visible after ` + + `${CSS_SELECTOR_TIMEOUT_MS} ms`); + } + } + } + + if (loadingCompleteCss) { + log('verbose', 'Waiting for existence of all:', + colors.cyan(loadingCompleteCss.join(', '))); + for (const css of loadingCompleteCss) { + if (!(await waitForSelectorExistence(page, css))) { + throw new Error(`${colors.cyan(testName)} | The CSS selector ` + + `${colors.cyan(css)} does not match any elements in the page`); + } + } + + log('verbose', 'Waiting for visibility of all:', + colors.cyan(loadingCompleteCss.join(', '))); + for (const css of loadingCompleteCss) { + if (!(await waitForElementVisibility(page, css, {visible: true}))) { + throw new Error(`${colors.cyan(testName)} | An element with the CSS ` + + `selector ${colors.cyan(css)} is still invisible after ` + + `${CSS_SELECTOR_TIMEOUT_MS} ms`); + } + } + } +} + +/** + * Wait for all AMP loader dot to disappear. + * + * @param {!puppeteer.Page} page page to wait on. + * @return {boolean} true if the loader dot disappeared before the timeout. + */ +async function waitForLoaderDot(page) { + return await waitForElementVisibility( + page, '.i-amphtml-loader-dot', {hidden: true}); +} + +/** + * Wait until the element is either hidden or visible or until timed out. + * + * @param {!puppeteer.Page} page page to check the visibility of elements in. + * @param {string} selector CSS selector for elements to wait on. + * @param {!Object} options with key 'visible' OR 'hidden' set to true. + * @return {boolean} true if the expectation is met before the timeout. + */ +async function waitForElementVisibility(page, selector, options) { + const waitForVisible = Boolean(options['visible']); + const waitForHidden = Boolean(options['hidden']); + if (waitForVisible == waitForHidden) { + log('fatal', 'waitForElementVisibility must be called with exactly one of', + "'visible' or 'hidden' set to true."); + } + + let attempt = 0; + do { + const elementsAreVisible = []; + + for (const elementHandle of await page.$$(selector)) { + const boundingBox = await elementHandle.boundingBox(); + const elementIsVisible = boundingBox != null && boundingBox.height > 0 && + boundingBox.width > 0; + elementsAreVisible.push(elementIsVisible); + } + + if (elementsAreVisible.length) { + log('verbose', 'Found', colors.cyan(elementsAreVisible.length), + 'element(s) matching the CSS selector', colors.cyan(selector)); + log('verbose', 'Expecting all element visibilities to be', + colors.cyan(waitForVisible), '; they are', + colors.cyan(elementsAreVisible)); + } else { + log('verbose', 'No', colors.cyan(selector), 'matches found'); + } + // Since we assert that waitForVisible == !waitForHidden, there is no need + // to check equality to both waitForVisible and waitForHidden. + if (elementsAreVisible.every( + elementIsVisible => elementIsVisible == waitForVisible)) { + return true; + } + + await sleep(CSS_SELECTOR_RETRY_MS); + attempt++; + } while (attempt < CSS_SELECTOR_RETRY_ATTEMPTS); + return false; +} + +/** + * Wait until the CSS selector exists in the page or until timed out. + * + * @param {!puppeteer.Page} page page to check the existence of the selector in. + * @param {string} selector CSS selector. + * @return {boolean} true if the element exists before the timeout. + */ +async function waitForSelectorExistence(page, selector) { + let attempt = 0; + do { + if ((await page.$(selector)) !== null) { + return true; + } + await sleep(CSS_SELECTOR_RETRY_MS); + attempt++; + } while (attempt < CSS_SELECTOR_RETRY_ATTEMPTS); + return false; +} + +module.exports = {log, verifyCssElements}; diff --git a/build-system/tasks/visual-diff/index.js b/build-system/tasks/visual-diff/index.js new file mode 100644 index 000000000000..7d30eab20f5c --- /dev/null +++ b/build-system/tasks/visual-diff/index.js @@ -0,0 +1,680 @@ +/** + * 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. + */ +'use strict'; + +const argv = require('minimist')(process.argv.slice(2)); +const BBPromise = require('bluebird'); +const colors = require('ansi-colors'); +const fs = require('fs'); +const gulp = require('gulp-help')(require('gulp')); +const JSON5 = require('json5'); +const path = require('path'); +const request = BBPromise.promisify(require('request')); +const sleep = require('sleep-promise'); +const tryConnect = require('try-net-connect'); +const {execOrDie, execScriptAsync} = require('../../exec'); +const {gitBranchName, gitBranchPoint, gitCommitterEmail} = require('../../git'); +const {log, verifyCssElements} = require('./helpers'); +const {PercyAssetsLoader} = require('./percy-assets-loader'); + +// optional dependencies for local development (outside of visual diff tests) +let puppeteer; +let Percy; + +// CSS widths: iPhone: 375, Pixel: 411, Desktop: 1400. +const DEFAULT_SNAPSHOT_OPTIONS = {widths: [375, 411, 1400]}; +const SNAPSHOT_EMPTY_BUILD_OPTIONS = {widths: [375]}; +const VIEWPORT_WIDTH = 1400; +const VIEWPORT_HEIGHT = 100000; +const HOST = 'localhost'; +const PORT = 8000; +const BASE_URL = `http://${HOST}:${PORT}`; +const WEBSERVER_TIMEOUT_RETRIES = 10; +const NAVIGATE_TIMEOUT_MS = 3000; +const MAX_PARALLEL_TABS = 10; +const WAIT_FOR_TABS_MS = 1000; +const BUILD_STATUS_URL = 'https://amphtml-percy-status-checker.appspot.com/status'; +const BUILD_PROCESSING_POLLING_INTERVAL_MS = 5 * 1000; // Poll every 5 seconds +const BUILD_PROCESSING_TIMEOUT_MS = 15 * 1000; // Wait for up to 10 minutes +const MASTER_BRANCHES_REGEXP = /^(?:master|release|canary|amp-release-.*)$/; +const PERCY_BUILD_URL = 'https://percy.io/ampproject/amphtml/builds'; + +const ROOT_DIR = path.resolve(__dirname, '../../../'); +const WRAP_IN_IFRAME_SCRIPT = fs.readFileSync( + path.resolve(__dirname, 'snippets/iframe-wrapper.js'), 'utf8'); + +let browser_; +let webServerProcess_; + +/** + * Override PERCY_* environment variables if passed via gulp task parameters. + */ +function maybeOverridePercyEnvironmentVariables() { + ['percy_project', 'percy_token', 'percy_branch'].forEach(variable => { + if (variable in argv) { + process.env[variable.toUpperCase()] = argv[variable]; + } + }); +} + +/** + * Disambiguates branch names by decorating them with the commit author name. + * We do this for all non-push builds in order to prevent them from being used + * as baselines for future builds. + */ +function setPercyBranch() { + if (!process.env['PERCY_BRANCH'] && + (!argv.master || !process.env['TRAVIS'])) { + const userName = gitCommitterEmail(); + const branchName = process.env['TRAVIS'] ? + process.env['TRAVIS_PULL_REQUEST_BRANCH'] : gitBranchName(); + process.env['PERCY_BRANCH'] = userName + '-' + branchName; + } +} + +/** + * Set the branching point's SHA to an env variable. + * + * This will let Percy determine which build to use as the baseline for this new + * build. + * + * Only does something on Travis, and for non-master branches, since master + * builds are always built on top of the previous commit (we use the squash and + * merge method for pull requests.) + */ +function setPercyTargetCommit() { + if (process.env.TRAVIS && !argv.master) { + process.env['PERCY_TARGET_COMMIT'] = gitBranchPoint(/* fromMerge */ true); + } +} + +/** + * Launches a background AMP webserver for unminified js using gulp. + * + * Waits until the server is up and reachable, and ties its lifecycle to this + * process's lifecycle. + * + * @return {!Promise} a Promise that resolves when the web server is launched + * and reachable. + */ +async function launchWebServer() { + webServerProcess_ = execScriptAsync( + `gulp serve --host ${HOST} --port ${PORT} ${process.env.WEBSERVER_QUIET}`, + { + stdio: argv.webserver_debug ? + ['ignore', process.stdout, process.stderr] : + 'ignore', + }); + + webServerProcess_.on('close', code => { + code = code || 0; + if (code != 0) { + log('fatal', colors.cyan("'serve'"), + `errored with code ${code}. Cannot continue with visual diff tests`); + } + }); + + let resolver, rejecter; + const deferred = new Promise((resolverIn, rejecterIn) => { + resolver = resolverIn; + rejecter = rejecterIn; + }); + tryConnect({ + host: HOST, + port: PORT, + retries: WEBSERVER_TIMEOUT_RETRIES, // retry timeout defaults to 1 sec + }).on('connected', () => { + return resolver(webServerProcess_); + }).on('timeout', rejecter); + return deferred; +} + +/** + * Checks the current status of a Percy build. + * + * @param {string} buildId ID of the ongoing Percy build. + * @return {!JsonObject} The full response from the build status server. + */ +async function getBuildStatus(buildId) { + const statusUri = `${BUILD_STATUS_URL}?build_id=${buildId}`; + try { + return (await request(statusUri, {json: true})).body; + } catch (error) { + log('fatal', 'Failed to query Percy build status:', error); + } +} + +/** + * Waits for Percy to finish processing a build. + * @param {string} buildId ID of the ongoing Percy build. + * @return {!JsonObject} The eventual status of the Percy build. + */ +async function waitForBuildCompletion(buildId) { + log('info', 'Waiting for Percy build', colors.cyan(buildId), + 'to be processed...'); + const startTime = Date.now(); + let status = await getBuildStatus(buildId); + while (status.state != 'finished' && status.state != 'failed' && + Date.now() - startTime < BUILD_PROCESSING_TIMEOUT_MS) { + await sleep(BUILD_PROCESSING_POLLING_INTERVAL_MS); + status = await getBuildStatus(buildId); + } + return status; +} + +/** + * Verifies that a Percy build succeeded and didn't contain any visual diffs. + * @param {!JsonObject} status The eventual status of the Percy build. + * @param {string} buildId ID of the Percy build. + */ +function verifyBuildStatus(status, buildId) { + switch (status.state) { + case 'finished': + if (status.total_comparisons_diff > 0) { + if (MASTER_BRANCHES_REGEXP.test(status.branch)) { + // If there are visual diffs on master or a release branch, fail + // Travis. For master, print instructions for how to approve new + // visual changes. + if (status.branch == 'master') { + log('error', 'Found visual diffs. If the changes are intentional,', + 'you must approve the build at', + colors.cyan(`${PERCY_BUILD_URL}/${buildId}`), + 'in order to update the baseline snapshots.'); + } else { + log('error', `Found visual diffs on branch ${status.branch}`); + } + } else { + // For PR branches, just print a warning since the diff may be into + // intentional, with instructions for how to approve the new snapshots + // so they are used as the baseline for future visual diff builds. + log('warning', 'Percy build', colors.cyan(buildId), + 'contains visual diffs.'); + log('warning', 'If they are intentional, you must first approve the', + 'build at', colors.cyan(`${PERCY_BUILD_URL}/${buildId}`), + 'to allow your PR to be merged.'); + } + } else { + log('info', 'Percy build', colors.cyan(buildId), + 'contains no visual diffs.'); + } + break; + + case 'pending': + case 'processing': + log('error', 'Percy build not processed after', + `${BUILD_PROCESSING_TIMEOUT_MS}ms`); + break; + + case 'failed': + default: + log('error', `Percy build failed: ${status.failure_reason}`); + break; + } +} + +/** + * Launches a Puppeteer controlled browser. + * + * Waits until the browser is up and reachable, and ties its lifecycle to this + * process's lifecycle. + * + * @return {!puppeteer.Browser} a Puppeteer controlled browser. + */ +async function launchBrowser() { + const browserOptions = { + args: ['--no-sandbox', '--disable-extensions', '--disable-gpu'], + dumpio: argv.chrome_debug, + headless: true, + }; + + try { + browser_ = await puppeteer.launch(browserOptions); + } catch (error) { + log('fatal', error); + } + + // Every action on the browser or its pages adds a listener to the + // Puppeteer.Connection.Events.Disconnected event. This is a temporary + // workaround for the Node runtime warning that is emitted once 11 listeners + // are added to the same object. + browser_._connection.setMaxListeners(9999); + + return browser_; +} + +/** + * Opens a new browser tab, resizes its viewport, and returns a Page handler. + * + * @param {!puppeteer.Browser} browser a Puppeteer controlled browser. + */ +async function newPage(browser) { + const page = await browser.newPage(); + await page.setViewport({ + width: VIEWPORT_WIDTH, + height: VIEWPORT_HEIGHT, + }); + page.setDefaultNavigationTimeout(NAVIGATE_TIMEOUT_MS); + await page.setJavaScriptEnabled(true); + return page; +} + +/** + * Runs the visual tests. + * + * @param {!Array} assetGlobs an array of glob strings to load assets + * from. + * @param {!Array} webpages an array of JSON objects containing + * details about the pages to snapshot. + */ +async function runVisualTests(assetGlobs, webpages) { + // Create a Percy client and start a build. + const percy = createPercyPuppeteerController(assetGlobs); + await percy.startBuild(); + const {buildId} = percy; + fs.writeFileSync('PERCY_BUILD_ID', buildId); + log('info', 'Started Percy build', colors.cyan(buildId)); + if (process.env['PERCY_TARGET_COMMIT']) { + log('info', 'The Percy build is baselined on top of commit', + colors.cyan(process.env['PERCY_TARGET_COMMIT'])); + } + + try { + // Take the snapshots. + await generateSnapshots(percy, webpages); + } finally { + // Tell Percy we're finished taking snapshots. + await percy.finalizeBuild(); + } + + // check if the build failed early. + const status = await getBuildStatus(buildId); + if (status.state == 'failed') { + log('fatal', 'Build', colors.cyan(buildId), 'failed!'); + } else { + log('info', 'Build', colors.cyan(buildId), + 'is now being processed by Percy.'); + } +} + +/** + * Create a new Percy-Puppeteer controller and return it. + * + * @param {!Array} assetGlobs an array of glob strings to load assets + * from. + * @return {!Percy} a Percy-Puppeteer controller. + */ +function createPercyPuppeteerController(assetGlobs) { + if (!argv.percy_disabled) { + return new Percy({ + loaders: [new PercyAssetsLoader(assetGlobs, ROOT_DIR)], + }); + } else { + return { + startBuild: () => {}, + snapshot: () => {}, + finalizeBuild: () => {}, + buildId: '[PERCY_DISABLED]', + }; + } +} + +/** + * Sets the AMP config, launches a server, and generates Percy snapshots for a + * set of given webpages. + * + * @param {!Percy} percy a Percy-Puppeteer controller. + * @param {!Array} webpages an array of JSON objects containing + * details about the pages to snapshot. + */ +async function generateSnapshots(percy, webpages) { + const numUnfilteredPages = webpages.length; + webpages = webpages.filter(webpage => !webpage.flaky); + if (numUnfilteredPages != webpages.length) { + log('info', 'Skipping', colors.cyan(numUnfilteredPages - webpages.length), + 'flaky pages'); + } + if (argv.grep) { + webpages = webpages.filter(webpage => argv.grep.test(webpage.name)); + log('info', colors.cyan(`--grep ${argv.grep}`), 'matched', + colors.cyan(webpages.length), 'pages'); + } + + // Expand all the interactive tests. Every test should have a base test with + // no interactions, and each test that has in interactive tests file should + // load those tests here. + for (const webpage of webpages) { + webpage.tests_ = { + '': async() => {}, + }; + if (webpage.interactive_tests) { + Object.assign(webpage.tests_, + require(path.resolve(ROOT_DIR, webpage.interactive_tests))); + } + } + + const totalTests = webpages.reduce( + (numTests, webpage) => numTests + Object.keys(webpage.tests_).length, 0); + if (!totalTests) { + log('fatal', 'No pages left to test!'); + } else { + log('info', 'Executing', colors.cyan(totalTests), 'visual diff tests on', + colors.cyan(webpages.length), 'pages'); + } + + const browser = await launchBrowser(); + if (argv.master) { + const page = await newPage(browser); + await page.goto( + `${BASE_URL}/examples/visual-tests/blank-page/blank.html`); + await percy.snapshot('Blank page', page, SNAPSHOT_EMPTY_BUILD_OPTIONS); + } + + log('verbose', 'Generating snapshots...'); + if (!(await snapshotWebpages(percy, browser, webpages))) { + log('fatal', 'Some tests have failed locally.'); + } +} + +/** + * Generates Percy snapshots for a set of given webpages. + * + * @param {!Percy} percy a Percy-Puppeteer controller. + * @param {!puppeteer.Browser} browser a Puppeteer controlled browser. + * @param {!Array} webpages an array of JSON objects containing + * details about the webpages to snapshot. + * @return {boolean} true if all tests passed locally (does not indicate whether + * the tests passed on Percy). + */ +async function snapshotWebpages(percy, browser, webpages) { + const pagePromises = {}; + const testErrors = []; + for (const webpage of webpages) { + const {viewport, name: pageName} = webpage; + const fullUrl = `${BASE_URL}/${webpage.url}`; + for (const [testName, testFunction] of Object.entries(webpage.tests_)) { + while (Object.keys(pagePromises).length >= MAX_PARALLEL_TABS) { + await sleep(WAIT_FOR_TABS_MS); + } + + const page = await newPage(browser); + const name = testName ? `${pageName} (${testName})` : pageName; + log('verbose', 'Visual diff test', colors.yellow(name)); + + if (viewport) { + log('verbose', 'Setting explicit viewport size of', + colors.yellow(`${viewport.width}×${viewport.height}`)); + await page.setViewport({ + width: viewport.width, + height: viewport.height, + }); + } + log('verbose', 'Navigating to page', colors.yellow(fullUrl)); + + // Navigate to an empty page first to support different webpages that only + // modify the #anchor name. + await page.goto('about:blank').then(() => {}, () => {}); + + // Puppeteer is flaky when it comes to catching navigation requests, so + // ignore timeouts. If this was a real non-loading page, this will be + // caught in the resulting Percy build. Also attempt to wait until there + // are no more network requests. This method is flaky since Puppeteer + // doesn't always understand Chrome's network activity, so ignore timeouts + // again. + const pagePromise = page.goto(fullUrl, {waitUntil: 'networkidle0'}) + .then(() => {}, () => {}) + .then(async() => { + log('verbose', 'Navigation to page', colors.yellow(name), + 'is done, verifying page'); + + await page.bringToFront(); + + await verifyCssElements(page, name, webpage.forbidden_css, + webpage.loading_incomplete_css, webpage.loading_complete_css); + + if (webpage.loading_complete_delay_ms) { + log('verbose', 'Waiting', + colors.cyan(`${webpage.loading_complete_delay_ms}ms`), + 'for loading to complete'); + await sleep(webpage.loading_complete_delay_ms); + } + + await testFunction(page, name); + + const snapshotOptions = Object.assign({}, DEFAULT_SNAPSHOT_OPTIONS); + + if (webpage.enable_percy_javascript) { + snapshotOptions.enableJavaScript = true; + // Remove all scripts that have an external source, leaving only + // those scripts that are inlined in the page inside a ` + +``` + html_format: AMP +``` + +This tells the validator that this tag +should be valid in AMP format documents. Tags can also be valid in `AMP4ADS` +format documents, if the tag should be used in an ad format. If you are unsure, +leave the tag as an `AMP` format tag only for now. Additional formats can be +added later. + +``` + tag_name: "SCRIPT" +``` + +This tells the validator that we are defining a tag with the ` - - - - -

    Welcome to the mobile web

    - - -``` - -## Required mark-up - -AMP HTML documents MUST - -- start with the doctype ``. -- contain a top-level `` tag (`` is accepted as well). -- contain `` and `` tags (They are optional in HTML). -- contain a `` tag inside their head that points to the regular HTML version of the AMP HTML document or to itself if no such HTML version exists. -- contain a `` tag as the first child of their head tag. -- contain a `` tag inside their head tag. It's also recommended to include `initial-scale=1`. -- contain a `` tag as the last element in their head. -- contain the [AMP boilerplate code](../spec/amp-boilerplate.md) in their head tag. - -Most HTML tags can be used unchanged in AMP HTML. -Certain tags have equivalent custom AMP HTML tags; -other HTML tags are outright banned -(see [HTML Tags in the specification](../spec/amp-html-format.md)). - -# Include an image - -Content pages include more features than just the content. -To get you started, -here's the basic AMP HTML page now with an image: - -```html - - - - - Hello, AMPs - - - - - - - -

    Welcome to the mobile web

    - - - -``` - -Learn more about -[how to include common features](../docs/include_features.md). - -# Add some styles - -AMPs are web pages; add custom styling using common CSS properties. - -Style elements inside ` - - - - -

    Welcome to the mobile web

    - - - -``` - -Learn more about adding elements, including extended components, -in [How to Include Common Features](../docs/include_features.md). - -# Page layout - -Externally-loaded resources (like images, ads, videos, etc.) must have height -and width attributes. This ensures that sizes of all elements can be -calculated by the browser via CSS automatically and element sizes won't be -recalculated because of external resources, preventing the page from jumping -around as resources load. - -Moreover, use of the style attribute for tags is not permitted, as this -optimizes impact rendering speed in unpredictable ways. - - - -Learn more in the [AMP HTML Components specification](../spec/amp-html-components.md). - -# Test the page - -Test the page by viewing the page in your local server -and validating the page using the -[Chrome DevTools console](https://developers.google.com/web/tools/javascript/console/). - -1. Include your page in your local directory, for example, -`/ampproject/amphtml/examples`. -2. Get your web server up and running locally. -For a quick web server, run `python -m SimpleHTTPServer`. -4. Open your page, for example, go to -[http://localhost:8000/released.amp.html](http://localhost:8000/released.amp.html). -5. Add "#development=1" to the URL, for example, -[http://localhost:8000/released.amp.html#development=1](http://localhost:8000/released.amp.html#development=1). -6. Open the Chrome DevTools console and check for validation errors. - - - -# Final steps before publishing - -Congrats! You've tested your page locally and fixed all validation errors. - -Learn more about tools that can help you get your content production ready in -[Set Up Your Build Tools](https://developers.google.com/web/tools/setup/workspace/setup-buildtools). diff --git a/docs/include_features.md b/docs/include_features.md deleted file mode 100644 index bf37b65bccf6..000000000000 --- a/docs/include_features.md +++ /dev/null @@ -1,312 +0,0 @@ -# How to Include Common Features - -AMP HTML components make it simple for you to control your content. -Learn how to include common features in your pages using these elements. - -Make sure to review the documentation for each component individually as a reference: -* [AMP HTML Built-in Components](../builtins/README.md) -* [AMP HTML Extended Components](../extensions/README.md). - -# Display an iframe - -Display an iframe in your page using the -[`amp-iframe`](../extensions/amp-iframe/amp-iframe.md) element. - -`amp-iframe` requirements: - -* Must be at least 600px or 75% of the first viewport away from the top (except for iframes implemented with a placeholder, as described below). -* Can only request resources via HTTPS, and they must not be in the same origin as the container, -unless they do not specify allow-same-origin. - -To include an `amp-iframe` in your page, -first include the following script to the ``, which loads the additional code for the extended component: - -```html - -``` - -An example `amp-iframe` from the -[released.amp example](https://github.com/ampproject/amphtml/blob/master/examples/released.amp.html): - -```html - - -``` - -* It is possible to have an `amp-iframe` appear on the top of a document when the `amp-ifame` has a `placeholder` element as shown in the example below. - -```html - - - -``` -- The `amp-iframe` must contain an element with the `placeholder` attribute, (for instance an `amp-img` element) which would be rendered as a placeholder till the iframe is ready to be displayed. -- Iframe readiness will be inferred by listening to `onload` of the iframe or an `embed-ready` postmesssage which would be sent by the Iframe document, whichever comes first. - -Example of IFrame embed-ready request: -```javascript -window.parent./*OK*/postMessage({ - sentinel: 'amp', - type: 'embed-ready' -}, '*'); -``` - -# Media - -Include images, video, and audio in your page using AMP media elements. - -## Include an image - -Include an image in your page -using the [`amp-img`](../builtins/amp-img.md) element. - -`amp-img` requirements: - -* Must include an explicit width and height. -* Recommended: include a placeholder in case the image resource fails to load. - -Responsive image example: -```html - -``` -Fixed-size image example: -```html - -``` -Hidden image example: -```html - -``` -The AMP HTML runtime can effectively manage image resources, -choosing to delay or prioritize resource loading -based on the viewport position, system resources, connection bandwidth, or other factors. - -If the resource requested by the `amp-img` component fails to load, -the space will be blank. -Set a placeholder background color or other visual -using a CSS selector and style on the element itself: -```css -amp-img { - background-color: grey; -} -``` -## Include an animated image - -Include an animated image in your page -using the [`amp-anim`](../extensions/amp-anim/amp-anim.md) element. - -The `amp-anim` element is very similar to the `amp-img` element, -and provides additional functionality to manage loading and playing -of animated images such as GIFs. - -To include an `amp-anim` in your page, -first include the following script to the ``: - -```html - -``` - -The `amp-anim` component can also have an optional placeholder child -to display while the `src` file is loading. -The placeholder is specified via the `placeholder` attribute: -```html - - - - -``` -## Embed a Tweet - -Embed a Twitter Tweet in your page -using the [`amp-twitter`](../extensions/amp-twitter/amp-twitter.md) element. - -To include a tweet in your page, -first include the following script to the ``: - -```html - -``` - -Currently tweets are automatically proportionally scaled -to fit the provided size, -but this may yield less than ideal appearance. -Manually tweak the provided width and height or use the media attribute -to select the aspect ratio based on screen width. - -Example `amp-twitter` from the -[twitter.amp example](../examples/twitter.amp.html): -```html - - -``` - - - -## Include a video - -Include a video in your page -using the [`amp-video`](../builtins/amp-video.md) element. - -Only use this element for direct HTML5 video file embeds. -The element loads the video resource specified by the `src` attribute lazily, -at a time determined by the AMP HTML runtime. - -Include a placeholder before the video starts, and a fallback, -if the browser doesn't support HTML5 video, for example: -```html - -
    -

    Your browser doesn’t support HTML5 video

    -
    -
    -``` -## Include a youtube video - -Include a youtube video in your page -using the [`amp-youtube`](../extensions/amp-youtube/amp-youtube.md) element. - -You must include the following script in the ``: - -```html - -``` - -The Youtube `data-videoid` can be found in every Youtube video page URL. -For example, in https://www.youtube.com/watch?v=Z1q71gFeRqM, -Z1q71gFeRqM is the video id. - -Use `layout="responsive"` to yield correct layouts for 16:9 aspect ratio videos: -```html - - -``` -## Include an audio resource - -Include an audio resource in your page, -using the [`amp-audio`](../extensions/amp-audio/amp-audio.md) element. - -You must include the following script in the ``: - -```html - -``` - -Only use this element for direct HTML5 audio file embeds. -Like all embedded external resources in an AMP page, -the element loads the audio resource specified by the `src` attribute lazily, -at a time determined by the AMP HTML runtime. - -Include a placeholder before the audio starts, and a fallback, -if the browser doesn't support HTML5 audio, for example: -```html - -
    -

    Your browser doesn’t support HTML5 audio

    -
    - - -
    -``` -# Add Border Box Sizing - -Included in the base amp css is a class of `amp-border-box` that will set `box-sizing: border-box` on all elements -nested under that class. You can set this on your `html` tag to provide your page with default `border-box` sizing. -Individual elements can override this by beating or matching the CSS specificity of `.amp-border-box`. -# Count user page views - -Count user page views -using the [`amp-pixel`](../builtins/amp-pixel.md) element. - -The `amp-pixel` element takes a simple URL to send a GET request -to when the tracking pixel is loaded. - -Use the special string `$RANDOM` to add a random number -to the URL if required. - -For example, `` -makes a request to something like `https://www.my-analytics.com/?rand=8390278471201`, -where the $RANDOM value is randomly generated upon each impression. - -An example `amp-pixel` from the -[everything.amp example](https://github.com/ampproject/amphtml/blob/master/examples/everything.amp.html): -```html - -``` -# Monetization through ads - -The following ad networks are supported in AMP HTML pages: - -- [A9](../ads/a9.md) -- [AdReactor](../ads/adreactor.md) -- [AdSense](../ads/adsense.md) -- [AdTech](../ads/adtech.md) -- [Doubleclick](../ads/doubleclick.md) - -## Display an ad - -Display an ad in your page -using the [`amp-ad`](../builtins/amp-ad.md) element. -Only ads served via HTTPS are supported. - -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) -and executes the ad network’s JS inside that iframe sandbox. - -You must specify the ad width and height, and the ad network type. -The `type` identifies the ad network's template. -Different ad types require different `data-*` attributes. -```html - - -``` -If supported by the ad network, -include a `placeholder` -to be shown if no ad is available: -```html - -
    Have a great day!
    -
    -``` diff --git a/examples/3q-player.amp.html b/examples/3q-player.amp.html new file mode 100644 index 000000000000..eb6a1c74caa3 --- /dev/null +++ b/examples/3q-player.amp.html @@ -0,0 +1,41 @@ + + + + + 3Q AMP Example + + + + + + + + + +

    3Q Player AMP Examples

    + +

    Responsive

    + + + + +

    Actions

    + + + + + + +

    Autoplay

    + + +
    + + + diff --git a/examples/OWNERS.yaml b/examples/OWNERS.yaml new file mode 100644 index 000000000000..88cdc3de803b --- /dev/null +++ b/examples/OWNERS.yaml @@ -0,0 +1,2 @@ +- aghassemi +- lannka diff --git a/examples/a4a-fullwidth.amp.html b/examples/a4a-fullwidth.amp.html new file mode 100644 index 000000000000..77b9988aa3b0 --- /dev/null +++ b/examples/a4a-fullwidth.amp.html @@ -0,0 +1,168 @@ + + + + + AdSense full-width ad examples + + + + + + + + + +

    Content!

    +
    + +
    + +
    + +

    + Canem 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 hecuba. + 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 poodle 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. +

    + + +
    +
    +
    +
    +
    + + + 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..247c27c17709 --- /dev/null +++ b/examples/a4a.amp.html @@ -0,0 +1,99 @@ + + + + + AMP A4A examples + + + + + + + + + + +

    A4A Examples

    + +

    Fake ad network

    + +
    +
    +
    + +

    TripleLift ad network

    + +
    +
    +
    + +

    Fake cloudflare as ad network ad

    + +
    +
    +
    + +

    GMOSSP ad network

    + +
    +
    +
    + +

    Regular ad

    + + + +

    A4A ad

    + + + + diff --git a/examples/ac-creative.js b/examples/ac-creative.js new file mode 100644 index 000000000000..56aa7c853b53 --- /dev/null +++ b/examples/ac-creative.js @@ -0,0 +1,87 @@ +if (!window.context){ + // window.context doesn't exist yet, must perform steps to create it + // before using it + console.log("window.context NOT READY"); + + // must add listener for the creation of window.context + window.addEventListener('amp-windowContextCreated', function(){ + console.log("window.context created and ready to use"); + window.context.onResizeSuccess(resizeSuccessCallback); + window.context.onResizeDenied(resizeDeniedCallback); + }); + + // load ampcontext-lib.js which will create window.context + ampContextScript = document.createElement('script'); + ampContextScript.src = "https://localhost:8000/dist.3p/current/ampcontext-lib.js"; + document.head.appendChild(ampContextScript); +} + +function intersectionCallback(payload){ + var changes = payload.changes; + // Code below is simply an example. + var latestChange = changes[changes.length - 1]; + + // Amp-ad width and height. + var w = latestChange.boundingClientRect.width; + var h = latestChange.boundingClientRect.height; + + // Visible width and height. + var vw = latestChange.intersectionRect.width; + var vh = latestChange.intersectionRect.height; + + // Position in the viewport. + var vx = latestChange.boundingClientRect.x; + var vy = latestChange.boundingClientRect.y; + + // Viewable percentage. + var viewablePerc = (vw * vh) / (w * h) * 100; + + console.log(viewablePerc, w, h, vw, vh, vx, vy); + +} + +function dummyCallback(changes){ + console.log(changes); +} + +var shouldStopVis = false; +var stopVisFunc; +var shouldStopInt = false; +var stopIntFunc; + +function resizeSuccessCallback(requestedHeight, requestedWidth){ + console.log("Success!"); + console.log(this); + resizeTo(requestedHeight, requestedWidth); + console.log(requestedHeight); + console.log(requestedWidth); +} + +function resizeTo(height, width){ + this.innerWidth = width; + this.innerHeight = height; +} + +function resizeDeniedCallback(requestedHeight, requestedWidth){ + console.log("DENIED"); + console.log(requestedHeight); + console.log(requestedWidth); +} + +function toggleObserveIntersection(){ + if (shouldStopInt){ + stopIntFunc(); + } else { + stopIntFunc = window.context.observeIntersection(intersectionCallback); + } + shouldStopInt = !shouldStopInt; +} + +function toggleObserveVisibility(){ + if (shouldStopVis){ + stopVisFunc(); + } else { + stopVisFunc = window.context.observePageVisibility(dummyCallback); + } + shouldStopVis = !shouldStopVis; +} diff --git a/examples/accordion.amp.html b/examples/accordion.amp.html new file mode 100644 index 000000000000..4fe46bd0ec8f --- /dev/null +++ b/examples/accordion.amp.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + +
    +

    Section 1

    +

    Bunch of awesome content

    +
    +
    +

    Section 2

    +
    Bunch of awesome content
    +
    +
    +

    Section 3

    + +
    +
    +

    Properly nested amp-accordion

    + +
    +

    Nested section

    +

    It's possible to nest amp-accordions.

    +
    +
    +
    +
    +
    The header tag is supported as well.
    +

    Even more awesome.

    +
    +
    + + diff --git a/examples/ad-lightbox.amp.html b/examples/ad-lightbox.amp.html new file mode 100644 index 000000000000..b611fce35281 --- /dev/null +++ b/examples/ad-lightbox.amp.html @@ -0,0 +1,80 @@ + + + + + + AMP Ad Lightbox Prototype Example + + + + + + + + + + + +
    + + +
    + + + + + + + + + + + diff --git a/examples/ads-legacy.amp.html b/examples/ads-legacy.amp.html new file mode 100644 index 000000000000..4a8663d42158 --- /dev/null +++ b/examples/ads-legacy.amp.html @@ -0,0 +1,496 @@ + + + + + Ad examples + + + + + + + + + + +

    A9

    + + + +

    Weborama

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

    Fixed positioned ad (will not render)

    + + +
    + +

    Ad.Agio

    + + + +

    Adblade

    + + + +

    ADITION

    + + + +

    Adman

    + + + + +

    AdReactor

    + + + +

    AdSense

    + + + +

    AdSpirit

    + + + +

    AdTech

    + + + +

    Ad Up Technology

    + + + +

    Teads

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

    AppNexus single ad with tagid (placement id)

    + + + +

    AppNexus single ad with member and code

    + + + +

    AppNexus with JSON based configuration multi ad

    + + + + + + +

    Atomx

    + + + +

    Doubleclick

    + + + +

    Doubleclick with JSON based parameters

    + + + +

    Doubleclick no ad with fallback

    + +
    + Thank you for trying AMP! + +

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

    +
    +
    + +

    Doubleclick no ad with placeholder and fallback

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

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

    +
    +
    + +

    Doubleclick with overriden size

    + + +

    Challenging ad.

    + + + +

    Criteo

    + + + +

    Flite

    + + + +

    MANTIS

    + + + + + + +

    Media Impact

    + + + +

    plista responsive widget

    + + + +

    Taboola responsive widget

    + + + +

    DotAndAds masthead

    + + + +

    DotAndAds 300x250 box

    + + + +

    Industrybrains

    + + + +

    Open AdStream single ad

    + + + +

    OpenX

    + + + + +

    SmartAdServer ad

    + + + +

    SOVRN

    + + + +

    Yieldmo

    + + + +

    Revcontent Widget with placeholder and fallback

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

    Sortable ad

    + + + +

    TripleLift

    + + + +

    Rubicon Project Smart Tag

    + +
    Ad Loading...
    +
    Ad Load Failed.
    +
    + +

    I-Mobile 320x50 banner

    + + + +

    GMOSSP 320x50 banner

    + + + +

    Yieldbot 300x250

    + + + +

    AdStir 320x50 banner

    + + + +

    Colombia ad

    + + + +

    Sharethrough

    + + + +

    E-Planning 320x50

    + + + +

    Geniee SSP

    + + + +

    PulsePoint 300x250

    + + + + + diff --git a/examples/ads.amp.html b/examples/ads.amp.html index ee046886d9d3..f93f240ab901 100644 --- a/examples/ads.amp.html +++ b/examples/ads.amp.html @@ -6,43 +6,478 @@ + + + + +
    +
    + + + +
    +
    + +

    24smi

    + + -

    A9

    +

    A8

    + + + +

    A9 Search Ads

    + + + +

    A9 Recom Unsaved

    + + + +

    A9 Custom Ads

    + + + +

    A9 Recom Ads Sync

    + + + +

    A9 Recom Ads Async

    + + + + +

    AccessTrade

    + + + +

    Ad.Agio

    - - -
    -

    A9

    - - -
    + type="adagio" + data-sid="39" + data-loc="amp_ampw_amps_ampp_300x250" + data-keywords="" + data-uservars=""> + + +

    Adblade

    + + + +

    AdButler

    + + + +

    adincube

    + + + +

    ADITION

    + + + +

    Ad Generation

    + + + +

    Adhese

    + +
    +
    +
    + + +
    +
    +
    + +

    AdFox

    + + + +

    Adman

    + + + +

    AdmanMedia

    + + + +

    Admixer

    + + + +
    asd
    +
    + +

    AdOcean

    + + + + + + +

    AdPicker

    + + + +

    AdPlugg

    + +

    AdReactor

    - AdReactor

    AdSense

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

    AdTech

    - AdsNative + + + +

    AdSpeed

    + +
    +
    +
    + +

    AdSpirit

    + + + +

    AdStir 320x50 banner

    + + + +

    AdTech (1x1 fake image ad)

    + +

    AdThrive 320x50 banner

    + + + +

    AdUnity (300x250 demo banner)

    + + + +

    AdThrive 300x250 banner

    + + + +

    Ad Up Technology

    + + + +

    Adventive

    + + + +

    Adverline

    + + + +

    Adverticum

    + + + +

    AdvertServe

    + + + +

    Adyoulike

    + + +

    Affiliate-B

    + + + +

    AJA

    + + + +

    AMoAd banner

    + + + +

    AMoAd native

    + + + +

    AppNexus with JSON based configuration multi ad

    + + + + + + +

    AppVador

    + + + +

    Atomx

    + + + + + + +

    Bidtellect

    + + + +

    brainy

    + + + +

    Broadstreet Ads

    + +
    +
    +
    + +

    Bringhub Mini-Storefront

    + + + +

    CA A.J.A. Infeed

    + + + +

    CA ProFit-X

    + + + +

    Cedato

    + + + +

    Chargeads

    + + + +

    Connatix

    + + + +

    Content.ad Banner 320x50

    + + + +

    Content.ad Banner 300x250

    + + + +

    Content.ad Banner 300x250 2x2

    + + + +

    Content.ad Banner 300x600

    + + + +

    Content.ad Banner 300x600 5x2

    + + + +

    Criteo Passback

    +

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

    + + + + +

    Criteo Standalone

    +

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

    + + + + +

    Criteo RTA

    +

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

    + + + + +

    CSA

    + + + +

    Custom leaderboard

    + + + + +

    Custom square

    + + + + +

    Custom leaderboard with no slot specified

    + + + + +

    Cxense Display

    + + + +

    Dable

    + + + +

    Directadvert

    + + + +

    DistroScale

    + + + +

    DotAndAds masthead

    + + + +

    DotAndAds 300x250 box

    + + +

    Doubleclick

    - + data-slot="/4119129/mobile_ad_banner">

    Doubleclick with JSON based parameters

    - + json='{"targeting":{"sport":["rugby","cricket"]},"categoryExclusions":["health"],"tagForChildDirectedTreatment":0}'> -

    Doubleclick no ad with fallback

    - Doubleclick no ad + -
    - Thank you for trying AMP! + data-slot="/4119129/doesnt-exist"> + -

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

    -
    +

    Doubleclick with overriden size

    + -

    Doubleclick no ad with placeholder and fallback

    - Doubleclick challenging ad + -
    - Placeholder here!!! -
    -
    - Thank you for trying AMP! + layout="fixed" + data-slot="/35096353/amptesting/badvideoad"> + -

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

    -
    +

    eADV

    + -

    Doubleclick with overriden size

    - Engageya widget + + + +

    Epeex

    + + + +

    E-Planning 320x50

    + + + +

    Ezoic

    + + + +

    FlexOneELEPHANT

    + + + +

    FlexOneHARRIER

    + + + +

    Felmat

    + + + +

    Flite

    + + + +

    fluct

    + + + +

    Fusion

    + + + +

    Geniee SSP

    + + + +

    Giraff

    + + + +

    GMOSSP 320x50 banner

    + + + +

    GumGum 300x100 banner

    + + + +

    Holder 300x250 banner

    + + + +

    iBillboard 300x250 banner

    + + + +

    I-Mobile 320x50 banner

    + + + +

    Imonomy 728x90 banner

    + + + +

    Imedia

    + + + + + + +

    Index Exchange Header Tag

    + + + +

    Industrybrains

    + + + +

    InMobi

    + + + +

    Innity

    + + + + + + +

    Kargo

    + + + +

    Kiosked

    + + + +

    Kixer

    + + + +

    Kuadio

    + + + +

    Ligatus

    + + + +

    LockerDome

    + + + +

    LOKA

    + + + +

    MADS

    + + + +

    MANTIS

    + + + + + + +

    Media Impact

    + + + +

    Media.Net Header Bidder Tag

    + + + +

    Media.Net Contextual Monetization Tag

    + + + +

    Mediavine

    + +
    +
    +
    + +

    Medyanet

    + + + +

    Meg

    + + + +

    MicroAd 320x50 banner

    + + + +

    MixiMedia

    + + + +

    Mixpo

    + + + +

    Monetizer101

    + + + +

    myTarget

    + + + +

    myWidget

    + + + +

    Nativo

    + + + +

    Navegg

    +
    It is common to see a no-fill ad in this example.
    + + json='{"targeting":{"sport":["rugby","cricket"]}}'> + + +

    Nend

    + + + +

    NETLETIX

    + + + +

    Noddus

    + -

    Challenging ad.

    - - -

    plista

    - Nokta + + + +

    Open AdStream single ad

    + + + +

    OpenX

    + + + + +

    Outbrain widget

    + + + +

    Pixels Examples

    + + + +

    Plista responsive widget

    + + + +

    polymorphicAds

    + + + +

    popIn native ad

    + + + +

    Postquare widget

    + + + +

    Pressboard

    + + + +

    PubExchange

    + + + +

    PubGuru

    + + + +

    Pubmine 300x250

    + + + +

    PulsePoint Header Bidding 300x250

    + + + +

    PulsePoint 300x250

    + + + +

    Purch 300x250

    + + + +

    Quora

    + + + +

    Rambler&Co

    + + + +

    Realclick

    + + + +

    recomAD

    + + + +

    Relap

    + + + +

    Revcontent Responsive Tag

    + + + +

    RevJet Tag

    + + + +

    Red for Publishers

    + + + +

    Rubicon Project Smart Tag

    + + + +

    RUNative

    + + + +

    Sekindo

    + + + +

    Sharethrough

    + + + +

    Sklik

    + + + +

    SlimCut Media

    + + + +

    SmartAdServer ad

    + + + +

    smartclip

    + + + +

    SMI2

    + + + +

    sogou ad

    + + + + + + +

    Sortable ad

    + + + +

    SOVRN

    + + + +

    SpotX

    + + + +

    SunMedia

    + + + +

    Swoop

    +

    Taboola responsive widget

    - + - -

    DotAndAds masthead

    - + +

    Teads

    + - -

    DotAndAds 300x250 box

    + +

    TripleLift

    + + + +

    Trugaze

    + + + +

    UAS

    + type="uas" + json='{"accId": "132109", "adUnit": "10002912", "sizes": [[300, 250]], "targetings": {"country": ["India", "USA"], "car": "Civic"}, "locLat": "12.24", "locLon": "24.13", "locSrc": "wifi", "pageURL": "mydomain.com"}'> + +

    Unruly

    + + + +

    UZOU

    + + + +

    Weborama

    + + + +

    Widespace Panorama Ad

    + + + +

    Widespace Takeover Ad

    + + + +

    Wisteria Ad

    + + + +

    Xlift native ad

    + + + +

    Yahoo Display

    + + + +

    YahooJP YDN

    + + + +

    Yandex

    + + + +

    Yengo

    + + + +

    Yieldbot

    + + + +

    YIELD ONE

    + + + +

    Yieldmo

    + + + +

    Yieldpro

    + + + +

    ValueCommerce

    + + + +

    Video intelligence

    + + + +

    Videonow

    + + + +

    Viralize

    + + + +

    VMFive

    + + + +

    Webediads

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

    WP Media

    + + +

    ZEDO

    + + + +

    Zen

    + + + +

    ZergNet

    + + + +

    Zucks

    + + + diff --git a/examples/ads/inabox.adchoices.html b/examples/ads/inabox.adchoices.html new file mode 100644 index 000000000000..eda5a87dd9d8 --- /dev/null +++ b/examples/ads/inabox.adchoices.html @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + +
    +
    +
    +
    Ad
    + +
    +
    +
    + + + + + + + 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/adzerk.amp.html b/examples/adzerk.amp.html new file mode 100644 index 000000000000..1ef0e6b8df66 --- /dev/null +++ b/examples/adzerk.amp.html @@ -0,0 +1,36 @@ + + + + + Adzerk examples + + + + + + + + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + diff --git a/examples/alp.amp.html b/examples/alp.amp.html new file mode 100644 index 000000000000..8e962e183791 --- /dev/null +++ b/examples/alp.amp.html @@ -0,0 +1,32 @@ + + + + + ALP examples + + + + + + + + +

    ALP

    + +

    target=_blank

    + + + +

    target=_top

    + + + + diff --git a/examples/alp/creative.html b/examples/alp/creative.html new file mode 100644 index 000000000000..338fc9f93a5a --- /dev/null +++ b/examples/alp/creative.html @@ -0,0 +1,3 @@ + + + diff --git a/examples/amp-3d-gltf.amp.html b/examples/amp-3d-gltf.amp.html new file mode 100644 index 000000000000..f4e3dfdfc7be --- /dev/null +++ b/examples/amp-3d-gltf.amp.html @@ -0,0 +1,78 @@ + + + + + amp-3d-gltf example + + + + + + + + + + +

    Usage examples for <amp-3d-gltf />

    + +

    Layout: fixed

    +
    + +
    + +

    Layout: responsive

    +
    + + + + +
    + +

    With placeholder

    +
    + +
    Loading...
    +
    +
    + +

    With fallback

    +
    + +
    Something went wrong
    +
    +
    + + diff --git a/examples/amp-ad-exit.amp.html b/examples/amp-ad-exit.amp.html new file mode 100644 index 000000000000..b108ba0c2db9 --- /dev/null +++ b/examples/amp-ad-exit.amp.html @@ -0,0 +1,112 @@ + + + + amp-ad-exit example + + + + + + + + + + + + +
    +

    amp-ad-exit example

    +
    +

    product 1

    +
    +
    +

    product 2

    +
    +

    <A> tags don't work well (try middle-clicking) +

    + +
    + +
    amp-carousel example
    +
    The exit event is on the parent <div>
    +
    + But the buttons don't trigger an exit because of a default InactiveElement + filter that matches their
    amp-carousel-button
    class. +
    +
    +
    + + diff --git a/examples/amp-ad-template.amp.html b/examples/amp-ad-template.amp.html new file mode 100644 index 000000000000..464308572550 --- /dev/null +++ b/examples/amp-ad-template.amp.html @@ -0,0 +1,17 @@ + + + + + Amp Ad Common Example/Debug + + + + + + + + +

    Amp Ad Template Example/Debug

    + + + diff --git a/examples/amp-addthis.amp.html b/examples/amp-addthis.amp.html new file mode 100644 index 000000000000..1c1f1d57be67 --- /dev/null +++ b/examples/amp-addthis.amp.html @@ -0,0 +1,38 @@ + + + + + amp-addthis example + + + + + + + + + +
    + + +
    + + + + diff --git a/examples/amp-byside-content.amp.html b/examples/amp-byside-content.amp.html new file mode 100644 index 000000000000..eb36c1cdd080 --- /dev/null +++ b/examples/amp-byside-content.amp.html @@ -0,0 +1,55 @@ + + + + + BySide Content example + + + + + + + + +

    Responsive layout content (with restricted max width)

    +
    + +
    + +

    Fixed height content with overflow

    + + +

    Fixed layout content

    + + + diff --git a/examples/amp-consent-iframe.amp.html b/examples/amp-consent-iframe.amp.html new file mode 100644 index 000000000000..a6c36437b6da --- /dev/null +++ b/examples/amp-consent-iframe.amp.html @@ -0,0 +1,338 @@ + + + + + AMP Consent Test + + + + + + + + + + + + +
    + +
    +
    +

    Image that is blocked by consent

    + + +

    Image that is NOT blocked by consent

    + + + +
    + + +
    +
    +

    Lorem Ipsum

    + +

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

    +
    + +
    + Revoke consent + +
    + +
    + +
    +
    +

    + 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. +

    +
    +
    + + + + + + + +
    + Can I load the image?? + + + +
    +
    + Post Prompt UI + +
    +
    +
    +
    + +
    + +
    + + diff --git a/examples/amp-consent.amp.html b/examples/amp-consent.amp.html new file mode 100644 index 000000000000..0170b8377fcb --- /dev/null +++ b/examples/amp-consent.amp.html @@ -0,0 +1,342 @@ + + + + + AMP Consent Test + + + + + + + + + + + +
    + +
    +
    +

    Image that is blocked by '_till_responded' consent

    + + +

    Image that is blocked by '_till_accepted' consent

    + + +

    Image that is blocked by '_auto_reject' consent

    + + +

    Image that is blocked by 'default' consent

    + + +

    Image that is blocked by default (not specified) consent

    + + +

    Image that is NOT blocked by consent

    + + + + +
    + Can I load the image?? + + + +
    +
    + Post Prompt UI + +
    +
    +
    + + +
    +
    +

    Lorem Ipsum

    + +

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

    +
    + +
    + Revoke consent + +
    + +
    + +
    +
    +

    + 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/amp-date-countdown.amp.html b/examples/amp-date-countdown.amp.html new file mode 100644 index 000000000000..750ff888c6df --- /dev/null +++ b/examples/amp-date-countdown.amp.html @@ -0,0 +1,155 @@ + + + + + + amp-date-countdown example + + + + + + + + + + + + + + + + +

    + When Timer hits 0, will hide the timer itself and hide this message. +

    +

    + When Timer hits 0, will hide the timer itself and display this message. +

    + + + + + + + + + + + + + + diff --git a/examples/amp-delight-player.amp.html b/examples/amp-delight-player.amp.html new file mode 100644 index 000000000000..eec3ad188223 --- /dev/null +++ b/examples/amp-delight-player.amp.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + +

    Delight AMP Examples

    + +

    Responsive

    + + + +

    Non-responsive

    + + + + \ No newline at end of file diff --git a/examples/amp-embedly-card.amp.html b/examples/amp-embedly-card.amp.html new file mode 100644 index 000000000000..f515e14b755f --- /dev/null +++ b/examples/amp-embedly-card.amp.html @@ -0,0 +1,29 @@ + + + + + amp-embedly-card example + + + + + + + + + + + + diff --git a/examples/amp-geo.amp.html b/examples/amp-geo.amp.html new file mode 100644 index 000000000000..8a9752551c20 --- /dev/null +++ b/examples/amp-geo.amp.html @@ -0,0 +1,98 @@ + + + + + amp-geo example + + + + + + + + + + + + + + +

    My article title

    + + The amp-geo config on this page is: + +
    +    
    +    <amp-geo layout="nodisplay">
    +      <script type="application/json">
    +        {
    +          "ISOCountryGroups": {
    +            "nafta": [ "ca", "mx", "us", "unknown" ],
    +            "waldo": [ "unknown" ],
    +            "anz": [ "au", "nz" ]
    +          }
    +        }
    +      </script>
    +    </amp-geo>
    +    
    +  
    +

    + This means that if your country is "unknown" you will appear in the + "nafta" and "unknown" groups. To change the country for testing + append #amp-geo=<code> to the url and reload the page + (eg #amp-geo=nz) +

    + + + +

    +

    + +

    There is an amp-pixel variable substitution example here

    + +

    There is an amp-analytics variable substitution example here

    + + + + + \ No newline at end of file diff --git a/examples/amp-gist.amp.html b/examples/amp-gist.amp.html new file mode 100644 index 000000000000..569a3234cc66 --- /dev/null +++ b/examples/amp-gist.amp.html @@ -0,0 +1,37 @@ + + + + + amp-gist example + + + + + + + + +

    AMP-GIST Examples

    +

    Long Example

    + + +

    Smaller Example

    + + +

    Single File Example

    + + + + diff --git a/examples/amp-imgur.amp.html b/examples/amp-imgur.amp.html new file mode 100644 index 000000000000..f39061dcdf17 --- /dev/null +++ b/examples/amp-imgur.amp.html @@ -0,0 +1,15 @@ + + + + + amp-imgur example + + + + + + + + + + diff --git a/examples/amp-layout-intrinsic.amp.html b/examples/amp-layout-intrinsic.amp.html new file mode 100644 index 000000000000..483aba3e1235 --- /dev/null +++ b/examples/amp-layout-intrinsic.amp.html @@ -0,0 +1,114 @@ + + + + + Intrinsic layout example + + + + + + + + +

    Intrinsic

    +

    Responsive amp elements that are floated behave differently to regular html element like images. Due to the way they + are sized they default to 0x0. With + intrinsic layout they inflate until they either reach their natural size or hit a CSS constraint like max-width.

    + +

    amp-fit-text

    +
    + + Text in an intrinsic + amp-fit-text. This text will scale as the page resizes. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Integer fringilla arcu eu quam cursus cursus. Pellentesque aliquet elit mi. Vestibulum ultrices odio ligula. Fusce + quis erat pulvinar, pulvinar augue sed, consequat dolor. Suspendisse non ex at nunc viverra auctor. Quisque egestas + id metus vel euismod. Praesent euismod gravida augue nec consectetur. Nulla imperdiet venenatis nibh, in congue ligula + lobortis at. Sed et est leo. Vestibulum eget porta quam. Donec sed magna in velit faucibus semper eu vel ipsum. Suspendisse + lacus nisl, aliquet semper tellus et, lacinia auctor mauris. Duis in nibh quis diam viverra tempor et quis erat. + Aenean nec viverra turpis. Praesent quis lectus id augue ultrices venenatis. Aenean id hendrerit mi. + +
    +

    amp-layout

    +
    + +
    + Text in an intrinsic + div using + amp-layout. This text will overflow (hidden) when the page resizes. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Integer fringilla arcu eu quam cursus cursus. Pellentesque aliquet elit mi. Vestibulum ultrices + odio ligula. Fusce quis erat pulvinar, pulvinar augue sed, consequat dolor. Suspendisse non ex at nunc viverra + auctor. Quisque egestas id metus vel euismod. Praesent euismod gravida augue nec consectetur. Nulla imperdiet venenatis + nibh, in congue ligula lobortis at. Sed et est leo. Vestibulum eget porta quam. Donec sed magna in velit faucibus + semper eu vel ipsum. Suspendisse lacus nisl, aliquet semper tellus et, lacinia auctor mauris. Duis in nibh quis + diam viverra tempor et quis erat. Aenean nec viverra turpis. Praesent quis lectus id augue ultrices venenatis. + Aenean id hendrerit mi. +
    +
    +
    +

    amp-video

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

    Actions

    + + + + + +
    + +

    amp-img

    +

    layout=intrinsic 1280x872 floated

    +
    + +
    + + + diff --git a/examples/amp-layout.amp.html b/examples/amp-layout.amp.html new file mode 100644 index 000000000000..bdfd7235293e --- /dev/null +++ b/examples/amp-layout.amp.html @@ -0,0 +1,38 @@ + + + + + amp-layout example + + + + + + + +

    amp-layout

    + +

    responsive - 2x1 ratio

    + + This `amp-layout` acts like a `div` that always keeps a `2x1` aspect ratio. + + +

    fixed - 100x100

    + + will act like a 100px by 100px div + + +

    responsive - svg - 1x1 ratio

    + + + + Sorry, your browser does not support inline SVG. + + + + + diff --git a/examples/amp-list-with-form.amp.html b/examples/amp-list-with-form.amp.html new file mode 100644 index 000000000000..4569ebed6b57 --- /dev/null +++ b/examples/amp-list-with-form.amp.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + +

    <amp-list> with <form>

    +

    <form> has referenced <template>s

    + + + + + +

    <form> has nested <template>s

    + + + + + diff --git a/examples/amp-list.amp.html b/examples/amp-list.amp.html new file mode 100644 index 000000000000..bd02e14179f6 --- /dev/null +++ b/examples/amp-list.amp.html @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + +
    + Refresh All Lists +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Show more +
    +
    + + + + +
    + Show more +
    +
    + + + + diff --git a/examples/amp-mathml.amp.html b/examples/amp-mathml.amp.html new file mode 100644 index 000000000000..26a0b6e9787a --- /dev/null +++ b/examples/amp-mathml.amp.html @@ -0,0 +1,31 @@ + + + + + amp-mathml example + + + + + + + +

    The Quadratic Formula

    + + +

    Cauchy's Integral Formula

    + + +

    Double angle formula for Cosines

    + + +

    Inline formula.

    + This is an example of a formula placed inline in the middle of a block of text. This shows how the formula will fit inside a block of text and can be styled with CSS. + + diff --git a/examples/amp-mowplayer.amp.html b/examples/amp-mowplayer.amp.html new file mode 100644 index 000000000000..f25c467db208 --- /dev/null +++ b/examples/amp-mowplayer.amp.html @@ -0,0 +1,28 @@ + + + + + amp-mowplayer example + + + + + + + + +

    MowPlayer AMP Examples

    + +

    Responsive single video

    + + + +

    Non responsive with a playlist

    + + + + diff --git a/examples/amp-next-page.amp.html b/examples/amp-next-page.amp.html new file mode 100644 index 000000000000..de17a465103d --- /dev/null +++ b/examples/amp-next-page.amp.html @@ -0,0 +1,153 @@ + + + + + AMP next page examples + + + + + + + + + + + + + + + +

    Content discovery

    +

    +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae libero porta nulla iaculis viverra. Vestibulum consectetur scelerisque varius. Nam pulvinar dui a tortor hendrerit, id iaculis elit auctor. Nam dapibus felis in gravida ornare. Nulla varius id mauris sed venenatis. Sed turpis ex, aliquet nec fermentum eget, sollicitudin non est. Suspendisse tincidunt ornare tortor, at vehicula orci aliquam a. Aliquam eleifend odio quis quam dignissim posuere. Proin ac dolor rhoncus, consectetur ipsum non, dictum est. Nam sollicitudin est eu est aliquet eleifend. Duis condimentum, nisl eu finibus auctor, lectus odio fringilla nisi, quis cursus libero magna imperdiet purus. In condimentum vehicula est, nec varius est suscipit vitae. Maecenas ut sapien diam. Vivamus viverra nisl at quam pellentesque posuere. Cras ut nibh non arcu dignissim elementum. +

    +

    +Vestibulum a nisi est. Fusce consequat iaculis vehicula. Aliquam luctus nunc risus, ut congue augue tristique nec. In venenatis aliquam tristique. Integer eleifend diam vel nunc porttitor sollicitudin. Aliquam quis ullamcorper tortor. Morbi et dui vitae elit finibus sodales. Donec pharetra tincidunt neque, eget sagittis nisi sodales vel. Duis elit massa, malesuada a rhoncus sed, bibendum nec velit. Duis iaculis magna suscipit felis fermentum accumsan. Nunc venenatis pellentesque magna at tincidunt. Mauris nec placerat augue, eu auctor elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum pretium ligula diam, a eleifend lectus porttitor non. Integer vitae tempus enim, non pellentesque sem. Nam non sem lacinia, lacinia nisi non, consectetur enim. +

    +

    +Maecenas sed quam nec dolor rhoncus rutrum ut non mauris. Pellentesque ante dolor, pretium quis enim eu, condimentum volutpat augue. Donec nulla nisl, ullamcorper et hendrerit vel, sagittis sit amet ex. Mauris a enim in sem feugiat mollis in eu lorem. Quisque finibus a diam ut facilisis. Sed ultricies massa ut urna dapibus semper. Integer id nunc dictum, luctus ligula eu, bibendum ex. Etiam tincidunt sapien ut lorem pharetra feugiat. Aliquam erat volutpat. Sed vehicula tincidunt mauris, vitae cursus nisl. Nulla imperdiet ex at venenatis dignissim. +

    +

    +Sed pretium sed ex eu varius. Praesent sapien purus, tincidunt at dolor ac, porttitor fermentum enim. Morbi rhoncus quam eu lorem ultrices, vitae tempor felis volutpat. Suspendisse auctor, quam quis suscipit dictum, felis lectus imperdiet velit, a faucibus quam diam et risus. Etiam varius in arcu sed cursus. Praesent pulvinar enim nibh, eu euismod ante pretium et. Pellentesque nec vestibulum eros. Praesent nec venenatis ipsum. Duis pharetra suscipit mauris quis dictum. Pellentesque sed porttitor tellus. Aenean rutrum blandit est in tincidunt. Nulla ante orci, pellentesque id imperdiet at, posuere quis dui. Cras fringilla lobortis lectus. Mauris vitae convallis orci. Mauris sodales faucibus nulla vitae posuere. +

    +

    +Donec vehicula nisi eget metus blandit, at semper nunc porttitor. Vestibulum sit amet posuere risus, at mattis nibh. Maecenas ultricies scelerisque nibh et feugiat. Praesent mattis, nibh viverra consequat rhoncus, turpis leo venenatis orci, vitae mollis libero magna eget massa. In dapibus, metus sit amet venenatis finibus, lacus metus rhoncus massa, sit amet mattis tortor massa vitae nunc. Mauris a enim sagittis, condimentum tortor vitae, egestas nulla. Ut dictum laoreet sapien non blandit. Etiam fermentum, magna et tincidunt maximus, ex orci sollicitudin felis, ut dapibus orci ipsum quis leo. Nam accumsan tortor sit amet orci gravida, eget dapibus metus dapibus. Fusce congue ultrices dignissim. Duis quis metus in mi pharetra tempus. Etiam dapibus tellus vitae blandit rhoncus. Fusce commodo risus id sapien ultrices vehicula. +

    +

    +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae libero porta nulla iaculis viverra. Vestibulum consectetur scelerisque varius. Nam pulvinar dui a tortor hendrerit, id iaculis elit auctor. Nam dapibus felis in gravida ornare. Nulla varius id mauris sed venenatis. Sed turpis ex, aliquet nec fermentum eget, sollicitudin non est. Suspendisse tincidunt ornare tortor, at vehicula orci aliquam a. Aliquam eleifend odio quis quam dignissim posuere. Proin ac dolor rhoncus, consectetur ipsum non, dictum est. Nam sollicitudin est eu est aliquet eleifend. Duis condimentum, nisl eu finibus auctor, lectus odio fringilla nisi, quis cursus libero magna imperdiet purus. In condimentum vehicula est, nec varius est suscipit vitae. Maecenas ut sapien diam. Vivamus viverra nisl at quam pellentesque posuere. Cras ut nibh non arcu dignissim elementum. +

    +

    +Vestibulum a nisi est. Fusce consequat iaculis vehicula. Aliquam luctus nunc risus, ut congue augue tristique nec. In venenatis aliquam tristique. Integer eleifend diam vel nunc porttitor sollicitudin. Aliquam quis ullamcorper tortor. Morbi et dui vitae elit finibus sodales. Donec pharetra tincidunt neque, eget sagittis nisi sodales vel. Duis elit massa, malesuada a rhoncus sed, bibendum nec velit. Duis iaculis magna suscipit felis fermentum accumsan. Nunc venenatis pellentesque magna at tincidunt. Mauris nec placerat augue, eu auctor elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum pretium ligula diam, a eleifend lectus porttitor non. Integer vitae tempus enim, non pellentesque sem. Nam non sem lacinia, lacinia nisi non, consectetur enim. +

    +

    +Maecenas sed quam nec dolor rhoncus rutrum ut non mauris. Pellentesque ante dolor, pretium quis enim eu, condimentum volutpat augue. Donec nulla nisl, ullamcorper et hendrerit vel, sagittis sit amet ex. Mauris a enim in sem feugiat mollis in eu lorem. Quisque finibus a diam ut facilisis. Sed ultricies massa ut urna dapibus semper. Integer id nunc dictum, luctus ligula eu, bibendum ex. Etiam tincidunt sapien ut lorem pharetra feugiat. Aliquam erat volutpat. Sed vehicula tincidunt mauris, vitae cursus nisl. Nulla imperdiet ex at venenatis dignissim. +

    +

    +Sed pretium sed ex eu varius. Praesent sapien purus, tincidunt at dolor ac, porttitor fermentum enim. Morbi rhoncus quam eu lorem ultrices, vitae tempor felis volutpat. Suspendisse auctor, quam quis suscipit dictum, felis lectus imperdiet velit, a faucibus quam diam et risus. Etiam varius in arcu sed cursus. Praesent pulvinar enim nibh, eu euismod ante pretium et. Pellentesque nec vestibulum eros. Praesent nec venenatis ipsum. Duis pharetra suscipit mauris quis dictum. Pellentesque sed porttitor tellus. Aenean rutrum blandit est in tincidunt. Nulla ante orci, pellentesque id imperdiet at, posuere quis dui. Cras fringilla lobortis lectus. Mauris vitae convallis orci. Mauris sodales faucibus nulla vitae posuere. +

    +

    +Donec vehicula nisi eget metus blandit, at semper nunc porttitor. Vestibulum sit amet posuere risus, at mattis nibh. Maecenas ultricies scelerisque nibh et feugiat. Praesent mattis, nibh viverra consequat rhoncus, turpis leo venenatis orci, vitae mollis libero magna eget massa. In dapibus, metus sit amet venenatis finibus, lacus metus rhoncus massa, sit amet mattis tortor massa vitae nunc. Mauris a enim sagittis, condimentum tortor vitae, egestas nulla. Ut dictum laoreet sapien non blandit. Etiam fermentum, magna et tincidunt maximus, ex orci sollicitudin felis, ut dapibus orci ipsum quis leo. Nam accumsan tortor sit amet orci gravida, eget dapibus metus dapibus. Fusce congue ultrices dignissim. Duis quis metus in mi pharetra tempus. Etiam dapibus tellus vitae blandit rhoncus. Fusce commodo risus id sapien ultrices vehicula. +

    +

    +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae libero porta nulla iaculis viverra. Vestibulum consectetur scelerisque varius. Nam pulvinar dui a tortor hendrerit, id iaculis elit auctor. Nam dapibus felis in gravida ornare. Nulla varius id mauris sed venenatis. Sed turpis ex, aliquet nec fermentum eget, sollicitudin non est. Suspendisse tincidunt ornare tortor, at vehicula orci aliquam a. Aliquam eleifend odio quis quam dignissim posuere. Proin ac dolor rhoncus, consectetur ipsum non, dictum est. Nam sollicitudin est eu est aliquet eleifend. Duis condimentum, nisl eu finibus auctor, lectus odio fringilla nisi, quis cursus libero magna imperdiet purus. In condimentum vehicula est, nec varius est suscipit vitae. Maecenas ut sapien diam. Vivamus viverra nisl at quam pellentesque posuere. Cras ut nibh non arcu dignissim elementum. +

    +

    +Vestibulum a nisi est. Fusce consequat iaculis vehicula. Aliquam luctus nunc risus, ut congue augue tristique nec. In venenatis aliquam tristique. Integer eleifend diam vel nunc porttitor sollicitudin. Aliquam quis ullamcorper tortor. Morbi et dui vitae elit finibus sodales. Donec pharetra tincidunt neque, eget sagittis nisi sodales vel. Duis elit massa, malesuada a rhoncus sed, bibendum nec velit. Duis iaculis magna suscipit felis fermentum accumsan. Nunc venenatis pellentesque magna at tincidunt. Mauris nec placerat augue, eu auctor elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum pretium ligula diam, a eleifend lectus porttitor non. Integer vitae tempus enim, non pellentesque sem. Nam non sem lacinia, lacinia nisi non, consectetur enim. +

    +

    +Maecenas sed quam nec dolor rhoncus rutrum ut non mauris. Pellentesque ante dolor, pretium quis enim eu, condimentum volutpat augue. Donec nulla nisl, ullamcorper et hendrerit vel, sagittis sit amet ex. Mauris a enim in sem feugiat mollis in eu lorem. Quisque finibus a diam ut facilisis. Sed ultricies massa ut urna dapibus semper. Integer id nunc dictum, luctus ligula eu, bibendum ex. Etiam tincidunt sapien ut lorem pharetra feugiat. Aliquam erat volutpat. Sed vehicula tincidunt mauris, vitae cursus nisl. Nulla imperdiet ex at venenatis dignissim. +

    +

    +Sed pretium sed ex eu varius. Praesent sapien purus, tincidunt at dolor ac, porttitor fermentum enim. Morbi rhoncus quam eu lorem ultrices, vitae tempor felis volutpat. Suspendisse auctor, quam quis suscipit dictum, felis lectus imperdiet velit, a faucibus quam diam et risus. Etiam varius in arcu sed cursus. Praesent pulvinar enim nibh, eu euismod ante pretium et. Pellentesque nec vestibulum eros. Praesent nec venenatis ipsum. Duis pharetra suscipit mauris quis dictum. Pellentesque sed porttitor tellus. Aenean rutrum blandit est in tincidunt. Nulla ante orci, pellentesque id imperdiet at, posuere quis dui. Cras fringilla lobortis lectus. Mauris vitae convallis orci. Mauris sodales faucibus nulla vitae posuere. +

    +

    +Donec vehicula nisi eget metus blandit, at semper nunc porttitor. Vestibulum sit amet posuere risus, at mattis nibh. Maecenas ultricies scelerisque nibh et feugiat. Praesent mattis, nibh viverra consequat rhoncus, turpis leo venenatis orci, vitae mollis libero magna eget massa. In dapibus, metus sit amet venenatis finibus, lacus metus rhoncus massa, sit amet mattis tortor massa vitae nunc. Mauris a enim sagittis, condimentum tortor vitae, egestas nulla. Ut dictum laoreet sapien non blandit. Etiam fermentum, magna et tincidunt maximus, ex orci sollicitudin felis, ut dapibus orci ipsum quis leo. Nam accumsan tortor sit amet orci gravida, eget dapibus metus dapibus. Fusce congue ultrices dignissim. Duis quis metus in mi pharetra tempus. Etiam dapibus tellus vitae blandit rhoncus. Fusce commodo risus id sapien ultrices vehicula. +

    +

    +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae libero porta nulla iaculis viverra. Vestibulum consectetur scelerisque varius. Nam pulvinar dui a tortor hendrerit, id iaculis elit auctor. Nam dapibus felis in gravida ornare. Nulla varius id mauris sed venenatis. Sed turpis ex, aliquet nec fermentum eget, sollicitudin non est. Suspendisse tincidunt ornare tortor, at vehicula orci aliquam a. Aliquam eleifend odio quis quam dignissim posuere. Proin ac dolor rhoncus, consectetur ipsum non, dictum est. Nam sollicitudin est eu est aliquet eleifend. Duis condimentum, nisl eu finibus auctor, lectus odio fringilla nisi, quis cursus libero magna imperdiet purus. In condimentum vehicula est, nec varius est suscipit vitae. Maecenas ut sapien diam. Vivamus viverra nisl at quam pellentesque posuere. Cras ut nibh non arcu dignissim elementum. +

    +

    +Vestibulum a nisi est. Fusce consequat iaculis vehicula. Aliquam luctus nunc risus, ut congue augue tristique nec. In venenatis aliquam tristique. Integer eleifend diam vel nunc porttitor sollicitudin. Aliquam quis ullamcorper tortor. Morbi et dui vitae elit finibus sodales. Donec pharetra tincidunt neque, eget sagittis nisi sodales vel. Duis elit massa, malesuada a rhoncus sed, bibendum nec velit. Duis iaculis magna suscipit felis fermentum accumsan. Nunc venenatis pellentesque magna at tincidunt. Mauris nec placerat augue, eu auctor elit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum pretium ligula diam, a eleifend lectus porttitor non. Integer vitae tempus enim, non pellentesque sem. Nam non sem lacinia, lacinia nisi non, consectetur enim. +

    +

    +Maecenas sed quam nec dolor rhoncus rutrum ut non mauris. Pellentesque ante dolor, pretium quis enim eu, condimentum volutpat augue. Donec nulla nisl, ullamcorper et hendrerit vel, sagittis sit amet ex. Mauris a enim in sem feugiat mollis in eu lorem. Quisque finibus a diam ut facilisis. Sed ultricies massa ut urna dapibus semper. Integer id nunc dictum, luctus ligula eu, bibendum ex. Etiam tincidunt sapien ut lorem pharetra feugiat. Aliquam erat volutpat. Sed vehicula tincidunt mauris, vitae cursus nisl. Nulla imperdiet ex at venenatis dignissim. +

    +

    +Sed pretium sed ex eu varius. Praesent sapien purus, tincidunt at dolor ac, porttitor fermentum enim. Morbi rhoncus quam eu lorem ultrices, vitae tempor felis volutpat. Suspendisse auctor, quam quis suscipit dictum, felis lectus imperdiet velit, a faucibus quam diam et risus. Etiam varius in arcu sed cursus. Praesent pulvinar enim nibh, eu euismod ante pretium et. Pellentesque nec vestibulum eros. Praesent nec venenatis ipsum. Duis pharetra suscipit mauris quis dictum. Pellentesque sed porttitor tellus. Aenean rutrum blandit est in tincidunt. Nulla ante orci, pellentesque id imperdiet at, posuere quis dui. Cras fringilla lobortis lectus. Mauris vitae convallis orci. Mauris sodales faucibus nulla vitae posuere. +

    +

    +Donec vehicula nisi eget metus blandit, at semper nunc porttitor. Vestibulum sit amet posuere risus, at mattis nibh. Maecenas ultricies scelerisque nibh et feugiat. Praesent mattis, nibh viverra consequat rhoncus, turpis leo venenatis orci, vitae mollis libero magna eget massa. In dapibus, metus sit amet venenatis finibus, lacus metus rhoncus massa, sit amet mattis tortor massa vitae nunc. Mauris a enim sagittis, condimentum tortor vitae, egestas nulla. Ut dictum laoreet sapien non blandit. Etiam fermentum, magna et tincidunt maximus, ex orci sollicitudin felis, ut dapibus orci ipsum quis leo. Nam accumsan tortor sit amet orci gravida, eget dapibus metus dapibus. Fusce congue ultrices dignissim. Duis quis metus in mi pharetra tempus. Etiam dapibus tellus vitae blandit rhoncus. Fusce commodo risus id sapien ultrices vehicula. +

    +

    +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae libero porta nulla iaculis viverra. Vestibulum consectetur scelerisque varius. Nam pulvinar dui a tortor hendrerit, id iaculis elit auctor. Nam dapibus felis in gravida ornare. Nulla varius id mauris sed venenatis. Sed turpis ex, aliquet nec fermentum eget, sollicitudin non est. Suspendisse tincidunt ornare tortor, at vehicula orci aliquam a. Aliquam eleifend odio quis quam dignissim posuere. Proin ac dolor rhoncus, consectetur ipsum non, dictum est. Nam sollicitudin est eu est aliquet eleifend. Duis condimentum, nisl eu finibus auctor, lectus odio fringilla nisi, quis cursus libero magna imperdiet purus. In condimentum vehicula est, nec varius est suscipit vitae. Maecenas ut sapien diam. Vivamus viverra nisl at quam pellentesque posuere. Cras ut nibh non arcu dignissim elementum. +

    +

    +Donec vehicula nisi eget metus blandit, at semper nunc porttitor. Vestibulum sit amet posuere risus, at mattis nibh. Maecenas ultricies scelerisque nibh et feugiat. Praesent mattis, nibh viverra consequat rhoncus, turpis leo venenatis orci, vitae mollis libero magna eget massa. In dapibus, metus sit amet venenatis finibus, lacus metus rhoncus massa, sit amet mattis tortor massa vitae nunc. Mauris a enim sagittis, condimentum tortor vitae, egestas nulla. Ut dictum laoreet sapien non blandit. Etiam fermentum, magna et tincidunt maximus, ex orci sollicitudin felis, ut dapibus orci ipsum quis leo. Nam accumsan tortor sit amet orci gravida, eget dapibus metus dapibus. Fusce congue ultrices dignissim. Duis quis metus in mi pharetra tempus. Etiam dapibus tellus vitae blandit rhoncus. Fusce commodo risus id sapien ultrices vehicula. +

    + +
    + READ ANOTHER ARTICLE FROM OUR SITE! +
    + +
    + + diff --git a/examples/amp-orientation-observer-3d-parallax.amp.html b/examples/amp-orientation-observer-3d-parallax.amp.html new file mode 100644 index 000000000000..60a0780d9c5c --- /dev/null +++ b/examples/amp-orientation-observer-3d-parallax.amp.html @@ -0,0 +1,119 @@ + + + + + amp-orientation-observer + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +

    + Lorem Ipsum
    Dolor Sit
    Lorem Ipsum +

    +
    +
    + + + diff --git a/examples/amp-orientation-observer-amp-3d-gltf.amp.html b/examples/amp-orientation-observer-amp-3d-gltf.amp.html new file mode 100644 index 000000000000..269c67d06f7d --- /dev/null +++ b/examples/amp-orientation-observer-amp-3d-gltf.amp.html @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + +
    +
    + + + +
    + +
    +
    +
    + + + \ No newline at end of file diff --git a/examples/amp-orientation-observer-panorama.amp.html b/examples/amp-orientation-observer-panorama.amp.html new file mode 100644 index 000000000000..b35e47dd8e33 --- /dev/null +++ b/examples/amp-orientation-observer-panorama.amp.html @@ -0,0 +1,58 @@ + + + + + amp-orientation-observer + + + + + + + + + + + + + + + +
    + + +
    + + diff --git a/examples/amp-orientation-observer-scroll.amp.html b/examples/amp-orientation-observer-scroll.amp.html new file mode 100644 index 000000000000..634170bec5df --- /dev/null +++ b/examples/amp-orientation-observer-scroll.amp.html @@ -0,0 +1,68 @@ + + + + + amp-orientation-observer + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    Move phone along y axis to scroll

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam id est et dui maximus sagittis vel imperdiet nibh. Nam finibus nisl sit amet neque gravida pellentesque. Phasellus vulputate, tortor ut pretium ultricies, nibh massa vestibulum nisi, a vestibulum orci velit eget nisl. Maecenas dui nulla, consectetur sed porttitor ac, varius quis nibh. In pellentesque nisi quis ligula fringilla, non lobortis felis tincidunt. Nullam hendrerit sodales purus, nec rhoncus urna ultricies ac. Cras vestibulum pulvinar libero, quis faucibus nisi. Quisque ut lacus vitae justo faucibus mollis vel bibendum neque. Quisque pretium nunc in nunc vulputate, interdum ullamcorper nisl convallis. Cras vitae finibus urna. Sed enim turpis, consectetur eu velit nec, sodales mollis justo. Curabitur pretium luctus felis sagittis rhoncus. Donec vitae vehicula erat, non pellentesque odio. Vestibulum accumsan rhoncus placerat. Donec rutrum ullamcorper sodales. Vestibulum pretium ut diam faucibus tincidunt. + + Aenean vestibulum nisl nec arcu eleifend posuere. Aliquam nec feugiat nibh. Cras pretium ut purus quis pharetra. Morbi urna augue, lobortis ac porttitor vitae, feugiat eget dui. Nam lacinia commodo tellus vel sagittis. Aliquam id pharetra metus. Nam tristique vulputate maximus. Donec consequat aliquam lacus, ac fermentum magna iaculis tristique. In urna dolor, rutrum non mattis at, luctus nec lectus. Maecenas consequat, massa at placerat convallis, arcu elit consequat nulla, vel egestas justo augue vitae mauris. Nullam est diam, aliquam sit amet posuere non, condimentum et ante. Ut at ullamcorper ipsum. Nullam non efficitur sapien. Praesent vitae odio diam. Nullam dignissim hendrerit neque non tempor. + + Praesent non arcu volutpat, maximus risus in, rutrum tellus. Quisque nec porttitor purus. Quisque hendrerit erat consequat dignissim vehicula. Quisque tortor orci, bibendum et lacinia at, porttitor nec est. Nunc venenatis sollicitudin dui, et fermentum lectus euismod vitae. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus diam ante, sagittis non finibus sed, pulvinar id dolor. Maecenas ac facilisis lacus. Suspendisse potenti. + + Curabitur dignissim consequat ante, quis facilisis ipsum faucibus nec. Suspendisse potenti. Mauris lorem eros, finibus id tincidunt quis, bibendum at urna. Aenean quis tortor ultricies, sodales ipsum id, imperdiet felis. Maecenas vestibulum cursus est convallis imperdiet. In convallis tempus lacus, suscipit tempus turpis mattis id. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin massa massa, molestie eu pretium in, eleifend vel erat. Pellentesque dictum turpis sit amet neque elementum aliquam a sed metus. Donec dignissim laoreet massa vel vehicula. Praesent pretium arcu consequat augue dignissim, at ultrices quam placerat. Proin justo odio, fermentum sed malesuada eu, facilisis non quam. In molestie enim nec ligula tempus, sed condimentum dui molestie. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec lobortis, lacus cursus varius malesuada, lacus enim blandit ipsum, non hendrerit est sapien et ex. Nullam quis pulvinar nulla, vitae venenatis massa. + + In mattis at eros id ullamcorper. Aenean ac hendrerit magna. Sed volutpat sapien quis justo accumsan eleifend. Praesent lobortis, velit sed elementum molestie, urna nibh malesuada lacus, vitae efficitur tellus elit ac augue. Aliquam erat volutpat. Praesent ultricies felis erat, ut luctus nulla pellentesque in. Etiam lacinia neque id neque egestas tempor. +

    End of text

    +
    +
    + + diff --git a/examples/amp-orientation-observer.amp.html b/examples/amp-orientation-observer.amp.html new file mode 100644 index 000000000000..51f8629bb90f --- /dev/null +++ b/examples/amp-orientation-observer.amp.html @@ -0,0 +1,161 @@ + + + + + amp-orientation-observer + + + + + + + + + + + + + + + + + + + +
    +
    + +

    Rotate phone along y axis

    +
    + + + +
    +
    +
    + +
    + +

    Rotate phone along x axis

    +
    + + + +
    +
    +
    + +
    + +

    Rotate phone horizontally

    +
    + + + +
    +
    +
    + +
    +
    + + diff --git a/examples/amp-position-observer.amp.html b/examples/amp-position-observer.amp.html new file mode 100644 index 000000000000..393bf38fa6f2 --- /dev/null +++ b/examples/amp-position-observer.amp.html @@ -0,0 +1,160 @@ + + + + + + amp-position-observer + + + + + + + + + + + + + + + + + +
    +
    + +

    Scrollbound animations with amp-position-observer and amp-animation

    +

    The following animation does one round (12pm-6pm) via scrolling when fully visible between the middle 80% of the viewport

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

    Custom visibility start-end with amp-position-observer and amp-animation

    +

    The following animation will start when the scene is 50% visible and pauses when 50% invisible

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

    Custom visibility end with amp-position-observer and amp-video

    +

    If played, the following video will pause when 20% invisible

    +
    + + + + + +
    + +
    +
    +
    + + diff --git a/examples/amp-rate-limited.amp.html b/examples/amp-rate-limited.amp.html new file mode 100644 index 000000000000..09baa9b62e47 --- /dev/null +++ b/examples/amp-rate-limited.amp.html @@ -0,0 +1,72 @@ + + + + + Rate Limited Input Examples in AMP + + + + + + + + +

    On Change Slider

    +

    This slider's value should update on mouse up, after the user finishes dragging the slider.

    +
    +
    + +
    +
    +

    Throttled Slider

    +

    This slider's value should update at a throttled rate of once every 100ms, while the user is dragging the slider.

    +
    +
    + +
    +
    +

    Debounced Slider

    +

    This slider's value should update 300ms after the user stops moving the slider, but not necessarily after mouse up.

    +
    +
    + +
    +
    + + diff --git a/examples/amp-riddle-quiz.amp.html b/examples/amp-riddle-quiz.amp.html new file mode 100644 index 000000000000..1ecac197d63c --- /dev/null +++ b/examples/amp-riddle-quiz.amp.html @@ -0,0 +1,17 @@ + + + + + amp-riddle-quiz example + + + + + + + +
    + +
    + + diff --git a/examples/amp-script/hello-world.amp.html b/examples/amp-script/hello-world.amp.html new file mode 100644 index 000000000000..d4e56e3bf5c7 --- /dev/null +++ b/examples/amp-script/hello-world.amp.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + +
    + + diff --git a/examples/amp-script/hello-world.html b/examples/amp-script/hello-world.html new file mode 100644 index 000000000000..1247fda690bd --- /dev/null +++ b/examples/amp-script/hello-world.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/examples/amp-script/hello-world.js b/examples/amp-script/hello-world.js new file mode 100644 index 000000000000..24be1ed6eb46 --- /dev/null +++ b/examples/amp-script/hello-world.js @@ -0,0 +1,42 @@ +/** + * Copyright 2018 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. + */ + +document.getElementById('hello').addEventListener('click', () => { + const el = document.createElement('h1'); + el.textContent = 'Hello World!'; + document.body.appendChild(el); +}); + +// should be allowed. +document.getElementById('amp-img').addEventListener('click', () => { + const el = document.createElement('amp-img'); + el.setAttribute('width', '300'); + el.setAttribute('height', '200'); + el.setAttribute('src', '/examples/img/hero@1x.jpg') + document.body.appendChild(el); +}); + +// + + + + + + + + +
    +
    + +

    todos

    +
    +
    +
    + + + + diff --git a/examples/amp-script/todomvc.es6.js b/examples/amp-script/todomvc.es6.js new file mode 100644 index 000000000000..9655af5ca6a4 --- /dev/null +++ b/examples/amp-script/todomvc.es6.js @@ -0,0 +1,57 @@ +var $jscomp={scope:{},getGlobal:function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global?global:a}};$jscomp.global=$jscomp.getGlobal(this);$jscomp.initSymbol=function(){$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol);$jscomp.initSymbol=function(){}};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(a){return"jscomp_symbol_"+a+$jscomp.symbolCounter_++}; +$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();$jscomp.global.Symbol.iterator||($jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));$jscomp.initSymbolIterator=function(){}};$jscomp.makeIterator=function(a){$jscomp.initSymbolIterator();if(a[$jscomp.global.Symbol.iterator])return a[$jscomp.global.Symbol.iterator]();var b=0;return{next:function(){return b==a.length?{done:!0}:{done:!1,value:a[b++]}}}}; +$jscomp.arrayFromIterator=function(a){for(var b,c=[];!(b=a.next()).done;)c.push(b.value);return c};$jscomp.arrayFromIterable=function(a){return a instanceof Array?a:$jscomp.arrayFromIterator($jscomp.makeIterator(a))}; +$jscomp.inherits=function(a,b){function c(){}c.prototype=b.prototype;a.prototype=new c;a.prototype.constructor=a;for(var d in b)if($jscomp.global.Object.defineProperties){var e=$jscomp.global.Object.getOwnPropertyDescriptor(b,d);e&&$jscomp.global.Object.defineProperty(a,d,e)}else a[d]=b[d]};$jscomp.array=$jscomp.array||{};$jscomp.array.done_=function(){return{done:!0,value:void 0}}; +$jscomp.array.arrayIterator_=function(a,b){a instanceof String&&(a=String(a));var c=0;$jscomp.initSymbol();$jscomp.initSymbolIterator();var d={},e=(d.next=function(){if(cb;)--c in this?this[--a]=this[c]:delete this[a];return this};$jscomp.array.copyWithin$install=function(){Array.prototype.copyWithin||(Array.prototype.copyWithin=$jscomp.array.copyWithin)}; +$jscomp.array.fill=function(a,b,c){null!=c&&a.length||(c=this.length||0);c=Number(c);for(b=Number((void 0===b?0:b)||0);b>>0;if(0===a)return 32;var b=0;0===(a&4294901760)&&(a<<=16,b+=16);0===(a&4278190080)&&(a<<=8,b+=8);0===(a&4026531840)&&(a<<=4,b+=4);0===(a&3221225472)&&(a<<=2,b+=2);0===(a&2147483648)&&b++;return b};$jscomp.math.imul=function(a,b){a=Number(a);b=Number(b);var c=a&65535,d=b&65535;return c*d+((a>>>16&65535)*d+c*(b>>>16&65535)<<16>>>0)|0};$jscomp.math.sign=function(a){a=Number(a);return 0===a||isNaN(a)?a:0a&&-.25a&&-.25a?-b:b};$jscomp.math.acosh=function(a){a=Number(a);return Math.log(a+Math.sqrt(a*a-1))};$jscomp.math.asinh=function(a){a=Number(a);if(0===a)return a;var b=Math.log(Math.abs(a)+Math.sqrt(a*a+1));return 0>a?-b:b}; +$jscomp.math.atanh=function(a){a=Number(a);return($jscomp.math.log1p(a)-$jscomp.math.log1p(-a))/2}; +$jscomp.math.hypot=function(a,b,c){for(var d=[],e=2;ef){a/=f;b/=f;g=a*a+b*b;d=$jscomp.makeIterator(d);for(e=d.next();!e.done;e=d.next())e=e.value,e=Number(e)/f,g+=e*e;return Math.sqrt(g)*f}f=a*a+b*b;d=$jscomp.makeIterator(d);for(e=d.next();!e.done;e=d.next())e=e.value,e=Number(e),f+= +e*e;return Math.sqrt(f)};$jscomp.math.trunc=function(a){a=Number(a);if(isNaN(a)||Infinity===a||-Infinity===a||0===a)return a;var b=Math.floor(Math.abs(a));return 0>a?-b:b};$jscomp.math.cbrt=function(a){if(0===a)return a;a=Number(a);var b=Math.pow(Math.abs(a),1/3);return 0>a?-b:b};$jscomp.number=$jscomp.number||{};$jscomp.number.isFinite=function(a){return"number"!==typeof a?!1:!isNaN(a)&&Infinity!==a&&-Infinity!==a}; +$jscomp.number.isInteger=function(a){return $jscomp.number.isFinite(a)?a===Math.floor(a):!1};$jscomp.number.isNaN=function(a){return"number"===typeof a&&isNaN(a)};$jscomp.number.isSafeInteger=function(a){return $jscomp.number.isInteger(a)&&Math.abs(a)<=$jscomp.number.MAX_SAFE_INTEGER};$jscomp.number.EPSILON=Math.pow(2,-52);$jscomp.number.MAX_SAFE_INTEGER=9007199254740991;$jscomp.number.MIN_SAFE_INTEGER=-9007199254740991;$jscomp.object=$jscomp.object||{}; +$jscomp.object.assign=function(a,b){for(var c=[],d=1;dd||1114111=d?c+=String.fromCharCode(d):(d-=65536,c+=String.fromCharCode(d>>>10&1023|55296),c+=String.fromCharCode(d&1023|56320))}return c}; +$jscomp.string.repeat=function(a){var b=this.toString();if(0>a||1342177279>>=1)b+=b;return c};$jscomp.string.repeat$install=function(){String.prototype.repeat||(String.prototype.repeat=$jscomp.string.repeat)}; +$jscomp.string.codePointAt=function(a){var b=this.toString(),c=b.length;a=Number(a)||0;if(0<=a&&ad||56319a||57343=e};$jscomp.string.startsWith$install=function(){String.prototype.startsWith||(String.prototype.startsWith=$jscomp.string.startsWith)}; +$jscomp.string.endsWith=function(a,b){$jscomp.string.noRegExp_(a,"endsWith");var c=this.toString();a+="";void 0===b&&(b=c.length);for(var d=Math.max(0,Math.min(b|0,c.length)),e=a.length;0=e};$jscomp.string.endsWith$install=function(){String.prototype.endsWith||(String.prototype.endsWith=$jscomp.string.endsWith)}; +var module$src$item={},Item$$module$src$item,ItemList$$module$src$item,Empty$$module$src$item={Record:{}},EmptyItemQuery$$module$src$item,emptyItemQuery$$module$src$item=Empty$$module$src$item.Record,ItemQuery$$module$src$item,ItemUpdate$$module$src$item;module$src$item.emptyItemQuery=emptyItemQuery$$module$src$item;var module$src$store={},Store$$module$src$store=function(a,b){var c=window.localStorage,d;this.getLocalStorage=function(){return d||JSON.parse(c.getItem(a)||"[]")};this.setLocalStorage=function(b){c.setItem(a,JSON.stringify(d=b))};b&&b()};Store$$module$src$store.prototype.find=function(a,b){var c=this.getLocalStorage(),d;b(c.filter(function(b){for(d in a)if(a[d]!==b[d])return!1;return!0}))}; +Store$$module$src$store.prototype.update=function(a,b){for(var c=a.id,d=this.getLocalStorage(),e=d.length,f;e--;)if(d[e].id===c){for(f in a)d[e][f]=a[f];break}this.setLocalStorage(d);b&&b()};Store$$module$src$store.prototype.insert=function(a,b){var c=this.getLocalStorage();c.push(a);this.setLocalStorage(c);b&&b()};Store$$module$src$store.prototype.remove=function(a,b){var c,d=this.getLocalStorage().filter(function(b){for(c in a)if(a[c]!==b[c])return!0;return!1});this.setLocalStorage(d);b&&b(d)}; +Store$$module$src$store.prototype.count=function(a){this.find(module$src$item.emptyItemQuery,function(b){for(var c=b.length,d=c,e=0;d--;)e+=b[d].completed;a(c,c-e,e)})};module$src$store["default"]=Store$$module$src$store;var module$src$helpers={};function qs$$module$src$helpers(a,b){return(b||document).querySelector(a)}function $on$$module$src$helpers(a,b,c,d){a.addEventListener(b,c,!!d)}function $delegate$$module$src$helpers(a,b,c,d,e){$on$$module$src$helpers(a,c,function(c){for(var e=c.target,h=a.querySelectorAll(b),k=h.length;k--;)if(h[k]===e){d.call(e,c);break}},!!e)}var escapeForHTML$$module$src$helpers=function(a){return a.replace(/[&<]/g,function(a){return"&"===a?"&":"<"})};module$src$helpers.qs=qs$$module$src$helpers; +module$src$helpers.$on=$on$$module$src$helpers;module$src$helpers.$delegate=$delegate$$module$src$helpers;module$src$helpers.escapeForHTML=escapeForHTML$$module$src$helpers;var module$src$template={},Template$$module$src$template=function(){};Template$$module$src$template.prototype.itemList=function(a){return a.reduce(function(a,c){return a+('\n
  • \n\t
    \n\t\t\n\t\t\n\t\t\n\t
    \n
  • ')},"")}; +Template$$module$src$template.prototype.itemCounter=function(a){return a+" item"+(1!==a?"s":"")+" left"};module$src$template["default"]=Template$$module$src$template;var module$src$view={},_itemId$$module$src$view=function(a){return parseInt(a.parentNode.dataset.id||a.parentNode.parentNode.dataset.id,10)},ENTER_KEY$$module$src$view=13,ESCAPE_KEY$$module$src$view=27,View$$module$src$view=function(a){var b=this;this.template=a;this.$todoList=module$src$helpers.qs(".todo-list");this.$todoItemCounter=module$src$helpers.qs(".todo-count");this.$clearCompleted=module$src$helpers.qs(".clear-completed");this.$main=module$src$helpers.qs(".main");this.$toggleAll=module$src$helpers.qs(".toggle-all"); +this.$newTodo=module$src$helpers.qs(".new-todo");module$src$helpers.$delegate(this.$todoList,"li label","dblclick",function(a){b.editItem(a.target)})};View$$module$src$view.prototype.editItem=function(a){var b=a.parentElement.parentElement;b.classList.add("editing");var c=document.createElement("input");c.className="edit";c.value=a.innerText;b.appendChild(c);c.focus()};View$$module$src$view.prototype.showItems=function(a){this.$todoList.innerHTML=this.template.itemList(a)}; +View$$module$src$view.prototype.removeItem=function(a){(a=module$src$helpers.qs('[data-id="'+a+'"]'))&&this.$todoList.removeChild(a)};View$$module$src$view.prototype.setItemsLeft=function(a){this.$todoItemCounter.innerHTML=this.template.itemCounter(a)};View$$module$src$view.prototype.setClearCompletedButtonVisibility=function(a){this.$clearCompleted.style.display=a?"block":"none"};View$$module$src$view.prototype.setMainVisibility=function(a){this.$main.style.display=a?"block":"none"}; +View$$module$src$view.prototype.setCompleteAllCheckbox=function(a){this.$toggleAll.checked=!!a};View$$module$src$view.prototype.updateFilterButtons=function(a){module$src$helpers.qs(".filters .selected").className="";module$src$helpers.qs('.filters [href="#/'+a+'"]').className="selected"};View$$module$src$view.prototype.clearNewTodo=function(){this.$newTodo.value=""}; +View$$module$src$view.prototype.setItemComplete=function(a,b){var c=module$src$helpers.qs('[data-id="'+a+'"]');c&&(c.className=b?"completed":"",module$src$helpers.qs("input",c).checked=b)};View$$module$src$view.prototype.editItemDone=function(a,b){var c=module$src$helpers.qs('[data-id="'+a+'"]'),d=module$src$helpers.qs("input.edit",c);c.removeChild(d);c.classList.remove("editing");module$src$helpers.qs("label",c).textContent=b}; +View$$module$src$view.prototype.bindAddItem=function(a){module$src$helpers.$on(this.$newTodo,"change",function(b){(b=b.target.value.trim())&&a(b)})};View$$module$src$view.prototype.bindRemoveCompleted=function(a){module$src$helpers.$on(this.$clearCompleted,"click",a)};View$$module$src$view.prototype.bindToggleAll=function(a){module$src$helpers.$on(this.$toggleAll,"click",function(b){a(b.target.checked)})}; +View$$module$src$view.prototype.bindRemoveItem=function(a){module$src$helpers.$delegate(this.$todoList,".destroy","click",function(b){a(_itemId$$module$src$view(b.target))})};View$$module$src$view.prototype.bindToggleItem=function(a){module$src$helpers.$delegate(this.$todoList,".toggle","click",function(b){b=b.target;a(_itemId$$module$src$view(b),b.checked)})}; +View$$module$src$view.prototype.bindEditItemSave=function(a){module$src$helpers.$delegate(this.$todoList,"li .edit","blur",function(b){b=b.target;b.dataset.iscanceled||a(_itemId$$module$src$view(b),b.value.trim())},!0);module$src$helpers.$delegate(this.$todoList,"li .edit","keypress",function(a){var c=a.target;a.keyCode===ENTER_KEY$$module$src$view&&c.blur()})}; +View$$module$src$view.prototype.bindEditItemCancel=function(a){module$src$helpers.$delegate(this.$todoList,"li .edit","keyup",function(b){var c=b.target;b.keyCode===ESCAPE_KEY$$module$src$view&&(c.dataset.iscanceled=!0,c.blur(),a(_itemId$$module$src$view(c)))})};module$src$view["default"]=View$$module$src$view;var module$src$controller={},Controller$$module$src$controller=function(a,b){var c=this;this.store=a;this.view=b;b.bindAddItem(this.addItem.bind(this));b.bindEditItemSave(this.editItemSave.bind(this));b.bindEditItemCancel(this.editItemCancel.bind(this));b.bindRemoveItem(this.removeItem.bind(this));b.bindToggleItem(function(a,b){c.toggleCompleted(a,b);c._filter()});b.bindRemoveCompleted(this.removeCompletedItems.bind(this));b.bindToggleAll(this.toggleAll.bind(this));this._activeRoute="";this._lastActiveRoute= +null};Controller$$module$src$controller.prototype.setView=function(a){this._activeRoute=a=a.replace(/^#\//,"");this._filter();this.view.updateFilterButtons(a)};Controller$$module$src$controller.prototype.addItem=function(a){var b=this;this.store.insert({id:Date.now(),title:a,completed:!1},function(){b.view.clearNewTodo();b._filter(!0)})}; +Controller$$module$src$controller.prototype.editItemSave=function(a,b){var c=this;b.length?this.store.update({id:a,title:b},function(){c.view.editItemDone(a,b)}):this.removeItem(a)};Controller$$module$src$controller.prototype.editItemCancel=function(a){var b=this;this.store.find({id:a},function(c){b.view.editItemDone(a,c[0].title)})};Controller$$module$src$controller.prototype.removeItem=function(a){var b=this;this.store.remove({id:a},function(){b._filter();b.view.removeItem(a)})}; +Controller$$module$src$controller.prototype.removeCompletedItems=function(){this.store.remove({completed:!0},this._filter.bind(this))};Controller$$module$src$controller.prototype.toggleCompleted=function(a,b){var c=this;this.store.update({id:a,completed:b},function(){c.view.setItemComplete(a,b)})}; +Controller$$module$src$controller.prototype.toggleAll=function(a){var b=this;this.store.find({completed:!a},function(c){c=$jscomp.makeIterator(c);for(var d=c.next();!d.done;d=c.next())b.toggleCompleted(d.value.id,a)});this._filter()}; +Controller$$module$src$controller.prototype._filter=function(a){var b=this,c=this._activeRoute;(a||""!==this._lastActiveRoute||this._lastActiveRoute!==c)&&this.store.find({"":module$src$item.emptyItemQuery,active:{completed:!1},completed:{completed:!0}}[c],this.view.showItems.bind(this.view));this.store.count(function(a,c,f){b.view.setItemsLeft(c);b.view.setClearCompletedButtonVisibility(f);b.view.setCompleteAllCheckbox(f===a);b.view.setMainVisibility(a)});this._lastActiveRoute=c}; +module$src$controller["default"]=Controller$$module$src$controller;var store$$module$src$app=new module$src$store["default"]("todos-vanilla-es6"),template$$module$src$app=new module$src$template["default"],view$$module$src$app=new module$src$view["default"](template$$module$src$app),controller$$module$src$app=new module$src$controller["default"](store$$module$src$app,view$$module$src$app),setView$$module$src$app=function(){return controller$$module$src$app.setView(document.location.hash)};module$src$helpers.$on(window,"load",setView$$module$src$app); +module$src$helpers.$on(window,"hashchange",setView$$module$src$app); \ No newline at end of file diff --git a/examples/amp-script/todomvc.js b/examples/amp-script/todomvc.js new file mode 100644 index 000000000000..b81272c876a4 --- /dev/null +++ b/examples/amp-script/todomvc.js @@ -0,0 +1,1645 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _preact = __webpack_require__(1); + + var _dbmon = __webpack_require__(2); + + var _index = __webpack_require__(3); + + var _index2 = _interopRequireDefault(_index); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { return;("Cannot call a class as a function"); } } + + function _possibleConstructorReturn(self, call) { if (!self) { return;("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { return;("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + /** "Hello World" component w/ a button click listener. */ + var Hello = function (_Component) { + _inherits(Hello, _Component); + + function Hello(props) { + _classCallCheck(this, Hello); + + var _this = _possibleConstructorReturn(this, _Component.call(this, props)); + + _this.state = { clicked: false }; + return _this; + } + + Hello.prototype.render = function render() { + var _this2 = this; + + return (0, _preact.h)( + 'div', + null, + (0, _preact.h)( + 'p', + null, + 'Hello ', + this.props.name, + '! Button was clicked? ', + this.state.clicked + ), + (0, _preact.h)( + 'button', + { onClick: function onClick() { + return _this2.setState({ clicked: true }); + } }, + 'Button' + ) + ); + }; + + return Hello; + }(_preact.Component); + + /** To-do list adapted from example on https://reactjs.org. */ + + + var TodoApp = function (_Component2) { + _inherits(TodoApp, _Component2); + + function TodoApp(props) { + _classCallCheck(this, TodoApp); + + var _this3 = _possibleConstructorReturn(this, _Component2.call(this, props)); + + _this3.state = { + items: [], + text: '', + id: 0, + focused: false + }; + + _this3.handleChange = _this3.handleChange.bind(_this3); + _this3.handleFocus = _this3.handleFocus.bind(_this3); + _this3.handleSubmit = _this3.handleSubmit.bind(_this3); + return _this3; + } + + TodoApp.prototype.render = function render() { + return (0, _preact.h)( + 'div', + null, + (0, _preact.h)( + 'h3', + null, + 'TODO' + ), + (0, _preact.h)(TodoList, { items: this.state.items }), + (0, _preact.h)('input', { + onChange: this.handleChange, + onFocus: this.handleFocus, + value: this.state.text + }), + (0, _preact.h)( + 'button', + { onClick: this.handleSubmit }, + 'Add #', + this.state.items.length + 1 + ) + ); + }; + + TodoApp.prototype.handleChange = function handleChange(e) { + this.setState({ text: e.target.value }); + }; + + TodoApp.prototype.handleFocus = function handleFocus(e) { + // Clear placeholder text on first focus. + if (!this.state.focused) { + this.setState({ text: '', focused: true }); + } + }; + + TodoApp.prototype.handleSubmit = function handleSubmit(e) { + if (!this.state.text.length) { + return; + } + var newItem = { + text: this.state.text, + id: this.state.id + }; + this.setState(function (prevState) { + return { + items: prevState.items.concat(newItem), + text: '', + id: prevState.id + 1 + }; + }); + }; + + return TodoApp; + }(_preact.Component); + + var TodoList = function (_Component3) { + _inherits(TodoList, _Component3); + + function TodoList() { + _classCallCheck(this, TodoList); + + return _possibleConstructorReturn(this, _Component3.apply(this, arguments)); + } + + TodoList.prototype.render = function render() { + return (0, _preact.h)( + 'ul', + null, + this.props.items.map(function (item) { + return (0, _preact.h)( + 'li', + { key: item.id }, + item.text + ); + }) + ); + }; + + return TodoList; + }(_preact.Component); + + /** Timer example from https://reactjs.org */ + + + var Timer = function (_Component4) { + _inherits(Timer, _Component4); + + function Timer(props) { + _classCallCheck(this, Timer); + + var _this5 = _possibleConstructorReturn(this, _Component4.call(this, props)); + + _this5.state = { seconds: 0 }; + return _this5; + } + + Timer.prototype.tick = function tick() { + this.setState(function (prevState) { + return { + seconds: prevState.seconds + 1 + }; + }); + }; + + Timer.prototype.componentDidMount = function componentDidMount() { + var _this6 = this; + + this.interval = setInterval(function () { + return _this6.tick(); + }, 1000); + }; + + Timer.prototype.componentWillUnmount = function componentWillUnmount() { + clearInterval(this.interval); + }; + + Timer.prototype.render = function render() { + return (0, _preact.h)( + 'div', + null, + 'Seconds: ', + this.state.seconds + ); + }; + + return Timer; + }(_preact.Component); + + // TODO(willchou): Support rendering to nodes other than body. + // render(, document.body); + // render(, document.body); + // render(, document.body); + // render(, document.body); + + + (0, _preact.render)((0, _preact.h)(_index2.default, null), document.body); + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + !function(global, factory) { + true ? factory(exports) : 'function' == typeof define && define.amd ? define([ 'exports' ], factory) : factory(global.preact = global.preact || {}); + }(this, function(exports) { + function VNode(nodeName, attributes, children) { + this.nodeName = nodeName; + this.attributes = attributes; + this.children = children; + this.key = attributes && attributes.key; + } + function h(nodeName, attributes) { + var lastSimple, child, simple, i, children = []; + for (i = arguments.length; i-- > 2; ) stack.push(arguments[i]); + if (attributes && attributes.children) { + if (!stack.length) stack.push(attributes.children); + delete attributes.children; + } + while (stack.length) if ((child = stack.pop()) instanceof Array) for (i = child.length; i--; ) stack.push(child[i]); else if (null != child && child !== !1) { + if ('number' == typeof child || child === !0) child = String(child); + simple = 'string' == typeof child; + if (simple && lastSimple) children[children.length - 1] += child; else { + children.push(child); + lastSimple = simple; + } + } + var p = new VNode(nodeName, attributes || void 0, children); + if (options.vnode) options.vnode(p); + return p; + } + function extend(obj, props) { + if (props) for (var i in props) obj[i] = props[i]; + return obj; + } + function clone(obj) { + return extend({}, obj); + } + function delve(obj, key) { + for (var p = key.split('.'), i = 0; i < p.length && obj; i++) obj = obj[p[i]]; + return obj; + } + function isFunction(obj) { + return 'function' == typeof obj; + } + function isString(obj) { + return 'string' == typeof obj; + } + function hashToClassName(c) { + var str = ''; + for (var prop in c) if (c[prop]) { + if (str) str += ' '; + str += prop; + } + return str; + } + function cloneElement(vnode, props) { + return h(vnode.nodeName, extend(clone(vnode.attributes), props), arguments.length > 2 ? [].slice.call(arguments, 2) : vnode.children); + } + function createLinkedState(component, key, eventPath) { + var path = key.split('.'); + return function(e) { + var t = e && e.target || this, state = {}, obj = state, v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? t.type.match(/^che|rad/) ? t.checked : t.value : e, i = 0; + for (;i < path.length - 1; i++) obj = obj[path[i]] || (obj[path[i]] = !i && component.state[path[i]] || {}); + obj[path[i]] = v; + component.setState(state); + }; + } + function enqueueRender(component) { + if (!component._dirty && (component._dirty = !0) && 1 == items.push(component)) (options.debounceRendering || defer)(rerender); + } + function rerender() { + var p, list = items; + items = []; + while (p = list.pop()) if (p._dirty) renderComponent(p); + } + function isFunctionalComponent(vnode) { + var nodeName = vnode && vnode.nodeName; + return nodeName && isFunction(nodeName) && !(nodeName.prototype && nodeName.prototype.render); + } + function buildFunctionalComponent(vnode, context) { + return vnode.nodeName(getNodeProps(vnode), context || EMPTY); + } + function isSameNodeType(node, vnode) { + if (isString(vnode)) return node instanceof Text; + if (isString(vnode.nodeName)) return !node._componentConstructor && isNamedNode(node, vnode.nodeName); + if (isFunction(vnode.nodeName)) return (node._componentConstructor ? node._componentConstructor === vnode.nodeName : !0) || isFunctionalComponent(vnode); else ; + } + function isNamedNode(node, nodeName) { + return node.normalizedNodeName === nodeName || toLowerCase(node.nodeName) === toLowerCase(nodeName); + } + function getNodeProps(vnode) { + var props = clone(vnode.attributes); + props.children = vnode.children; + var defaultProps = vnode.nodeName.defaultProps; + if (defaultProps) for (var i in defaultProps) if (void 0 === props[i]) props[i] = defaultProps[i]; + return props; + } + function removeNode(node) { + var p = node.parentNode; + if (p) p.removeChild(node); + } + function setAccessor(node, name, old, value, isSvg) { + if ('className' === name) name = 'class'; + if ('class' === name && value && 'object' == typeof value) value = hashToClassName(value); + if ('key' === name) ; else if ('class' === name && !isSvg) node.className = value || ''; else if ('style' === name) { + if (!value || isString(value) || isString(old)) node.style.cssText = value || ''; + if (value && 'object' == typeof value) { + if (!isString(old)) for (var i in old) if (!(i in value)) node.style[i] = ''; + for (var i in value) node.style[i] = 'number' == typeof value[i] && !NON_DIMENSION_PROPS[i] ? value[i] + 'px' : value[i]; + } + } else if ('dangerouslySetInnerHTML' === name) node.innerHTML = value && value.__html || ''; else if ('o' == name[0] && 'n' == name[1]) { + var l = node._listeners || (node._listeners = {}); + name = toLowerCase(name.substring(2)); + if (value) { + if (!l[name]) node.addEventListener(name, eventProxy, !!NON_BUBBLING_EVENTS[name]); + } else if (l[name]) node.removeEventListener(name, eventProxy, !!NON_BUBBLING_EVENTS[name]); + l[name] = value; + } else if ('list' !== name && 'type' !== name && !isSvg && name in node) { + setProperty(node, name, null == value ? '' : value); + if (null == value || value === !1) node.removeAttribute(name); + } else { + var ns = isSvg && name.match(/^xlink\:?(.+)/); + if (null == value || value === !1) if (ns) node.removeAttributeNS('http://www.w3.org/1999/xlink', toLowerCase(ns[1])); else node.removeAttribute(name); else if ('object' != typeof value && !isFunction(value)) if (ns) node.setAttributeNS('http://www.w3.org/1999/xlink', toLowerCase(ns[1]), value); else node.setAttribute(name, value); + } + } + function setProperty(node, name, value) { + try { + node[name] = value; + } catch (e) {} + } + function eventProxy(e) { + return this._listeners[e.type](options.event && options.event(e) || e); + } + function collectNode(node) { + removeNode(node); + if (node instanceof Element) { + node._component = node._componentConstructor = null; + var _name = node.normalizedNodeName || toLowerCase(node.nodeName); + (nodes[_name] || (nodes[_name] = [])).push(node); + } + } + function createNode(nodeName, isSvg) { + var name = toLowerCase(nodeName), node = nodes[name] && nodes[name].pop() || (isSvg ? document.createElementNS('http://www.w3.org/2000/svg', nodeName) : document.createElement(nodeName)); + node.normalizedNodeName = name; + return node; + } + function flushMounts() { + var c; + while (c = mounts.pop()) { + if (options.afterMount) options.afterMount(c); + if (c.componentDidMount) c.componentDidMount(); + } + } + function diff(dom, vnode, context, mountAll, parent, componentRoot) { + if (!diffLevel++) { + isSvgMode = parent instanceof SVGElement; + hydrating = dom && !(ATTR_KEY in dom); + } + var ret = idiff(dom, vnode, context, mountAll); + if (parent && ret.parentNode !== parent) parent.appendChild(ret); + if (!--diffLevel) { + hydrating = !1; + if (!componentRoot) flushMounts(); + } + return ret; + } + function idiff(dom, vnode, context, mountAll) { + var originalAttributes = vnode && vnode.attributes; + while (isFunctionalComponent(vnode)) vnode = buildFunctionalComponent(vnode, context); + if (null == vnode) vnode = ''; + if (isString(vnode)) { + if (dom && dom instanceof Text) { + if (dom.nodeValue != vnode) dom.nodeValue = vnode; + } else { + if (dom) recollectNodeTree(dom); + dom = document.createTextNode(vnode); + } + dom[ATTR_KEY] = !0; + return dom; + } + if (isFunction(vnode.nodeName)) return buildComponentFromVNode(dom, vnode, context, mountAll); + var out = dom, nodeName = String(vnode.nodeName), prevSvgMode = isSvgMode, vchildren = vnode.children; + isSvgMode = 'svg' === nodeName ? !0 : 'foreignObject' === nodeName ? !1 : isSvgMode; + if (!dom) out = createNode(nodeName, isSvgMode); else if (!isNamedNode(dom, nodeName)) { + out = createNode(nodeName, isSvgMode); + while (dom.firstChild) out.appendChild(dom.firstChild); + if (dom.parentNode) dom.parentNode.replaceChild(out, dom); + recollectNodeTree(dom); + } + var fc = out.firstChild, props = out[ATTR_KEY]; + if (!props) { + out[ATTR_KEY] = props = {}; + for (var a = out.attributes, i = a.length; i--; ) props[a[i].name] = a[i].value; + } + diffAttributes(out, vnode.attributes, props); + if (!hydrating && vchildren && 1 === vchildren.length && 'string' == typeof vchildren[0] && fc && fc instanceof Text && !fc.nextSibling) { + if (fc.nodeValue != vchildren[0]) fc.nodeValue = vchildren[0]; + } else if (vchildren && vchildren.length || fc) innerDiffNode(out, vchildren, context, mountAll); + if (originalAttributes && 'function' == typeof originalAttributes.ref) (props.ref = originalAttributes.ref)(out); + isSvgMode = prevSvgMode; + return out; + } + function innerDiffNode(dom, vchildren, context, mountAll) { + var j, c, vchild, child, originalChildren = dom.childNodes, children = [], keyed = {}, keyedLen = 0, min = 0, len = originalChildren.length, childrenLen = 0, vlen = vchildren && vchildren.length; + if (len) for (var i = 0; i < len; i++) { + var _child = originalChildren[i], props = _child[ATTR_KEY], key = vlen ? (c = _child._component) ? c.__key : props ? props.key : null : null; + if (null != key) { + keyedLen++; + keyed[key] = _child; + } else if (hydrating || props) children[childrenLen++] = _child; + } + if (vlen) for (var i = 0; i < vlen; i++) { + vchild = vchildren[i]; + child = null; + var key = vchild.key; + if (null != key) { + if (keyedLen && key in keyed) { + child = keyed[key]; + keyed[key] = void 0; + keyedLen--; + } + } else if (!child && min < childrenLen) for (j = min; j < childrenLen; j++) { + c = children[j]; + if (c && isSameNodeType(c, vchild)) { + child = c; + children[j] = void 0; + if (j === childrenLen - 1) childrenLen--; + if (j === min) min++; + break; + } + } + child = idiff(child, vchild, context, mountAll); + if (child && child !== dom) if (i >= len) dom.appendChild(child); else if (child !== originalChildren[i]) { + if (child === originalChildren[i + 1]) removeNode(originalChildren[i]); + dom.insertBefore(child, originalChildren[i] || null); + } + } + if (keyedLen) for (var i in keyed) if (keyed[i]) recollectNodeTree(keyed[i]); + while (min <= childrenLen) { + child = children[childrenLen--]; + if (child) recollectNodeTree(child); + } + } + function recollectNodeTree(node, unmountOnly) { + var component = node._component; + if (component) unmountComponent(component, !unmountOnly); else { + if (node[ATTR_KEY] && node[ATTR_KEY].ref) node[ATTR_KEY].ref(null); + if (!unmountOnly) collectNode(node); + var c; + while (c = node.lastChild) recollectNodeTree(c, unmountOnly); + } + } + function diffAttributes(dom, attrs, old) { + for (var _name in old) if (!(attrs && _name in attrs) && null != old[_name]) setAccessor(dom, _name, old[_name], old[_name] = void 0, isSvgMode); + if (attrs) for (var _name2 in attrs) if (!('children' === _name2 || 'innerHTML' === _name2 || _name2 in old && attrs[_name2] === ('value' === _name2 || 'checked' === _name2 ? dom[_name2] : old[_name2]))) setAccessor(dom, _name2, old[_name2], old[_name2] = attrs[_name2], isSvgMode); + } + function collectComponent(component) { + var name = component.constructor.name, list = components[name]; + if (list) list.push(component); else components[name] = [ component ]; + } + function createComponent(Ctor, props, context) { + var inst = new Ctor(props, context), list = components[Ctor.name]; + Component.call(inst, props, context); + if (list) for (var i = list.length; i--; ) if (list[i].constructor === Ctor) { + inst.nextBase = list[i].nextBase; + list.splice(i, 1); + break; + } + return inst; + } + function setComponentProps(component, props, opts, context, mountAll) { + if (!component._disable) { + component._disable = !0; + if (component.__ref = props.ref) delete props.ref; + if (component.__key = props.key) delete props.key; + if (!component.base || mountAll) { + if (component.componentWillMount) component.componentWillMount(); + } else if (component.componentWillReceiveProps) component.componentWillReceiveProps(props, context); + if (context && context !== component.context) { + if (!component.prevContext) component.prevContext = component.context; + component.context = context; + } + if (!component.prevProps) component.prevProps = component.props; + component.props = props; + component._disable = !1; + if (0 !== opts) if (1 === opts || options.syncComponentUpdates !== !1 || !component.base) renderComponent(component, 1, mountAll); else enqueueRender(component); + if (component.__ref) component.__ref(component); + } + } + function renderComponent(component, opts, mountAll, isChild) { + if (!component._disable) { + var skip, rendered, inst, cbase, props = component.props, state = component.state, context = component.context, previousProps = component.prevProps || props, previousState = component.prevState || state, previousContext = component.prevContext || context, isUpdate = component.base, nextBase = component.nextBase, initialBase = isUpdate || nextBase, initialChildComponent = component._component; + if (isUpdate) { + component.props = previousProps; + component.state = previousState; + component.context = previousContext; + if (2 !== opts && component.shouldComponentUpdate && component.shouldComponentUpdate(props, state, context) === !1) skip = !0; else if (component.componentWillUpdate) component.componentWillUpdate(props, state, context); + component.props = props; + component.state = state; + component.context = context; + } + component.prevProps = component.prevState = component.prevContext = component.nextBase = null; + component._dirty = !1; + if (!skip) { + if (component.render) rendered = component.render(props, state, context); + if (component.getChildContext) context = extend(clone(context), component.getChildContext()); + while (isFunctionalComponent(rendered)) rendered = buildFunctionalComponent(rendered, context); + var toUnmount, base, childComponent = rendered && rendered.nodeName; + if (isFunction(childComponent)) { + var childProps = getNodeProps(rendered); + inst = initialChildComponent; + if (inst && inst.constructor === childComponent && childProps.key == inst.__key) setComponentProps(inst, childProps, 1, context); else { + toUnmount = inst; + inst = createComponent(childComponent, childProps, context); + inst.nextBase = inst.nextBase || nextBase; + inst._parentComponent = component; + component._component = inst; + setComponentProps(inst, childProps, 0, context); + renderComponent(inst, 1, mountAll, !0); + } + base = inst.base; + } else { + cbase = initialBase; + toUnmount = initialChildComponent; + if (toUnmount) cbase = component._component = null; + if (initialBase || 1 === opts) { + if (cbase) cbase._component = null; + base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, !0); + } + } + if (initialBase && base !== initialBase && inst !== initialChildComponent) { + var baseParent = initialBase.parentNode; + if (baseParent && base !== baseParent) { + baseParent.replaceChild(base, initialBase); + if (!toUnmount) { + initialBase._component = null; + recollectNodeTree(initialBase); + } + } + } + if (toUnmount) unmountComponent(toUnmount, base !== initialBase); + component.base = base; + if (base && !isChild) { + var componentRef = component, t = component; + while (t = t._parentComponent) (componentRef = t).base = base; + base._component = componentRef; + base._componentConstructor = componentRef.constructor; + } + } + if (!isUpdate || mountAll) mounts.unshift(component); else if (!skip) { + if (component.componentDidUpdate) component.componentDidUpdate(previousProps, previousState, previousContext); + if (options.afterUpdate) options.afterUpdate(component); + } + var fn, cb = component._renderCallbacks; + if (cb) while (fn = cb.pop()) fn.call(component); + if (!diffLevel && !isChild) flushMounts(); + } + } + function buildComponentFromVNode(dom, vnode, context, mountAll) { + var c = dom && dom._component, oldDom = dom, isDirectOwner = c && dom._componentConstructor === vnode.nodeName, isOwner = isDirectOwner, props = getNodeProps(vnode); + while (c && !isOwner && (c = c._parentComponent)) isOwner = c.constructor === vnode.nodeName; + if (c && isOwner && (!mountAll || c._component)) { + setComponentProps(c, props, 3, context, mountAll); + dom = c.base; + } else { + if (c && !isDirectOwner) { + unmountComponent(c, !0); + dom = oldDom = null; + } + c = createComponent(vnode.nodeName, props, context); + if (dom && !c.nextBase) { + c.nextBase = dom; + oldDom = null; + } + setComponentProps(c, props, 1, context, mountAll); + dom = c.base; + if (oldDom && dom !== oldDom) { + oldDom._component = null; + recollectNodeTree(oldDom); + } + } + return dom; + } + function unmountComponent(component, remove) { + if (options.beforeUnmount) options.beforeUnmount(component); + var base = component.base; + component._disable = !0; + if (component.componentWillUnmount) component.componentWillUnmount(); + component.base = null; + var inner = component._component; + if (inner) unmountComponent(inner, remove); else if (base) { + if (base[ATTR_KEY] && base[ATTR_KEY].ref) base[ATTR_KEY].ref(null); + component.nextBase = base; + if (remove) { + removeNode(base); + collectComponent(component); + } + var c; + while (c = base.lastChild) recollectNodeTree(c, !remove); + } + if (component.__ref) component.__ref(null); + if (component.componentDidUnmount) component.componentDidUnmount(); + } + function Component(props, context) { + this._dirty = !0; + this.context = context; + this.props = props; + if (!this.state) this.state = {}; + } + function render(vnode, parent, merge) { + return diff(merge, vnode, {}, !1, parent); + } + var options = {}; + var stack = []; + var lcCache = {}; + var toLowerCase = function(s) { + return lcCache[s] || (lcCache[s] = s.toLowerCase()); + }; + var resolved = 'undefined' != typeof Promise && Promise.resolve(); + var defer = resolved ? function(f) { + resolved.then(f); + } : setTimeout; + var EMPTY = {}; + var ATTR_KEY = 'undefined' != typeof Symbol ? Symbol.for('preactattr') : '__preactattr_'; + var NON_DIMENSION_PROPS = { + boxFlex: 1, + boxFlexGroup: 1, + columnCount: 1, + fillOpacity: 1, + flex: 1, + flexGrow: 1, + flexPositive: 1, + flexShrink: 1, + flexNegative: 1, + fontWeight: 1, + lineClamp: 1, + lineHeight: 1, + opacity: 1, + order: 1, + orphans: 1, + strokeOpacity: 1, + widows: 1, + zIndex: 1, + zoom: 1 + }; + var NON_BUBBLING_EVENTS = { + blur: 1, + error: 1, + focus: 1, + load: 1, + resize: 1, + scroll: 1 + }; + var items = []; + var nodes = {}; + var mounts = []; + var diffLevel = 0; + var isSvgMode = !1; + var hydrating = !1; + var components = {}; + extend(Component.prototype, { + linkState: function(key, eventPath) { + var c = this._linkedStates || (this._linkedStates = {}); + return c[key + eventPath] || (c[key + eventPath] = createLinkedState(this, key, eventPath)); + }, + setState: function(state, callback) { + var s = this.state; + if (!this.prevState) this.prevState = clone(s); + extend(s, isFunction(state) ? state(s, this.props) : state); + if (callback) (this._renderCallbacks = this._renderCallbacks || []).push(callback); + enqueueRender(this); + }, + forceUpdate: function() { + renderComponent(this, 2); + }, + render: function() {} + }); + exports.h = h; + exports.cloneElement = cloneElement; + exports.Component = Component; + exports.render = render; + exports.rerender = rerender; + exports.options = options; + }); + //# sourceMappingURL=preact.js.map + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + exports.__esModule = true; + exports.DBMon = undefined; + + var _preact = __webpack_require__(1); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { return;("Cannot call a class as a function"); } } + + function _possibleConstructorReturn(self, call) { if (!self) { return;("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { return;("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + var ENV = ENV || function () { + + var first = true; + var counter = 0; + var data; + var _base; + (_base = String.prototype).lpad || (_base.lpad = function (padding, toLength) { + return padding.repeat((toLength - this.length) / padding.length).concat(this); + }); + + function formatElapsed(value) { + var str = parseFloat(value).toFixed(2); + if (value > 60) { + minutes = Math.floor(value / 60); + comps = (value % 60).toFixed(2).split('.'); + seconds = comps[0].lpad('0', 2); + ms = comps[1]; + str = minutes + ":" + seconds + "." + ms; + } + return str; + } + + function getElapsedClassName(elapsed) { + var className = 'Query elapsed'; + if (elapsed >= 10.0) { + className += ' warn_long'; + } else if (elapsed >= 1.0) { + className += ' warn'; + } else { + className += ' short'; + } + return className; + } + + function countClassName(queries) { + var countClassName = "label"; + if (queries >= 20) { + countClassName += " label-important"; + } else if (queries >= 10) { + countClassName += " label-warning"; + } else { + countClassName += " label-success"; + } + return countClassName; + } + + function updateQuery(object) { + if (!object) { + object = {}; + } + var elapsed = Math.random() * 15; + object.elapsed = elapsed; + object.formatElapsed = formatElapsed(elapsed); + object.elapsedClassName = getElapsedClassName(elapsed); + object.query = "SELECT blah FROM something"; + object.waiting = Math.random() < 0.5; + if (Math.random() < 0.2) { + object.query = " in transaction"; + } + if (Math.random() < 0.1) { + object.query = "vacuum"; + } + return object; + } + + function cleanQuery(value) { + if (value) { + value.formatElapsed = ""; + value.elapsedClassName = ""; + value.query = ""; + value.elapsed = null; + value.waiting = null; + } else { + return { + query: "***", + formatElapsed: "", + elapsedClassName: "" + }; + } + } + + function generateRow(object, keepIdentity, counter) { + var nbQueries = Math.floor(Math.random() * 10 + 1); + if (!object) { + object = {}; + } + object.lastMutationId = counter; + object.nbQueries = nbQueries; + if (!object.lastSample) { + object.lastSample = {}; + } + if (!object.lastSample.topFiveQueries) { + object.lastSample.topFiveQueries = []; + } + if (keepIdentity) { + // for Angular optimization + if (!object.lastSample.queries) { + object.lastSample.queries = []; + for (var l = 0; l < 12; l++) { + object.lastSample.queries[l] = cleanQuery(); + } + } + for (var j in object.lastSample.queries) { + var value = object.lastSample.queries[j]; + if (j <= nbQueries) { + updateQuery(value); + } else { + cleanQuery(value); + } + } + } else { + object.lastSample.queries = []; + for (var j = 0; j < 12; j++) { + if (j < nbQueries) { + var value = updateQuery(cleanQuery()); + object.lastSample.queries.push(value); + } else { + object.lastSample.queries.push(cleanQuery()); + } + } + } + for (var i = 0; i < 5; i++) { + var source = object.lastSample.queries[i]; + object.lastSample.topFiveQueries[i] = source; + } + object.lastSample.nbQueries = nbQueries; + object.lastSample.countClassName = countClassName(nbQueries); + return object; + } + + function getData(keepIdentity) { + var oldData = data; + if (!keepIdentity) { + // reset for each tick when !keepIdentity + data = []; + for (var i = 1; i <= ENV.rows; i++) { + data.push({ dbname: 'cluster' + i, query: "", formatElapsed: "", elapsedClassName: "" }); + data.push({ dbname: 'cluster' + i + ' slave', query: "", formatElapsed: "", elapsedClassName: "" }); + } + } + if (!data) { + // first init when keepIdentity + data = []; + for (var i = 1; i <= ENV.rows; i++) { + data.push({ dbname: 'cluster' + i }); + data.push({ dbname: 'cluster' + i + ' slave' }); + } + oldData = data; + } + for (var i in data) { + var row = data[i]; + if (!keepIdentity && oldData && oldData[i]) { + row.lastSample = oldData[i].lastSample; + } + if (!row.lastSample || Math.random() < ENV.mutations()) { + counter = counter + 1; + if (!keepIdentity) { + row.lastSample = null; + } + generateRow(row, keepIdentity, counter); + } else { + data[i] = oldData[i]; + } + } + first = false; + return { + toArray: function toArray() { + return data; + } + }; + } + + var mutationsValue = 0.5; + + function mutations(value) { + if (value) { + mutationsValue = value; + // document.querySelector('#ratioval').innerHTML = 'mutations : ' + (mutationsValue * 100).toFixed(0) + '%'; + return mutationsValue; + } else { + return mutationsValue; + } + } + + // var body = document.querySelector('body'); + // var theFirstChild = body.firstChild; + + // var sliderContainer = document.createElement('div'); + // sliderContainer.style.cssText = "display: flex"; + // var slider = document.createElement('input'); + // var text = document.createElement('label'); + // text.innerHTML = 'mutations : ' + (mutationsValue * 100).toFixed(0) + '%'; + // text.id = "ratioval"; + // slider.setAttribute("type", "range"); + // slider.style.cssText = 'margin-bottom: 10px; margin-top: 5px'; + // slider.addEventListener('change', function(e) { + // ENV.mutations(e.target.value / 100); + // }); + // sliderContainer.appendChild(text); + // sliderContainer.appendChild(slider); + // body.insertBefore(sliderContainer, theFirstChild); + + return { + generateData: getData, + rows: 50, + timeout: 1000, + mutations: mutations + }; + }(); + + var Query = function (_Component) { + _inherits(Query, _Component); + + function Query() { + _classCallCheck(this, Query); + + return _possibleConstructorReturn(this, _Component.apply(this, arguments)); + } + + Query.prototype.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) { + if (nextProps.elapsedClassName !== this.props.elapsedClassName) return true; + if (nextProps.formatElapsed !== this.props.formatElapsed) return true; + if (nextProps.query !== this.props.query) return true; + return false; + }; + + Query.prototype.render = function render() { + return (0, _preact.h)( + 'td', + { className: "Query " + this.props.elapsedClassName }, + this.props.formatElapsed, + (0, _preact.h)( + 'div', + { className: 'popover left' }, + (0, _preact.h)( + 'div', + { className: 'popover-content' }, + this.props.query + ), + (0, _preact.h)('div', { className: 'arrow' }) + ) + ); + }; + + return Query; + }(_preact.Component); + + var Database = function (_Component2) { + _inherits(Database, _Component2); + + function Database() { + _classCallCheck(this, Database); + + return _possibleConstructorReturn(this, _Component2.apply(this, arguments)); + } + + Database.prototype.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) { + if (nextProps.lastMutationId === this.props.lastMutationId) return false; + return true; + }; + + Database.prototype.render = function render() { + var lastSample = this.props.lastSample; + return (0, _preact.h)( + 'tr', + { key: this.props.dbname }, + (0, _preact.h)( + 'td', + { className: 'dbname' }, + this.props.dbname + ), + (0, _preact.h)( + 'td', + { className: 'query-count' }, + (0, _preact.h)( + 'span', + { className: this.props.lastSample.countClassName }, + this.props.lastSample.nbQueries + ) + ), + this.props.lastSample.topFiveQueries.map(function (query, index) { + return (0, _preact.h)(Query, { key: index, + query: query.query, + elapsed: query.elapsed, + formatElapsed: query.formatElapsed, + elapsedClassName: query.elapsedClassName }); + }) + ); + }; + + return Database; + }(_preact.Component); + + var DBMon = exports.DBMon = function (_Component3) { + _inherits(DBMon, _Component3); + + function DBMon(props) { + _classCallCheck(this, DBMon); + + var _this3 = _possibleConstructorReturn(this, _Component3.call(this, props)); + + _this3.state = { databases: [] }; + return _this3; + } + + DBMon.prototype.loadSamples = function loadSamples() { + this.setState({ + databases: ENV.generateData(true).toArray() + }); + // Monitoring.renderRate.ping(); + setTimeout(this.loadSamples.bind(this), ENV.timeout); + }; + + DBMon.prototype.componentDidMount = function componentDidMount() { + this.loadSamples(); + }; + + DBMon.prototype.render = function render() { + var databases = this.state.databases.map(function (database) { + return (0, _preact.h)(Database, { + key: database.dbname, + lastMutationId: database.lastMutationId, + dbname: database.dbname, + samples: database.samples, + lastSample: database.lastSample }); + }); + + return (0, _preact.h)( + 'div', + null, + (0, _preact.h)( + 'table', + { className: 'table table-striped latest-data' }, + (0, _preact.h)( + 'tbody', + null, + databases + ) + ) + ); + }; + + return DBMon; + }(_preact.Component); + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + exports.__esModule = true; + + var _preact = __webpack_require__(1); + + var _model = __webpack_require__(4); + + var _model2 = _interopRequireDefault(_model); + + var _footer = __webpack_require__(6); + + var _footer2 = _interopRequireDefault(_footer); + + var _item = __webpack_require__(7); + + var _item2 = _interopRequireDefault(_item); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _objectDestructuringEmpty(obj) { if (obj == null) return;("Cannot destructure undefined"); } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { return;("Cannot call a class as a function"); } } + + function _possibleConstructorReturn(self, call) { if (!self) { return;("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { return;("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + var ENTER_KEY = 13; + + var FILTERS = { + all: function all(todo) { + return true; + }, + active: function active(todo) { + return !todo.completed; + }, + completed: function completed(todo) { + return todo.completed; + } + }; + + var TodoMvcApp = function (_Component) { + _inherits(TodoMvcApp, _Component); + + function TodoMvcApp() { + _classCallCheck(this, TodoMvcApp); + + var _this = _possibleConstructorReturn(this, _Component.call(this)); + + _this.handleInput = function (event) { + _this.setState({ newTodo: event.target.value }); + }; + + _this.handleNewTodoKeyDown = function (e) { + if (e.keyCode !== ENTER_KEY) return; + e.preventDefault(); + + var val = _this.state.newTodo.trim(); + if (val) { + _this.model.addTodo(val); + _this.setState({ newTodo: '' }); + } + }; + + _this.toggleAll = function (event) { + var checked = event.target.checked; + _this.model.toggleAll(checked); + }; + + _this.toggle = function (todo) { + _this.model.toggle(todo); + }; + + _this.destroy = function (todo) { + _this.model.destroy(todo); + }; + + _this.edit = function (todo) { + _this.setState({ editing: todo.id }); + }; + + _this.save = function (todoToSave, text) { + _this.model.save(todoToSave, text); + _this.setState({ editing: null }); + }; + + _this.cancel = function () { + _this.setState({ editing: null }); + }; + + _this.clearCompleted = function () { + _this.model.clearCompleted(); + }; + + _this.model = new _model2.default('preact-todos', function () { + return _this.setState({}); + }); + addEventListener('hashchange', _this.handleRoute.bind(_this)); + _this.handleRoute(); + return _this; + } + + TodoMvcApp.prototype.handleRoute = function handleRoute() { + var nowShowing = String(location.hash || '').split('/').pop(); + if (!FILTERS[nowShowing]) { + nowShowing = 'all'; + } + this.setState({ nowShowing: nowShowing }); + }; + + TodoMvcApp.prototype.render = function render(_ref, _ref2) { + var _this2 = this; + + var _ref2$nowShowing = _ref2.nowShowing, + nowShowing = _ref2$nowShowing === undefined ? ALL_TODOS : _ref2$nowShowing, + newTodo = _ref2.newTodo, + editing = _ref2.editing; + + _objectDestructuringEmpty(_ref); + + var todos = this.model.todos, + shownTodos = todos.filter(FILTERS[nowShowing]), + activeTodoCount = todos.reduce(function (a, todo) { + return a + (todo.completed ? 0 : 1); + }, 0), + completedCount = todos.length - activeTodoCount; + + + return (0, _preact.h)( + 'div', + null, + (0, _preact.h)( + 'header', + { 'class': 'header' }, + (0, _preact.h)( + 'h1', + null, + 'todos' + ), + (0, _preact.h)('input', { + 'class': 'new-todo', + placeholder: 'What needs to be done?', + value: this.state.newTodo, + onKeyDown: this.handleNewTodoKeyDown, + onInput: this.handleInput, + autoFocus: true + }) + ), + todos.length ? (0, _preact.h)( + 'section', + { 'class': 'main' }, + (0, _preact.h)('input', { + 'class': 'toggle-all', + type: 'checkbox', + onChange: this.toggleAll, + checked: activeTodoCount === 0 + }), + (0, _preact.h)( + 'ul', + { 'class': 'todo-list' }, + shownTodos.map(function (todo) { + return (0, _preact.h)(_item2.default, { + todo: todo, + onToggle: _this2.toggle, + onDestroy: _this2.destroy, + onEdit: _this2.edit, + editing: editing === todo.id, + onSave: _this2.save, + onCancel: _this2.cancel + }); + }) + ) + ) : null, + activeTodoCount || completedCount ? (0, _preact.h)(_footer2.default, { + count: activeTodoCount, + completedCount: completedCount, + nowShowing: nowShowing, + onClearCompleted: this.clearCompleted + }) : null + ); + }; + + return TodoMvcApp; + }(_preact.Component); + + exports.default = TodoMvcApp; + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + exports.__esModule = true; + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _util = __webpack_require__(5); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { return;("Cannot call a class as a function"); } } + + var TodoModel = function () { + function TodoModel(key, sub) { + _classCallCheck(this, TodoModel); + + this.key = key; + this.todos = (0, _util.store)(key) || []; + this.onChanges = [sub]; + } + + TodoModel.prototype.inform = function inform() { + (0, _util.store)(this.key, this.todos); + this.onChanges.forEach(function (cb) { + return cb(); + }); + }; + + TodoModel.prototype.addTodo = function addTodo(title) { + this.todos = this.todos.concat({ + id: (0, _util.uuid)(), + title: title, + completed: false + }); + this.inform(); + }; + + TodoModel.prototype.toggleAll = function toggleAll(completed) { + this.todos = this.todos.map(function (todo) { + return _extends({}, todo, { completed: completed }); + }); + this.inform(); + }; + + TodoModel.prototype.toggle = function toggle(todoToToggle) { + this.todos = this.todos.map(function (todo) { + return todo !== todoToToggle ? todo : _extends({}, todo, { completed: !todo.completed }); + }); + this.inform(); + }; + + TodoModel.prototype.destroy = function destroy(todo) { + this.todos = this.todos.filter(function (t) { + return t !== todo; + }); + this.inform(); + }; + + TodoModel.prototype.save = function save(todoToSave, title) { + this.todos = this.todos.map(function (todo) { + return todo !== todoToSave ? todo : _extends({}, todo, { title: title }); + }); + this.inform(); + }; + + TodoModel.prototype.clearCompleted = function clearCompleted() { + this.todos = this.todos.filter(function (todo) { + return !todo.completed; + }); + this.inform(); + }; + + return TodoModel; + }(); + + exports.default = TodoModel; + +/***/ }, +/* 5 */ +/***/ function(module, exports) { + + 'use strict'; + + exports.__esModule = true; + exports.uuid = uuid; + exports.pluralize = pluralize; + exports.store = store; + function uuid() { + var uuid = ''; + for (var i = 0; i < 32; i++) { + var random = Math.random() * 16 | 0; + if (i === 8 || i === 12 || i === 16 || i === 20) { + uuid += '-'; + } + uuid += (i === 12 ? 4 : i === 16 ? random & 3 | 8 : random).toString(16); + } + return uuid; + } + + function pluralize(count, word) { + return count === 1 ? word : word + 's'; + } + + function store(namespace, data) { + if (data) return localStorage[namespace] = JSON.stringify(data); + + var store = localStorage[namespace]; + return store && JSON.parse(store) || []; + } + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + exports.__esModule = true; + + var _preact = __webpack_require__(1); + + var _util = __webpack_require__(5); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { return;("Cannot call a class as a function"); } } + + function _possibleConstructorReturn(self, call) { if (!self) { return;("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { return;("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + var TodoFooter = function (_Component) { + _inherits(TodoFooter, _Component); + + function TodoFooter() { + _classCallCheck(this, TodoFooter); + + return _possibleConstructorReturn(this, _Component.apply(this, arguments)); + } + + TodoFooter.prototype.render = function render(_ref) { + var nowShowing = _ref.nowShowing, + count = _ref.count, + completedCount = _ref.completedCount, + onClearCompleted = _ref.onClearCompleted; + + return (0, _preact.h)( + 'footer', + { 'class': 'footer' }, + (0, _preact.h)( + 'span', + { 'class': 'todo-count' }, + (0, _preact.h)( + 'strong', + null, + count + ), + ' ', + (0, _util.pluralize)(count, 'item'), + ' left' + ), + (0, _preact.h)( + 'ul', + { 'class': 'filters' }, + (0, _preact.h)( + 'li', + null, + (0, _preact.h)( + 'a', + { href: '#/', 'class': nowShowing == 'all' && 'selected' }, + 'All' + ) + ), + ' ', + (0, _preact.h)( + 'li', + null, + (0, _preact.h)( + 'a', + { href: '#/active', 'class': nowShowing == 'active' && 'selected' }, + 'Active' + ) + ), + ' ', + (0, _preact.h)( + 'li', + null, + (0, _preact.h)( + 'a', + { href: '#/completed', 'class': nowShowing == 'completed' && 'selected' }, + 'Completed' + ) + ) + ), + completedCount > 0 && (0, _preact.h)( + 'button', + { 'class': 'clear-completed', onClick: onClearCompleted }, + 'Clear completed' + ) + ); + }; + + return TodoFooter; + }(_preact.Component); + + exports.default = TodoFooter; + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + exports.__esModule = true; + + var _preact = __webpack_require__(1); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { return;("Cannot call a class as a function"); } } + + function _possibleConstructorReturn(self, call) { if (!self) { return;("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { return;("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + var ESCAPE_KEY = 27; + var ENTER_KEY = 13; + + var TodoItem = function (_Component) { + _inherits(TodoItem, _Component); + + function TodoItem() { + var _temp, _this, _ret; + + _classCallCheck(this, TodoItem); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return _ret = (_temp = (_this = _possibleConstructorReturn(this, _Component.call.apply(_Component, [this].concat(args))), _this), _this.handleSubmit = function () { + var _this$props = _this.props, + onSave = _this$props.onSave, + onDestroy = _this$props.onDestroy, + todo = _this$props.todo, + val = _this.state.editText.trim(); + + if (val) { + onSave(todo, val); + _this.setState({ editText: val }); + } else { + onDestroy(todo); + } + }, _this.handleEdit = function () { + var _this$props2 = _this.props, + onEdit = _this$props2.onEdit, + todo = _this$props2.todo; + + onEdit(todo); + _this.setState({ editText: todo.title }); + }, _this.toggle = function (e) { + var _this$props3 = _this.props, + onToggle = _this$props3.onToggle, + todo = _this$props3.todo; + + onToggle(todo); + e.preventDefault(); + }, _this.handleKeyDown = function (e) { + if (e.which === ESCAPE_KEY) { + var todo = _this.props.todo; + + _this.setState({ editText: todo.title }); + _this.props.onCancel(todo); + } else if (e.which === ENTER_KEY) { + _this.handleSubmit(); + } + }, _this.handleDestroy = function () { + _this.props.onDestroy(_this.props.todo); + }, _temp), _possibleConstructorReturn(_this, _ret); + } + + // shouldComponentUpdate({ todo, editing, editText }) { + // return ( + // todo !== this.props.todo || + // editing !== this.props.editing || + // editText !== this.state.editText + // ); + // } + + TodoItem.prototype.componentDidUpdate = function componentDidUpdate() { + // TODO(willchou): Support Element#querySelector? + // let node = this.base && this.base.querySelector('.edit'); + // if (node) node.focus(); + }; + + TodoItem.prototype.render = function render(_ref, _ref2) { + var _ref$todo = _ref.todo, + title = _ref$todo.title, + completed = _ref$todo.completed, + onToggle = _ref.onToggle, + onDestroy = _ref.onDestroy, + editing = _ref.editing; + var editText = _ref2.editText; + + return (0, _preact.h)( + "li", + { "class": { completed: completed, editing: editing } }, + (0, _preact.h)( + "div", + { "class": "view" }, + (0, _preact.h)("input", { + "class": "toggle", + type: "checkbox", + checked: completed, + onChange: this.toggle + }), + (0, _preact.h)( + "label", + { onDblClick: this.handleEdit }, + title + ), + (0, _preact.h)("button", { "class": "destroy", onClick: this.handleDestroy }) + ), + editing && (0, _preact.h)("input", { + "class": "edit", + value: editText, + onBlur: this.handleSubmit, + onInput: this.linkState('editText'), + onKeyDown: this.handleKeyDown + }) + ); + }; + + return TodoItem; + }(_preact.Component); + + exports.default = TodoItem; + +/***/ } +/******/ ]); +//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/examples/amp-script/todomvc.uglify.js b/examples/amp-script/todomvc.uglify.js new file mode 100644 index 000000000000..ab799852d317 --- /dev/null +++ b/examples/amp-script/todomvc.uglify.js @@ -0,0 +1 @@ +!function(t){var e={};function n(o){if(e[o])return e[o].exports;var r=e[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,n),r.loaded=!0,r.exports}n.m=t,n.c=e,n.p="",n(0)}([function(t,e,n){"use strict";var o,r=n(1),a=(n(2),n(3)),i=(o=a)&&o.__esModule?o:{default:o};function s(t,e){if(t)return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function l(t,e){"function"!=typeof e&&null!==e||(t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e))}(function(t){function e(e){var n=s(this,t.call(this,e));return n.state={clicked:!1},n}l(e,t),e.prototype.render=function(){var t=this;return(0,r.h)("div",null,(0,r.h)("p",null,"Hello ",this.props.name,"! Button was clicked? ",this.state.clicked),(0,r.h)("button",{onClick:function(){return t.setState({clicked:!0})}},"Button"))}})(r.Component),function(t){function e(e){var n=s(this,t.call(this,e));return n.state={items:[],text:"",id:0,focused:!1},n.handleChange=n.handleChange.bind(n),n.handleFocus=n.handleFocus.bind(n),n.handleSubmit=n.handleSubmit.bind(n),n}l(e,t),e.prototype.render=function(){return(0,r.h)("div",null,(0,r.h)("h3",null,"TODO"),(0,r.h)(u,{items:this.state.items}),(0,r.h)("input",{onChange:this.handleChange,onFocus:this.handleFocus,value:this.state.text}),(0,r.h)("button",{onClick:this.handleSubmit},"Add #",this.state.items.length+1))},e.prototype.handleChange=function(t){this.setState({text:t.target.value})},e.prototype.handleFocus=function(t){this.state.focused||this.setState({text:"",focused:!0})},e.prototype.handleSubmit=function(t){if(this.state.text.length){var e={text:this.state.text,id:this.state.id};this.setState(function(t){return{items:t.items.concat(e),text:"",id:t.id+1}})}}}(r.Component);var u=function(t){function e(){return s(this,t.apply(this,arguments))}return l(e,t),e.prototype.render=function(){return(0,r.h)("ul",null,this.props.items.map(function(t){return(0,r.h)("li",{key:t.id},t.text)}))},e}(r.Component);!function(t){function e(e){var n=s(this,t.call(this,e));return n.state={seconds:0},n}l(e,t),e.prototype.tick=function(){this.setState(function(t){return{seconds:t.seconds+1}})},e.prototype.componentDidMount=function(){var t=this;this.interval=setInterval(function(){return t.tick()},1e3)},e.prototype.componentWillUnmount=function(){clearInterval(this.interval)},e.prototype.render=function(){return(0,r.h)("div",null,"Seconds: ",this.state.seconds)}}(r.Component);(0,r.render)((0,r.h)(i.default,null),document.body)},function(t,e,n){(function(t){function e(t,e){var n,o,r,a,i=[];for(a=arguments.length;a-- >2;)k.push(arguments[a]);for(e&&e.children&&(k.length||k.push(e.children),delete e.children);k.length;)if((o=k.pop())instanceof Array)for(a=o.length;a--;)k.push(o[a]);else null!=o&&!1!==o&&("number"!=typeof o&&!0!==o||(o=String(o)),(r="string"==typeof o)&&n?i[i.length-1]+=o:(i.push(o),n=r));var s=new function(t,e,n){this.nodeName=t,this.attributes=e,this.children=n,this.key=e&&e.key}(t,e||void 0,i);return w.vnode&&w.vnode(s),s}function n(t,e){if(e)for(var n in e)t[n]=e[n];return t}function o(t){return n({},t)}function r(t){return"function"==typeof t}function a(t){return"string"==typeof t}function i(t){!t._dirty&&(t._dirty=!0)&&1==U.push(t)&&(w.debounceRendering||T)(s)}function s(){var t,e=U;for(U=[];t=e.pop();)t._dirty&&S(t)}function l(t){var e=t&&t.nodeName;return e&&r(e)&&!(e.prototype&&e.prototype.render)}function u(t,e){return t.nodeName(p(t),e||j)}function c(t,e){return t.normalizedNodeName===e||M(t.nodeName)===M(e)}function p(t){var e=o(t.attributes);e.children=t.children;var n=t.nodeName.defaultProps;if(n)for(var r in n)void 0===e[r]&&(e[r]=n[r]);return e}function d(t){var e=t.parentNode;e&&e.removeChild(t)}function f(t,e,n,o,i){if("className"===e&&(e="class"),"class"===e&&o&&"object"==typeof o&&(o=function(t){var e="";for(var n in t)t[n]&&(e&&(e+=" "),e+=n);return e}(o)),"key"===e);else if("class"!==e||i)if("style"===e){if((!o||a(o)||a(n))&&(t.style.cssText=o||""),o&&"object"==typeof o){if(!a(n))for(var s in n)s in o||(t.style[s]="");for(var s in o)t.style[s]="number"!=typeof o[s]||q[s]?o[s]:o[s]+"px"}}else if("dangerouslySetInnerHTML"===e)t.innerHTML=o&&o.__html||"";else if("o"==e[0]&&"n"==e[1]){var l=t._listeners||(t._listeners={});e=M(e.substring(2)),o?l[e]||t.addEventListener(e,h,!!P[e]):l[e]&&t.removeEventListener(e,h,!!P[e]),l[e]=o}else if("list"!==e&&"type"!==e&&!i&&e in t)!function(t,e,n){try{t[e]=n}catch(t){}}(t,e,null==o?"":o),null!=o&&!1!==o||t.removeAttribute(e);else{var u=i&&e.match(/^xlink\:?(.+)/);null==o||!1===o?u?t.removeAttributeNS("http://www.w3.org/1999/xlink",M(u[1])):t.removeAttribute(e):"object"==typeof o||r(o)||(u?t.setAttributeNS("http://www.w3.org/1999/xlink",M(u[1]),o):t.setAttribute(e,o))}else t.className=o||""}function h(t){return this._listeners[t.type](w.event&&w.event(t)||t)}function m(t,e){var n=M(t),o=A[n]&&A[n].pop()||(e?document.createElementNS("http://www.w3.org/2000/svg",t):document.createElement(t));return o.normalizedNodeName=n,o}function v(){for(var t;t=F.pop();)w.afterMount&&w.afterMount(t),t.componentDidMount&&t.componentDidMount()}function y(t,e,n,o,r,a){B++||(I=r instanceof SVGElement,L=t&&!(E in t));var i=b(t,e,n,o);return r&&i.parentNode!==r&&r.appendChild(i),--B||(L=!1,a||v()),i}function b(t,e,n,o){for(var i=e&&e.attributes;l(e);)e=u(e,n);if(null==e&&(e=""),a(e))return t&&t instanceof Text?t.nodeValue!=e&&(t.nodeValue=e):(t&&g(t),t=document.createTextNode(e)),t[E]=!0,t;if(r(e.nodeName))return function(t,e,n,o){var r=t&&t._component,a=t,i=r&&t._componentConstructor===e.nodeName,s=i,l=p(e);for(;r&&!s&&(r=r._parentComponent);)s=r.constructor===e.nodeName;r&&s&&(!o||r._component)?(C(r,l,3,n,o),t=r.base):(r&&!i&&(x(r,!0),t=a=null),r=_(e.nodeName,l,n),t&&!r.nextBase&&(r.nextBase=t,a=null),C(r,l,1,n,o),t=r.base,a&&t!==a&&(a._component=null,g(a)));return t}(t,e,n,o);var s=t,h=String(e.nodeName),v=I,y=e.children;if(I="svg"===h||"foreignObject"!==h&&I,t){if(!c(t,h)){for(s=m(h,I);t.firstChild;)s.appendChild(t.firstChild);t.parentNode&&t.parentNode.replaceChild(s,t),g(t)}}else s=m(h,I);var S=s.firstChild,N=s[E];if(!N){s[E]=N={};for(var w=s.attributes,k=w.length;k--;)N[w[k].name]=w[k].value}return function(t,e,n){for(var o in n)e&&o in e||null==n[o]||f(t,o,n[o],n[o]=void 0,I);if(e)for(var r in e)"children"===r||"innerHTML"===r||r in n&&e[r]===("value"===r||"checked"===r?t[r]:n[r])||f(t,r,n[r],n[r]=e[r],I)}(s,e.attributes,N),!L&&y&&1===y.length&&"string"==typeof y[0]&&S&&S instanceof Text&&!S.nextSibling?S.nodeValue!=y[0]&&(S.nodeValue=y[0]):(y&&y.length||S)&&function(t,e,n,o){var i,s,u,p,f=t.childNodes,h=[],m={},v=0,y=0,_=f.length,C=0,S=e&&e.length;if(_)for(var x=0;x<_;x++){var N=f[x],w=N[E],k=S?(s=N._component)?s.__key:w?w.key:null:null;null!=k?(v++,m[k]=N):(L||w)&&(h[C++]=N)}if(S)for(var x=0;x=_?t.appendChild(p):p!==f[x]&&(p===f[x+1]&&d(f[x]),t.insertBefore(p,f[x]||null)))}var O,M;if(v)for(var x in m)m[x]&&g(m[x]);for(;y<=C;)(p=h[C--])&&g(p)}(s,y,n,o),i&&"function"==typeof i.ref&&(N.ref=i.ref)(s),I=v,s}function g(t,e){var n,o=t._component;if(o)x(o,!e);else for(t[E]&&t[E].ref&&t[E].ref(null),e||function(t){if(d(t),t instanceof Element){t._component=t._componentConstructor=null;var e=t.normalizedNodeName||M(t.nodeName);(A[e]||(A[e]=[])).push(t)}}(t);n=t.lastChild;)g(n,e)}function _(t,e,n){var o=new t(e,n),r=W[t.name];if(N.call(o,e,n),r)for(var a=r.length;a--;)if(r[a].constructor===t){o.nextBase=r[a].nextBase,r.splice(a,1);break}return o}function C(t,e,n,o,r){t._disable||(t._disable=!0,(t.__ref=e.ref)&&delete e.ref,(t.__key=e.key)&&delete e.key,!t.base||r?t.componentWillMount&&t.componentWillMount():t.componentWillReceiveProps&&t.componentWillReceiveProps(e,o),o&&o!==t.context&&(t.prevContext||(t.prevContext=t.context),t.context=o),t.prevProps||(t.prevProps=t.props),t.props=e,t._disable=!1,0!==n&&(1!==n&&!1===w.syncComponentUpdates&&t.base?i(t):S(t,1,r)),t.__ref&&t.__ref(t))}function S(t,e,a,i){if(!t._disable){var s,c,d,f,h=t.props,m=t.state,b=t.context,N=t.prevProps||h,k=t.prevState||m,O=t.prevContext||b,M=t.base,D=t.nextBase,T=M||D,j=t._component;if(M&&(t.props=N,t.state=k,t.context=O,2!==e&&t.shouldComponentUpdate&&!1===t.shouldComponentUpdate(h,m,b)?s=!0:t.componentWillUpdate&&t.componentWillUpdate(h,m,b),t.props=h,t.state=m,t.context=b),t.prevProps=t.prevState=t.prevContext=t.nextBase=null,t._dirty=!1,!s){for(t.render&&(c=t.render(h,m,b)),t.getChildContext&&(b=n(o(b),t.getChildContext()));l(c);)c=u(c,b);var E,q,P=c&&c.nodeName;if(r(P)){var U=p(c);(d=j)&&d.constructor===P&&U.key==d.__key?C(d,U,1,b):(E=d,(d=_(P,U,b)).nextBase=d.nextBase||D,d._parentComponent=t,t._component=d,C(d,U,0,b),S(d,1,a,!0)),q=d.base}else f=T,(E=j)&&(f=t._component=null),(T||1===e)&&(f&&(f._component=null),q=y(f,c,b,a||!M,T&&T.parentNode,!0));if(T&&q!==T&&d!==j){var A=T.parentNode;A&&q!==A&&(A.replaceChild(q,T),E||(T._component=null,g(T)))}if(E&&x(E,q!==T),t.base=q,q&&!i){for(var I=t,L=t;L=L._parentComponent;)(I=L).base=q;q._component=I,q._componentConstructor=I.constructor}}!M||a?F.unshift(t):s||(t.componentDidUpdate&&t.componentDidUpdate(N,k,O),w.afterUpdate&&w.afterUpdate(t));var W,Q=t._renderCallbacks;if(Q)for(;W=Q.pop();)W.call(t);B||i||v()}}function x(t,e){w.beforeUnmount&&w.beforeUnmount(t);var n=t.base;t._disable=!0,t.componentWillUnmount&&t.componentWillUnmount(),t.base=null;var o=t._component;if(o)x(o,e);else if(n){var r;for(n[E]&&n[E].ref&&n[E].ref(null),t.nextBase=n,e&&(d(n),function(t){var e=t.constructor.name,n=W[e];n?n.push(t):W[e]=[t]}(t));r=n.lastChild;)g(r,!e)}t.__ref&&t.__ref(null),t.componentDidUnmount&&t.componentDidUnmount()}function N(t,e){this._dirty=!0,this.context=e,this.props=t,this.state||(this.state={})}var w={},k=[],O={},M=function(t){return O[t]||(O[t]=t.toLowerCase())},D="undefined"!=typeof Promise&&Promise.resolve(),T=D?function(t){D.then(t)}:setTimeout,j={},E="undefined"!=typeof Symbol?Symbol.for("preactattr"):"__preactattr_",q={boxFlex:1,boxFlexGroup:1,columnCount:1,fillOpacity:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,fontWeight:1,lineClamp:1,lineHeight:1,opacity:1,order:1,orphans:1,strokeOpacity:1,widows:1,zIndex:1,zoom:1},P={blur:1,error:1,focus:1,load:1,resize:1,scroll:1},U=[],A={},F=[],B=0,I=!1,L=!1,W={};n(N.prototype,{linkState:function(t,e){var n=this._linkedStates||(this._linkedStates={});return n[t+e]||(n[t+e]=function(t,e,n){var o=e.split(".");return function(e){for(var r=e&&e.target||this,i={},s=i,l=a(n)?function(t,e){for(var n=e.split("."),o=0;o2?[].slice.call(arguments,2):t.children)},t.Component=N,t.render=function(t,e,n){return y(n,t,{},!1,e)},t.rerender=s,t.options=w})(e)},function(t,e,n){"use strict";e.__esModule=!0,e.DBMon=void 0;var o=n(1);function r(t,e){if(t)return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function a(t,e){"function"!=typeof e&&null!==e||(t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e))}var i=i||function(){var t,e,n=0;function o(t){t||(t={});var e,n,o=15*Math.random();return t.elapsed=o,t.formatElapsed=(e=o,n=parseFloat(e).toFixed(2),e>60&&(minutes=Math.floor(e/60),comps=(e%60).toFixed(2).split("."),seconds=comps[0].lpad("0",2),ms=comps[1],n=minutes+":"+seconds+"."+ms),n),t.elapsedClassName=function(t){var e="Query elapsed";return e+=t>=10?" warn_long":t>=1?" warn":" short"}(o),t.query="SELECT blah FROM something",t.waiting=Math.random()<.5,Math.random()<.2&&(t.query=" in transaction"),Math.random()<.1&&(t.query="vacuum"),t}function r(t){if(!t)return{query:"***",formatElapsed:"",elapsedClassName:""};t.formatElapsed="",t.elapsedClassName="",t.query="",t.elapsed=null,t.waiting=null}function a(t,e,n){var a=Math.floor(10*Math.random()+1);if(t||(t={}),t.lastMutationId=n,t.nbQueries=a,t.lastSample||(t.lastSample={}),t.lastSample.topFiveQueries||(t.lastSample.topFiveQueries=[]),e){if(!t.lastSample.queries){t.lastSample.queries=[];for(var i=0;i<12;i++)t.lastSample.queries[i]=r()}for(var s in t.lastSample.queries){var l=t.lastSample.queries[s];s<=a?o(l):r(l)}}else{t.lastSample.queries=[];for(s=0;s<12;s++)if(s=20?" label-important":t>=10?" label-warning":" label-success"}(a),t}(e=String.prototype).lpad||(e.lpad=function(t,e){return t.repeat((e-this.length)/t.length).concat(this)});var s=.5;return{generateData:function(e){var o=t;if(!e){t=[];for(var r=1;r<=i.rows;r++)t.push({dbname:"cluster"+r,query:"",formatElapsed:"",elapsedClassName:""}),t.push({dbname:"cluster"+r+" slave",query:"",formatElapsed:"",elapsedClassName:""})}if(!t){for(t=[],r=1;r<=i.rows;r++)t.push({dbname:"cluster"+r}),t.push({dbname:"cluster"+r+" slave"});o=t}for(var r in t){var s=t[r];!e&&o&&o[r]&&(s.lastSample=o[r].lastSample),!s.lastSample||Math.random()0&&(0,o.h)("button",{class:"clear-completed",onClick:i},"Clear completed"))},a}(o.Component);e.default=a},function(t,e,n){"use strict";e.__esModule=!0;var o=n(1);function r(t,e){if(t)return!e||"object"!=typeof e&&"function"!=typeof e?t:e}var a=27,i=13,s=function(t){var e,n;function s(){for(var e,n,o=arguments.length,s=Array(o),l=0;l + + + + Skimlinks Example + + + + + + + + + + + + + + + +

    Simple affiliated link

    + + Merchant + + +

    Non-merchant link

    + + Just a random non-merchant site. + + + + + + + + + + + \ No newline at end of file diff --git a/examples/amp-story-auto-ads-payload.json b/examples/amp-story-auto-ads-payload.json new file mode 100644 index 000000000000..f5833a9fbaba --- /dev/null +++ b/examples/amp-story-auto-ads-payload.json @@ -0,0 +1,12 @@ +{ + "templateId": "template-1", + "data": { + "imgSrc": "https://i.imgur.com/4wUqhsQ.jpg", + "impressionUrl": "https://example.com/track?iid=18745543" + }, + "vars": { + "ctaType": "SHOP", + "ctaUrl": "https://ampproject.org", + "impressionId": "ac2d1s2E3B" + } +} diff --git a/examples/amp-story-auto-ads.amp.html b/examples/amp-story-auto-ads.amp.html new file mode 100644 index 000000000000..1dea3715d7ea --- /dev/null +++ b/examples/amp-story-auto-ads.amp.html @@ -0,0 +1,210 @@ + + + + + amp-story + + + + + + + + + + + + + + + + + + + + + + +

    fade-in

    +
    +
    +
    +
    + + + +

    twirl-in

    +
    +
    +
    +
    + + + +

    fly-in-left

    +
    +
    +
    +
    + + + +

    fly-in-right

    +
    +
    +
    +
    + + + +

    fly-in-top

    +
    +
    +
    +
    + + + +

    fly-in-bottom

    +
    +
    +
    +
    + + + +

    rotate-in-left

    +
    +
    +
    +
    + + + +

    rotate-in-right

    +
    +
    +
    +
    + + + +

    drop-in

    +
    +
    +
    +
    + + + +

    whoosh-in-left

    +
    +
    +
    +
    + + + +

    whoosh-in-right

    +
    +
    +
    +
    +
    + + diff --git a/examples/amp-story.0.1.amp.html b/examples/amp-story.0.1.amp.html new file mode 100644 index 000000000000..0e5e436a74a5 --- /dev/null +++ b/examples/amp-story.0.1.amp.html @@ -0,0 +1,17 @@ + + + + + + amp-story + + + + + + + + + + + diff --git a/examples/amp-story/access.html b/examples/amp-story/access.html new file mode 100644 index 000000000000..23d574d32aa6 --- /dev/null +++ b/examples/amp-story/access.html @@ -0,0 +1,89 @@ + + + + + + + + My Story with access + + + + + + + + + + + + + + + + + + + + +

    Free page!

    +
    +
    + + + + + + +

    You're reading premium content!

    +
    +
    +
    + + diff --git a/examples/amp-story/ampconf.html b/examples/amp-story/ampconf.html new file mode 100644 index 000000000000..53bd01666149 --- /dev/null +++ b/examples/amp-story/ampconf.html @@ -0,0 +1,306 @@ + + + + + + + + Key Highlights of AMP Conf 2018 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Key Highlights of AMP Conf 2018

    + +
    +
    + + + + + + +

    Overview

    +

    We held the second AMP Conf to celebrate the breadth of the AMP + community and announce the latest AMP innovations. We engaged 400+ + devs in-person over two days and thousands globally on live stream.

    +

    Here are the key launches by the AMP team and others this year

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

    Introducing

    +

    AMP Stories

    +
    +
    +
    + + + + + + +

    A visual storytelling format for the open web

    +

    Providing content publishers with a mobile-focused + format for delivering news and information as visual, tap-through + stories on the open web.

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

    Introducing

    +

    AMP For Email

    +
    +
    +
    + + + + + + +

    Bringing the power of AMP to Gmail

    +

    New spec allows developers to create more engaging, + interactive, and actionable email experiences with AMP content.

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

    Discover

    +

    AMP for
    E-Commerce

    +
    +
    +
    + + + + + + +

    Improve conversions with fast, user-friendly experiences

    +

    With instant page load, your customers can find the products they want quickly and easily.

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

    Join the worldwide AMP Roadshow

    +
    + + +

    Sign up Now

    +
    +
    +
    + + + +
    + + diff --git a/examples/amp-story/analytics.html b/examples/amp-story/analytics.html new file mode 100644 index 000000000000..d96aa6333cdd --- /dev/null +++ b/examples/amp-story/analytics.html @@ -0,0 +1,121 @@ + + + + + + + + + My Story + + + + + + + + + + + + + + + +

    Page One

    +
    +
    + + + +

    Page Two

    +
    +
    + + + +

    Page Three

    +
    + + click me + +
    + + + +

    Page Four

    + + +
    +
    + + + +
    + + diff --git a/examples/amp-story/animations-presets.html b/examples/amp-story/animations-presets.html new file mode 100644 index 000000000000..868d2beeea35 --- /dev/null +++ b/examples/amp-story/animations-presets.html @@ -0,0 +1,240 @@ + + + + + + amp-story + + + + + + + + + + + + + +

    pulse

    +
    +
    +
    +
    + + + +

    fade-in

    +
    +
    +
    +
    + + + +

    twirl-in

    +
    +
    +
    +
    + + + +

    fly-in-left

    +
    +
    +
    +
    + + + +

    fly-in-right

    +
    +
    +
    +
    + + + +

    fly-in-top

    +
    +
    +
    +
    + + + +

    fly-in-bottom

    +
    +
    +
    +
    + + + +

    rotate-in-left

    +
    +
    +
    +
    + + + +

    rotate-in-right

    +
    +
    +
    +
    + + + +

    drop-in

    +
    +
    +
    +
    + + + +

    whoosh-in-left

    +
    +
    +
    +
    + + + +

    whoosh-in-right

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

    zoom-in

    +
    +
    + + + + + + + + +

    zoom-out

    +
    +
    + + + + + + + + +

    pan-left

    +
    +
    + + + + + + + + +

    pan-right

    +
    +
    + + + + + + + + +

    pan-up

    +
    +
    + + + + + + + + +

    pan-down

    +
    +
    +
    + + + diff --git a/examples/amp-story/animations-sequence.html b/examples/amp-story/animations-sequence.html new file mode 100644 index 000000000000..caee9e96c6c2 --- /dev/null +++ b/examples/amp-story/animations-sequence.html @@ -0,0 +1,90 @@ + + + + + amp-story + + + + + + + + + + + +

    STAMP animation sequencing

    +
    + [A] pulse +
    +
    + [B] fly-in-left +
    +
    + [C1] fade-in +
    +
    + [C2] pulse +
    +
    + [X] bounce +
    +
    +
    +
    + + diff --git a/examples/amp-story/bookend.html b/examples/amp-story/bookend.html new file mode 100644 index 000000000000..006f41da27da --- /dev/null +++ b/examples/amp-story/bookend.html @@ -0,0 +1,70 @@ + + + + + + + My Story + + + + + + + + + + + + +

    Advance to see the bookend!

    +
    +
    +
    + + + + + diff --git a/examples/amp-story/bookend.json b/examples/amp-story/bookend.json new file mode 100644 index 000000000000..82d5776aa3e6 --- /dev/null +++ b/examples/amp-story/bookend.json @@ -0,0 +1,15 @@ +{ + "share-providers": { + "facebook": true, + "whatsapp": true + }, + "related-articles": { + "test": [ + { + "title": "This is an example article", + "url": "http://example.com/article.html", + "image": "http://placehold.it/256x128" + } + ] + } +} diff --git a/examples/amp-story/bookendv1.html b/examples/amp-story/bookendv1.html new file mode 100644 index 000000000000..5ef5f344a562 --- /dev/null +++ b/examples/amp-story/bookendv1.html @@ -0,0 +1,63 @@ + + + + + + + My Story + + + + + + + + + + +

    Advance to see the bookend!

    +
    +
    + + +
    + + diff --git a/examples/amp-story/bookendv1.json b/examples/amp-story/bookendv1.json new file mode 100644 index 000000000000..f8d6ab0efb75 --- /dev/null +++ b/examples/amp-story/bookendv1.json @@ -0,0 +1,99 @@ +{ + "bookendVersion": "v1.0", + "shareProviders": [ + "email", + {"provider": "facebook", "app_id": "254325784911610"}, + {"provider": "twitter", "text": "This is custom share text that I would like for the Twitter platform"}, + "whatsapp" + ], + "components": [ + { + "type": "heading", + "text": "test" + }, + { + "type": "small", + "title": "This is an example article", + "url": "/article.html", + "image": "http://placehold.it/500x600" + }, + { + "type": "small", + "title": "This is an example article2", + "url": "http://example.com/article.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "This is an example article3", + "url": "http://example.com/article.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "heading", + "text": "test2" + }, + { + "type": "small", + "title": "This is an example article", + "url": "http://example.com/article.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "portrait", + "title": "Heading for portrait", + "category": "This is an example portrait", + "url": "http://example.com/article.html", + "image": "http://placehold.it/350x470" + }, + { + "type": "landscape", + "title": "TRAPPIST-1 Planets May Still Be Wet Enough for Life", + "url": "http://example.com/article.html", + "category": "astronomy", + "image": "http://placehold.it/360x760" + }, + { + "type": "small", + "title": "This is an example article", + "url": "http://example.com/article.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "This is an example article", + "url": "http://example.com/article.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "cta-link", + "links": [ + { + "text": "buttonA", + "url": "google.com" + }, + { + "text": "buttonB", + "url": "google.com" + }, + { + "text": "longtext longtext longtext longtext longtext longtext longt", + "url": "google.com" + } + ] + }, + { + "type": "heading", + "text": "credits" + }, + { + "type": "textbox", + "text": [ + "Food by Enrique McPizza", + "Choreography by Gabriel Filly", + "Script by Alan Ecma S.", + "Direction by Jon Tarantino" + ] + } + ] +} diff --git a/examples/amp-story/consent-geo.html b/examples/amp-story/consent-geo.html new file mode 100644 index 000000000000..60e5d07bdf0f --- /dev/null +++ b/examples/amp-story/consent-geo.html @@ -0,0 +1,94 @@ + + + + + + + + + My Story with geo consent + + + + + + + + + + + + + + + + + + + + + + +

    You just accepted or rejected the consent!

    +
    +
    +
    + + diff --git a/examples/amp-story/consent.html b/examples/amp-story/consent.html new file mode 100644 index 000000000000..ea3b2da7107c --- /dev/null +++ b/examples/amp-story/consent.html @@ -0,0 +1,90 @@ + + + + + + + + My Story with consent + + + + + + + + + + + + + + + + + + + +

    You just accepted or rejected the consent!

    +
    +
    + + + +
    + + diff --git a/examples/amp-story/cta-layer-outlink.html b/examples/amp-story/cta-layer-outlink.html new file mode 100644 index 000000000000..07bb82a892c1 --- /dev/null +++ b/examples/amp-story/cta-layer-outlink.html @@ -0,0 +1,158 @@ + + + + + + + + My Story + + + + + + + + + + + + +

    no amp-story-cta-layer allowed on first page

    +
    +
    + + + + + + + Call to action! + + + + + +

    vertical

    +
    + + Call to action! + +
    + + + +
    A paragraph
    +
    A longer paragraph
    +
    A much longer paragraph
    +
    The looooongest Paragraph. By far. So very very much longer.
    +
    + + Call to action! + +
    + + + +
    Paragraph 1
    +
    + + Call to action! + +
    + + + +
    Paragraph 1
    +
    Paragraph 2
    +
    Paragraph 3
    +
    Paragraph 4
    +
    Paragraph 5
    +
    Paragraph 6
    +
    Paragraph 7
    +
    Paragraph 8
    +
    + + Call to action! + +
    + + + +
    Paragraph 1
    +
    Paragraph 2
    +
    Paragraph 3
    +
    + + Call to action! + +
    + + + +

    horizontal

    +
    + + Call to action! + +
    + + + +
    Paragraph 1
    +
    Paragraph 2
    +
    + + Call to action! + +
    + + + +
    Paragraph 1
    +
    Paragraph 2
    +
    Paragraph 3
    +
    Paragraph 4
    +
    Paragraph 5
    +
    Paragraph 6
    +
    + + Call to action! + +
    + +
    + + + diff --git a/examples/amp-story/desktop-backgrounds.html b/examples/amp-story/desktop-backgrounds.html new file mode 100644 index 000000000000..add7e4884a6e --- /dev/null +++ b/examples/amp-story/desktop-backgrounds.html @@ -0,0 +1,76 @@ + + + + + + + My Story + + + + + + + + + + + + + + + +

    Hello, amp-story!

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

    Second Page

    +

    This is the second page of this story.

    +
    +
    + + + +

    Third Page

    +

    This is the third page of this story.

    +
    +
    + + + +
    + + diff --git a/examples/amp-story/doubleclick.html b/examples/amp-story/doubleclick.html new file mode 100644 index 000000000000..63daefb2de72 --- /dev/null +++ b/examples/amp-story/doubleclick.html @@ -0,0 +1,204 @@ + + + + + + + + + + My Story + + + + + + + + + + + + + + + +

    Ads served from doubleclick

    +

    This is the cover page.

    +
    +
    + + + +

    Auto advance page

    +

    This page has auto-advance enabled

    +
    +
    + + + +

    Short version of video

    + + +
    +
    + + + +

    Full length video

    + + +
    +
    + + + +

    Audio

    + + +
    +
    + + + +

    six Page

    +
    +
    + + + +

    seven Page

    +
    +
    + + + +

    eight Page

    +
    +
    + + + +

    nine Page

    +
    +
    + + + +

    ten Page

    +
    +
    + + + +

    eleven Page

    +
    +
    + + + +

    twelve Page

    +
    +
    + + + +

    thirteen Page

    +
    +
    + + + +

    fourteen Page

    +
    +
    + + + +

    fifteen Page

    +
    +
    + + + +

    sixteen Page

    +
    +
    + + + +

    seventeen Page

    +
    +
    + + + +

    eighteen Page

    +
    +
    + + + +

    nineteen Page

    +
    +
    + + + +

    twenty Page

    +
    +
    + + + +
    + + diff --git a/examples/amp-story/grid-layer-templates.html b/examples/amp-story/grid-layer-templates.html new file mode 100644 index 000000000000..6b51725d9d00 --- /dev/null +++ b/examples/amp-story/grid-layer-templates.html @@ -0,0 +1,126 @@ + + + + + + + My Story + + + + + + + + + + + + + +

    Templates

    +

    Predefined layouts for your stories.

    +
      +
    • fill
    • +
    • vertical
    • +
    • horizontal
    • +
    +
    +
    + + + +

    fill

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

    vertical

    +
    +
    + + + +
    A paragraph
    +
    A longer paragraph
    +
    A much longer paragraph
    +
    The looooongest Paragraph. By far. So very very much longer.
    +
    +
    + + + +
    Paragraph 1
    +
    +
    + + + +
    Paragraph 1
    +
    Paragraph 2
    +
    Paragraph 3
    +
    Paragraph 4
    +
    Paragraph 5
    +
    Paragraph 6
    +
    Paragraph 7
    +
    Paragraph 8
    +
    +
    + + + +
    Paragraph 1
    +
    Paragraph 2
    +
    Paragraph 3
    +
    +
    + + + +

    horizontal

    +
    +
    + + +
    Paragraph 1
    +
    Paragraph 2
    +
    +
    + + + +
    Paragraph 1
    +
    Paragraph 2
    +
    Paragraph 3
    +
    Paragraph 4
    +
    Paragraph 5
    +
    Paragraph 6
    +
    +
    + +
    + + diff --git a/examples/amp-story/helloworld-bookend.html b/examples/amp-story/helloworld-bookend.html new file mode 100644 index 000000000000..88801886b527 --- /dev/null +++ b/examples/amp-story/helloworld-bookend.html @@ -0,0 +1,62 @@ + + + + + + + My Story + + + + + + + + + + + +

    Cover page - manual advance

    +

    3 page story with a bookend at the end

    +
    +
    + + + +

    Page 1 - auto advance after 3s

    +
    +
    + + + +

    Page 2 - manual advance

    +
    +
    + + + +
    + + diff --git a/examples/amp-story/helloworld.html b/examples/amp-story/helloworld.html new file mode 100644 index 000000000000..b33c0a869a0c --- /dev/null +++ b/examples/amp-story/helloworld.html @@ -0,0 +1,54 @@ + + + + + + + My Story + + + + + + + + + + + +

    Hello World

    +

    This is the cover page of this story.

    +
    +
    + + + +

    First Page

    +

    This is the first page of this story.

    +
    +
    + + + +

    Second Page

    +

    This is the second page of this story.

    +
    +
    +
    + + diff --git a/examples/amp-story/img-city1.jpeg b/examples/amp-story/img-city1.jpeg new file mode 100644 index 000000000000..7570928a9400 Binary files /dev/null and b/examples/amp-story/img-city1.jpeg differ diff --git a/examples/amp-story/img-city2.jpeg b/examples/amp-story/img-city2.jpeg new file mode 100644 index 000000000000..e88a530f9e96 Binary files /dev/null and b/examples/amp-story/img-city2.jpeg differ diff --git a/examples/amp-story/img/AMP-Brand-White-Icon.svg b/examples/amp-story/img/AMP-Brand-White-Icon.svg new file mode 100755 index 000000000000..61055f2bb6fa --- /dev/null +++ b/examples/amp-story/img/AMP-Brand-White-Icon.svg @@ -0,0 +1,12 @@ + + + + AMP-Brand-White-Icon + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/examples/amp-story/img/ad-coffee.jpg b/examples/amp-story/img/ad-coffee.jpg new file mode 100644 index 000000000000..c49012653895 Binary files /dev/null and b/examples/amp-story/img/ad-coffee.jpg differ diff --git a/examples/amp-story/img/ad-google-home.jpg b/examples/amp-story/img/ad-google-home.jpg new file mode 100755 index 000000000000..937ce90b01e9 Binary files /dev/null and b/examples/amp-story/img/ad-google-home.jpg differ diff --git a/examples/amp-story/img/ad-hangouts.jpg b/examples/amp-story/img/ad-hangouts.jpg new file mode 100755 index 000000000000..867da3528230 Binary files /dev/null and b/examples/amp-story/img/ad-hangouts.jpg differ diff --git a/examples/amp-story/img/ad-youtube-tv.jpg b/examples/amp-story/img/ad-youtube-tv.jpg new file mode 100755 index 000000000000..bd1098d32892 Binary files /dev/null and b/examples/amp-story/img/ad-youtube-tv.jpg differ diff --git a/examples/amp-story/img/blue-gmail.jpg b/examples/amp-story/img/blue-gmail.jpg new file mode 100755 index 000000000000..552b71250eeb Binary files /dev/null and b/examples/amp-story/img/blue-gmail.jpg differ diff --git a/examples/amp-story/img/blue-stuff.jpg b/examples/amp-story/img/blue-stuff.jpg new file mode 100755 index 000000000000..4be6c043416d Binary files /dev/null and b/examples/amp-story/img/blue-stuff.jpg differ diff --git a/examples/amp-story/img/dark-overlay.png b/examples/amp-story/img/dark-overlay.png new file mode 100755 index 000000000000..94fd105a7f36 Binary files /dev/null and b/examples/amp-story/img/dark-overlay.png differ diff --git a/examples/amp-story/img/green-phone.jpg b/examples/amp-story/img/green-phone.jpg new file mode 100755 index 000000000000..16d04b18de9d Binary files /dev/null and b/examples/amp-story/img/green-phone.jpg differ diff --git a/examples/amp-story/img/green-stuff.jpg b/examples/amp-story/img/green-stuff.jpg new file mode 100755 index 000000000000..77c1f565de5a Binary files /dev/null and b/examples/amp-story/img/green-stuff.jpg differ diff --git a/examples/amp-story/img/overview.jpg b/examples/amp-story/img/overview.jpg new file mode 100755 index 000000000000..938f101c373a Binary files /dev/null and b/examples/amp-story/img/overview.jpg differ diff --git a/examples/amp-story/img/poster.jpg b/examples/amp-story/img/poster.jpg new file mode 100755 index 000000000000..9ed00d98368b Binary files /dev/null and b/examples/amp-story/img/poster.jpg differ diff --git a/examples/amp-story/img/poster0.png b/examples/amp-story/img/poster0.png new file mode 100644 index 000000000000..fab8d6a382ac Binary files /dev/null and b/examples/amp-story/img/poster0.png differ diff --git a/examples/amp-story/img/poster2.jpg b/examples/amp-story/img/poster2.jpg new file mode 100755 index 000000000000..b88cd8defa45 Binary files /dev/null and b/examples/amp-story/img/poster2.jpg differ diff --git a/examples/amp-story/img/poster3.jpg b/examples/amp-story/img/poster3.jpg new file mode 100755 index 000000000000..a28697124a23 Binary files /dev/null and b/examples/amp-story/img/poster3.jpg differ diff --git a/examples/amp-story/img/poster4.jpg b/examples/amp-story/img/poster4.jpg new file mode 100755 index 000000000000..faf351b1bd6b Binary files /dev/null and b/examples/amp-story/img/poster4.jpg differ diff --git a/examples/amp-story/img/poster5.jpg b/examples/amp-story/img/poster5.jpg new file mode 100755 index 000000000000..68a26f46ee4b Binary files /dev/null and b/examples/amp-story/img/poster5.jpg differ diff --git a/examples/amp-story/img/roadshow.jpg b/examples/amp-story/img/roadshow.jpg new file mode 100755 index 000000000000..e6df47636648 Binary files /dev/null and b/examples/amp-story/img/roadshow.jpg differ diff --git a/examples/amp-story/json/bookend.json b/examples/amp-story/json/bookend.json new file mode 100644 index 000000000000..14d44e35b94e --- /dev/null +++ b/examples/amp-story/json/bookend.json @@ -0,0 +1,106 @@ +{ + "bookendVersion": "v1.0", + "shareProviders": [ + "email", + {"provider": "facebook", "app_id": "254325784911610"}, + "whatsapp" + ], + "components": [ + { + "type": "heading", + "text": "Getting Started" + }, + { + "type": "small", + "title": "Hello World", + "url": "hello-world.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "Hello World with Bookend", + "url": "hello-world-bookend.html", + "image": "http://placehold.it/256x128" + }, + + { + "type": "heading", + "text": "Animation Examples" + }, + { + "type": "small", + "title": "Animation Presets", + "url": "animation-presets.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "Animation Sequence", + "url": "animation-presets.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "Visual Effects", + "url": "visual-effects.html", + "image": "http://placehold.it/256x128" + }, + + { + "type": "heading", + "text": "Bookend Examples" + }, + { + "type": "small", + "title": "Bookend v0.1", + "url": "bookend.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "Bookend v1.0", + "url": "bookendv1.html", + "image": "http://placehold.it/256x128" + }, + + { + "type": "heading", + "text": "Layers" + }, + { + "type": "small", + "title": "CTA (Call-to-Action) Layer", + "url": "cta-layer-outlink.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "Grid Layer Templates", + "url": "grid-layer-templates.html", + "image": "http://placehold.it/256x128" + }, + + { + "type": "heading", + "text": "Miscellaneous" + }, + { + "type": "small", + "title": "Analytics", + "url": "analytics.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "Progress Bar", + "url": "progress-bar.html", + "image": "http://placehold.it/256x128" + }, + { + "type": "small", + "title": "Desktop Backgrounds", + "url": "desktop-backgrounds.html", + "image": "http://placehold.it/256x128" + } + ] +} diff --git a/examples/amp-story/progress-bar.html b/examples/amp-story/progress-bar.html new file mode 100644 index 000000000000..4f0466f898eb --- /dev/null +++ b/examples/amp-story/progress-bar.html @@ -0,0 +1,109 @@ + + + + + + + + + My Story + + + + + + + + + + + +

    Progress Bar examples with bookend

    +

    This is the cover page. It does not have auto-advance enabled

    +
    +
    + + + +

    Auto advance page

    +

    This page has auto-advance enabled

    +
    +
    + + + +

    Short version of video

    + + +
    +
    + + + +

    Full length video

    + + +
    +
    + + + +

    Audio

    + + +
    +
    + + + +

    Fifth Page

    +

    This is the fifth page of this story.

    +
    +
    + + + +
    + + diff --git a/examples/amp-story/rtl.html b/examples/amp-story/rtl.html new file mode 100644 index 000000000000..a14abc6d3749 --- /dev/null +++ b/examples/amp-story/rtl.html @@ -0,0 +1,306 @@ + + + + + + + + Key Highlights of AMP Conf 2018 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Key Highlights of AMP Conf 2018

    + +
    +
    + + + + + + +

    Overview

    +

    We held the second AMP Conf to celebrate the breadth of the AMP + community and announce the latest AMP innovations. We engaged 400+ + devs in-person over two days and thousands globally on live stream.

    +

    Here are the key launches by the AMP team and others this year

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

    Introducing

    +

    AMP Stories

    +
    +
    +
    + + + + + + +

    A visual storytelling format for the open web

    +

    Providing content publishers with a mobile-focused + format for delivering news and information as visual, tap-through + stories on the open web.

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

    Introducing

    +

    AMP For Email

    +
    +
    +
    + + + + + + +

    Bringing the power of AMP to Gmail

    +

    New spec allows developers to create more engaging, + interactive, and actionable email experiences with AMP content.

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

    Discover

    +

    AMP for
    E-Commerce

    +
    +
    +
    + + + + + + +

    Improve conversions with fast, user-friendly experiences

    +

    With instant page load, your customers can find the products they want quickly and easily.

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

    Join the worldwide AMP Roadshow

    +
    + + +

    Sign up Now

    +
    +
    +
    + + + +
    + + diff --git a/examples/amp-story/video/ad-google-home.mp4 b/examples/amp-story/video/ad-google-home.mp4 new file mode 100755 index 000000000000..046792303888 Binary files /dev/null and b/examples/amp-story/video/ad-google-home.mp4 differ diff --git a/examples/amp-story/video/ecommerce.mp4 b/examples/amp-story/video/ecommerce.mp4 new file mode 100755 index 000000000000..b1963d96c86d Binary files /dev/null and b/examples/amp-story/video/ecommerce.mp4 differ diff --git a/examples/amp-story/video/ecommerce.vtt b/examples/amp-story/video/ecommerce.vtt new file mode 100644 index 000000000000..62a45a19b50d --- /dev/null +++ b/examples/amp-story/video/ecommerce.vtt @@ -0,0 +1,20 @@ +WEBVTT + +00:00.000 --> 00:00.500 +(Lisa Wang) +I'm Lisa. + +00:00.500 --> 00:02.000 +I'm a product manager at Google. + +00:02.000 --> 00:04.500 +And I lead our e-commerce initiatives. + +00:04.500 --> 00:05.500 +And today I'm just going to walk + +00:05.500 --> 00:07.500 +through some of our best practices + +00:07.500 --> 00:10.000 +for building e-commerce experiences in AMP. diff --git a/examples/amp-story/video/gmail-animation.mp4 b/examples/amp-story/video/gmail-animation.mp4 new file mode 100755 index 000000000000..17365dcb6ca9 Binary files /dev/null and b/examples/amp-story/video/gmail-animation.mp4 differ diff --git a/examples/amp-story/video/gmail.mp4 b/examples/amp-story/video/gmail.mp4 new file mode 100755 index 000000000000..15ad5917b696 Binary files /dev/null and b/examples/amp-story/video/gmail.mp4 differ diff --git a/examples/amp-story/video/gmail.vtt b/examples/amp-story/video/gmail.vtt new file mode 100644 index 000000000000..a3d658f8dfee --- /dev/null +++ b/examples/amp-story/video/gmail.vtt @@ -0,0 +1,23 @@ +WEBVTT + +00:00.000 --> 00:01.500 +(Aakash Sahney) +and I'm really excited to share more + +00:01.500 --> 00:03.500 +about the announcement we made earlier today, + +00:03.500 --> 00:06.000 +which is like a whole new way to use AMP. + +00:06.000 --> 00:08.500 +We're going to talk about what's possible with AMP for email, + +00:08.500 --> 00:11.000 +show off some super slick demos from the three partners + +00:11.000 --> 00:13.500 +that Paul just mentioned, and tell you a bit about how + +00:13.500 --> 00:15.000 +you can get started. \ No newline at end of file diff --git a/examples/amp-story/video/p1.mp4 b/examples/amp-story/video/p1.mp4 new file mode 100755 index 000000000000..7947333e6da9 Binary files /dev/null and b/examples/amp-story/video/p1.mp4 differ diff --git a/examples/amp-story/video/p1.vtt b/examples/amp-story/video/p1.vtt new file mode 100644 index 000000000000..71ae951c12eb --- /dev/null +++ b/examples/amp-story/video/p1.vtt @@ -0,0 +1,35 @@ +WEBVTT + +NOTE Malte Ubl on AMP stories + +00:00.000 --> 00:02.500 +(Malte Ubl) +...and there's going to be surfacing of these stories + +00:02.500 --> 00:04.000 +(Malte Ubl) +in Google Search. + +NOTE Lisa Wang on AMP Toolbox Optimizer + +00:04.500 --> 00:06.000 +(Lisa Wang) +So I'm also really excited to announce today + +00:06.000 --> 00:09.000 +(Lisa Wang) +that we're releasing the AMP Toolbox Optimizer, which + +00:09.000 --> 00:12.000 +(Lisa Wang) +is basically a toolkit that developers can use to optimize... + +NOTE Aakash Sahney on AMP for email + +00:06.000 --> 00:08.500 +(Aakash Sahney) +So with AMP for email, users can quickly take actions on things + +00:08.500 --> 00:11.000 +(Aakash Sahney) +like RSVP'ing to an event. diff --git a/examples/amp-story/video/stamp-animation.mp4 b/examples/amp-story/video/stamp-animation.mp4 new file mode 100755 index 000000000000..423fb70f1886 Binary files /dev/null and b/examples/amp-story/video/stamp-animation.mp4 differ diff --git a/examples/amp-story/video/stamp.mp4 b/examples/amp-story/video/stamp.mp4 new file mode 100755 index 000000000000..ef9f9c48edaf Binary files /dev/null and b/examples/amp-story/video/stamp.mp4 differ diff --git a/examples/amp-story/video/stamp.vtt b/examples/amp-story/video/stamp.vtt new file mode 100644 index 000000000000..2befa4794337 --- /dev/null +++ b/examples/amp-story/video/stamp.vtt @@ -0,0 +1,26 @@ +WEBVTT + +00:00.000 --> 00:02.000 +(Jon Newmuis) +So today, we're excited to announce the developer + +00:02.000 --> 00:06.000 +preview of AMP stories, an open format for visual storytelling + +00:06.000 --> 00:09.000 +on mobile. + +00:09.000 --> 00:12.500 +To demonstrate, here's a story by CNN using the format. + +00:12.500 --> 00:16.500 +As you can see, it's very visual with these full bleed images. + +00:16.500 --> 00:19.000 +And you can also include video assets as well. + +00:19.000 --> 00:22.000 +The short-form bite-sized text is more easily + +00:22.000 --> 00:24.000 +consumable on mobile. \ No newline at end of file diff --git a/examples/amp-story/visual-effects.html b/examples/amp-story/visual-effects.html new file mode 100644 index 000000000000..3322b2eb8c84 --- /dev/null +++ b/examples/amp-story/visual-effects.html @@ -0,0 +1,109 @@ + + + + + + + + + + + Visual Effects + + + + + + + + +
    + + +
    +
    +
    + + + +
    + + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + + +
    + + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    + + + diff --git a/examples/amp-subscriptions.amp.html b/examples/amp-subscriptions.amp.html new file mode 100644 index 000000000000..94dcd0d50388 --- /dev/null +++ b/examples/amp-subscriptions.amp.html @@ -0,0 +1,380 @@ + + + + + subscriptions example + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    +
    + + +
    + +
    +
    +

    Lorem Ipsum

    + +

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

    +
    + +
    + + + +
    + + +
    Login
    + +
    + Login or subscribe to read more. +
    + +
    +

    + 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/amp-timeago.amp.html b/examples/amp-timeago.amp.html new file mode 100644 index 000000000000..1e6e0c6393be --- /dev/null +++ b/examples/amp-timeago.amp.html @@ -0,0 +1,75 @@ + + + + + amp-timeago example + + + + + + + + + + + + +

    Basic example

    + Saturday 11 April 2018 00.37 +

    amp-bind example

    + Saturday 11 April 2018 00.37 + +

    Locale examples

    + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 + Saturday 11 April 2018 00.37 +

    Cutoff example (100 days)

    + Saturday 11 April 2018 00.37 + + diff --git a/examples/amp-video-iframe.html b/examples/amp-video-iframe.html new file mode 100644 index 000000000000..873571bf4cf1 --- /dev/null +++ b/examples/amp-video-iframe.html @@ -0,0 +1,49 @@ + + + + + amp-video-iframe + + + + + + + + + +
    + This is a placeholder +
    +
    + This is a fallback +
    +
    + + diff --git a/examples/amp-video-iframe/frame.html b/examples/amp-video-iframe/frame.html new file mode 100644 index 000000000000..83ba86e64f22 --- /dev/null +++ b/examples/amp-video-iframe/frame.html @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + diff --git a/examples/amp-video.amp.html b/examples/amp-video.amp.html new file mode 100644 index 000000000000..c06ba96cb144 --- /dev/null +++ b/examples/amp-video.amp.html @@ -0,0 +1,105 @@ + + + + + AMP #0 + + + + + + + + + +

    amp-video

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

    Actions

    + + + + + + +

    MediaSession API

    +

    + If you play this on a mobile device, the playback notification will have + a proper poster, artist, album and title. +

    + + + +

    Autoplay

    + + + +

    Rotate-to-fullscren

    +

    Play this video, then rotate your device to enter fullscreen.

    + + +
    + + diff --git a/examples/amp-web-push.amp.html b/examples/amp-web-push.amp.html new file mode 100644 index 000000000000..3a36f73af409 --- /dev/null +++ b/examples/amp-web-push.amp.html @@ -0,0 +1,70 @@ + + + + + amp-web-push example + + + + + + + + + + + + + + + + + + + Looks like you've blocked notifications! + +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi eget + ligula eu augue scelerisque sodales. Vivamus consequat consectetur mauris, sed + mollis justo aliquam feugiat. Phasellus sagittis dui eget posuere pulvinar. + Curabitur leo urna, auctor in fringilla ut, eleifend in dolor. Aenean + imperdiet lectus et lectus tincidunt, eu porttitor nisi placerat. Class aptent + taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. + Praesent pretium tincidunt lectus sit amet semper. Sed egestas vulputate + ultricies. Nunc vestibulum porta ante sed iaculis. +

    + + diff --git a/examples/amp-yotpo.amp.html b/examples/amp-yotpo.amp.html new file mode 100644 index 000000000000..1d79e0f793ba --- /dev/null +++ b/examples/amp-yotpo.amp.html @@ -0,0 +1,49 @@ + + + + + amp-yotpo example + + + + + + + + + + + + + + diff --git a/examples/amp-youtube.amp.html b/examples/amp-youtube.amp.html new file mode 100644 index 000000000000..55fa8b2b7d7e --- /dev/null +++ b/examples/amp-youtube.amp.html @@ -0,0 +1,74 @@ + + + + + AMP #0 + + + + + + + + + +

    amp-youtube

    + + + +

    Actions

    + + + + + + +

    Autoplay

    + + +
    + +

    Live Channel Id

    + + + + + + diff --git a/examples/ampcontext-creative-json.html b/examples/ampcontext-creative-json.html new file mode 100644 index 000000000000..0da1a59a2701 --- /dev/null +++ b/examples/ampcontext-creative-json.html @@ -0,0 +1,15 @@ + + + + + + + + + Test Ad using ampcontext.js to create window.context + + diff --git a/examples/ampcontext-creative.html b/examples/ampcontext-creative.html new file mode 100644 index 000000000000..7af805b13665 --- /dev/null +++ b/examples/ampcontext-creative.html @@ -0,0 +1,104 @@ + + + + + + + + + Test Ad using ampcontext.js to create window.context + + + + + + diff --git a/examples/analytics-error-reporting.amp.html b/examples/analytics-error-reporting.amp.html new file mode 100644 index 000000000000..7deb307bec22 --- /dev/null +++ b/examples/analytics-error-reporting.amp.html @@ -0,0 +1,103 @@ + + + + + Errors for User Error Report + + + + + + + + + + + +

    These create 1P error

    +

    amp-img fake 1p error

    + + + +

    amp-anim fake 1p error

    + + + +

    + 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. +

    + +

    amp-pixel fake 1p error

    + + +

    IFrame fake 1p error

    + + + +

    amp-list fake 1p error

    + + + + +

    amp-youtube fake 1p error

    + + + +

    These create 3P error

    +

    fake 3p error

    + + + + + + + + + diff --git a/examples/analytics-html-attr.amp.html b/examples/analytics-html-attr.amp.html new file mode 100644 index 000000000000..ed15b830dd59 --- /dev/null +++ b/examples/analytics-html-attr.amp.html @@ -0,0 +1,27 @@ + + + + + 3P AMP Analytics Example + + + + + + + + + + Here is some text above the ad.
    + +
    Loading...
    +
    Could not display the fake ad :(
    +
    +
    + Here is some text below the ad.
    + + diff --git a/examples/analytics-iframe-transport-remote-frame.html b/examples/analytics-iframe-transport-remote-frame.html new file mode 100644 index 000000000000..a89a92ddb33f --- /dev/null +++ b/examples/analytics-iframe-transport-remote-frame.html @@ -0,0 +1,55 @@ + + + + + Requests Frame + + + + diff --git a/examples/analytics-iframe-transport.amp.html b/examples/analytics-iframe-transport.amp.html new file mode 100644 index 000000000000..7707a8a1b51b --- /dev/null +++ b/examples/analytics-iframe-transport.amp.html @@ -0,0 +1,27 @@ + + + + + 3P AMP Analytics Example + + + + + + + + + + Here is some text above the ad.
    + +
    Loading...
    +
    Could not display the fake ad :(
    +
    +
    + Here is some text below the ad.
    + + diff --git a/examples/analytics-notification-with-geo.amp.html b/examples/analytics-notification-with-geo.amp.html new file mode 100644 index 000000000000..c92a1004db86 --- /dev/null +++ b/examples/analytics-notification-with-geo.amp.html @@ -0,0 +1,134 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + + + + This notice is only shown in Canada, Mexico and USA. + I accept + + + + + Australia, New Zealand. Britain and Ireland notice. + I accept + + + + + Everywhere else including unknown countries. + I accept + + + + + + +

    Notification with amp-geo demo

    +

    This page has three "amp-user-notification" elements. They + are configured to trigger in different countries. One for NAFTA (CA, MX, US). One for multiple country groups and one for the negative matched country groups.

    +

    If you are using locahost or in the dev channel you can set your country using #amp-geo= with the two letter ISO country code. For example #amp-geo=us.

    + + diff --git a/examples/analytics-notification.amp.html b/examples/analytics-notification.amp.html index ae98b30c0197..67862dfbaa64 100644 --- a/examples/analytics-notification.amp.html +++ b/examples/analytics-notification.amp.html @@ -16,7 +16,7 @@ background: #46b6ac; } - amp-user-notification button { + amp-user-notification .btn { border: none; border-radius: 2px; @@ -66,11 +66,10 @@ + 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. - + I accept diff --git a/examples/analytics-reportWhen.amp.html b/examples/analytics-reportWhen.amp.html new file mode 100644 index 000000000000..87b25de19585 --- /dev/null +++ b/examples/analytics-reportWhen.amp.html @@ -0,0 +1,82 @@ + + + + + AMP Analytics reportWhen + + + + + + + + + + + +

    AMP Analytics reportWhen

    +
    + An event will be triggered once the image below becomes visible. But it won't be reported until the document exits + (e.g. an unload or pagehide event is fired). Look in the developer console, and then scroll to the bottom of the page + to show the image, and finally scroll back up and click the button just below this text. The network request will not + appear until the button is clicked. +
    + + + Click here to generate an unload 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. +

    + +
    + + diff --git a/examples/analytics-vendors.amp.html b/examples/analytics-vendors.amp.html new file mode 100644 index 000000000000..d3002cf81887 --- /dev/null +++ b/examples/analytics-vendors.amp.html @@ -0,0 +1,1434 @@ + + + + + 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 53227a23d1dd..a7775f8d29a4 100644 --- a/examples/analytics.amp.html +++ b/examples/analytics.amp.html @@ -12,68 +12,190 @@ padding: 10px; margin: 10px; } + #container { + position: absolute; + top: 10000px; + height: 10px; + } - + + + +

    Scroll down to see content

    +
    +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. +

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

    +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.config.json b/examples/analytics.config.json index e50f52ed18d9..54ce6de3af3f 100644 --- a/examples/analytics.config.json +++ b/examples/analytics.config.json @@ -1,15 +1,17 @@ { "requests": { - "event": "https://example.com?remote-test&title=${title}&r=${random}" + "event": "/analytics/remote-${type}?&title=${title}&r=${random}" }, "vars": { - "title": "Example Request" + "title": "Example Request", + "type": "pageview" }, "triggers": { "remotePageview": { "on": "visible", "request": "event" } - } + }, + "transport" : {"beacon": true, "xhrpost": false} } diff --git a/examples/anim-worklet.amp.html b/examples/anim-worklet.amp.html new file mode 100644 index 000000000000..19647e09232e --- /dev/null +++ b/examples/anim-worklet.amp.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    +
    + + diff --git a/examples/animations.amp.html b/examples/animations.amp.html new file mode 100644 index 000000000000..270919b2b030 --- /dev/null +++ b/examples/animations.amp.html @@ -0,0 +1,189 @@ + + + + + Animations Examples + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + +
    + + + + + + + + + + + +
    + + + diff --git a/examples/apester-media.amp.html b/examples/apester-media.amp.html new file mode 100644 index 000000000000..cab07578a198 --- /dev/null +++ b/examples/apester-media.amp.html @@ -0,0 +1,32 @@ + + + + + Apester Media Smart Unit + + + + + + + + +
    + + + + + + + + +
    + + + diff --git a/examples/article-access-iframe.amp.html b/examples/article-access-iframe.amp.html new file mode 100644 index 000000000000..e8843bb5072b --- /dev/null +++ b/examples/article-access-iframe.amp.html @@ -0,0 +1,323 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + + +
    + +
    +
    +

    Lorem Ipsum

    + +

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

    +
    + +
    + + + +
    + +
    + + [Close] +
    + + + +
    + 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-iframe.provider.html b/examples/article-access-iframe.provider.html new file mode 100644 index 000000000000..b03303bc5db3 --- /dev/null +++ b/examples/article-access-iframe.provider.html @@ -0,0 +1,41 @@ + + + + + + + + + Hidden iframe. + + diff --git a/examples/article-access-laterpay.amp.html b/examples/article-access-laterpay.amp.html new file mode 100644 index 000000000000..efcaa9f8148a --- /dev/null +++ b/examples/article-access-laterpay.amp.html @@ -0,0 +1,326 @@ + + + + + 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-multiple.amp.html b/examples/article-access-multiple.amp.html new file mode 100644 index 000000000000..837f2b0c0b7e --- /dev/null +++ b/examples/article-access-multiple.amp.html @@ -0,0 +1,381 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + + +
    + +
    +
    +

    Lorem Ipsum

    + +

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

    +
    + +
    + + + +
    + +
    + + [Close] +
    + + + +
    + 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 255aaedebeaa..2bed68556a5c 100644 --- a/examples/article-access.amp.html +++ b/examples/article-access.amp.html @@ -5,11 +5,13 @@ Lorem Ipsum | PublisherName + @@ -138,18 +140,60 @@ background: #ffa; } + .error-section { + margin: 16px; + margin-top: 24px; + padding: 16px; + background: #fa7; + } + - + + - + + + +
    @@ -182,7 +226,7 @@

    Lorem Ipsum

    by

    PublisherName News Reporter

    @@ -196,10 +240,22 @@

    Lorem Ipsum

    [Close] -
    + + 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-cid.amp.html b/examples/viewer-cid.amp.html new file mode 100644 index 000000000000..19feaac8d7ea --- /dev/null +++ b/examples/viewer-cid.amp.html @@ -0,0 +1,47 @@ + + + + + AMP Analytics + + + + + + + + + + + + This site uses cookies to personalize content. + + + + + + + + \ No newline at end of file diff --git a/examples/viewer-iframe-poll.html b/examples/viewer-iframe-poll.html new file mode 100644 index 000000000000..b61c0ed703cf --- /dev/null +++ b/examples/viewer-iframe-poll.html @@ -0,0 +1,585 @@ + + + + + Viewer + + + + + + + + + +
    +

    Viewer

    + One + Two + Three + ALP + Five + | + Visible +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    +
    + + diff --git a/examples/viewer-integr-messaging.js b/examples/viewer-integr-messaging.js deleted file mode 100644 index 44505bfd291a..000000000000 --- a/examples/viewer-integr-messaging.js +++ /dev/null @@ -1,169 +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. - */ - - -/** - * This is a very simple messaging protocol between viewer and viewer client. - * @param {!Window} target - * @param {function(string, *, boolean):(!Promise<*>|undefined)} - * requestProcessor - * @constructor - */ -function ViewerMessaging(target, requestProcessor) { - this.sentinel_ = '__AMP__'; - this.requestSentinel_ = this.sentinel_ + 'REQUEST'; - this.responseSentinel_ = this.sentinel_ + 'RESPONSE'; - - this.requestIdCounter_ = 0; - this.waitingForResponse_ = {}; - - this.target_ = target; - this.requestProcessor_ = requestProcessor; - - window.addEventListener('message', this.onMessage_.bind(this), false); -} - - -/** - * @param {string} eventType - * @param {*} payload - * @param {boolean} awaitResponse - * @return {!Promise<*>|undefined} - */ -ViewerMessaging.prototype.sendRequest = function(eventType, payload, - awaitResponse) { - var requestId = ++this.requestIdCounter_; - if (awaitResponse) { - var promise = new Promise(function(resolve, reject) { - this.waitingForResponse_[requestId] = {resolve: resolve, reject: reject}; - }.bind(this)); - this.sendMessage_(this.requestSentinel_, requestId, eventType, payload, - true); - return promise; - } - this.sendMessage_(this.requestSentinel_, requestId, eventType, payload, - false); - return undefined; -}; - - -/** - * @param {!Event} event - * @private - */ -ViewerMessaging.prototype.onMessage_ = function(event) { - if (event.source != this.target_) { - return; - } - // TODO: must check for origin/target. - var message = event.data; - if (message.sentinel == this.requestSentinel_) { - this.onRequest_(message); - } else if (message.sentinel == this.responseSentinel_) { - this.onResponse_(message); - } -}; - - -/** - * @param {*} message - * @private - */ -ViewerMessaging.prototype.onRequest_ = function(message) { - var requestId = message.requestId; - var promise = this.requestProcessor_(message.type, message.payload, - message.rsvp); - if (message.rsvp) { - if (!promise) { - this.sendResponseError_(requestId, 'no response'); - throw new Error('expected response but none given: ' + message.type); - } - promise.then(function(payload) { - this.sendResponse_(requestId, payload); - }.bind(this), function(reason) { - this.sendResponseError_(requestId, reason); - }.bind(this)); - } -}; - - -/** - * @param {*} message - * @private - */ -ViewerMessaging.prototype.onResponse_ = function(message) { - var requestId = message.requestId; - var pending = this.waitingForResponse_[requestId]; - if (pending) { - delete this.waitingForResponse_[requestId]; - if (message.type == 'ERROR') { - pending.reject(message.payload); - } else { - pending.resolve(message.payload); - } - } -}; - - -/** - * @param {string} sentinel - * @param {string} requestId - * @param {string} eventType - * @param {*} payload - * @param {boolean} awaitResponse - * @private - */ -ViewerMessaging.prototype.sendMessage_ = function(sentinel, requestId, - eventType, payload, awaitResponse) { - // TODO: must check for origin/target. - var message = { - sentinel: sentinel, - requestId: requestId, - type: eventType, - payload: payload, - rsvp: awaitResponse - }; - this.target_./*TODO-REVIEW*/postMessage(message, '*'); -}; - - -/** - * @param {number} requestId - * @param {*} payload - * @private - */ -ViewerMessaging.prototype.sendResponse_ = function(requestId, payload) { - this.sendMessage_(this.responseSentinel_, requestId, null, payload, false); -}; - - -/** - * @param {number} requestId - * @param {*} reason - * @private - */ -ViewerMessaging.prototype.sendResponseError_ = function(requestId, reason) { - this.sendMessage_(this.responseSentinel_, requestId, 'ERROR', reason, false); -}; - - -/** - * Super crude way to share ViewerMessaging class without any kind of module - * system or packaging. - */ -if (window['__AMP_VIEWER_MESSAGING_CALLBACK']) { - window['__AMP_VIEWER_MESSAGING_CALLBACK'](ViewerMessaging); -} diff --git a/examples/viewer-integr.js b/examples/viewer-integr.js deleted file mode 100644 index 8c4202790710..000000000000 --- a/examples/viewer-integr.js +++ /dev/null @@ -1,55 +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. - */ - - -/** - * Super crude way to share ViewerMessaging class without any kind of module - * system or packaging. - * @param {!Function} callback - */ -function whenMessagingLoaded(callback) { - window['__AMP_VIEWER_MESSAGING_CALLBACK'] = callback; - var script = document.createElement('script'); - script.src = './viewer-integr-messaging.js'; - document.head.appendChild(script); -} - - -/** - * This is a very naive implementation of Viewer/AMP integration, but it - * showcases all main APIs exposed by Viewer which are - * {@link Viewer.receiveMessage} and {@link Viewer.setMessageDeliverer}. - * - * The main thing that's missing in this file is any form of security - * validation. In the real world, postMessage and message event handler - * should both set origin information and validate it when received. - */ -(window.AMP = window.AMP || []).push(function(AMP) { - - var viewer = AMP.viewer; - - if (window.parent && window.parent != window) { - whenMessagingLoaded(function(ViewerMessaging) { - var messaging = new ViewerMessaging(window.parent, - function(type, payload, awaitResponse) { - return viewer.receiveMessage(type, payload, awaitResponse); - }); - viewer.setMessageDeliverer(function(type, payload, awaitResponse) { - return messaging.sendRequest(type, payload, awaitResponse); - }); - }); - } -}); diff --git a/examples/viewer-webview.html b/examples/viewer-webview.html new file mode 100644 index 000000000000..ddb97c6c563b --- /dev/null +++ b/examples/viewer-webview.html @@ -0,0 +1,586 @@ + + + + + Viewer + + + + + + + + + +
    +

    Viewer

    + One + Two + Three + ALP + Five + | + Visible +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    + +
    + Please wait, the AMP doc will appear here... +
    +
    +
    + + diff --git a/examples/viewer.html b/examples/viewer.html index 9d50b1153a2c..73d2015f3cb6 100644 --- a/examples/viewer.html +++ b/examples/viewer.html @@ -1,10 +1,11 @@ - + Viewer + - - - + +

    Vimeo

    + height="281" + layout="responsive"> + +

    Actions

    + + + + + + +

    Autoplay

    + +

    Dock

    +
    diff --git a/examples/viqeo.amp.html b/examples/viqeo.amp.html new file mode 100644 index 000000000000..8493019ad7ae --- /dev/null +++ b/examples/viqeo.amp.html @@ -0,0 +1,96 @@ + + + + + + Viqeo examples + + + + + + + + + + +

    amp-viqeo-player

    + + + +

    Actions

    + + + + + + +

    Autoplay

    + + + +

    Without autoplay

    + + + +
    + + + diff --git a/examples/visual-tests/amp-layout/amp-layout.amp.html b/examples/visual-tests/amp-layout/amp-layout.amp.html new file mode 100644 index 000000000000..3ad7d707f107 --- /dev/null +++ b/examples/visual-tests/amp-layout/amp-layout.amp.html @@ -0,0 +1,84 @@ + + + + + amp-layout example + + + + + + + +

    amp-layout

    + +

    responsive - 2x1 ratio - Text Node

    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo. + + +

    responsive - 2x1 ratio - DIV Node

    + +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo.
    +
    + +

    responsive - svg - 1x1 ratio

    + + + + Sorry, your browser does not support inline SVG. + + + +

    fixed - 100x100 - Text Node

    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo. + + +

    fixed - 100x100 - SPAN Node

    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo. + + +

    fixed height - 100 - Text Node

    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo. + + +

    fixed - 100x100 - SPAN Node

    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo. + + +

    With Placeholder

    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo. +
    + SHOULD NOT SEE THIS +
    +
    + +

    intrinsic - 200x100 - Text Node

    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo. + + +

    intrinsic - 200x100 ratio - DIV Node

    + +
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt augue ut dolor rutrum, sit amet tristique velit fringilla. Duis pretium ante dui, a congue erat elementum dictum. Suspendisse sit amet dui libero. Sed sodales quam in mauris aliquet laoreet. Ut iaculis leo ipsum, ac commodo tellus tempus lobortis. Nam molestie semper suscipit. Quisque consectetur purus et tortor mollis porttitor. Morbi tempus sit amet mauris eget mattis. Sed dignissim, quam ut mattis ullamcorper, lorem ex venenatis purus, a sollicitudin enim tortor sit amet justo.
    +
    + +

    intrinsic - svg - 100x100

    + + + + Sorry, your browser does not support inline SVG. + + + + + diff --git a/examples/visual-tests/amp-lightbox-gallery.html b/examples/visual-tests/amp-lightbox-gallery.html new file mode 100644 index 000000000000..0a45b7595bb7 --- /dev/null +++ b/examples/visual-tests/amp-lightbox-gallery.html @@ -0,0 +1,482 @@ + + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +

    Experimental Lightbox 2.0

    +
    + by + + Lorem Ipsum + + +
    + +
    +
    + + See all 4 skyscraper photos + +
    + Toronto's CN tower was built in 1976 and was the tallest free-standing structure until 2007. +
    +
    + + + +
    +
    +

    + 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. +

    + +

    + 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 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 +

    +
    +
    +
    +
    +
    + +
    + +
    + + + \ No newline at end of file diff --git a/examples/visual-tests/amp-list/amp-list-data.json b/examples/visual-tests/amp-list/amp-list-data.json new file mode 100644 index 000000000000..583252ab50a4 --- /dev/null +++ b/examples/visual-tests/amp-list/amp-list-data.json @@ -0,0 +1,13 @@ +{ + "items": [ + { + "title": "Video games", + "imageUrl": "img1.jpg" + }, + + { + "title": "Food", + "imageUrl": "img2.jpg" + } + ] +} diff --git a/examples/visual-tests/amp-list/amp-list.amp.html b/examples/visual-tests/amp-list/amp-list.amp.html new file mode 100644 index 000000000000..7255f680af60 --- /dev/null +++ b/examples/visual-tests/amp-list/amp-list.amp.html @@ -0,0 +1,75 @@ + + + + + amp-list examples + + + + + + + + + + + +

    Overflowing

    + + +
    + SEE MORE +
    +
    + +

    Complete

    + + +
    + SEE MORE +
    +
    + +

    Sanitizer should preserve custom AMP attributes e.g. "layout"

    +

    Expected: The images should fill the width of the its parent amp-list.

    + + + + + diff --git a/examples/visual-tests/amp-list/amp-list.amp.js b/examples/visual-tests/amp-list/amp-list.amp.js new file mode 100644 index 000000000000..329bd27a33f3 --- /dev/null +++ b/examples/visual-tests/amp-list/amp-list.amp.js @@ -0,0 +1,31 @@ +/** + * Copyright 2018 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'; + +const sleep = require('sleep-promise'); +const {verifyCssElements} = require('../../../build-system/tasks/visual-diff/helpers'); + +module.exports = { + + 'tap "see more" button': async (page, name) => { + await page.tap('div.amp-visible[overflow]'); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ ['div.amp-visible[overflow]'], + /* loadingCompleteCss */ null); + }, + +}; diff --git a/examples/visual-tests/amp-list/img1.jpg b/examples/visual-tests/amp-list/img1.jpg new file mode 100644 index 000000000000..53fcbad0a6ce Binary files /dev/null and b/examples/visual-tests/amp-list/img1.jpg differ diff --git a/examples/visual-tests/amp-list/img2.jpg b/examples/visual-tests/amp-list/img2.jpg new file mode 100644 index 000000000000..94d438395b6f Binary files /dev/null and b/examples/visual-tests/amp-list/img2.jpg differ diff --git a/examples/visual-tests/amp-sticky-ad/amp-sticky-ad.amp.html b/examples/visual-tests/amp-sticky-ad/amp-sticky-ad.amp.html new file mode 100644 index 000000000000..487017045681 --- /dev/null +++ b/examples/visual-tests/amp-sticky-ad/amp-sticky-ad.amp.html @@ -0,0 +1,159 @@ + + + + + + amp-sticky-ad + + + + + + + + + + + + + + +

    + 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. +

    + +

    + 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. +

    + +

    + 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. +

    + +

    + 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. +

    + + +

    + 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. +

    + +

    + 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. +

    + +

    + 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. +

    + +

    + 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. +

    + + diff --git a/examples/visual-tests/amp-story/amp-story-bookend.html b/examples/visual-tests/amp-story/amp-story-bookend.html new file mode 100644 index 000000000000..f62ae1757156 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-bookend.html @@ -0,0 +1,81 @@ + + + + + + + + amp-story-bookend visual diff + + + + + + + + + + + + + + + + +

    Hello world!

    +
    +
    + + + + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-bookend.rtl.html b/examples/visual-tests/amp-story/amp-story-bookend.rtl.html new file mode 100644 index 000000000000..29bbb2060d54 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-bookend.rtl.html @@ -0,0 +1,81 @@ + + + + + + + + amp-story-bookend visual diff + + + + + + + + + + + + + + + + +

    مرحبا بالعالم

    +
    +
    + + + + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-consent.html b/examples/visual-tests/amp-story/amp-story-consent.html new file mode 100644 index 000000000000..4d887095f54f --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-consent.html @@ -0,0 +1,93 @@ + + + + + + + + + amp-story-consent visual diff + + + + + + + + + + + + + + + + + + + + + + + +

    Hello world!

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-consent.rtl.html b/examples/visual-tests/amp-story/amp-story-consent.rtl.html new file mode 100644 index 000000000000..a5961afdd6f7 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-consent.rtl.html @@ -0,0 +1,93 @@ + + + + + + + + + amp-story-consent visual diff + + + + + + + + + + + + + + + + + + + + + + + +

    مرحبا بالعالم

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-cta-layer.html b/examples/visual-tests/amp-story/amp-story-cta-layer.html new file mode 100644 index 000000000000..9642bb73c25d --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-cta-layer.html @@ -0,0 +1,72 @@ + + + + + + + amp-story-cta-layer visual diff + + + + + + + + + + + + + CTA layers are disallowed on the first page of stories. This is just a placeholder page, so that the second page may be tested. + + + + + + + + + + + + + diff --git a/examples/visual-tests/amp-story/amp-story-grid-layer-template-fill.html b/examples/visual-tests/amp-story/amp-story-grid-layer-template-fill.html new file mode 100644 index 000000000000..7a913646dd14 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-grid-layer-template-fill.html @@ -0,0 +1,41 @@ + + + + + + + amp-story-grid-layer[template="fill"] visual diff + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/visual-tests/amp-story/amp-story-grid-layer-template-horizontal.html b/examples/visual-tests/amp-story/amp-story-grid-layer-template-horizontal.html new file mode 100644 index 000000000000..e657524d90f9 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-grid-layer-template-horizontal.html @@ -0,0 +1,43 @@ + + + + + + + amp-story-grid-layer[template="horizontal"] visual diff + + + + + + + + + + + + +

    Line 1

    +

    Line 2

    +

    Line 3

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-grid-layer-template-thirds.html b/examples/visual-tests/amp-story/amp-story-grid-layer-template-thirds.html new file mode 100644 index 000000000000..abbf14cd4b5a --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-grid-layer-template-thirds.html @@ -0,0 +1,43 @@ + + + + + + + amp-story-grid-layer[template="thirds"] visual diff + + + + + + + + + + + + +

    Line 1

    +

    Line 2

    +

    Line 3

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-grid-layer-template-vertical.html b/examples/visual-tests/amp-story/amp-story-grid-layer-template-vertical.html new file mode 100644 index 000000000000..146bff7d6a27 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-grid-layer-template-vertical.html @@ -0,0 +1,50 @@ + + + + + + + amp-story-grid-layer[template="vertical"] visual diff + + + + + + + + + + + + +

    Line 1

    +

    Line 2

    +

    Line 3

    +

    Line 4

    +

    Line 5

    +

    Line 6

    +

    Line 7

    +

    Line 8

    +

    Line 9

    +

    Line 10

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-tooltip-desktop.js b/examples/visual-tests/amp-story/amp-story-tooltip-desktop.js new file mode 100644 index 000000000000..59e7648a0294 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-tooltip-desktop.js @@ -0,0 +1,64 @@ +/** + * Copyright 2018 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'; + +const {verifyCssElements} = require('../../../build-system/tasks/visual-diff/helpers'); + +module.exports = { + 'tapping on a clickable anchor should show the tooltip': async (page, name) => { + await page.tap('.next-container > button.i-amphtml-story-button-move'); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['a.i-amphtml-story-tooltip']); + }, + 'tapping outside tooltip should hide it': async (page, name) => { + await page.tap('.next-container > button.i-amphtml-story-button-move'); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await page.waitFor('a.i-amphtml-story-tooltip'); + await page.tap('.i-amphtml-story-tooltip-layer'); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['.i-amphtml-story-tooltip-layer.i-amphtml-hidden']); + }, + 'tapping on tooltip should keep it open': async (page, name) => { + await page.tap('.next-container > button.i-amphtml-story-button-move'); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await page.waitFor('a.i-amphtml-story-tooltip'); + await page.tap('a.i-amphtml-story-tooltip'); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['a.i-amphtml-story-tooltip']); + }, + 'tapping arrow when tooltip is open should navigate': async (page, name) => { + await page.tap('.next-container > button.i-amphtml-story-button-move'); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await page.waitFor('a.i-amphtml-story-tooltip'); + await page.tap('button.i-amphtml-story-button-move'); + await page.waitFor(150); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['amp-story-page#cover[active]']); + }, + }; diff --git a/examples/visual-tests/amp-story/amp-story-tooltip.html b/examples/visual-tests/amp-story/amp-story-tooltip.html new file mode 100644 index 000000000000..1b4dd93563b5 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-tooltip.html @@ -0,0 +1,65 @@ + + + + + + + amp-story tooltip diff + + + + + + + + + + +

    Hello world!

    +

    Page one of two

    +
    +
    + + + +

    Hello world!

    +

    Page two of two

    + Click me! +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/amp-story-tooltip.js b/examples/visual-tests/amp-story/amp-story-tooltip.js new file mode 100644 index 000000000000..900a2f1b5a2a --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-tooltip.js @@ -0,0 +1,68 @@ +/** + * Copyright 2018 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'; + +const {verifyCssElements} = require('../../../build-system/tasks/visual-diff/helpers'); + +module.exports = { + 'tapping on a clickable anchor should show the tooltip': async (page, name) => { + const screen = page.touchscreen; + await screen.tap(200, 240); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['a.i-amphtml-story-tooltip']); + }, + 'tapping outside tooltip should hide it': async (page, name) => { + const screen = page.touchscreen; + await screen.tap(200, 240); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await page.waitFor('a.i-amphtml-story-tooltip'); + await page.tap('.i-amphtml-story-tooltip-layer'); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['.i-amphtml-story-tooltip-layer.i-amphtml-hidden']); + }, + 'tapping on tooltip should keep it open': async (page, name) => { + const screen = page.touchscreen; + await screen.tap(200, 240); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await page.waitFor('a.i-amphtml-story-tooltip'); + await page.tap('a.i-amphtml-story-tooltip'); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['a.i-amphtml-story-tooltip']); + }, + 'tapping arrow when tooltip is open should navigate': async (page, name) => { + const screen = page.touchscreen; + await screen.tap(200, 240); + await page.waitFor('amp-story-page#page-2[active]'); + await page.tap('a.title-small.center'); + await page.waitFor('a.i-amphtml-story-tooltip'); + await page.tap('button.i-amphtml-story-tooltip-nav-button-left'); + await page.waitFor(150); + await verifyCssElements(page, name, + /* forbiddenCss */ null, + /* loadingIncompleteCss */ null, + /* loadingCompleteCss */ ['amp-story-page#cover[active]']); + }, + }; diff --git a/examples/visual-tests/amp-story/amp-story-unsupported-browser-layer.html b/examples/visual-tests/amp-story/amp-story-unsupported-browser-layer.html new file mode 100644 index 000000000000..82904c256864 --- /dev/null +++ b/examples/visual-tests/amp-story/amp-story-unsupported-browser-layer.html @@ -0,0 +1,44 @@ + + + + + + + amp-story unsupported browser visual diff + + + + + + + + + + + + + This page should never be seen, since the browser is unsupported. + + + + + + + diff --git a/examples/visual-tests/amp-story/basic.html b/examples/visual-tests/amp-story/basic.html new file mode 100644 index 000000000000..779abd21343c --- /dev/null +++ b/examples/visual-tests/amp-story/basic.html @@ -0,0 +1,108 @@ + + + + + + + + amp-story rtl visual diff + + + + + + + + + + + + + + + + +

    Hello world!

    +

    Page one of four

    +
    +
    + + + + + + + +

    Hello world!

    +

    Page two of four

    +
    +
    + + + + + + + +

    Hello world!

    +

    Page three of four

    +
    +
    + + + + + + + +

    Hello world!

    +

    Page four of four

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/basic.rtl.html b/examples/visual-tests/amp-story/basic.rtl.html new file mode 100644 index 000000000000..c3680fa93b9a --- /dev/null +++ b/examples/visual-tests/amp-story/basic.rtl.html @@ -0,0 +1,108 @@ + + + + + + + + amp-story rtl visual diff + + + + + + + + + + + + + + + + +

    مرحبا بالعالم

    +

    الصفحة الأولى

    +
    +
    + + + + + + + +

    مرحبا بالعالم

    +

    الصفحة الثانية

    +
    +
    + + + + + + + +

    مرحبا بالعالم

    +

    الصفحة الثالثة

    +
    +
    + + + + + + + +

    مرحبا بالعالم

    +

    الصفحة الرابعة

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/bookend-ar.json b/examples/visual-tests/amp-story/bookend-ar.json new file mode 100644 index 000000000000..87bb68b697ae --- /dev/null +++ b/examples/visual-tests/amp-story/bookend-ar.json @@ -0,0 +1,61 @@ +{ + "bookendVersion": "v1.0", + "shareProviders": [ + "email", + "facebook", + "twitter", + "pinterest", + "whatsapp", + "gplus" + ], + "components": [ + { + "type": "heading", + "text": "أمثلة" + }, + { + "type": "small", + "title": "صغير", + "url": "http://example.com/1.html", + "image": "https://picsum.photos/1600/900?image=981" + }, + { + "type": "landscape", + "title": "أفقي", + "url": "http://example.com/2.html", + "image": "https://picsum.photos/1600/900?image=981" + }, + { + "type": "portrait", + "title": "عمودي", + "url": "http://example.com/3.html", + "image": "https://picsum.photos/1600/900?image=981" + }, + { + "type": "cta-link", + "links": [ + { + "text": "واحد", + "url": "http://example.com/4.html" + }, + { + "text": "اثنان", + "url": "http://example.com/4.html" + }, + { + "text": "ثلاثة", + "url": "http://example.com/4.html" + } + ] + }, + { + "type": "textbox", + "text": [ + "نهاد حداد", + "كاظم الساهر", + "أم كلثوم", + "فريد الأطرش" + ] + } + ] +} diff --git a/examples/visual-tests/amp-story/bookend-en.json b/examples/visual-tests/amp-story/bookend-en.json new file mode 100644 index 000000000000..57cf96817578 --- /dev/null +++ b/examples/visual-tests/amp-story/bookend-en.json @@ -0,0 +1,61 @@ +{ + "bookendVersion": "v1.0", + "shareProviders": [ + "email", + "facebook", + "twitter", + "pinterest", + "whatsapp", + "gplus" + ], + "components": [ + { + "type": "heading", + "text": "Other Examples" + }, + { + "type": "small", + "title": "Small article card", + "url": "http://example.com/1.html", + "image": "https://picsum.photos/1600/900?image=981" + }, + { + "type": "landscape", + "title": "Landscape article card", + "url": "http://example.com/2.html", + "image": "https://picsum.photos/1600/900?image=981" + }, + { + "type": "portrait", + "title": "Portrait article card", + "url": "http://example.com/3.html", + "image": "https://picsum.photos/1600/900?image=981" + }, + { + "type": "cta-link", + "links": [ + { + "text": "Link One", + "url": "http://example.com/4.html" + }, + { + "text": "Link Two", + "url": "http://example.com/4.html" + }, + { + "text": "Link Three", + "url": "http://example.com/4.html" + } + ] + }, + { + "type": "textbox", + "text": [ + "Food by Enrique McPizza", + "Choreography by Gabriel Filly", + "Script by Alan Ecma S.", + "Direction by Jon Tarantino" + ] + } + ] +} diff --git a/examples/visual-tests/amp-story/embed-mode-1.html b/examples/visual-tests/amp-story/embed-mode-1.html new file mode 100644 index 000000000000..957d31a093cd --- /dev/null +++ b/examples/visual-tests/amp-story/embed-mode-1.html @@ -0,0 +1,60 @@ + + + + + + + + amp-story embed mode 1 visual diff + + + + + + + + + + + + + + + + +

    Hello world!

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/embed-mode-2.html b/examples/visual-tests/amp-story/embed-mode-2.html new file mode 100644 index 000000000000..01a9e5428f16 --- /dev/null +++ b/examples/visual-tests/amp-story/embed-mode-2.html @@ -0,0 +1,60 @@ + + + + + + + + amp-story embed mode 2 visual diff + + + + + + + + + + + + + + + + +

    Hello world!

    +
    +
    + +
    + + + diff --git a/examples/visual-tests/amp-story/info-dialog.html b/examples/visual-tests/amp-story/info-dialog.html new file mode 100644 index 000000000000..bc28c6656f6f --- /dev/null +++ b/examples/visual-tests/amp-story/info-dialog.html @@ -0,0 +1,83 @@ + + + + + + + + amp-story info dialog visual diff + + + + + + + + + + + + + + + + +

    Hello world!

    +
    +
    + + + + +
    + + + diff --git a/examples/visual-tests/amp-story/info-dialog.rtl.html b/examples/visual-tests/amp-story/info-dialog.rtl.html new file mode 100644 index 000000000000..04fea7a061f2 --- /dev/null +++ b/examples/visual-tests/amp-story/info-dialog.rtl.html @@ -0,0 +1,83 @@ + + + + + + + + amp-story info dialog visual diff + + + + + + + + + + + + + + + + +

    مرحبا بالعالم

    +
    +
    + + + + +
    + + + diff --git a/examples/visual-tests/amp-story/share-menu.html b/examples/visual-tests/amp-story/share-menu.html new file mode 100644 index 000000000000..738f328ae26b --- /dev/null +++ b/examples/visual-tests/amp-story/share-menu.html @@ -0,0 +1,78 @@ + + + + + + + + amp-story share menu visual diff + + + + + + + + + + + + + + + + +

    Hello world!

    +
    +
    + + + + +
    + + + diff --git a/examples/visual-tests/amp-story/share-menu.rtl.html b/examples/visual-tests/amp-story/share-menu.rtl.html new file mode 100644 index 000000000000..0eb2a00b2197 --- /dev/null +++ b/examples/visual-tests/amp-story/share-menu.rtl.html @@ -0,0 +1,78 @@ + + + + + + + + amp-story share menu visual diff + + + + + + + + + + + + + + + + +

    مرحبا بالعالم

    +
    +
    + + + + +
    + + + diff --git a/examples/visual-tests/article-access.amp/article-access.amp.html b/examples/visual-tests/article-access.amp/article-access.amp.html new file mode 100644 index 000000000000..2bed68556a5c --- /dev/null +++ b/examples/visual-tests/article-access.amp/article-access.amp.html @@ -0,0 +1,373 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + + +
    + +
    +
    +

    Lorem Ipsum

    + +

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

    +
    + +
    + + + +
    + +
    + + [Close] +
    + + + +
    + 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/visual-tests/article-access.amp/img/hero@1x.jpg b/examples/visual-tests/article-access.amp/img/hero@1x.jpg new file mode 100644 index 000000000000..c3064ea39240 Binary files /dev/null and b/examples/visual-tests/article-access.amp/img/hero@1x.jpg differ diff --git a/examples/visual-tests/article-access.amp/img/hero@2x.jpg b/examples/visual-tests/article-access.amp/img/hero@2x.jpg new file mode 100644 index 000000000000..0e421ab1e409 Binary files /dev/null and b/examples/visual-tests/article-access.amp/img/hero@2x.jpg differ diff --git a/examples/visual-tests/article-access.amp/img/sample.jpg b/examples/visual-tests/article-access.amp/img/sample.jpg new file mode 100644 index 000000000000..79a5fbc5e160 Binary files /dev/null and b/examples/visual-tests/article-access.amp/img/sample.jpg differ diff --git a/examples/visual-tests/article-access.amp/img/sea@1x.jpg b/examples/visual-tests/article-access.amp/img/sea@1x.jpg new file mode 100644 index 000000000000..9d946f1a25c4 Binary files /dev/null and b/examples/visual-tests/article-access.amp/img/sea@1x.jpg differ diff --git a/examples/visual-tests/article-access.amp/img/sea@2x.jpg b/examples/visual-tests/article-access.amp/img/sea@2x.jpg new file mode 100644 index 000000000000..eff4d1ea8a31 Binary files /dev/null and b/examples/visual-tests/article-access.amp/img/sea@2x.jpg differ diff --git a/examples/visual-tests/article-fade-in.amp.html b/examples/visual-tests/article-fade-in.amp.html new file mode 100644 index 000000000000..52bdfd0e0b7d --- /dev/null +++ b/examples/visual-tests/article-fade-in.amp.html @@ -0,0 +1,102 @@ + + + + + AMP Article with fade-in animations + + + + + + + + +
    +
    +
    +
    + +

    + Default fade-in +

    +
    +
    +
    + +
    +
    +
    +
    + + + diff --git a/examples/visual-tests/article.amp/article.amp.html b/examples/visual-tests/article.amp/article.amp.html new file mode 100644 index 000000000000..92136c12e96a --- /dev/null +++ b/examples/visual-tests/article.amp/article.amp.html @@ -0,0 +1,453 @@ + + + + + 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/visual-tests/article.amp/img/hero@1x.jpg b/examples/visual-tests/article.amp/img/hero@1x.jpg new file mode 100644 index 000000000000..c3064ea39240 Binary files /dev/null and b/examples/visual-tests/article.amp/img/hero@1x.jpg differ diff --git a/examples/visual-tests/article.amp/img/hero@2x.jpg b/examples/visual-tests/article.amp/img/hero@2x.jpg new file mode 100644 index 000000000000..0e421ab1e409 Binary files /dev/null and b/examples/visual-tests/article.amp/img/hero@2x.jpg differ diff --git a/examples/visual-tests/article.amp/img/sample.jpg b/examples/visual-tests/article.amp/img/sample.jpg new file mode 100644 index 000000000000..79a5fbc5e160 Binary files /dev/null and b/examples/visual-tests/article.amp/img/sample.jpg differ diff --git a/examples/visual-tests/article.amp/img/sea@1x.jpg b/examples/visual-tests/article.amp/img/sea@1x.jpg new file mode 100644 index 000000000000..9d946f1a25c4 Binary files /dev/null and b/examples/visual-tests/article.amp/img/sea@1x.jpg differ diff --git a/examples/visual-tests/article.amp/img/sea@2x.jpg b/examples/visual-tests/article.amp/img/sea@2x.jpg new file mode 100644 index 000000000000..eff4d1ea8a31 Binary files /dev/null and b/examples/visual-tests/article.amp/img/sea@2x.jpg differ diff --git a/examples/visual-tests/blank-page/.gitkeep b/examples/visual-tests/blank-page/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/examples/visual-tests/blank-page/blank.html b/examples/visual-tests/blank-page/blank.html new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/examples/visual-tests/font.amp.404/font.amp.html b/examples/visual-tests/font.amp.404/font.amp.html new file mode 100644 index 000000000000..9e0184363f4f --- /dev/null +++ b/examples/visual-tests/font.amp.404/font.amp.html @@ -0,0 +1,105 @@ + + + + + Font example + + + + + + + + + +

    Lorem Ipsum

    + +

    amp-font

    + +

    + "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..." + "There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..." +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur gravida ipsum vel hendrerit ultricies. Sed bibendum erat sit amet dui mattis imperdiet. Duis feugiat lobortis neque nec accumsan. Nam lacinia placerat enim in cursus. Sed id gravida arcu, sed condimentum mauris. Vestibulum convallis risus ut est ultrices mollis. Curabitur sit amet lorem et leo maximus consectetur non aliquet risus. Pellentesque tempus malesuada eros quis convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dolor risus, convallis eget bibendum at, auctor vel enim. Phasellus posuere dictum fermentum. +

    +

    + 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. +

    +

    + Pellentesque ultricies quam diam, sit amet sollicitudin ante imperdiet a. Pellentesque porta semper nisi, et lobortis est laoreet ac. Vivamus pulvinar egestas purus, vitae pharetra nisl mattis at. Duis gravida ac quam vel commodo. Pellentesque dignissim luctus magna, a fermentum ligula porttitor non. Duis sodales interdum urna, eu viverra elit dapibus vitae. Sed aliquet magna non erat suscipit, vel elementum nisl lacinia. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In eu tempor purus. Cras non libero vehicula, finibus arcu eget, eleifend nulla. Vestibulum non sollicitudin turpis. Fusce condimentum posuere risus nec congue. Curabitur aliquam lorem dolor. +

    +

    + Sed mollis, justo ac volutpat gravida, nisl eros imperdiet massa, sed aliquam nulla odio in urna. Pellentesque eros urna, rutrum vel luctus id, ultrices a libero. Morbi ante justo, gravida et velit at, convallis tristique metus. Nam elementum luctus facilisis. Vestibulum nec augue dignissim, gravida odio nec, volutpat libero. Cras vitae sagittis tellus, sed tincidunt ipsum. Maecenas et mauris id nunc porttitor tempus sit amet at libero. +

    +

    + Aenean ullamcorper risus quam, molestie sodales lacus volutpat at. Nunc vulputate est ut faucibus faucibus. Proin posuere viverra vestibulum. Vestibulum pretium nunc ut euismod sollicitudin. Etiam ornare posuere libero. Vestibulum urna massa, viverra sed ullamcorper ac, hendrerit sed dui. Vestibulum interdum lectus tellus, ut consequat quam pulvinar vitae. Mauris porttitor nulla porta urna convallis accumsan. Curabitur eget ante in libero fringilla elementum. Curabitur tempus arcu massa, gravida tristique erat convallis vitae. Sed lacinia elit justo, eget mollis dui vehicula sit amet. Nunc dignissim condimentum nunc, id pulvinar purus mattis ut. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque lacinia nibh ac enim laoreet imperdiet. +

    + + + + + + + diff --git a/examples/visual-tests/font.amp/font.amp.html b/examples/visual-tests/font.amp/font.amp.html new file mode 100644 index 000000000000..bf61d99e1e1f --- /dev/null +++ b/examples/visual-tests/font.amp/font.amp.html @@ -0,0 +1,105 @@ + + + + + Font example + + + + + + + + + +

    Lorem Ipsum

    + +

    amp-font

    + +

    + "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..." + "There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..." +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur gravida ipsum vel hendrerit ultricies. Sed bibendum erat sit amet dui mattis imperdiet. Duis feugiat lobortis neque nec accumsan. Nam lacinia placerat enim in cursus. Sed id gravida arcu, sed condimentum mauris. Vestibulum convallis risus ut est ultrices mollis. Curabitur sit amet lorem et leo maximus consectetur non aliquet risus. Pellentesque tempus malesuada eros quis convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dolor risus, convallis eget bibendum at, auctor vel enim. Phasellus posuere dictum fermentum. +

    +

    + 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. +

    +

    + Pellentesque ultricies quam diam, sit amet sollicitudin ante imperdiet a. Pellentesque porta semper nisi, et lobortis est laoreet ac. Vivamus pulvinar egestas purus, vitae pharetra nisl mattis at. Duis gravida ac quam vel commodo. Pellentesque dignissim luctus magna, a fermentum ligula porttitor non. Duis sodales interdum urna, eu viverra elit dapibus vitae. Sed aliquet magna non erat suscipit, vel elementum nisl lacinia. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In eu tempor purus. Cras non libero vehicula, finibus arcu eget, eleifend nulla. Vestibulum non sollicitudin turpis. Fusce condimentum posuere risus nec congue. Curabitur aliquam lorem dolor. +

    +

    + Sed mollis, justo ac volutpat gravida, nisl eros imperdiet massa, sed aliquam nulla odio in urna. Pellentesque eros urna, rutrum vel luctus id, ultrices a libero. Morbi ante justo, gravida et velit at, convallis tristique metus. Nam elementum luctus facilisis. Vestibulum nec augue dignissim, gravida odio nec, volutpat libero. Cras vitae sagittis tellus, sed tincidunt ipsum. Maecenas et mauris id nunc porttitor tempus sit amet at libero. +

    +

    + Aenean ullamcorper risus quam, molestie sodales lacus volutpat at. Nunc vulputate est ut faucibus faucibus. Proin posuere viverra vestibulum. Vestibulum pretium nunc ut euismod sollicitudin. Etiam ornare posuere libero. Vestibulum urna massa, viverra sed ullamcorper ac, hendrerit sed dui. Vestibulum interdum lectus tellus, ut consequat quam pulvinar vitae. Mauris porttitor nulla porta urna convallis accumsan. Curabitur eget ante in libero fringilla elementum. Curabitur tempus arcu massa, gravida tristique erat convallis vitae. Sed lacinia elit justo, eget mollis dui vehicula sit amet. Nunc dignissim condimentum nunc, id pulvinar purus mattis ut. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque lacinia nibh ac enim laoreet imperdiet. +

    + + + + + + + diff --git a/examples/visual-tests/font.amp/fonts/ComicAMP.ttf b/examples/visual-tests/font.amp/fonts/ComicAMP.ttf new file mode 100644 index 000000000000..a9c900d92fcc Binary files /dev/null and b/examples/visual-tests/font.amp/fonts/ComicAMP.ttf differ diff --git a/examples/visual-tests/font.amp/fonts/ComicAMPBold.ttf b/examples/visual-tests/font.amp/fonts/ComicAMPBold.ttf new file mode 100644 index 000000000000..6a92bc149d1e Binary files /dev/null and b/examples/visual-tests/font.amp/fonts/ComicAMPBold.ttf differ diff --git a/examples/visual-tests/video/rotate-to-fullscreen.html b/examples/visual-tests/video/rotate-to-fullscreen.html new file mode 100644 index 000000000000..fc6bb290d7c7 --- /dev/null +++ b/examples/visual-tests/video/rotate-to-fullscreen.html @@ -0,0 +1,33 @@ + + + + + Rotate to fullscreen + + + + + + + + + + +

    amp-video

    +

    Rotate-to-fullscreen

    + +
    + This is a placeholder +
    +
    + This is a fallback +
    +
    + + diff --git a/examples/viz-vega.amp.html b/examples/viz-vega.amp.html new file mode 100644 index 000000000000..67c14fdb78dd --- /dev/null +++ b/examples/viz-vega.amp.html @@ -0,0 +1,111 @@ + + + + + Vega Visualization examples + + + + + + + + + + +

    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/vk.amp.html b/examples/vk.amp.html new file mode 100644 index 000000000000..e54c784fbe9a --- /dev/null +++ b/examples/vk.amp.html @@ -0,0 +1,79 @@ + + + + + amp-vk example + + + + + + + +
    +

    VK Post

    + +

    Layout: Responsive

    + + + + +

    Layout: Fixed

    + + + + +

    Layout: Flex item

    + +
    + + +
    + +

    VK Poll

    + +

    Layout: Fixed

    + + + + +

    Layout: Responsive

    + + + +
    + + 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/wistiaplayer.amp.html b/examples/wistiaplayer.amp.html new file mode 100644 index 000000000000..7f069c1731c5 --- /dev/null +++ b/examples/wistiaplayer.amp.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/examples/youtube.amp.html b/examples/youtube.amp.html new file mode 100644 index 000000000000..9efda19af4e0 --- /dev/null +++ b/examples/youtube.amp.html @@ -0,0 +1,77 @@ + + + + + AMP #0 + + + + + + + + + + + + diff --git a/extensions/OWNERS.yaml b/extensions/OWNERS.yaml new file mode 100644 index 000000000000..eb1254c6948a --- /dev/null +++ b/extensions/OWNERS.yaml @@ -0,0 +1 @@ +- aghassemi diff --git a/extensions/README.md b/extensions/README.md index 08cd5429fa82..3e99fa84c264 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -2,54 +2,22 @@ AMP Extensions are either extended components or extended templates. - ## AMP HTML Extended Components Extended components must be explicitly included into the document as custom elements. -For example, to include a youtube video in your page +For example, to include a YouTube video in your page include the following script in the ``: ```html ``` -Current list of extended components: - -| Component | Description | -| --------------------------------------------- | ------------------------------------------------------------------------------------------- | -| [`amp-anim`](amp-anim/amp-anim.md) | Runtime-managed animated image, most typically a GIF. | -| [`amp-audio`](amp-audio/amp-audio.md) | Replacement for the HTML5 `audio` tag. | -| [`amp-brightcove`](amp-brightcove/amp-brightcove.md) | Displays a Brightcove Video Cloud or Perform player. | -| [`amp-carousel`](amp-carousel/amp-carousel.md) | Generic carousel for displaying multiple similar pieces of content along a horizontal axis. | -| [`amp-fit-text`](amp-fit-text/amp-fit-text.md) | Expand or shrink font size to fit the content within the space given. | -| [`amp-font`](amp-font/amp-font.md) | Trigger and monitor the loading of custom fonts. | -| [`amp-iframe`](amp-iframe/amp-iframe.md) | Displays an iframe. | -| [`amp-image-lightbox`](amp-image-lightbox/amp-image-lightbox.md) | Allows for a “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-lightbox`](amp-lightbox/amp-lightbox.md) | Allows for a “lightbox” or similar experience. | -| [`amp-list`](amp-list/amp-list.md) | A dynamic list that can download data and create list items using a template | -| [`amp-twitter`](amp-twitter/amp-twitter.md) | Displays a Twitter Tweet. | -| [`amp-vine`](amp-vine/amp-vine.md) | Displays a Vine simple embed. | -| [`amp-youtube`](amp-youtube/amp-youtube.md) | Displays a Youtube video. | - - -## AMP HTML Extended Templates - -NOT LAUNCHED YET +## Current list of components -Extended templates must be explicitly included into the document as custom templates. +See the [Components](https://www.ampproject.org/docs/reference/components) list. -For example, to include a amp-mustache template in your page -include the following script in the ``: -```html - -``` - -Current list of extended templates: +## 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-3d-gltf/0.1/amp-3d-gltf.js b/extensions/amp-3d-gltf/0.1/amp-3d-gltf.js new file mode 100644 index 000000000000..cc92dbd7cd66 --- /dev/null +++ b/extensions/amp-3d-gltf/0.1/amp-3d-gltf.js @@ -0,0 +1,240 @@ +/** + * Copyright 2018 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 {ActionTrust} from '../../../src/action-constants'; +import {Deferred} from '../../../src/utils/promise'; +import {assertHttpsUrl, resolveRelativeUrl} from '../../../src/url'; +import {dev} from '../../../src/log'; +import {dict} from '../../../src/utils/object'; +import {getIframe, preloadBootstrap} from '../../../src/3p-frame'; +import {isLayoutSizeDefined} from '../../../src/layout'; +import {listenFor, postMessage} from '../../../src/iframe-helper'; +import {removeElement} from '../../../src/dom'; + +const TAG = 'amp-3d-gltf'; + +const isWebGLSupported = () => { + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl') + || canvas.getContext('experimental-webgl'); + return gl && gl instanceof WebGLRenderingContext; +}; + +export class Amp3dGltf extends AMP.BaseElement { + + /** @param {!AmpElement} element */ + constructor(element) { + super(element); + + /** @private {?Element} */ + this.iframe_ = null; + + /** @private {!Deferred} */ + this.willBeReady_ = new Deferred(); + + /** @private {!Deferred} */ + this.willBeLoaded_ = new Deferred(); + + /** @private {!JsonObject} */ + this.context_ = dict(); + + /** @private {?Function} */ + this.unlistenMessage_ = null; + } + + /** + * @param {boolean=} opt_onLayout + * @override + */ + preconnectCallback(opt_onLayout) { + preloadBootstrap(this.win, this.preconnect); + this.preconnect.url('https://cdnjs.cloudflare.com/ajax/libs/three.js/91/three.js', opt_onLayout); + this.preconnect.url('https://cdn.jsdelivr.net/npm/three@0.91/examples/js/loaders/GLTFLoader.js', opt_onLayout); + this.preconnect.url('https://cdn.jsdelivr.net/npm/three@0.91/examples/js/controls/OrbitControls.js', opt_onLayout); + } + + /** @override */ + unlayoutCallback() { + if (this.iframe_) { + removeElement(this.iframe_); + this.iframe_ = null; + } + if (this.unlistenMessage_) { + this.unlistenMessage_(); + } + + this.willBeReady_ = new Deferred(); + this.willBeLoaded_ = new Deferred(); + + return true; + } + + /** @override */ + buildCallback() { + const getOption = (name, fmt, dflt) => + this.element.hasAttribute(name) + ? fmt(this.element.getAttribute(name)) + : dflt; + + const bool = x => x !== 'false'; + const string = x => x; + const number = x => parseFloat(x); + + const src = assertHttpsUrl( + getOption('src', string, ''), + this.element); + + const useAlpha = getOption('alpha', bool, false); + + this.context_ = dict({ + 'src': resolveRelativeUrl(src, this.getAmpDoc().getUrl()), + 'renderer': { + 'alpha': useAlpha, + 'antialias': getOption('antialiasing', bool, true), + }, + 'rendererSettings': { + 'clearAlpha': useAlpha ? 0 : 1, + 'clearColor': + getOption('clearColor', string, '#fff'), + 'maxPixelRatio': + getOption('maxPixelRatio', number, devicePixelRatio || 1), + }, + 'controls': { + 'enableZoom': getOption('enableZoom', bool, true), + 'autoRotate': getOption('autoRotate', bool, false), + }, + }); + this.registerAction('setModelRotation', invocation => { + this.sendCommandWhenReady_('setModelRotation', invocation.args) + .catch(e => dev() + .error('AMP-3D-GLTF', 'setModelRotation failed: %s', e)); + }, ActionTrust.LOW); + } + + /** @override */ + layoutCallback() { + if (!isWebGLSupported()) { + this.toggleFallback(true); + return Promise.resolve(); + } + + const iframe = getIframe( + this.win, this.element, '3d-gltf', this.context_ + ); + + this.applyFillContent(iframe, true); + this.iframe_ = iframe; + this.unlistenMessage_ = this.listenGltfViewerMessages_(); + + this.element.appendChild(this.iframe_); + + return this.willBeLoaded_.promise; + } + + /** @private */ + listenGltfViewerMessages_() { + if (!this.iframe_) { + return; + } + + const listenIframe = (evName, cb) => listenFor( + dev().assertElement(this.iframe_), + evName, + cb, + true); + + const disposers = [ + listenIframe('ready', this.willBeReady_.resolve), + listenIframe('loaded', this.willBeLoaded_.resolve), + listenIframe('error', () => { + this.toggleFallback(true); + }), + ]; + return () => disposers.forEach(d => d()); + } + + /** + * Sends a command to the viewer via postMessage when iframe is ready + * + * @param {string} action + * @param {(JsonObject|boolean)=} args + * @return {!Promise} + * @private + */ + sendCommandWhenReady_(action, args) { + return this.willBeReady_.promise.then(() => { + const message = dict({ + 'action': action, + 'args': args, + }); + + this.postMessage_('action', message); + }); + } + + /** + * Wraps postMessage for testing + * + * @param {string} type + * @param {!JsonObject} message + * @private + */ + postMessage_(type, message) { + postMessage( + dev().assertElement(this.iframe_), + type, + message, + '*', + true); + } + + /** + * @param {boolean} inViewport + * @override + */ + viewportCallback(inViewport) { + return this.sendCommandWhenReady_('toggleAmpViewport', inViewport); + } + + /** @override */ + pauseCallback() { + this.sendCommandWhenReady_('toggleAmpPlay', false); + } + + /** @override */ + resumeCallback() { + this.sendCommandWhenReady_('toggleAmpPlay', true); + } + + /** + * Sends `setSize` command when ready + * + */ + onLayoutMeasure() { + const box = this.getLayoutBox(); + this.sendCommandWhenReady_( + 'setSize', + dict({'width': box.width, 'height': box.height})); + } + + /** @override */ + isLayoutSupported(layout) { + return isLayoutSizeDefined(layout); + } +} + +AMP.extension(TAG, '0.1', AMP => { + AMP.registerElement(TAG, Amp3dGltf); +}); diff --git a/extensions/amp-3d-gltf/0.1/test/test-amp-3d-gltf.js b/extensions/amp-3d-gltf/0.1/test/test-amp-3d-gltf.js new file mode 100644 index 000000000000..280a30ded811 --- /dev/null +++ b/extensions/amp-3d-gltf/0.1/test/test-amp-3d-gltf.js @@ -0,0 +1,112 @@ +/** + * 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 '../amp-3d-gltf'; +import {createIframeWithMessageStub} from '../../../../testing/iframe'; + +describes.realWin('amp-3d-gltf', { + amp: { + extensions: ['amp-3d-gltf'], + }, + allowExternalResources: true, +}, env => { + let win; + let doc; + let iframe; + let testIndex = 0; + let sendFakeMessage = () => {}; + + beforeEach(() => { + win = env.win; + doc = win.document; + testIndex++; + const sentinel = 'amp3ptest' + testIndex; + iframe = createIframeWithMessageStub(win); + iframe.setAttribute('data-amp-3p-sentinel', sentinel); + iframe.name = 'test_nomaster'; + + sendFakeMessage = type => { + return new Promise(resolve => { + iframe.postMessageToParent({sentinel, type}); + setTimeout(resolve, 100); + }); + }; + }); + + const createElement = () => { + const amp3dGltfEl = doc.createElement('amp-3d-gltf'); + amp3dGltfEl.setAttribute('src', 'https://fake.com/fake.gltf'); + amp3dGltfEl.setAttribute('layout', 'fixed'); + amp3dGltfEl.setAttribute('width', '320'); + amp3dGltfEl.setAttribute('height', '240'); + + doc.body.appendChild(amp3dGltfEl); + + return amp3dGltfEl.build() + .then(() => { + const amp3dGltf = amp3dGltfEl.implementation_; + sandbox.stub(amp3dGltf, 'iframe_') + .get(() => iframe) + .set(() => {}); + + const willLayout = amp3dGltfEl.layoutCallback(); + + return sendFakeMessage('ready') + .then(() => sendFakeMessage('loaded')) + .then(() => willLayout) + .then(() => amp3dGltf); + }); + }; + + // TODO (#16080): this test keeps timing out for some reason. + // Unskip when we figure out root cause. + it.skip('renders iframe', () => { + return createElement() + .then(() => { + expect(!!doc.body.querySelector('amp-3d-gltf > iframe')).to.be.true; + }); + }); + + // TODO (#16080): this test times out on Travis. Re-enable when fixed. + it.skip('sends toggleAmpViewport(false) when exiting viewport', () => { + return createElement() + .then(amp3dGltf => { + const postMessageSpy = sandbox.spy(amp3dGltf, 'postMessage_'); + return amp3dGltf.viewportCallback(false).then(() => { + expect(postMessageSpy.calledOnce).to.be.true; + expect(postMessageSpy.firstCall.args[0]).to.equal('action'); + expect(postMessageSpy.firstCall.args[1].action) + .to.equal('toggleAmpViewport'); + expect(postMessageSpy.firstCall.args[1].args).to.be.false; + }); + }); + }); + + // TODO (#16080): this test times out on Travis. Re-enable when fixed. + it.skip('sends toggleAmpViewport(true) when entering viewport', () => { + return createElement() + .then(amp3dGltf => { + const postMessageSpy = sandbox.spy(amp3dGltf, 'postMessage_'); + return amp3dGltf.viewportCallback(true).then(() => { + expect(postMessageSpy.calledOnce).to.be.true; + expect(postMessageSpy.firstCall.args[0]).to.equal('action'); + expect(postMessageSpy.firstCall.args[1].action) + .to.equal('toggleAmpViewport'); + expect(postMessageSpy.firstCall.args[1].args).to.be.true; + }); + }); + }); +}); diff --git a/extensions/amp-3d-gltf/0.1/test/validator-amp-3d-gltf.html b/extensions/amp-3d-gltf/0.1/test/validator-amp-3d-gltf.html new file mode 100644 index 000000000000..1f5b925c1323 --- /dev/null +++ b/extensions/amp-3d-gltf/0.1/test/validator-amp-3d-gltf.html @@ -0,0 +1,50 @@ + + + + + + + amp-3d-gltf example + + + + + + + + + + + + + + + + diff --git a/extensions/amp-3d-gltf/0.1/test/validator-amp-3d-gltf.out b/extensions/amp-3d-gltf/0.1/test/validator-amp-3d-gltf.out new file mode 100644 index 000000000000..0ec86ccc3e42 --- /dev/null +++ b/extensions/amp-3d-gltf/0.1/test/validator-amp-3d-gltf.out @@ -0,0 +1,53 @@ +FAIL +| +| +| +| +| +| +| amp-3d-gltf example +| +| +| +| +| +| +| +| +| +| +| +| +| +| +>> ^~~~~~~~~ +amp-3d-gltf/0.1/test/validator-amp-3d-gltf.html:48:2 The mandatory attribute 'src' is missing in tag 'amp-3d-gltf'. (see https://www.ampproject.org/docs/reference/components/amp-3d-gltf) [AMP_TAG_PROBLEM] +| +| diff --git a/extensions/amp-3d-gltf/OWNERS.yaml b/extensions/amp-3d-gltf/OWNERS.yaml new file mode 100644 index 000000000000..b829dfd17174 --- /dev/null +++ b/extensions/amp-3d-gltf/OWNERS.yaml @@ -0,0 +1,2 @@ +- mixtur +- alanorozco diff --git a/extensions/amp-3d-gltf/amp-3d-gltf.md b/extensions/amp-3d-gltf/amp-3d-gltf.md new file mode 100644 index 000000000000..897df5027f66 --- /dev/null +++ b/extensions/amp-3d-gltf/amp-3d-gltf.md @@ -0,0 +1,108 @@ + + +# `amp-3d-gltf` + + + + + + + + + + + + + + + + + + +
    DescriptionDisplays GL Transmission Format (gITF) 3D models.
    Required Script<script async custom-element="amp-3d-gltf" src="https://cdn.ampproject.org/v0/amp-3d-gltf-0.1.js"></script>
    Supported Layoutsfill, fixed, fixed-height, flex-item, responsive
    ExamplesSee AMP By Example's amp-3d-gltf example.
    + +## Usage + +The `amp-3d-gltf` component displays 3D models that are in gITF format. + +**Note**: A WebGL capable browser is required to display these models. + +### Example + +```html + +``` + +### Limitations + +Currently, only works with glTF 2.0. + +Unsupported features: +- embeded cameras +- animation + +### CORS + +`amp-3d-gltf` makes a `fetch` request from the origin `https://.ampproject.net` so `access-control-allow-origin: *.ampproject.net` must be set on the response header of the endpoint specified as `src`. Wildcard is needed since the origin has a random sub-domain component to it. + +## Attributes + +##### src [required] +A required attribute that specifies the URL to the gltf file. + +##### alpha [optional] + +A Boolean attribute that specifies whether free space on canvas is transparent. By default, free space is filled with black. +Default value is `false`. + +##### antialiasing [optional] + +A Boolean attribute that specifies whether to turn on antialiasing. Default value is `false`. + +##### clearColor [optional] + +A string that must contain valid CSS color, that will be used to fill free space on canvas. + +##### maxPixelRatio [optional] + +A numeric value that specifies the upper limit for the pixelRatio render option. The default is `window.devicePixelRatio`. + +##### autoRotate [optional] +A Boolean attribute that specifies whether to automatically rotate the camera around the model's center. Default value is `false`. + +##### enableZoom [optional] + +A Boolean attribute that specifies whether to turn on zoom. Default value is `true`. + +## Actions + +##### setModelRotation(x, y, z, xMin, xMax, yMin, yMax, zMin, zMax) +sets model rotation. rotation order is ZYX + +- x/y/z - number 0..1, defaults to previous value of model rotation. +- min/max - angle in radians, defaults to 0 / pi * 2, defines target range + +for example `setModelRotation(x=0.5, xMin=0, xMax=3.14)` will change `x` component of rotation to `1.57`. + +## Validation +See [amp-3d-gltf rules](https://github.com/ampproject/amphtml/blob/master/extensions/amp-3d-gltf/validator-amp-3d-gltf.protoascii) in the AMP validator specification. diff --git a/extensions/amp-3d-gltf/validator-amp-3d-gltf.protoascii b/extensions/amp-3d-gltf/validator-amp-3d-gltf.protoascii new file mode 100644 index 000000000000..5c38067e3246 --- /dev/null +++ b/extensions/amp-3d-gltf/validator-amp-3d-gltf.protoascii @@ -0,0 +1,73 @@ +# +# Copyright 2018 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. +# + +tags: { # amp-3d-gltf + html_format: AMP + tag_name: "SCRIPT" + extension_spec: { + name: "amp-3d-gltf" + version: "0.1" + version: "latest" + } + attr_lists: "common-extension-attrs" +} +tags: { # + html_format: AMP + tag_name: "AMP-3D-GLTF" + requires_extension: "amp-3d-gltf" + attrs: { + name: "alpha" + value: "false" + value: "true" + } + attrs: { + name: "antialiasing" + value: "false" + value: "true" + } + attrs: { + name: "autorotate" + value: "false" + value: "true" + } + attrs: { + name: "clearcolor" + } + attrs: { + name: "enablezoom" + value: "false" + value: "true" + } + attrs: { + name: "maxpixelratio" + value_regex: "[+-]?(\\d*\\.)?\\d+" + } + attrs: { + name: "src" + mandatory: true + value_url: { + protocol: "https" + } + } + attr_lists: "extended-amp-global" + amp_layout: { + supported_layouts: FILL + supported_layouts: FIXED + supported_layouts: FIXED_HEIGHT + supported_layouts: FLEX_ITEM + supported_layouts: RESPONSIVE + } +} diff --git a/extensions/amp-3q-player/0.1/amp-3q-player.js b/extensions/amp-3q-player/0.1/amp-3q-player.js new file mode 100644 index 000000000000..b6707543d08d --- /dev/null +++ b/extensions/amp-3q-player/0.1/amp-3q-player.js @@ -0,0 +1,292 @@ +/** + * 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 {Deferred} from '../../../src/utils/promise'; +import {Services} from '../../../src/services'; +import {VideoEvents} from '../../../src/video-interface'; +import { + createFrameFor, + objOrParseJson, + redispatch, +} from '../../../src/iframe-video'; +import {dev, user} from '../../../src/log'; +import { + fullscreenEnter, + fullscreenExit, + isFullscreenElement, + removeElement, +} from '../../../src/dom'; +import {getData, listen} from '../../../src/event-helper'; +import { + installVideoManagerForDoc, +} from '../../../src/service/video-manager-impl'; +import {isLayoutSizeDefined} from '../../../src/layout'; + + +const TAG = 'amp-3q-player'; + + +/** @implements {../../../src/video-interface.VideoInterface} */ +class Amp3QPlayer extends AMP.BaseElement { + + /** @param {!AmpElement} element */ + constructor(element) { + super(element); + + /** @private {?Element} */ + this.iframe_ = null; + + /** @private {?Function} */ + this.unlistenMessage_ = null; + + /** @private {?Promise} */ + this.playerReadyPromise_ = null; + + /** @private {?Function} */ + this.playerReadyResolver_ = null; + + this.dataId = null; + } + + /** + * @param {boolean=} opt_onLayout + * @override + */ + preconnectCallback(opt_onLayout) { + this.preconnect.url('https://playout.3qsdn.com', opt_onLayout); + } + + /** @override */ + buildCallback() { + const {element: el} = this; + + this.dataId = user().assert( + el.getAttribute('data-id'), + 'The data-id attribute is required for %s', + el); + + const deferred = new Deferred(); + this.playerReadyPromise_ = deferred.promise; + this.playerReadyResolver_ = deferred.resolve; + + installVideoManagerForDoc(el); + Services.videoManagerForDoc(el).register(this); + } + + /** @override */ + layoutCallback() { + const iframe = createFrameFor(this, + 'https://playout.3qsdn.com/' + + encodeURIComponent(dev().assertString(this.dataId)) + // Autoplay is handled by VideoManager + + '?autoplay=false&=true'); + + this.iframe_ = iframe; + + this.unlistenMessage_ = listen( + this.win, + 'message', + this.sdnBridge_.bind(this)); + + return this.loadPromise(this.iframe_).then(() => this.playerReadyPromise_); + } + + /** @override */ + unlayoutCallback() { + if (this.iframe_) { + removeElement(this.iframe_); + this.iframe_ = null; + } + if (this.unlistenMessage_) { + this.unlistenMessage_(); + } + + const deferred = new Deferred(); + this.playerReadyPromise_ = deferred.promise; + this.playerReadyResolver_ = deferred.resolve; + + return true; + } + + /** @override */ + isLayoutSupported(layout) { + return isLayoutSizeDefined(layout); + } + + /** @override */ + viewportCallback(visible) { + this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible}); + } + + /** @override */ + pauseCallback() { + if (this.iframe_) { + this.pause(); + } + } + + /** + * + * @param {!Event} event + * @private + */ + sdnBridge_(event) { + if (event.source) { + if (event.source != this.iframe_.contentWindow) { + return; + } + } + + const data = objOrParseJson(getData(event)); + if (data === undefined) { + return; + } + + const eventType = data['data']; + + if (eventType == 'ready') { + this.playerReadyResolver_(); + } + + redispatch(this.element, eventType, { + 'ready': VideoEvents.LOAD, + 'playing': VideoEvents.PLAYING, + 'paused': VideoEvents.PAUSE, + 'muted': VideoEvents.MUTED, + 'unmuted': VideoEvents.UNMUTED, + }); + } + + /** + * + * @private + * @param {string} message + */ + sdnPostMessage_(message) { + this.playerReadyPromise_.then(() => { + if (this.iframe_ && this.iframe_.contentWindow) { + this.iframe_.contentWindow./*OK*/postMessage(message, '*'); + } + }); + } + + // VideoInterface Implementation. See ../src/video-interface.VideoInterface + /** @override */ + play() { + this.sdnPostMessage_('play2'); + } + + /** @override */ + pause() { + this.sdnPostMessage_('pause'); + } + + /** @override */ + mute() { + this.sdnPostMessage_('mute'); + } + + /** @override */ + unmute() { + this.sdnPostMessage_('unmute'); + } + + /** @override */ + supportsPlatform() { + return true; + } + + /** @override */ + isInteractive() { + return true; + } + + /** @override */ + showControls() { + this.sdnPostMessage_('showControlbar'); + } + + /** @override */ + hideControls() { + this.sdnPostMessage_('hideControlbar'); + } + + /** + * @override + */ + fullscreenEnter() { + if (!this.iframe_) { + return; + } + fullscreenEnter(dev().assertElement(this.iframe_)); + } + + /** + * @override + */ + fullscreenExit() { + if (!this.iframe_) { + return; + } + fullscreenExit(dev().assertElement(this.iframe_)); + } + + /** @override */ + isFullscreen() { + if (!this.iframe_) { + return false; + } + return isFullscreenElement(dev().assertElement(this.iframe_)); + } + + /** @override */ + getMetadata() { + // Not implemented + } + + /** @override */ + preimplementsMediaSessionAPI() { + return false; + } + + /** @override */ + preimplementsAutoFullscreen() { + return false; + } + + /** @override */ + getCurrentTime() { + // Not supported. + return 0; + } + + /** @override */ + getDuration() { + // Not supported. + return 1; + } + + /** @override */ + getPlayedRanges() { + // Not supported. + return []; + } +} + + +AMP.extension(TAG, '0.1', AMP => { + AMP.registerElement(TAG, Amp3QPlayer); +}); diff --git a/extensions/amp-3q-player/0.1/test/test-amp-3q-player.js b/extensions/amp-3q-player/0.1/test/test-amp-3q-player.js new file mode 100644 index 000000000000..19b4dce85f6a --- /dev/null +++ b/extensions/amp-3q-player/0.1/test/test-amp-3q-player.js @@ -0,0 +1,103 @@ +/** + * 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 '../amp-3q-player'; +import {Services} from '../../../../src/services'; +import {VideoEvents} from '../../../../src/video-interface'; +import {listenOncePromise} from '../../../../src/event-helper'; + + +describes.realWin('amp-3q-player', { + amp: { + extensions: ['amp-3q-player'], + }, +}, function(env) { + let win; + let doc; + let timer; + + beforeEach(() => { + win = env.win; + doc = win.document; + timer = Services.timerFor(win); + }); + + function get3QElement(playoutId) { + const player = doc.createElement('amp-3q-player'); + if (playoutId) { + player.setAttribute('data-id', playoutId); + } + doc.body.appendChild(player); + return player.build().then(() => { + player.layoutCallback(); + const iframe = player.querySelector('iframe'); + player.implementation_.sdnBridge_({ + source: iframe.contentWindow, + data: JSON.stringify({data: 'ready'}), + }); + }).then(() => { + return player; + }); + } + + it('renders', () => { + return get3QElement( + 'c8dbe7f4-7f7f-11e6-a407-0cc47a188158').then(player => { + const iframe = player.querySelector('iframe'); + expect(iframe).to.not.be.null; + expect(iframe.src).to.equal('https://playout.3qsdn.com/c8dbe7f4-7f7f-11e6-a407-0cc47a188158?autoplay=false&=true'); + }); + }); + + it('requires data-id', () => { + return allowConsoleError(() => { + return get3QElement('').should.eventually.be.rejectedWith( + /The data-id attribute is required/); + }); + }); + + it('should forward events from amp-3q-player to the amp element', () => { + return get3QElement( + 'c8dbe7f4-7f7f-11e6-a407-0cc47a188158').then(player => { + + const iframe = player.querySelector('iframe'); + + return Promise.resolve().then(() => { + const p = listenOncePromise(player, VideoEvents.MUTED); + sendFakeMessage(player, iframe, 'muted'); + return p; + }).then(() => { + const p = listenOncePromise(player, VideoEvents.PLAYING); + sendFakeMessage(player, iframe, 'playing'); + return p; + }).then(() => { + const p = listenOncePromise(player, VideoEvents.PAUSE); + sendFakeMessage(player, iframe, 'paused'); + return p; + }).then(() => { + const p = listenOncePromise(player, VideoEvents.UNMUTED); + sendFakeMessage(player, iframe, 'unmuted'); + const successTimeout = timer.promise(10); + return Promise.race([p, successTimeout]); + }); + }); + }); + + function sendFakeMessage(player, iframe, command) { + player.implementation_.sdnBridge_( + {source: iframe.contentWindow, data: JSON.stringify({data: command})}); + } +}); diff --git a/extensions/amp-3q-player/0.1/test/validator-amp-3q-player.html b/extensions/amp-3q-player/0.1/test/validator-amp-3q-player.html new file mode 100644 index 000000000000..7f46cc9a9c26 --- /dev/null +++ b/extensions/amp-3q-player/0.1/test/validator-amp-3q-player.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extensions/amp-3q-player/0.1/test/validator-amp-3q-player.out b/extensions/amp-3q-player/0.1/test/validator-amp-3q-player.out new file mode 100644 index 000000000000..76a0d81156f5 --- /dev/null +++ b/extensions/amp-3q-player/0.1/test/validator-amp-3q-player.out @@ -0,0 +1,50 @@ +FAIL +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| > ^~~~~~~~~ +amp-3q-player/0.1/test/validator-amp-3q-player.html:41:4 The mandatory attribute 'data-id' is missing in tag 'amp-3q-player'. (see https://www.ampproject.org/docs/reference/components/amp-3q-player) [AMP_TAG_PROBLEM] +| height=360 +| width=640 +| layout="responsive"> +| +| +| diff --git a/extensions/amp-3q-player/OWNERS.yaml b/extensions/amp-3q-player/OWNERS.yaml new file mode 100644 index 000000000000..0084fab49de3 --- /dev/null +++ b/extensions/amp-3q-player/OWNERS.yaml @@ -0,0 +1,2 @@ +- cvializ +- alanorozco diff --git a/extensions/amp-3q-player/amp-3q-player.md b/extensions/amp-3q-player/amp-3q-player.md new file mode 100644 index 000000000000..6f27cd72ac82 --- /dev/null +++ b/extensions/amp-3q-player/amp-3q-player.md @@ -0,0 +1,69 @@ + + +# `amp-3q-player` + + + + + + + + + + + + + + +
    DescriptionEmbeds videos from 3Q SDN.
    Required Script<script async custom-element="amp-3q-player" src="https://cdn.ampproject.org/v0/amp-3q-player-0.1.js"></script>
    Supported Layoutsfill, fixed, flex-item, responsive
    + +[TOC] + +## Example + +With the `responsive` layout, the width and height in this should yield correct layouts for 16:9 aspect ratio videos: + +```html + +``` + +## Attributes + +##### data-id (required) + +The sdnPlayoutId from 3Q SDN. + +##### autoplay (optional) + +If this attribute is present, and the browser supports autoplay: + +* the video is automatically muted before autoplay starts +* when the video is scrolled out of view, the video is paused +* when the video is scrolled into view, the video resumes playback +* when the user taps the video, the video is unmuted +* if the user has interacted with the video (e.g., mutes/unmutes, pauses/resumes, etc.), and the video is scrolled in or out of view, the state of the video remains as how the user left it. For example, if the user pauses the video, then scrolls the video out of view and returns to the video, the video is still paused. + +##### common attributes + +This element includes [common attributes](https://www.ampproject.org/docs/reference/common_attributes) extended to AMP components. + +## Validation + +See [amp-3q-player rules](https://github.com/ampproject/amphtml/blob/master/extensions/amp-3q-player/validator-amp-3q-player.protoascii) in the AMP validator specification. diff --git a/extensions/amp-3q-player/validator-amp-3q-player.protoascii b/extensions/amp-3q-player/validator-amp-3q-player.protoascii new file mode 100644 index 000000000000..9db22d96a367 --- /dev/null +++ b/extensions/amp-3q-player/validator-amp-3q-player.protoascii @@ -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. +# + +tags: { # amp-3q-player + html_format: AMP + tag_name: "SCRIPT" + extension_spec: { + name: "amp-3q-player" + version: "0.1" + version: "latest" + } + attr_lists: "common-extension-attrs" +} +tags: { # + html_format: AMP + tag_name: "AMP-3Q-PLAYER" + requires_extension: "amp-3q-player" + attrs: { + name: "autoplay" + value: "" + } + attrs: { + name: "data-id" + mandatory: true + } + attr_lists: "extended-amp-global" + amp_layout: { + supported_layouts: FILL + supported_layouts: FIXED + supported_layouts: FLEX_ITEM + supported_layouts: RESPONSIVE + } +} 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..b1ee0e0b6dc9 --- /dev/null +++ b/extensions/amp-a4a/0.1/a4a-variable-source.js @@ -0,0 +1,199 @@ +/** + * 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 {Services} from '../../../src/services'; +import { + VariableSource, + getNavigationData, + getTimingDataAsync, + getTimingDataSync, +} from '../../../src/service/variable-source'; +import {user} from '../../../src/log'; + + +const WHITELISTED_VARIABLES = [ + 'AMPDOC_HOST', + 'AMPDOC_HOSTNAME', + 'AMPDOC_URL', + 'AMP_VERSION', + 'AVAILABLE_SCREEN_HEIGHT', + 'AVAILABLE_SCREEN_WIDTH', + 'BACKGROUND_STATE', + 'BROWSER_LANGUAGE', + 'CANONICAL_HOST', + 'CANONICAL_HOSTNAME', + 'CANONICAL_PATH', + 'CANONICAL_URL', + 'COUNTER', + 'DOCUMENT_CHARSET', + 'DOCUMENT_REFERRER', + 'FIRST_CONTENTFUL_PAINT', + 'FIRST_VIEWPORT_READY', + 'MAKE_BODY_VISIBLE', + 'PAGE_VIEW_ID', + 'RANDOM', + 'SCREEN_COLOR_DEPTH', + 'SCREEN_HEIGHT', + 'SCREEN_WIDTH', + 'SCROLL_HEIGHT', + 'SCROLL_LEFT', + 'SCROLL_TOP', + 'SCROLL_WIDTH', + 'SHARE_TRACKING_INCOMING', + 'SHARE_TRACKING_OUTGOING', + 'SOURCE_HOST', + 'SOURCE_HOSTNAME', + 'SOURCE_PATH', + 'SOURCE_URL', + 'TIMESTAMP', + 'TIMEZONE', + 'TIMEZONE_CODE', + 'TITLE', + 'TOTAL_ENGAGED_TIME', + 'USER_AGENT', + 'VARIANT', + 'VARIANTS', + 'VIEWER', + 'VIEWPORT_HEIGHT', + 'VIEWPORT_WIDTH', +]; + +/** Provides A4A specific variable substitution. */ +export class A4AVariableSource extends VariableSource { + /** + * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc + * @param {!Window} embedWin + */ + constructor(ampdoc, embedWin) { + super(ampdoc); + /** @private {VariableSource} global variable source for fallback. */ + this.globalVariableSource_ = Services.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'); + }); + + this.set('HTML_ATTR', + /** @type {function(...*)} */(this.htmlAttributeBinding_.bind(this))); + + this.set('CLIENT_ID', () => null); + + 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); + } + } + + /** + * Provides a binding for getting attributes from the DOM. + * Most such bindings are provided in src/service/url-replacements-impl, but + * this one needs access to this.win_.document, which if the amp-analytics + * tag is contained within an amp-ad tag will NOT be the parent/publisher + * page. Hence the need to put it here. + * @param {string} cssSelector Elements matching this selector will be + * included, provided they have at least one of the attributeNames + * set, up to a max of 10. May be URI encoded. + * @param {...string} var_args Additional params will be the names of + * attributes whose values will be returned. There should be at least 1. + * @return {string} A stringified JSON array containing one member for each + * matching element. Each member will contain the names and values of the + * specified attributes, if the corresponding element has that attribute. + * Note that if an element matches the cssSelected but has none of the + * requested attributes, then nothing will be included in the array + * for that element. + */ + htmlAttributeBinding_(cssSelector, var_args) { + // Generate an error if cssSelector matches more than this many elements + const HTML_ATTR_MAX_ELEMENTS_TO_TRAVERSE = 20; + + // Of the elements matched by cssSelector, see which contain one or more + // of the specified attributes, and return an array of at most this many. + const HTML_ATTR_MAX_ELEMENTS_TO_RETURN = 10; + + // Only allow at most this many attributeNames to be specified. + const HTML_ATTR_MAX_ATTRS = 10; + + const TAG = 'A4AVariableSource'; + + const attributeNames = Array.prototype.slice.call(arguments, 1); + if (!cssSelector || !attributeNames.length) { + return '[]'; + } + if (attributeNames.length > HTML_ATTR_MAX_ATTRS) { + user().error(TAG, `At most ${HTML_ATTR_MAX_ATTRS} may be requested.`); + return '[]'; + } + cssSelector = decodeURI(cssSelector); + let elements; + try { + elements = this.win_.document.querySelectorAll(cssSelector); + } catch (e) { + user().error(TAG, `Invalid selector: ${cssSelector}`); + return '[]'; + } + if (elements.length > HTML_ATTR_MAX_ELEMENTS_TO_TRAVERSE) { + user().error(TAG, 'CSS selector may match at most ' + + `${HTML_ATTR_MAX_ELEMENTS_TO_TRAVERSE} elements.`); + return '[]'; + } + const result = []; + for (let i = 0; i < elements.length && + result.length < HTML_ATTR_MAX_ELEMENTS_TO_RETURN; ++i) { + const currentResult = {}; + let foundAtLeastOneAttr = false; + for (let j = 0; j < attributeNames.length; ++j) { + const attributeName = attributeNames[j]; + if (elements[i].hasAttribute(attributeName)) { + currentResult[attributeName] = + elements[i].getAttribute(attributeName); + foundAtLeastOneAttr = true; + } + } + if (foundAtLeastOneAttr) { + result.push(currentResult); + } + } + return JSON.stringify(result); + } +} 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..00130d915fa4 --- /dev/null +++ b/extensions/amp-a4a/0.1/amp-a4a.js @@ -0,0 +1,1808 @@ +/** + * 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 {A4AVariableSource} from './a4a-variable-source'; +import { + CONSENT_POLICY_STATE, // eslint-disable-line no-unused-vars +} from '../../../src/consent-state'; +import {Layout, LayoutPriority, isLayoutSizeDefined} from '../../../src/layout'; +import {Services} from '../../../src/services'; +import {SignatureVerifier, VerificationStatus} from './signature-verifier'; +import { + applySandbox, + generateSentinel, + getDefaultBootstrapBaseUrl, +} from '../../../src/3p-frame'; +import { + assertHttpsUrl, + tryDecodeUriComponent, +} from '../../../src/url'; +import {cancellation, isCancellation} from '../../../src/error'; +import {createElementWithAttributes} from '../../../src/dom'; +import {dev, duplicateErrorIfNecessary, user} from '../../../src/log'; +import {dict} from '../../../src/utils/object'; +import { + getAmpAdRenderOutsideViewport, + incrementLoadingAds, + is3pThrottled, +} from '../../amp-ad/0.1/concurrent-load'; +import {getBinaryType, isExperimentOn} from '../../../src/experiments'; +import {getBinaryTypeNumericalCode} from '../../../ads/google/a4a/utils'; +import {getConsentPolicyState} from '../../../src/consent'; +import {getContextMetadata} from '../../../src/iframe-attributes'; +import {getMode} from '../../../src/mode'; +import {insertAnalyticsElement} from '../../../src/extension-analytics'; +import { + installFriendlyIframeEmbed, + setFriendlyIframeEmbedVisible, +} from '../../../src/friendly-iframe-embed'; +import { + installUrlReplacementsForEmbed, +} from '../../../src/service/url-replacements-impl'; +import {isAdPositionAllowed} from '../../../src/ad-helper'; +import {isArray, isEnumValue, isObject} from '../../../src/types'; +import {parseJson} from '../../../src/json'; +import {setStyle} from '../../../src/style'; +import {signingServerURLs} from '../../../ads/_a4a-config'; +import {triggerAnalyticsEvent} from '../../../src/analytics'; +import {tryResolve} from '../../../src/utils/promise'; +import {utf8Decode} from '../../../src/utils/bytes'; + +/** @type {Array} */ +const METADATA_STRINGS = [ + ''); + 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 = parseJson( + 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); + } + + const urls = Services.urlForDoc(this.getAmpDoc()); + metaData.customStylesheets.forEach(stylesheet => { + if (!isObject(stylesheet) || !stylesheet['href'] || + typeof stylesheet['href'] !== 'string' || + !urls.isSecure(stylesheet['href'])) { + throw new Error(errorMsg); + } + }); + } + if (isArray(metaDataObj['images'])) { + // Load maximum of 5 images. + metaData.images = metaDataObj['images'].splice(0, 5); + } + if (this.isSinglePageStoryAd) { + if (!metaDataObj['ctaUrl'] || !metaDataObj['ctaType']) { + throw new Error(INVALID_SPSA_RESPONSE); + } + this.element.setAttribute('data-vars-ctatype', metaDataObj['ctaType']); + this.element.setAttribute('data-vars-ctaurl', metaDataObj['ctaUrl']); + } + // 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 + metadataString.length, metadataEnd)); + if (this.isSinglePageStoryAd) { + throw err; + } + return null; + } + } + + /** + * @return {string} full url to safeframe implementation. + */ + getSafeframePath() { + return 'https://tpc.googlesyndication.com/safeframe/' + + `${this.safeframeVersion}/html/container.html`; + } + + /** + * Checks if the given lifecycle event has a corresponding amp-analytics event + * and fires the analytics trigger if so. + * @param {string} lifecycleStage + * @private + */ + maybeTriggerAnalyticsEvent_(lifecycleStage) { + if (!this.a4aAnalyticsConfig_) { + // No config exists that will listen to this event. + return; + } + const analyticsEvent = + dev().assert(LIFECYCLE_STAGE_TO_ANALYTICS_TRIGGER[lifecycleStage]); + const analyticsVars = /** @type {!JsonObject} */ (Object.assign( + dict({'time': Math.round(this.getNow_())}), + this.getA4aAnalyticsVars(analyticsEvent))); + triggerAnalyticsEvent(this.element, analyticsEvent, analyticsVars); + } + + /** + * Returns variables to be included on an analytics event. This can be + * overridden by specific network implementations. + * Note that this function is called for each time an analytics event is + * fired. + * @param {string} unusedAnalyticsEvent The name of the analytics event. + * @return {!JsonObject} + */ + getA4aAnalyticsVars(unusedAnalyticsEvent) { + return dict({}); + } + + /** + * Returns network-specific config for amp-analytics. It should overridden + * with network-specific configurations. + * This function may return null. If so, no amp-analytics element will be + * added to this A4A element and no A4A triggers will be fired. + * @return {?JsonObject} + */ + getA4aAnalyticsConfig() { return null; } + + /** + * Attempts to execute Real Time Config, if the ad network has enabled it. + * If it is not supported by the network, but the publisher has included + * the rtc-config attribute on the amp-ad element, warn. + * @param {?CONSENT_POLICY_STATE} consentState + * @return {Promise>|undefined} + */ + tryExecuteRealTimeConfig_(consentState) { + if (!!AMP.RealTimeConfigManager) { + try { + return new AMP.RealTimeConfigManager(this) + .maybeExecuteRealTimeConfig( + this.getCustomRealTimeConfigMacros_(), consentState); + } catch (err) { + user().error(TAG, 'Could not perform Real Time Config.', err); + } + } else if (this.element.getAttribute('rtc-config')) { + user().error(TAG, 'RTC not supported for ad network ' + + `${this.element.getAttribute('type')}`); + + } + } + + /** + * To be overriden by network impl. Should return a mapping of macro keys + * to values for substitution in publisher-specified URLs for RTC. + * @return {!Object} + */ + getCustomRealTimeConfigMacros_() { + return {}; + } + + /** + * Whether preferential render should still be utilized if web crypto is + * unavailable, and crypto signature header is present. + * @return {boolean} + */ + shouldPreferentialRenderWithoutCrypto() { + return false; + } + + /** + * @param {string=} headerValue Method as given in header. + */ + getNonAmpCreativeRenderingMethod(headerValue) { + if (headerValue) { + if (!isEnumValue(XORIGIN_MODE, headerValue)) { + dev().error( + 'AMP-A4A', `cross-origin render mode header ${headerValue}`); + } else { + return headerValue; + } + } + return Services.platformFor(this.win).isIos() ? + XORIGIN_MODE.NAMEFRAME : null; + } + + /** + * Returns base object that will be written to cross-domain iframe name + * attribute. + * @param {boolean=} opt_isSafeframe Whether creative is rendering into + * a safeframe. + * @return {!JsonObject|undefined} + */ + getAdditionalContextMetadata(opt_isSafeframe) {} + + /** + * Returns whether the received creative is verified AMP. + * @return {boolean} True if the creative is verified AMP, false otherwise. + */ + isVerifiedAmpCreative() { + return this.isVerifiedAmpCreative_; + } +} + +/** + * Attachs query string portion of ad url to error. + * @param {!Error} error + * @param {?string} adUrl + */ +export function assignAdUrlToError(error, adUrl) { + if (!adUrl || (error.args && error.args['au'])) { + return; + } + const adQueryIdx = adUrl.indexOf('?'); + if (adQueryIdx == -1) { + return; + } + (error.args || (error.args = {}))['au'] = + adUrl.substring(adQueryIdx + 1, adQueryIdx + 251); +} + +/** + * Returns the signature verifier for the given window. Lazily creates it if it + * doesn't already exist. + * + * This ensures that only one signature verifier exists per window, which allows + * multiple Fast Fetch ad slots on a page (even ones from different ad networks) + * to share the same cached public keys. + * + * @param {!Window} win + * @return {!SignatureVerifier} + * @visibleForTesting + */ +export function signatureVerifierFor(win) { + const propertyName = 'AMP_FAST_FETCH_SIGNATURE_VERIFIER_'; + return win[propertyName] || + (win[propertyName] = new SignatureVerifier(win, signingServerURLs)); +} diff --git a/extensions/amp-a4a/0.1/amp-ad-network-base.js b/extensions/amp-a4a/0.1/amp-ad-network-base.js new file mode 100644 index 000000000000..1353bb0cae8a --- /dev/null +++ b/extensions/amp-a4a/0.1/amp-ad-network-base.js @@ -0,0 +1,224 @@ +/** + * Copyright 2018 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 { + FailureType, + RecoveryModeType, +} from './amp-ad-type-defs'; +import {Services} from '../../../src/services'; +import {dev} from '../../../src/log'; +import {isLayoutSizeDefined} from '../../../src/layout'; +import {map} from '../../../src/utils/object'; +import {sendXhrRequest} from './amp-ad-utils'; + +const TAG = 'amp-ad-network-base'; + +/** + * @abstract + */ +export class AmpAdNetworkBase extends AMP.BaseElement { + + /** + * Creates an instance of AmpAdNetworkBase. + * @param {!AmpElement} element + */ + constructor(element) { + super(element); + + /** @private {?Promise} */ + this.adResponsePromise_ = null; + + /** @private {Object} */ + this.validators_ = map(); + + /** @private {Object} */ + this.renderers_ = map(); + + /** @private {Object} */ + this.recoveryModes_ = map(); + + /** @const @private {!Object} */ + this.context_ = {}; + + // Register default error modes. + for (const failureType in FailureType) { + this.recoveryModes_[failureType] = RecoveryModeType.COLLAPSE; + } + + /** + * Number of times to retry a failed request. Zero by default. + * @type {number} + */ + this.retryLimit_ = 0; + } + + /** @override */ + isLayoutSupported(layout) { + return isLayoutSizeDefined(layout); + } + + /** @override */ + onLayoutMeasure() { + this.sendRequest_(); + } + + /** @override */ + layoutCallback() { + dev().assert(this.adResponsePromise_, + 'layoutCallback invoked before XHR request!'); + return this.adResponsePromise_ + .then(response => this.invokeValidator_(response)) + .then(validatorResult => this.invokeRenderer_(validatorResult)) + .catch(error => this.handleFailure_(error.type, error.msg)); + } + + /** + * @param {!FailureType} failure + * @param {!RecoveryModeType} recovery + * @final + */ + onFailure(failure, recovery) { + if (this.recoveryModes_[failure]) { + dev().warn(TAG, + `Recovery mode for failure type ${failure} already registered!`); + } + this.recoveryModes_[failure] = recovery; + } + + /** + * @param {!./amp-ad-type-defs.Validator} validator + * @param {string=} type + * @final + */ + registerValidator(validator, type = 'default') { + if (this.validators_[type]) { + dev().warn(TAG, `${type} validator already registered.`); + } + this.validators_[type] = validator; + } + + /** + * @param {!./amp-ad-type-defs.Renderer} renderer + * @param {string} type + * @final + */ + registerRenderer(renderer, type) { + if (this.renderers_[type]) { + dev().warn(TAG, `Rendering mode already registered for type '${type}'`); + } + this.renderers_[type] = renderer; + } + + /** + * @return {!Object} The context object passed to validators and renderers. + */ + getContext() { + return this.context_; + } + + /** + * @return {string} The finalized ad request URL. + * @protected + * @abstract + */ + getRequestUrl() { + // Subclass must override. + } + + /** @param {number} retries */ + setRequestRetries(retries) { + this.retryLimit_ = retries; + } + + /** + * Sends ad request. + * @private + */ + sendRequest_() { + Services.viewerForDoc(this.getAmpDoc()).whenFirstVisible().then(() => { + const url = this.getRequestUrl(); + this.adResponsePromise_ = sendXhrRequest(this.win, url); + }); + } + + /** + * Processes the ad response as soon as the XHR request returns. + * @param {?Response} response + * @return {!Promise} + * @private + */ + invokeValidator_(response) { + if (!response.arrayBuffer) { + return Promise.reject(this.handleFailure_(FailureType.INVALID_RESPONSE)); + } + return response.arrayBuffer().then(unvalidatedBytes => { + const validatorType = response.headers.get('AMP-Ad-Response-Type') + || 'default'; + dev().assert(this.validators_[validatorType], + 'Validator never registered!'); + return this.validators_[validatorType].validate( + this.context_, unvalidatedBytes, response.headers) + .catch(err => + Promise.reject({type: FailureType.VALIDATOR_ERROR, msg: err})); + }); + } + + /** + * @param {!./amp-ad-type-defs.ValidatorOutput} validatorOutput + * @return {!Promise} + * @private + */ + invokeRenderer_(validatorOutput) { + const renderer = this.renderers_[validatorOutput.type]; + dev().assert(renderer, 'Renderer for AMP creatives never registered!'); + return renderer.render( + this.context_, this.element, validatorOutput.creativeData) + .catch(err => + Promise.reject({type: FailureType.RENDERER_ERROR, msg: err})); + } + + /** + * @param {FailureType} failureType + * @param {*=} error + * @private + */ + handleFailure_(failureType, error) { + const recoveryMode = this.recoveryModes_[failureType]; + if (error) { + dev().warn(TAG, error); + } + switch (recoveryMode) { + case RecoveryModeType.COLLAPSE: + this.forceCollapse_(); + break; + case RecoveryModeType.RETRY: + if (this.retryLimit_--) { + this.sendRequest_(); + } + break; + default: + dev().error(TAG, 'Invalid recovery mode!'); + } + } + + /** + * Collapses slot by setting its size to 0x0. + * @private + */ + forceCollapse_() { + this.attemptChangeSize(0, 0); + } +} diff --git a/extensions/amp-a4a/0.1/amp-ad-template-helper.js b/extensions/amp-a4a/0.1/amp-ad-template-helper.js new file mode 100644 index 000000000000..40214e54d99b --- /dev/null +++ b/extensions/amp-a4a/0.1/amp-ad-template-helper.js @@ -0,0 +1,123 @@ +/** + * Copyright 2018 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 {LruCache} from '../../../src/utils/lru-cache'; +import {Services} from '../../../src/services'; +import {createElementWithAttributes} from '../../../src/dom'; +import {dev} from '../../../src/log'; +import {dict} from '../../../src/utils/object'; +import {getMode} from '../../../src/mode'; +import {isArray} from '../../../src/types'; +import {parseUrlDeprecated} from '../../../src/url'; +import {urls} from '../../../src/config'; + +/** @private {!Object} */ +const TEMPLATE_CORS_CONFIG = { + mode: 'cors', + method: 'GET', + // This should be cached across publisher domains, so don't append + // __amp_source_origin to the URL. + ampCors: false, + credentials: 'omit', +}; + +export class AmpAdTemplateHelper { + + /** + * @param {!Window} win + */ + constructor(win) { + /** @private {!Window} */ + this.win_ = win; + + /** @private {LruCache} */ + this.cache_ = new LruCache(5); + } + + /** + * Fetch and parse template from AMP cache. Result is stored in global in + * order to reduce overhead when template is used multiple times. + * @param {string} templateUrl Canonical URL to template. + * @return {!Promise} + */ + fetch(templateUrl) { + const proxyUrl = getMode(this.win_).localDev && !isNaN(templateUrl) + ? `http://ads.localhost:${this.win_.location.port}` + + `/a4a_template/adzerk/${templateUrl}` + : this.getTemplateProxyUrl_(templateUrl); + let templatePromise = this.cache_.get(proxyUrl); + if (!templatePromise) { + templatePromise = Services.xhrFor(this.win_) + .fetchText(proxyUrl, TEMPLATE_CORS_CONFIG) + .then(response => response.text()); + this.cache_.put(proxyUrl, templatePromise); + } + dev().assert(templatePromise); + return /** @type{!Promise} */ (templatePromise); + } + + /** + * @param {!JsonObject} templateValues The values to macro in. + * @param {!Element} element Parent element containing template. + * @return {!Promise} Promise which resolves after rendering completes. + */ + render(templateValues, element) { + return Services.templatesFor(this.win_) + .findAndRenderTemplate(element, templateValues); + } + + /** + * @param {!Element} element + * @param {!Array|!JsonObject} analyticsValue + */ + insertAnalytics(element, analyticsValue) { + analyticsValue = /**@type {!Array}*/ + (isArray(analyticsValue) ? analyticsValue : [analyticsValue]); + for (let i = 0; i < analyticsValue.length; i++) { + const config = analyticsValue[i]; + const analyticsEle = element.ownerDocument.createElement('amp-analytics'); + if (config['remote']) { + analyticsEle.setAttribute('config', config['remote']); + } + if (config['type']) { + analyticsEle.setAttribute('type', config['type']); + } + if (config['inline']) { + const scriptElem = createElementWithAttributes( + element.ownerDocument, + 'script', dict({ + 'type': 'application/json', + })); + scriptElem.textContent = JSON.stringify(config['inline']); + analyticsEle.appendChild(scriptElem); + } + element.appendChild(analyticsEle); + } + } + + /** + * Converts the canonical template URL to the CDN proxy URL. + * @param {string} url + * @return {string} + */ + getTemplateProxyUrl_(url) { + const cdnUrlSuffix = urls.cdn.slice(8); + const loc = parseUrlDeprecated(url); + return loc.origin.indexOf(cdnUrlSuffix) > 0 ? url : + 'https://' + loc.hostname.replace(/-/g, '--').replace(/\./g, '-') + + '.' + cdnUrlSuffix + '/ad/s/' + loc.hostname + loc.pathname; + } +} diff --git a/extensions/amp-a4a/0.1/amp-ad-type-defs.js b/extensions/amp-a4a/0.1/amp-ad-type-defs.js new file mode 100644 index 000000000000..8d64b9c182ff --- /dev/null +++ b/extensions/amp-a4a/0.1/amp-ad-type-defs.js @@ -0,0 +1,109 @@ +/** + * Copyright 2018 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 {{ + width: string, + height: string, + layout: string, + }} */ +export let LayoutInfoDef; + +/** @enum {string} */ +export const FailureType = { + REQUEST_ERROR: 'REQUEST_ERROR', + INVALID_RESPONSE: 'INVALID_RESPONSE', + EMPTY_RESPONSE: 'EMPTY_RESPONSE', + VALIDATOR_ERROR: 'VALIDATOR_ERROR', + RENDERER_ERROR: 'RENDERER_ERROR', +}; + +/** @enum {string} */ +export const RecoveryModeType = { + COLLAPSE: 'COLLAPSE', + RETRY: 'RETRY', +}; + +/** @enum {string} */ +export const ValidatorResult = { + AMP: 'AMP', + NON_AMP: 'NON_AMP', +}; + +/** @enum {string} */ +export const AdResponseType = { + CRYPTO: 'crypto', + TEMPLATE: 'template', +}; + +/** @typedef {{ + type: !ValidatorResult, + adResponseType: AdResponseType, + creativeData: !Object, + }} */ +export let ValidatorOutput; + +/** @typedef {{ + minifiedCreative: string, + customElementExtensions: !Array, + customStylesheets: !Array<{href: string}>, + images: (Array|undefined), + }} */ +export let CreativeMetaDataDef; + +/** @typedef {{ + templateUrl: string, + data: (JsonObject|undefined), + analytics: (JsonObject|undefined), + }} */ +export let AmpTemplateCreativeDef; + +/** @typedef {{ + creative: (string|undefined), + rawCreativeBytes: (!ArrayBuffer|undefined), + sentinel: (string|undefined), + additionalContextMetadata: (!JsonObject|undefined), + }} */ +export let CrossDomainDataDef; + +/** + * @abstract + */ +export class Validator { + /** + * @param {!Object} unusedContext + * @param {!ArrayBuffer} unusedUnvalidatedBytes + * @param {!Headers} unusedHeaders + * @return {!Promise} + * @abstract + */ + validate(unusedContext, unusedUnvalidatedBytes, unusedHeaders) {} +} + +/** + * @abstract + */ +export class Renderer { + /** + * @param {!Object} unusedContext + * @param {!Element} unusedContainerElement + * @param {!Object} unusedCreativeData Data passed from validator to renderer. + * This should only contain information generated by the validator; all + * other information should be put in context. + * @return {!Promise} + * @abstract + */ + render(unusedContext, unusedContainerElement, unusedCreativeData) {} +} diff --git a/extensions/amp-a4a/0.1/amp-ad-utils.js b/extensions/amp-a4a/0.1/amp-ad-utils.js new file mode 100644 index 000000000000..f756bf658660 --- /dev/null +++ b/extensions/amp-a4a/0.1/amp-ad-utils.js @@ -0,0 +1,133 @@ +/** + * Copyright 2018 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 {Services} from '../../../src/services'; +import {dev} from '../../../src/log'; +import {isArray, isObject} from '../../../src/types'; +import {isSecureUrlDeprecated} from '../../../src/url'; +import {parseJson} from '../../../src/json'; + +const TAG = 'amp-ad-util'; + +/** + * Sends a CORS XHR request to the given URL. + * @param {!Window} win + * @param {string} url Request URL to send XHR to. + * @return {!Promise} + */ +export function sendXhrRequest(win, url) { + return Services.xhrFor(win).fetch(url, { + mode: 'cors', + method: 'GET', + credentials: 'include', + }); +} + +/** @type {Array} */ +const METADATA_STRINGS = [ + ''); + if (metadataEnd < 0) { + // Couldn't find a metadata blob. + dev().warn(TAG, + 'Could not locate closing script tag for amp meta data in: %s', + creative); + return null; + } + try { + const metaDataObj = parseJson( + 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' || + !isSecureUrlDeprecated(stylesheet['href'])) { + throw new Error(errorMsg); + } + }); + } + if (isArray(metaDataObj['images'])) { + // Load maximum of 5 images. + metaData.images = metaDataObj['images'].splice(0, 5); + } + // 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, 'Invalid amp metadata: %s', + creative.slice(metadataStart + metadataString.length, metadataEnd)); + return null; + } +} diff --git a/extensions/amp-a4a/0.1/callout-vendors.js b/extensions/amp-a4a/0.1/callout-vendors.js new file mode 100644 index 000000000000..a95af1bbe36e --- /dev/null +++ b/extensions/amp-a4a/0.1/callout-vendors.js @@ -0,0 +1,112 @@ +/** + * 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 {getMode} from '../../../src/mode'; + +////////////////////////////////////////////////////////////////// +// // +// IMPORTANT: All keys in RTC_VENDORS must be lowercase // +// otherwise the vendor endpoint will not be used. // +// // +////////////////////////////////////////////////////////////////// + +// Note: disableKeyAppend is an option specifically for DoubleClick's +// implementation of RTC. It prevents the vendor ID from being +// appended onto each key of the RTC response, for each vendor. +// This appending is done to prevent a collision case during merge +// that would cause one RTC response to overwrite another if they +// share key names. +/** @typedef {{ + url: string, + macros: Array, + errorReportingUrl: (string|undefined), + disableKeyAppend: boolean}} */ +let RtcVendorDef; + +/** @const {!Object} */ +export const RTC_VENDORS = { + // Add vendors here + medianet: { + url: 'https://amprtc.media.net/rtb/getrtc?cid=CID&w=ATTR(width)&h=ATTR(height)&ow=ATTR(data-override-width)&oh=ATTR(data-override-height)&ms=ATTR(data-multi-size)&slot=ATTR(data-slot)&tgt=TGT&curl=CANONICAL_URL&to=TIMEOUT&purl=HREF', + macros: ['CID'], + errorReportingUrl: 'https://qsearch-a.akamaihd.net/log?logid=kfk&evtid=projectevents&project=amprtc_error&error=ERROR_TYPE&rd=HREF', + disableKeyAppend: true, + }, + prebidappnexus: { + url: 'https://prebid.adnxs.com/pbs/v1/openrtb2/amp?tag_id=PLACEMENT_ID&w=ATTR(width)&h=ATTR(height)&ow=ATTR(data-override-width)&oh=ATTR(data-override-height)&ms=ATTR(data-multi-size)&slot=ATTR(data-slot)&targeting=TGT&curl=CANONICAL_URL&timeout=TIMEOUT&adcid=ADCID&purl=HREF', + macros: ['PLACEMENT_ID'], + disableKeyAppend: true, + }, + prebidrubicon: { + url: 'https://prebid-server.rubiconproject.com/openrtb2/amp?tag_id=REQUEST_ID&w=ATTR(width)&h=ATTR(height)&ow=ATTR(data-override-width)&oh=ATTR(data-override-height)&ms=ATTR(data-multi-size)&slot=ATTR(data-slot)&targeting=TGT&curl=CANONICAL_URL&timeout=TIMEOUT&adc=ADCID&purl=HREF', + macros: ['REQUEST_ID'], + disableKeyAppend: true, + }, + indexexchange: { + url: 'https://amp.casalemedia.com/amprtc?v=1&w=ATTR(width)&h=ATTR(height)&ow=ATTR(data-override-width)&oh=ATTR(data-override-height)&ms=ATTR(data-multi-size)&s=SITE_ID&p=CANONICAL_URL', + macros: ['SITE_ID'], + disableKeyAppend: true, + }, + lotame: { + url: 'https://ad.crwdcntrl.net/5/pe=y/c=CLIENT_ID/an=AD_NETWORK', + macros: ['CLIENT_ID', 'AD_NETWORK'], + disableKeyAppend: true, + }, + yieldbot: { + url: 'https://i.yldbt.com/m/YB_PSN/v1/amp/init?curl=CANONICAL_URL&sn=YB_SLOT&w=ATTR(width)&h=ATTR(height)&ow=ATTR(data-override-width)&oh=ATTR(data-override-height)&ms=ATTR(data-multi-size)&aup=ATTR(data-slot)&pvi=PAGEVIEWID&tgt=TGT&adcid=ADCID&href=HREF', + macros: ['YB_PSN', 'YB_SLOT'], + disableKeyAppend: true, + }, + salesforcedmp: { + url: 'https://cdn.krxd.net/userdata/v2/amp/ORGANIZATION_ID?segments_key=SEGMENTS_KEY&kuid_key=USER_KEY', + macros: ['ORGANIZATION_ID', 'SEGMENTS_KEY', 'USER_KEY'], + disableKeyAppend: true, + }, + purch: { + url: 'https://ads.servebom.com/tmntag.js?v=1.2&fmt=amp&o={%22p%22%3APLACEMENT_ID}&div_id=DIV_ID', + macros: ['PLACEMENT_ID', 'DIV_ID'], + disableKeyAppend: true, + }, + aps: { + url: 'https://aax.amazon-adsystem.com/e/dtb/bid?src=PUB_ID&=1&u=CANONICAL_URL&slots=%5B%7B%22sd%22%3A%22ATTR(data-slot)%22%2C%22s%22%3A%5B%22ATTR(width)xATTR(height)%22%5D%7D%5D&pj=PARAMS', + macros: ['PUB_ID', 'PARAMS'], + disableKeyAppend: true, + }, + openwrap: { + // PubMatic OpenWrap + url: 'https://ow.pubmatic.com/amp?v=1&w=ATTR(width)&h=ATTR(height)&ms=ATTR(data-multi-size)&auId=ATTR(data-slot)&purl=HREF&pubId=PUB_ID&profId=PROFILE_ID', + macros: ['PUB_ID', 'PROFILE_ID'], + errorReportingUrl: 'https://ow.pubmatic.com/amp_error?e=ERROR_TYPE&h=HREF', + disableKeyAppend: true, + }, + criteo: { + url: 'https://bidder.criteo.com/amp/rtc?zid=ZONE_ID&nid=NETWORK_ID&psubid=PUBLISHER_SUB_ID&lir=LINE_ITEM_RANGES&w=ATTR(width)&h=ATTR(height)&ow=ATTR(data-override-width)&oh=ATTR(data-override-height)&ms=ATTR(data-multi-size)&slot=ATTR(data-slot)&timeout=TIMEOUT&href=HREF', + macros: ['ZONE_ID', 'NETWORK_ID', 'PUBLISHER_SUB_ID', 'LINE_ITEM_RANGES'], + disableKeyAppend: true, + }, +}; + +// DO NOT MODIFY: Setup for tests +if (getMode().localDev || getMode().test) { + RTC_VENDORS['fakevendor'] = /** @type {RtcVendorDef} */({ + url: 'https://localhost:8000/examples/rtcE1.json?slot_id=SLOT_ID&page_id=PAGE_ID&foo_id=FOO_ID', + macros: ['SLOT_ID', 'PAGE_ID', 'FOO_ID'], + }); + RTC_VENDORS['fakevendor2'] = /** @type {RtcVendorDef} */({ + url: 'https://localhost:8000/examples/rtcE1.json?slot_id=SLOT_ID&page_id=PAGE_ID&foo_id=FOO_ID', + errorReportingUrl: 'https://localhost:8000/examples/ERROR_TYPE', + disableKeyAppend: true, + }); +} diff --git a/extensions/amp-a4a/0.1/cryptographic-validator.js b/extensions/amp-a4a/0.1/cryptographic-validator.js new file mode 100644 index 000000000000..22911f7ff96c --- /dev/null +++ b/extensions/amp-a4a/0.1/cryptographic-validator.js @@ -0,0 +1,81 @@ +/** + * Copyright 2018 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 { + AdResponseType, + Validator, + ValidatorResult, +} from './amp-ad-type-defs'; +import {SignatureVerifier, VerificationStatus} from './signature-verifier'; +import {getAmpAdMetadata} from './amp-ad-utils'; +import {signingServerURLs} from '../../../ads/_a4a-config'; +import {user} from '../../../src/log'; +import {utf8Decode} from '../../../src/utils/bytes'; + +export const SIGNATURE_VERIFIER_PROPERTY_NAME = + 'AMP_FAST_FETCH_SIGNATURE_VERIFIER_'; + +const TAG = 'amp-ad-cryptographic-validator'; + +export class CryptographicValidator extends Validator { + /** @param {!Window} win */ + getSignatureVerifier_(win) { + // TODO(levitzky) extract this into a service registered to ampdoc. + return win[SIGNATURE_VERIFIER_PROPERTY_NAME] || + (win[SIGNATURE_VERIFIER_PROPERTY_NAME] = + new SignatureVerifier(win, signingServerURLs)); + } + + /** + * @param {boolean} verificationSucceeded + * @param {!ArrayBuffer} bytes + * @return {!./amp-ad-type-defs.ValidatorOutput} + */ + createOutput_(verificationSucceeded, bytes) { + const creativeData = { + creativeMetadata: getAmpAdMetadata(utf8Decode(bytes)), + }; + return /** @type {!./amp-ad-type-defs.ValidatorOutput} */ ({ + type: verificationSucceeded && !!creativeData.creativeMetadata ? + ValidatorResult.AMP : ValidatorResult.NON_AMP, + adResponseType: AdResponseType.CRYPTO, + creativeData, + }); + } + + /** @override */ + validate(context, unvalidatedBytes, headers) { + return this.getSignatureVerifier_(context.win) + .verify(unvalidatedBytes, headers, /* lifecycleCallback */ + (unusedEventName, unusedExtraVariables) => {}) + .then(status => { + switch (status) { + case VerificationStatus.OK: + return this.createOutput_(true, unvalidatedBytes); + case VerificationStatus.UNVERIFIED: + // TODO(levitzky) Preferential render without crypto in some + // instances. + case VerificationStatus.CRYPTO_UNAVAILABLE: + // TODO(@taymonbeal, #9274): differentiate between these + case VerificationStatus.ERROR_KEY_NOT_FOUND: + case VerificationStatus.ERROR_SIGNATURE_MISMATCH: + user().error(TAG, + `Signature verification failed with status ${status}.`); + return this.createOutput_(false, unvalidatedBytes); + } + }); + } +} diff --git a/extensions/amp-a4a/0.1/friendly-frame-renderer.js b/extensions/amp-a4a/0.1/friendly-frame-renderer.js new file mode 100644 index 000000000000..c62d10dbc67f --- /dev/null +++ b/extensions/amp-a4a/0.1/friendly-frame-renderer.js @@ -0,0 +1,55 @@ +/** + * Copyright 2018 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 {Renderer} from './amp-ad-type-defs'; +import {dev} from '../../../src/log'; +import {renderCreativeIntoFriendlyFrame} from './friendly-frame-util'; + +/** + * @typedef {{ + * creativeMetadata: ./amp-ad-type-defs.CreativeMetaDataDef, + * }} + */ +export let CreativeData; + +/** + * Render a validated AMP creative directly in the parent page. + */ +export class FriendlyFrameRenderer extends Renderer { + + /** + * Constructs a FriendlyFrameRenderer instance. The instance values here are + * used by TemplateRenderer, which inherits from FriendlyFrameRenderer. + */ + constructor() { + super(); + } + + /** @override */ + render(context, element, creativeData) { + + creativeData = /** @type {CreativeData} */ (creativeData); + + const {size, adUrl} = context; + const {creativeMetadata} = creativeData; + + dev().assert(size, 'missing creative size'); + dev().assert(adUrl, 'missing ad request url'); + + return renderCreativeIntoFriendlyFrame( + adUrl, size, element, creativeMetadata); + } +} diff --git a/extensions/amp-a4a/0.1/friendly-frame-util.js b/extensions/amp-a4a/0.1/friendly-frame-util.js new file mode 100644 index 000000000000..5eecd0b11923 --- /dev/null +++ b/extensions/amp-a4a/0.1/friendly-frame-util.js @@ -0,0 +1,90 @@ +/** + * Copyright 2018 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 {A4AVariableSource} from '../../amp-a4a/0.1/a4a-variable-source'; +import {createElementWithAttributes} from '../../../src/dom'; +import {dict} from '../../../src/utils/object'; +import { + installFriendlyIframeEmbed, + setFriendlyIframeEmbedVisible, +} from '../../../src/friendly-iframe-embed'; +import { + installUrlReplacementsForEmbed, +} from '../../../src/service/url-replacements-impl'; +import {setStyle} from '../../../src/style'; + +/** + * Renders a creative into a "NameFrame" iframe. + * + * @param {string} adUrl The ad request URL. + * @param {!./amp-ad-type-defs.LayoutInfoDef} size The size and layout of the + * element. + * @param {!Element} element The ad slot element. + * @param {!./amp-ad-type-defs.CreativeMetaDataDef} creativeMetadata Metadata + * for the creative. Contains information like required extensions, fonts, and + * of course the creative itself. + * @return {!Promise} The iframe into which the creative was rendered. + */ +export function renderCreativeIntoFriendlyFrame( + adUrl, size, element, creativeMetadata) { + // Create and setup friendly iframe. + const iframe = /** @type {!HTMLIFrameElement} */( + createElementWithAttributes( + /** @type {!Document} */(element.ownerDocument), + 'iframe', + dict({ + // NOTE: It is possible for either width or height to be 'auto', + // a non-numeric value. + 'height': size.height, + 'width': size.width, + 'frameborder': '0', + 'allowfullscreen': '', + 'allowtransparency': '', + 'scrolling': 'no', + }))); + // TODO(glevitzky): Ensure that applyFillContent or equivalent is called. + + const fontsArray = []; + if (creativeMetadata.customStylesheets) { + creativeMetadata.customStylesheets.forEach(s => { + const href = s['href']; + if (href) { + fontsArray.push(href); + } + }); + } + + return installFriendlyIframeEmbed( + iframe, element, { + host: element, + url: /** @type {string} */ (adUrl), + html: creativeMetadata.minifiedCreative, + extensionIds: creativeMetadata.customElementExtensions || [], + fonts: fontsArray, + }, embedWin => { + installUrlReplacementsForEmbed(element.getAmpDoc(), embedWin, + new A4AVariableSource(element.getAmpDoc(), embedWin)); + }) + .then(friendlyIframeEmbed => { + setFriendlyIframeEmbedVisible( + friendlyIframeEmbed, element.isInViewport()); + // Ensure visibility hidden has been removed (set by boilerplate). + const frameDoc = friendlyIframeEmbed.iframe.contentDocument || + friendlyIframeEmbed.win.document; + setStyle(frameDoc.body, 'visibility', 'visible'); + return iframe; + }); +} diff --git a/extensions/amp-a4a/0.1/name-frame-renderer.js b/extensions/amp-a4a/0.1/name-frame-renderer.js new file mode 100644 index 000000000000..6f0650c224a1 --- /dev/null +++ b/extensions/amp-a4a/0.1/name-frame-renderer.js @@ -0,0 +1,74 @@ +/** + * Copyright 2018 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 {Renderer} from './amp-ad-type-defs'; +import {createElementWithAttributes} from '../../../src/dom'; +import {dict} from '../../../src/utils/object'; +import {getContextMetadata} from '../../../src/iframe-attributes'; +import {getDefaultBootstrapBaseUrl} from '../../../src/3p-frame'; +import {utf8Decode} from '../../../src/utils/bytes'; + +/** + * Render a non-AMP creative into a NameFrame. + */ +export class NameFrameRenderer extends Renderer { + /** @override */ + render(context, element, crossDomainData) { + crossDomainData = /** @type {!./amp-ad-type-defs.CrossDomainDataDef} */ ( + crossDomainData); + + if (!crossDomainData.creative && !crossDomainData.rawCreativeBytes) { + // No creative, nothing to do. + return Promise.resolve(); + } + + const creative = crossDomainData.creative || + // rawCreativeBytes must exist; if we're here, then `creative` must not + // exist, but the if-statement above guarantees that at least one of + // `creative` || `rawCreativeBytes` exists. + utf8Decode(/** @type {!ArrayBuffer} */ ( + crossDomainData.rawCreativeBytes)); + const srcPath = + getDefaultBootstrapBaseUrl(context.win, 'nameframe'); + const contextMetadata = getContextMetadata( + context.win, + element, + context.sentinel, + crossDomainData.additionalContextMetadata); + contextMetadata['creative'] = creative; + const attributes = dict({ + 'src': srcPath, + 'name': JSON.stringify(contextMetadata), + 'height': context.size.height, + 'width': context.size.width, + 'frameborder': '0', + 'allowfullscreen': '', + 'allowtransparency': '', + 'scrolling': 'no', + 'marginwidth': '0', + 'marginheight': '0', + }); + if (crossDomainData.sentinel) { + attributes['data-amp-3p-sentinel'] = crossDomainData.sentinel; + } + const iframe = createElementWithAttributes( + /** @type {!Document} */ (element.ownerDocument), 'iframe', + /** @type {!JsonObject} */ (attributes)); + // TODO(glevitzky): Ensure that applyFillContent or equivalent is called. + element.appendChild(iframe); + return Promise.resolve(); + } +} diff --git a/extensions/amp-a4a/0.1/real-time-config-manager.js b/extensions/amp-a4a/0.1/real-time-config-manager.js new file mode 100644 index 000000000000..6054323e2682 --- /dev/null +++ b/extensions/amp-a4a/0.1/real-time-config-manager.js @@ -0,0 +1,529 @@ +/** + * 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 {CONSENT_POLICY_STATE} from '../../../src/consent-state'; +import {RTC_VENDORS} from './callout-vendors'; +import {Services} from '../../../src/services'; +import {dev, user} from '../../../src/log'; +import {getMode} from '../../../src/mode'; +import {isArray, isObject} from '../../../src/types'; +import {isCancellation} from '../../../src/error'; +import {tryParseJson} from '../../../src/json'; + +/** @type {string} */ +const TAG = 'real-time-config'; + +/** @type {number} */ +const MAX_RTC_CALLOUTS = 5; + +/** @type {number} */ +const MAX_URL_LENGTH = 16384; + +/** @type {boolean} */ +const ERROR_REPORTING_ENABLED = getMode(window).localDev || + getMode(window).test || Math.random() < 0.01; + +/** @typedef {{ + urls: (undefined|Array| + Array<{url:string, errorReportingUrl:string, + sendRegardlessOfConsentState:(undefined|boolean|Array)}>), + vendors: (undefined|Object), + timeoutMillis: number, + errorReportingUrl: (undefined|string), + sendRegardlessOfConsentState: (undefined|boolean|Array) +}} */ +let RtcConfigDef; + +/** + * Enum starts at 4 because 1-3 reserved as: + * 1 = custom remote.html in use. + * 2 = RTC succeeded. + * 3 = deprecated generic RTC failures. + * @enum {string} + */ +export const RTC_ERROR_ENUM = { + // Occurs when response is unparseable as JSON + MALFORMED_JSON_RESPONSE: '4', + // Occurs when a publisher has specified the same url + // or vendor url (after macros are substituted) to call out to more than once. + DUPLICATE_URL: '5', + // Occurs when a URL fails isSecureUrl check. + INSECURE_URL: '6', + // Occurs when 5 valid callout urls have already been built, and additional + // urls are still specified. + MAX_CALLOUTS_EXCEEDED: '7', + // Occurs due to XHR failure. + NETWORK_FAILURE: '8', + // Occurs when a specified vendor does not exist in RTC_VENDORS. + UNKNOWN_VENDOR: '9', + // Occurs when request took longer than timeout + TIMEOUT: '10', + // Occurs when URL expansion time exceeded allowed timeout, request never + // sent. + MACRO_EXPAND_TIMEOUT: '11', +}; + +export class RealTimeConfigManager { + /** + * @param {!./amp-a4a.AmpA4A} a4aElement + */ + constructor(a4aElement) { + /** @private {!./amp-a4a.AmpA4A} */ + this.a4aElement_ = a4aElement; + + /** @private {!Window} */ + this.win_ = this.a4aElement_.win; + + /** @private {!Object} */ + this.seenUrls_ = {}; + + /** @private {?number} */ + this.rtcStartTime_ = null; + + /** @private {!Array>} */ + this.promiseArray_ = []; + + /** @private {?RtcConfigDef} */ + this.rtcConfig_ = null; + + /** @private !../../../src/service/ampdoc-impl.AmpDoc */ + this.ampDoc_ = this.a4aElement_.getAmpDoc(); + + /** @private {?CONSENT_POLICY_STATE} */ + this.consentState_ = null; + } + + /** + * @param {string} error + * @param {string} callout + * @param {string} errorReportingUrl + * @param {number=} opt_rtcTime + * @return {!Promise} + * @private + */ + buildErrorResponse_( + error, callout, errorReportingUrl, opt_rtcTime) { + dev().warn(TAG, `RTC callout to ${callout} caused ${error}`); + if (errorReportingUrl) { + this.sendErrorMessage(error, errorReportingUrl); + } + return Promise.resolve(/**@type {rtcResponseDef} */( + {error, callout, rtcTime: opt_rtcTime || 0})); + } + + /** + * @param {string} errorType Uses the RTC_ERROR_ENUM above. + * @param {string} errorReportingUrl + */ + sendErrorMessage(errorType, errorReportingUrl) { + if (!ERROR_REPORTING_ENABLED) { + return; + } + const whitelist = {ERROR_TYPE: true, HREF: true}; + const macros = { + ERROR_TYPE: errorType, + HREF: this.win_.location.href, + }; + const url = Services.urlReplacementsForDoc(this.ampDoc_).expandUrlSync( + errorReportingUrl, macros, whitelist); + new this.win_.Image().src = url; + } + + /** + * Converts a URL into its corresponding shortened callout string. + * We also truncate to a maximum length of 50 characters. + * For instance, if we are passed + * "https://example.com/example.php?foo=a&bar=b, then we return + * example.com/example.php + * @param {string} url + * @return {string} + */ + getCalloutParam_(url) { + const parsedUrl = Services.urlForDoc( + this.a4aElement_.getAmpDoc()).parse(url); + return (parsedUrl.hostname + parsedUrl.pathname).substr(0, 50); + } + + /** + * For a given A4A Element, sends out Real Time Config requests to + * any urls or vendors specified by the publisher. + * @param {!Object} customMacros The ad-network specified macro + * substitutions available to use. + * @param {?CONSENT_POLICY_STATE} consentState + * @return {Promise>|undefined} + * @visibleForTesting + */ + maybeExecuteRealTimeConfig(customMacros, consentState) { + if (!this.validateRtcConfig_(this.a4aElement_.element)) { + return; + } + this.consentState_ = consentState; + this.modifyRtcConfigForConsentStateSettings(); + customMacros = this.assignMacros(customMacros); + this.rtcStartTime_ = Date.now(); + this.handleRtcForCustomUrls(customMacros); + this.handleRtcForVendorUrls(customMacros); + return Promise.all(this.promiseArray_); + } + + /** + * Returns whether a given callout object is valid to send an RTC request + * to, for the given consentState. + * @param {Object|string} calloutConfig + * @param {boolean=} optIsGloballyValid + * @return {boolean} + * @visibleForTesting + */ + isValidCalloutForConsentState(calloutConfig, optIsGloballyValid) { + const {sendRegardlessOfConsentState} = calloutConfig; + if (!isObject(calloutConfig) || !sendRegardlessOfConsentState) { + return !!optIsGloballyValid; + } + + if (typeof sendRegardlessOfConsentState == 'boolean') { + return sendRegardlessOfConsentState; + } + + if (isArray(sendRegardlessOfConsentState)) { + for (let i = 0; i < sendRegardlessOfConsentState.length; i++) { + if (this.consentState_ == + CONSENT_POLICY_STATE[sendRegardlessOfConsentState[i]]) { + return true; + } else if (!CONSENT_POLICY_STATE[sendRegardlessOfConsentState[i]]) { + dev().warn(TAG, 'Invalid RTC consent state given: ' + + `${sendRegardlessOfConsentState[i]}`); + } + } + return false; + } + user().warn(TAG, 'Invalid value for sendRegardlessOfConsentState:' + + `${sendRegardlessOfConsentState}`); + return !!optIsGloballyValid; + } + + /** + * Goes through the RTC config, and for any URL that we should not callout + * as per the current consent state, deletes it from the RTC config. + * For example, if the RTC config looked like: + * {vendors: {vendorA: {'sendRegardlessOfConsentState': true} + * vendorB: {'macros': {'SLOT_ID': 1}}}, + * urls: ['https://www.rtc.example/example', + * {url: 'https://www.rtcSite2.example/example', + * sendRegardlessOfConsentState: ['UNKNOWN']}] + * } + * and the consentState is CONSENT_POLICY_STATE.UNKNOWN, + * then this method call would clear the callouts to vendorB, and to the first + * custom URL. + */ + modifyRtcConfigForConsentStateSettings() { + if (this.consentState_ == undefined || + this.consentState_ == CONSENT_POLICY_STATE.SUFFICIENT || + this.consentState_ == CONSENT_POLICY_STATE.UNKNOWN_NOT_REQUIRED) { + return; + } + + const isGloballyValid = this.isValidCalloutForConsentState(this.rtcConfig_); + this.rtcConfig_.urls = (this.rtcConfig_.urls || []).filter( + url => this.isValidCalloutForConsentState(url, isGloballyValid)); + + Object.keys(this.rtcConfig_.vendors || {}).forEach(vendor => { + if (!this.isValidCalloutForConsentState( + this.rtcConfig_.vendors[vendor], isGloballyValid)) { + delete this.rtcConfig_.vendors[vendor]; + } + }); + + } + + /** + * Assigns constant macros that should exist for all RTC to object of custom + * per-network macros. + * @param {!Object} macros + */ + assignMacros(macros) { + macros['TIMEOUT'] = () => this.rtcConfig_.timeoutMillis; + macros['CONSENT_STATE'] = () => this.consentState_; + return macros; + } + + /** + * Manages sending the RTC callouts for the Custom URLs. + * @param {!Object} customMacros The ad-network specified macro + */ + handleRtcForCustomUrls(customMacros) { + // For each publisher defined URL, inflate the url using the macros, + // and send the RTC request. + (this.rtcConfig_.urls || []).forEach(urlObj => { + let url, errorReportingUrl; + if (isObject(urlObj)) { + url = urlObj.url; + errorReportingUrl = urlObj.errorReportingUrl; + } else if (typeof urlObj == 'string') { + url = urlObj; + } else { + dev().warn(TAG, `Invalid url: ${urlObj}`); + } + this.inflateAndSendRtc_(url, + customMacros, + errorReportingUrl); + }); + } + + /** + * Manages sending the RTC callouts for all specified vendors. + * @param {!Object} customMacros The ad-network specified macro + */ + handleRtcForVendorUrls(customMacros) { + // For each vendor the publisher has specified, inflate the vendor + // url if it exists, and send the RTC request. + Object.keys(this.rtcConfig_.vendors || []).forEach(vendor => { + const vendorObject = RTC_VENDORS[vendor.toLowerCase()]; + const url = vendorObject ? vendorObject.url : ''; + const errorReportingUrl = vendorObject && vendorObject.errorReportingUrl ? + vendorObject.errorReportingUrl : ''; + if (!url) { + return this.promiseArray_.push( + this.buildErrorResponse_( + RTC_ERROR_ENUM.UNKNOWN_VENDOR, vendor, errorReportingUrl)); + } + // There are two valid configurations of the vendor object. + // It can either be an object of macros mapping string to string, + // or it can be an object with sub-objects, one of which can be + // 'macros'. This is for backwards compatability. + const vendorMacros = + isObject(this.rtcConfig_.vendors[vendor]['macros']) ? + this.rtcConfig_.vendors[vendor]['macros'] : + this.rtcConfig_.vendors[vendor]; + const validVendorMacros = {}; + Object.keys(vendorMacros).forEach(macro => { + if (!(vendorObject.macros && vendorObject.macros.includes(macro))) { + user().error(TAG, `Unknown macro: ${macro} for vendor: ${vendor}`); + } else { + const value = vendorMacros[macro]; + validVendorMacros[macro] = isObject(value) || isArray(value) ? + JSON.stringify(value) : value; + } + }); + // The ad network defined macros override vendor defined/pub specifed. + const macros = Object.assign(validVendorMacros, customMacros); + this.inflateAndSendRtc_(url, + macros, errorReportingUrl, + vendor.toLowerCase()); + }); + } + + /** + * @param {string} url + * @param {!Object} macros + * @param {string} errorReportingUrl + * @param {string=} opt_vendor + * @private + */ + inflateAndSendRtc_(url, + macros, errorReportingUrl, opt_vendor) { + let {timeoutMillis} = this.rtcConfig_; + const callout = opt_vendor || this.getCalloutParam_(url); + const checkStillCurrent = this.a4aElement_.verifyStillCurrent.bind( + this.a4aElement_)(); + /** + * The time that it takes to substitute the macros into the URL can vary + * depending on what the url requires to be substituted, i.e. a long + * async call. Thus, however long the URL replacement took is treated as a + * time penalty. + * @param {string} url + */ + const send = url => { + if (Object.keys(this.seenUrls_).length == MAX_RTC_CALLOUTS) { + return this.buildErrorResponse_( + RTC_ERROR_ENUM.MAX_CALLOUTS_EXCEEDED, + callout, errorReportingUrl); + } + if (!Services.urlForDoc( + this.a4aElement_.getAmpDoc()).isSecure(url)) { + return this.buildErrorResponse_(RTC_ERROR_ENUM.INSECURE_URL, + callout, errorReportingUrl); + } + if (this.seenUrls_[url]) { + return this.buildErrorResponse_(RTC_ERROR_ENUM.DUPLICATE_URL, + callout, errorReportingUrl); + } + this.seenUrls_[url] = true; + if (url.length > MAX_URL_LENGTH) { + url = this.truncUrl_(url); + } + return this.sendRtcCallout_( + url, timeoutMillis, callout, checkStillCurrent, + errorReportingUrl); + }; + + const whitelist = {}; + Object.keys(macros).forEach(key => whitelist[key] = true); + const urlReplacementStartTime = Date.now(); + this.promiseArray_.push(Services.timerFor(this.win_).timeoutPromise( + timeoutMillis, + Services.urlReplacementsForDoc(this.ampDoc_).expandUrlAsync( + url, macros, whitelist)).then(url => { + checkStillCurrent(); + timeoutMillis -= (urlReplacementStartTime - Date.now()); + return send(url); + }).catch(error => { + return isCancellation(error) ? undefined : + this.buildErrorResponse_(RTC_ERROR_ENUM.MACRO_EXPAND_TIMEOUT, + callout, errorReportingUrl); + })); + } + + + /** + * @param {string} url + * @return {string} + */ + truncUrl_(url) { + url = url.substr(0, MAX_URL_LENGTH - 12).replace(/%\w?$/, ''); + return url + '&__trunc__=1'; + } + + /** + * @param {string} url + * @param {number} timeoutMillis + * @param {string} callout + * @param {!Function} checkStillCurrent + * @param {string} errorReportingUrl + * @return {!Promise} + * @private + */ + sendRtcCallout_(url, timeoutMillis, callout, checkStillCurrent, + errorReportingUrl) { + /** + * Note: Timeout is enforced by timerFor, not the value of + * rtcTime. There are situations where rtcTime could thus + * end up being greater than timeoutMillis. + */ + return Services.timerFor(this.win_).timeoutPromise( + timeoutMillis, + Services.xhrFor(this.win_).fetchJson( + // NOTE(bradfrizzell): we could include ampCors:false allowing + // the request to be cached across sites but for now assume that + // is not a required feature. + url, {credentials: 'include'}).then(res => { + checkStillCurrent(); + return res.text().then(text => { + checkStillCurrent(); + const rtcTime = Date.now() - this.rtcStartTime_; + // An empty text response is allowed, not an error. + if (!text) { + return {rtcTime, callout}; + } + const response = tryParseJson(text); + return response ? {response, rtcTime, callout} : + this.buildErrorResponse_( + RTC_ERROR_ENUM.MALFORMED_JSON_RESPONSE, callout, + errorReportingUrl, rtcTime); + }); + })).catch(error => { + return isCancellation(error) ? undefined : + this.buildErrorResponse_( + // The relevant error message for timeout looks like it is + // just 'message' but is in fact 'messageXXX' where the + // X's are hidden special characters. That's why we use + // match here. + (/^timeout/.test(error.message)) ? + RTC_ERROR_ENUM.TIMEOUT : RTC_ERROR_ENUM.NETWORK_FAILURE, + callout, errorReportingUrl, Date.now() - this.rtcStartTime_); + }); + } + + /** + * Attempts to parse the publisher-defined RTC config off the amp-ad + * element, then validates that the rtcConfig exists, and contains + * an entry for either vendor URLs, or publisher-defined URLs. If the + * config contains an entry for timeoutMillis, validates that it is a + * number, or converts to a number if number-like, otherwise overwrites + * with the default. + * IMPORTANT: If the rtcConfig is invalid, RTC is aborted, and the ad + * request continues without RTC. + * @param {!Element} element + * @return {boolean} + */ + validateRtcConfig_(element) { + const defaultTimeoutMillis = 1000; + const unparsedRtcConfig = element.getAttribute('rtc-config'); + if (!unparsedRtcConfig) { + return false; + } + const rtcConfig = tryParseJson(unparsedRtcConfig); + if (!rtcConfig) { + user().warn(TAG, 'Could not JSON parse rtc-config attribute'); + return false; + } + + let timeout; + try { + user().assert(rtcConfig['vendors'] || rtcConfig['urls'], + 'RTC Config must specify vendors or urls'); + Object.keys(rtcConfig).forEach(key => { + switch (key) { + case 'vendors': + user().assert(isObject(rtcConfig[key]), 'RTC invalid vendors'); + break; + case 'urls': + user().assert(isArray(rtcConfig[key]), 'RTC invalid urls'); + break; + case 'timeoutMillis': + timeout = parseInt(rtcConfig[key], 10); + if (isNaN(timeout)) { + user().warn(TAG, 'Invalid RTC timeout is NaN, ' + + `using default timeout ${defaultTimeoutMillis}ms`); + timeout = undefined; + } else if (timeout >= defaultTimeoutMillis || timeout < 0) { + user().warn(TAG, `Invalid RTC timeout: ${timeout}ms, ` + + `using default timeout ${defaultTimeoutMillis}ms`); + timeout = undefined; + } + break; + default: + user().warn(TAG, `Unknown RTC Config key: ${key}`); + break; + } + }); + if (!Object.keys(rtcConfig['vendors'] || {}).length + && !(rtcConfig['urls'] || []).length) { + return false; + } + const validateErrorReportingUrl = urlObj => { + const errorUrl = urlObj['errorReportingUrl']; + if (errorUrl && !Services.urlForDoc( + this.a4aElement_.getAmpDoc()).isSecure(errorUrl)) { + dev().warn(TAG, `Insecure RTC errorReportingUrl: ${errorUrl}`); + urlObj['errorReportingUrl'] = undefined; + } + }; + (rtcConfig['urls'] || []).forEach(urlObj => { + if (isObject(urlObj)) { + validateErrorReportingUrl(urlObj); + } + }); + validateErrorReportingUrl(rtcConfig); + } catch (unusedErr) { + // This error would be due to the asserts above. + return false; + } + rtcConfig['timeoutMillis'] = timeout !== undefined ? + timeout : defaultTimeoutMillis; + this.rtcConfig_ = /** @type {RtcConfigDef} */(rtcConfig); + return true; + } +} +AMP.RealTimeConfigManager = RealTimeConfigManager; diff --git a/extensions/amp-a4a/0.1/refresh-intersection-observer-wrapper.js b/extensions/amp-a4a/0.1/refresh-intersection-observer-wrapper.js new file mode 100644 index 000000000000..fa15a926a8fc --- /dev/null +++ b/extensions/amp-a4a/0.1/refresh-intersection-observer-wrapper.js @@ -0,0 +1,98 @@ +/** + * Copyright 2018 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 { + IntersectionObserverPolyfill, +} from '../../../src/intersection-observer-polyfill'; +import {dev} from '../../../src/log'; + +export class RefreshIntersectionObserverWrapper { + /** + * A thin wrapper class to allow the IntersectionObserverPolyfill to work with + * refresh. + * @param {function(!Array)} callback + * @param {!AMP.BaseElement} baseElement + * @param {Object} config + */ + constructor(callback, baseElement, config) { + + /** + * @private @const {!IntersectionObserverPolyfill} + */ + this.intersectionObserver_ = new IntersectionObserverPolyfill( + callback, config); + + /** + * Stores elements and their original viewportCallback functions so that + * they can be reverted upon invocation of unobserve. + * @private {!Object} + */ + this.viewportCallbacks_ = {}; + + /** @private @const {!../../../src/service/viewport/viewport-impl.Viewport} */ + this.viewport_ = baseElement.getViewport(); + + /** + * Flag that indicates when #tick should be called on the observer + * polyfill. + * @private {boolean} + */ + this.updateObserver_ = false; + } + + /** + * Begin observing the given element. + * @param {!Element} element + */ + observe(element) { + // The attribute name is exported in refresh-manager.js as + // DATA_MANAGER_ID_NAME, but unfortunately, it can't be imported without + // creating a cyclical dependency. + const refreshId = element.getAttribute('data-amp-ad-refresh-id'); + dev().assert(refreshId, 'observe invoked on element without refresh id'); + + if (!this.viewportCallbacks_[refreshId]) { + const viewportCallback = element.viewportCallback.bind(element); + this.viewportCallbacks_[refreshId] = viewportCallback; + element.viewportCallback = inViewport => { + if (this.updateObserver_) { + this.intersectionObserver_.tick(this.viewport_.getRect()); + } + viewportCallback(inViewport); + }; + } + + this.updateObserver_ = true; + this.intersectionObserver_.observe(element); + // Elements that appear and remain within the viewport for the duration of + // their existence may never have viewportCallback invoked. To ensure that + // refresh is triggered, we need to make this initial call. + this.intersectionObserver_.tick(this.viewport_.getRect()); + } + + /** + * Cease observing the given element. + * @param {!Element} element + */ + unobserve(element) { + // We need to call 'tick' to update current host viewport state, otherwise + // the next time we call 'observe', the viewport state might be stale, and + // indicate that the element is in the viewport when it's not. + this.intersectionObserver_.tick(this.viewport_.getRect()); + this.intersectionObserver_.unobserve(element); + this.updateObserver_ = false; + } +} diff --git a/extensions/amp-a4a/0.1/refresh-manager.js b/extensions/amp-a4a/0.1/refresh-manager.js new file mode 100644 index 000000000000..545f30ebfcaa --- /dev/null +++ b/extensions/amp-a4a/0.1/refresh-manager.js @@ -0,0 +1,348 @@ +/** + * 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 { + RefreshIntersectionObserverWrapper, +} from './refresh-intersection-observer-wrapper'; +import {Services} from '../../../src/services'; +import {dev, user} from '../../../src/log'; + +/** + * - visibilePercentageMin: The percentage of pixels that need to be on screen + * for the creative to be considered "visible". + * - continuousTimeMin: The amount of continuous time, in milliseconds, that + * the creative must be on screen for in order to be considered "visible". + * + * @typedef {{ + * visiblePercentageMin: number, + * continuousTimeMin: number, + * }} + */ +export let RefreshConfig; + +export const MIN_REFRESH_INTERVAL = 30; +export const DATA_ATTR_NAME = 'data-enable-refresh'; +export const DATA_MANAGER_ID_NAME = 'data-amp-ad-refresh-id'; +export const METATAG_NAME = 'amp-ad-enable-refresh'; + +const TAG = 'AMP-AD'; + +/** + * Retrieves the publisher-specified refresh interval, if one were set. This + * function first checks for appropriate slot attributes and then for + * metadata tags, preferring whichever it finds first. + * @param {!Element} element + * @param {!Window} win + * @return {?number} + * @visibleForTesting + */ +export function getPublisherSpecifiedRefreshInterval(element, win) { + const refreshInterval = element.getAttribute(DATA_ATTR_NAME); + if (refreshInterval) { + return checkAndSanitizeRefreshInterval(refreshInterval); + } + let metaTag; + const metaTagContent = ((metaTag = win.document + .getElementsByName(METATAG_NAME)) + && metaTag[0] + && metaTag[0].getAttribute('content')); + if (!metaTagContent) { + return null; + } + const networkIntervalPairs = metaTagContent.split(','); + for (let i = 0; i < networkIntervalPairs.length; i++) { + const pair = networkIntervalPairs[i].split('='); + user().assert(pair.length == 2, 'refresh metadata config must be of ' + + 'the form `network_type=refresh_interval`'); + if (pair[0].toLowerCase() == element.getAttribute('type').toLowerCase()) { + return checkAndSanitizeRefreshInterval(pair[1]); + } + } + return null; +} + +/** + * Ensures that refreshInterval is a number no less than 30. Returns null if + * the given input fails to meet these criteria. This also converts from + * seconds to milliseconds. + * + * @param {(number|string)} refreshInterval + * @return {?number} + */ +function checkAndSanitizeRefreshInterval(refreshInterval) { + const refreshIntervalNum = Number(refreshInterval); + if (isNaN(refreshIntervalNum) || + refreshIntervalNum < MIN_REFRESH_INTERVAL) { + user().warn(TAG, + 'invalid refresh interval, must be a number no less than ' + + `${MIN_REFRESH_INTERVAL}: ${refreshInterval}`); + return null; + } + return refreshIntervalNum * 1000; +} + +/** + * Defines the DFA states for the refresh cycle. + * + * 1. All newly registered elements begin in the INITIAL state. + * 2. Only when the element enters the viewport with the specified + * intersection ratio does it transition into the VIEW_PENDING state. + * 3. If the element remains in the viewport for the specified duration, it + * will then transition into the REFRESH_PENDING state, otherwise it will + * transition back into the INITIAL state. + * 4. The element will remain in REFRESH_PENDING state until the refresh + * interval expires. + * 5. Once the interval expires, the element will return to the INITIAL state. + * + * @enum {string} + */ +const RefreshLifecycleState = { + /** + * Element has been registered, but not yet seen on screen. + */ + INITIAL: 'initial', + + /** + * The element has appeared in the viewport, but not yet for the required + * duration. + */ + VIEW_PENDING: 'view_pending', + + /** + * The element has been in the viewport for the required duration; the + * refresh interval for the element has begun. + */ + REFRESH_PENDING: 'refresh_pending', +}; + +/** + * An object containing the IntersectionObservers used to monitor elements. + * Each IO is configured to a different threshold, and all elements that + * share the same visiblePercentageMin will be monitored by the same IO. + * + * @const {!Object} + */ +const observers = {}; + +/** + * An object containing all currently active RefreshManagers. This is used in + * the IntersectionOberserver callback function to find the appropriate element + * target. + * + * @const {!Object} + */ +const managers = {}; + +/** + * Used to generate unique IDs for each RefreshManager. + * @type {number} + */ +let refreshManagerIdCounter = 0; + +/** + * Returns an instance of RefreshManager, if refresh is enabled on the page or + * slot. An optional predicate for eligibility may be passed. If refresh is not + * enabled, or fails the optional predicate, null will be returned. + * + * @param {!./amp-a4a.AmpA4A} a4a + * @param {function():boolean=} opt_predicate + * @return {?RefreshManager} + */ +export function getRefreshManager(a4a, opt_predicate) { + const refreshInterval = + getPublisherSpecifiedRefreshInterval(a4a.element, a4a.win); + if (!refreshInterval || (opt_predicate && !opt_predicate())) { + return null; + } + return new RefreshManager(a4a, { + visiblePercentageMin: 50, + continuousTimeMin: 1, + }, refreshInterval); +} + + +export class RefreshManager { + + /** + * @param {!./amp-a4a.AmpA4A} a4a The AmpA4A instance to be refreshed. + * @param {!RefreshConfig} config + * @param {number} refreshInterval + */ + constructor(a4a, config, refreshInterval) { + + /** @private {string} */ + this.state_ = RefreshLifecycleState.INITIAL; + + /** @const @private {!./amp-a4a.AmpA4A} */ + this.a4a_ = a4a; + + /** @const @private {!Window} */ + this.win_ = a4a.win; + + /** @const @private {!Element} */ + this.element_ = a4a.element; + + /** @const @private {string} */ + this.adType_ = this.element_.getAttribute('type').toLowerCase(); + + /** @const @private {?number} */ + this.refreshInterval_ = refreshInterval; + + /** @const @private {!RefreshConfig} */ + this.config_ = this.convertAndSanitizeConfiguration_(config); + + /** @const @private {!../../../src/service/timer-impl.Timer} */ + this.timer_ = Services.timerFor(this.win_); + + /** @private {?(number|string)} */ + this.refreshTimeoutId_ = null; + + /** @private {?(number|string)} */ + this.visibilityTimeoutId_ = null; + + const managerId = String(refreshManagerIdCounter++); + this.element_.setAttribute(DATA_MANAGER_ID_NAME, managerId); + managers[managerId] = this; + this.initiateRefreshCycle(); + } + + /** + * Returns an IntersectionObserver configured to the given threshold, creating + * one if one does not yet exist. + * + * @param {number} threshold + * @return {(!IntersectionObserver|!RefreshIntersectionObserverWrapper)} + */ + getIntersectionObserverWithThreshold_(threshold) { + + const thresholdString = String(threshold); + return observers[thresholdString] || + (observers[thresholdString] = 'IntersectionObserver' in this.win_ + ? new this.win_['IntersectionObserver'](this.ioCallback_, {threshold}) + : new RefreshIntersectionObserverWrapper( + this.ioCallback_, this.a4a_, {threshold})); + } + + /** + * Returns a function that will be invoked directly by the + * IntersectionObserver implementation. It will implement the core logic of + * the refresh lifecycle, including the transitions of the DFA. + * + * @param {!Array} entries + */ + ioCallback_(entries) { + entries.forEach(entry => { + const refreshManagerId = entry.target.getAttribute(DATA_MANAGER_ID_NAME); + dev().assert(refreshManagerId); + const refreshManager = managers[refreshManagerId]; + if (entry.target != refreshManager.element_) { + return; + } + switch (refreshManager.state_) { + case RefreshLifecycleState.INITIAL: + // First check if the element qualifies as "being on screen", i.e., + // that at least a minimum threshold of pixels is on screen. If so, + // begin a timer, set for the duration of the minimum time on screen + // threshold. If this timer runs out without interruption, then all + // viewability conditions have been met, and we can begin the refresh + // timer. + if (entry.intersectionRatio >= + refreshManager.config_.visiblePercentageMin) { + refreshManager.state_ = RefreshLifecycleState.VIEW_PENDING; + refreshManager.visibilityTimeoutId_ = refreshManager.timer_.delay( + () => { + refreshManager.state_ = RefreshLifecycleState.REFRESH_PENDING; + refreshManager.startRefreshTimer_(); + }, refreshManager.config_.continuousTimeMin); + } + break; + case RefreshLifecycleState.VIEW_PENDING: + // If the element goes off screen before the minimum on screen time + // duration elapses, place it back into INITIAL state. + if (entry.intersectionRatio < + refreshManager.config_.visiblePercentageMin) { + refreshManager.timer_.cancel(refreshManager.visibilityTimeoutId_); + refreshManager.visibilityTimeoutId_ = null; + refreshManager.state_ = RefreshLifecycleState.INITIAL; + } + break; + case RefreshLifecycleState.REFRESH_PENDING: + default: + break; + } + }); + } + + /** + * Initiates the refresh cycle by initiating the visibility manager on the + * element. + */ + initiateRefreshCycle() { + switch (this.state_) { + case RefreshLifecycleState.INITIAL: + this.getIntersectionObserverWithThreshold_( + this.config_.visiblePercentageMin).observe(this.element_); + break; + case RefreshLifecycleState.REFRESH_PENDING: + case RefreshLifecycleState.VIEW_PENDING: + default: + break; + + } + } + + /** + * Starts the refresh timer for the given monitored element. + * + * @return {!Promise} A promise that resolves to true when the + * refresh timer elapses successfully. + */ + startRefreshTimer_() { + return new Promise(resolve => { + this.refreshTimeoutId_ = this.timer_.delay(() => { + this.state_ = RefreshLifecycleState.INITIAL; + this.unobserve(); + this.a4a_.refresh(() => this.initiateRefreshCycle()); + resolve(true); + }, /** @type {number} */ (this.refreshInterval_)); + }); + } + + /** + * Converts config to appropriate units, modifying the argument in place. This + * also ensures that visiblePercentageMin is in the range of [0, 100]. + * @param {!RefreshConfig} config + * @return {!RefreshConfig} + */ + convertAndSanitizeConfiguration_(config) { + dev().assert(config['visiblePercentageMin'] >= 0 && + config['visiblePercentageMin'] <= 100, + 'visiblePercentageMin for refresh must be in the range [0, 100]'); + // Convert seconds to milliseconds. + config['continuousTimeMin'] *= 1000; + config['visiblePercentageMin'] /= 100; + return config; + } + + /** + * Stops the intersection observer from observing the element. + */ + unobserve() { + this.getIntersectionObserverWithThreshold_( + this.config_.visiblePercentageMin).unobserve(this.element_); + } +} + diff --git a/extensions/amp-a4a/0.1/signature-verifier.js b/extensions/amp-a4a/0.1/signature-verifier.js new file mode 100644 index 000000000000..285d9be594fb --- /dev/null +++ b/extensions/amp-a4a/0.1/signature-verifier.js @@ -0,0 +1,393 @@ +/** + * 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 {Services} from '../../../src/services'; +import {base64DecodeToBytes} from '../../../src/utils/base64'; +import {dev, user} from '../../../src/log'; +import {isArray} from '../../../src/types'; + +/** @visibleForTesting */ +export const AMP_SIGNATURE_HEADER = 'AMP-Fast-Fetch-Signature'; + +/** + * The result of an attempt to verify a Fast Fetch signature. The different + * error statuses are used for reporting errors to the ad network. + * + * @enum {number} + */ +export const VerificationStatus = { + + /** The ad was successfully verified as AMP. */ + OK: 0, + + /** + * Verification failed because of a factor beyond the ad network's control, + * such as a network connectivity failure, unavailability of Web Cryptography + * in the current browsing context, or a misbehaving signing service. + */ + UNVERIFIED: 1, + + /** + * Verification failed because the keypair ID provided by the ad network did + * not correspond to any public key offered by the signing service. + */ + ERROR_KEY_NOT_FOUND: 2, + + /** + * Verification failed because the signature provided by the ad network was + * not the correct cryptographic signature for the given creative data and + * public key. + */ + ERROR_SIGNATURE_MISMATCH: 3, + + /** + * Verification failed because the page does not have web crypto available, + * i.e. is not SSL. + */ + CRYPTO_UNAVAILABLE: 4, + +}; + +/** + * A window-level object that encapsulates the logic for obtaining public keys + * from Fast Fetch signing services and cryptographically verifying signatures + * of AMP creatives. + * + * Unlike an AMP service, a signature verifier is **stateful**. It maintains a + * cache of all public keys that it has previously downloaded and imported, and + * also keeps track of which keys and signing services have already had + * unsuccessful download or import attempts and should not be attempted again. + * + * This entire class is currently dead code in production, but will soon be + * introduced as an experiment. + */ +export class SignatureVerifier { + + /** + * @param {!Window} win + * @param {!Object} signingServerURLs a map from the name of + * each trusted signing service to the URL of its public key endpoint + */ + constructor(win, signingServerURLs) { + /** @private @const {!Window} */ + this.win_ = win; + + /** @private @const {!Object} */ + this.signingServerURLs_ = signingServerURLs; + + /** + * The cache where all the public keys are stored. + * + * This field has a lot of internal structure and its type's a little hairy, + * so here's a rundown of what each piece means: + * - If Web Cryptography isn't available in the current browsing context, + * then the entire field is null. Since the keys are of no use, we don't + * fetch them. + * - Otherwise, it's a map-like `Object` from signing service names (as + * defined in the Fast Fetch config registry) to "signer" objects. + * - The `promise` property of each signer resolves to a boolean indicating + * whether the most recent attempt to fetch and import that signing + * service's public keys was successful. If the promise is still pending, + * then an attempt is currently in progress. This property is mutable; + * its value is replaced with a new promise when a new attempt is made. + * Invariant: only one attempt may be in progress at a time, so this + * property may not be mutated while the current promise is pending. + * - The `keys` property of each signer is a map-like `Object` from keypair + * IDs to nullable key promises. (This means that a property access on + * this object may evaluate to `undefined`, `null`, or a `Promise` + * object.) The `keys` object is internally mutable; new keys are added + * to it as they are fetched. Invariant: the `keys` object may be mutated + * only while the corresponding `promise` object is pending; this ensures + * that callbacks chained to `promise` may observe `keys` without being + * subject to race conditions. + * - If a key promise (i.e., the value of a property access on the `keys` + * object) is absent (i.e., `undefined`), then no key with that keypair + * ID is present (but this could be because of a stale cache). If it's + * null, then no key with that keypair ID could be found even after + * cachebusting. If it's a `Promise` that resolves to `null`, then key + * data for that keypair ID was found but could not be imported + * successfully; this most likely indicates signing service misbehavior. + * The success case is a `Promise` that resolves to a `CryptoKey`. + * + * @private @const {?Object, keys: !Object>}>} + */ + this.signers_ = Services.cryptoFor(win).isPkcsAvailable() ? {} : null; + + /** + * Gets a notion of current time, in ms. The value is not necessarily + * absolute, so should be used only for computing deltas. When available, + * the performance system will be used; otherwise Date.now() will be + * returned. + * + * @private @const {function(): number} + */ + this.getNow_ = (win.performance && win.performance.now) ? + win.performance.now.bind(win.performance) : Date.now; + } + + /** + * Fetches and imports the public keyset for the named signing service, + * without any cachebusting. Hopefully, this will hit cache in many cases + * and not make an actual network round-trip. This method should be called + * as early as possible, once it's known which signing service is likely to + * be used, so that the network request and key imports can execute in + * parallel with other operations. + * + * @param {string} signingServiceName + */ + loadKeyset(signingServiceName) { + if (this.signers_ && !this.signers_[signingServiceName]) { + const keys = {}; + const promise = this.fetchAndAddKeys_(keys, signingServiceName, null); + this.signers_[signingServiceName] = {promise, keys}; + } + } + + /** + * Extracts a cryptographic signature from `headers` and attempts to verify + * that it's the correct cryptographic signature for `creative`. + * + * As a precondition, `loadKeyset(signingServiceName)` must have already been + * called. + * + * @param {!ArrayBuffer} creative + * @param {!Headers} headers + * @return {!Promise} + */ + verify(creative, headers) { + const signatureFormat = + /^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+):([A-Za-z0-9+/]{341}[AQgw]==)$/; + if (!headers.has(AMP_SIGNATURE_HEADER)) { + return Promise.resolve(VerificationStatus.UNVERIFIED); + } + const headerValue = headers.get(AMP_SIGNATURE_HEADER); + const match = signatureFormat.exec(headerValue); + if (!match) { + // TODO(@taymonbeal, #9274): replace this with real error reporting + user().error( + 'AMP-A4A', `Invalid signature header: ${headerValue.split(':')[0]}`); + return Promise.resolve(VerificationStatus.ERROR_SIGNATURE_MISMATCH); + } + return this.verifyCreativeAndSignature( + match[1], match[2], base64DecodeToBytes(match[3]), creative); + } + + /** + * Verifies that `signature` is the correct cryptographic signature for + * `creative`, with the public key from the named signing service identified + * by `keypairId`. + * + * As a precondition, `loadKeyset(signingServiceName)` must have already been + * called. + * + * If the keyset for the named signing service was imported successfully but + * did not include a key for `keypairId`, this may be the result of a stale + * browser cache. To work around this, `keypairId` is added to the public key + * endpoint URL as a query parameter and the keyset is re-fetched. Other kinds + * of failures, including network connectivity failures, are not retried. + * + * @param {string} signingServiceName + * @param {string} keypairId + * @param {!Uint8Array} signature + * @param {!ArrayBuffer} creative + * @return {!Promise} + * @visibleForTesting + */ + verifyCreativeAndSignature( + signingServiceName, keypairId, signature, creative) { + if (!this.signers_) { + // Web Cryptography isn't available. + return Promise.resolve(VerificationStatus.CRYPTO_UNAVAILABLE); + } + const signer = this.signers_[signingServiceName]; + dev().assert( + signer, 'Keyset for service %s not loaded before verification', + signingServiceName); + return signer.promise.then(success => { + if (!success) { + // The public keyset couldn't be fetched and imported. Probably a + // network connectivity failure. + return VerificationStatus.UNVERIFIED; + } + const keyPromise = signer.keys[keypairId]; + if (keyPromise === undefined) { + // We don't have this key, but maybe the cache is stale; try + // cachebusting. + signer.promise = + this.fetchAndAddKeys_(signer.keys, signingServiceName, keypairId) + .then(success => { + if (signer.keys[keypairId] === undefined) { + // We still don't have this key; make sure we never try + // again. + signer.keys[keypairId] = null; + } + return success; + }); + // This "recursive" call can recurse at most once. + return this.verifyCreativeAndSignature( + signingServiceName, keypairId, signature, creative); + } else if (keyPromise === null) { + // We don't have this key and we already tried cachebusting. + return VerificationStatus.ERROR_KEY_NOT_FOUND; + } else { + return keyPromise.then(key => { + if (!key) { + // This particular public key couldn't be imported. Probably the + // signing service's fault. + return VerificationStatus.UNVERIFIED; + } + const crypto = Services.cryptoFor(this.win_); + return crypto.verifyPkcs(key, signature, creative).then( + result => result ? VerificationStatus.OK : + VerificationStatus.ERROR_SIGNATURE_MISMATCH, + err => { + // Web Cryptography rejected the verification attempt. This + // hopefully won't happen in the wild, but browsers can be weird + // about this, so we need to guard against the possibility. + // Phone home to the AMP Project so that we can understand why + // this occurred. + const message = err && err.message; + dev().error( + 'AMP-A4A', `Failed to verify signature: ${message}`); + return VerificationStatus.UNVERIFIED; + }); + }); + } + }); + } + + /** + * Try to download the keyset for the named signing service and add a promise + * for each key to the `keys` object. + * + * @param {!Object>} keys the object to + * add each key promise to. This is mutated while the returned promise is + * pending. + * @param {string} signingServiceName + * @param {?string} keypairId the keypair ID to include in the query string + * for cachebusting purposes, or `null` if no cachebusting is needed + * @return {!Promise} resolves after the mutation of `keys` is + * complete, to `true` if the keyset was downloaded and parsed + * successfully (even if some keys were malformed), or `false` if a + * keyset-level failure occurred + * @private + */ + fetchAndAddKeys_(keys, signingServiceName, keypairId) { + let url = this.signingServerURLs_[signingServiceName]; + if (keypairId != null) { + url += '?kid=' + encodeURIComponent(keypairId); + } + // TODO(@taymonbeal, #11088): consider a timeout on this fetch + return Services.xhrFor(this.win_) + .fetchJson(url, { + mode: 'cors', + method: 'GET', + // This should be cached across publisher domains, so don't append + // __amp_source_origin to the URL. + ampCors: false, + credentials: 'omit', + }).then( + response => { + // These are assertions on signing service behavior required by + // the spec. However, nothing terrible happens if they aren't met + // and there's no meaningful error recovery to be done if they + // fail, so we don't need to do them at runtime in production. + // They're included in dev mode as a debugging aid. + dev().assert( + response.status === 200, + 'Fast Fetch keyset spec requires status code 200'); + dev().assert( + response.headers.get('Content-Type') == + 'application/jwk-set+json', + 'Fast Fetch keyset spec requires Content-Type: ' + + 'application/jwk-set+json'); + return response.json().then( + jsonResponse => { + const jwkSet = /** @type {!JsonObject} */ (jsonResponse); + // This is supposed to be a JSON Web Key Set, as defined in + // Section 5 of RFC 7517. However, the signing service could + // misbehave and send an arbitrary JSON value, so we have to + // type-check at runtime. + if (!jwkSet || !isArray(jwkSet['keys'])) { + signingServiceError( + signingServiceName, + `Key set (${JSON.stringify(jwkSet)}) has no "keys"`); + return false; + } + jwkSet['keys'].forEach(jwk => { + if (!jwk || typeof jwk['kid'] != 'string') { + signingServiceError( + signingServiceName, + `Key (${JSON.stringify(jwk)}) has no "kid"`); + } else if (keys[jwk['kid']] === undefined) { + // We haven't seen this keypair ID before. + keys[jwk['kid']] = + Services.cryptoFor(this.win_).importPkcsKey(jwk) + .catch(err => { + // Web Cryptography rejected the key + // import attempt. Either the signing + // service sent a malformed key or the + // browser is doing something weird. + const jwkData = JSON.stringify(jwk); + const message = err && err.message; + signingServiceError( + signingServiceName, + `Failed to import key (${ + jwkData + }): ${message}`); + return null; + }); + } + }); + return true; + }, + err => { + // The signing service didn't send valid JSON. + signingServiceError( + signingServiceName, + `Failed to parse JSON: ${err && err.response}`); + return false; + }); + }, + err => { + // Some kind of error occurred during the XHR. This could be a lot + // of things (and we have no type information), but if there's no + // `response` it's probably a network connectivity failure, so we + // ignore it. Unfortunately, we can't distinguish this from a CORS + // problem. + if (err && err.response) { + // This probably indicates a non-2xx HTTP status code. + signingServiceError( + signingServiceName, `Status code ${err.response.status}`); + } + return false; + }); + } +} + +/** + * Report an error caused by a signing service. Since signing services currently + * don't have their own error logging URLs, we just send everything to the AMP + * Project. + * + * @param {string} signingServiceName + * @param {string} message + * @private + */ +function signingServiceError(signingServiceName, message) { + dev().error( + 'AMP-A4A', `Signing service error for ${signingServiceName}: ${message}`); +} diff --git a/extensions/amp-a4a/0.1/template-renderer.js b/extensions/amp-a4a/0.1/template-renderer.js new file mode 100644 index 000000000000..11f0512bc39e --- /dev/null +++ b/extensions/amp-a4a/0.1/template-renderer.js @@ -0,0 +1,82 @@ +/** + * Copyright 2018 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 {Renderer} from './amp-ad-type-defs'; +import {dev} from '../../../src/log'; +import {getAmpAdTemplateHelper} from './template-validator'; +import {renderCreativeIntoFriendlyFrame} from './friendly-frame-util'; + +/** + * @typedef {{ + * size: ./amp-ad-type-defs.LayoutInfoDef, + * adUrl: string, + * creativeMetadata: ./amp-ad-type-defs.CreativeMetaDataDef, + * templateData: ./amp-ad-type-defs.AmpTemplateCreativeDef, + * }} + */ +export let CreativeData; + + +/** + * Render AMP creative into FriendlyFrame via templatization. + */ +export class TemplateRenderer extends Renderer { + + /** + * Constructs a TemplateRenderer instance. + */ + constructor() { + super(); + } + + /** @override */ + render(context, element, creativeData) { + + creativeData = /** @type {CreativeData} */ (creativeData); + + const {size, adUrl} = context; + const {creativeMetadata} = creativeData; + + dev().assert(size, 'missing creative size'); + dev().assert(adUrl, 'missing ad request url'); + + return renderCreativeIntoFriendlyFrame( + adUrl, size, element, creativeMetadata) + .then(iframe => { + const templateData = + /** @type {!./amp-ad-type-defs.AmpTemplateCreativeDef} */ ( + creativeData.templateData); + const {data} = templateData; + if (!data) { + return Promise.resolve(); + } + const templateHelper = getAmpAdTemplateHelper(context.win); + return templateHelper + .render(data, iframe.contentWindow.document.body) + .then(renderedElement => { + const {analytics} = templateData; + if (analytics) { + templateHelper.insertAnalytics(renderedElement, analytics); + } + // This element must exist, or #render() would have thrown. + const templateElement = iframe.contentWindow.document + .querySelector('template'); + templateElement.parentNode + .replaceChild(renderedElement, templateElement); + }); + }); + } +} diff --git a/extensions/amp-a4a/0.1/template-validator.js b/extensions/amp-a4a/0.1/template-validator.js new file mode 100644 index 000000000000..f82e139c394d --- /dev/null +++ b/extensions/amp-a4a/0.1/template-validator.js @@ -0,0 +1,104 @@ +/** + * Copyright 2018 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 { + AdResponseType, + Validator, + ValidatorResult, +} from './amp-ad-type-defs'; +import {AmpAdTemplateHelper} from '../../amp-a4a/0.1/amp-ad-template-helper'; +import {Services} from '../../../src/services'; +import {getAmpAdMetadata} from './amp-ad-utils'; +import {pushIfNotExist} from '../../../src/utils/array'; +import {tryParseJson} from '../../../src/json'; +import {utf8Decode} from '../../../src/utils/bytes'; + +/** @const {string} */ +export const AMP_TEMPLATED_CREATIVE_HEADER_NAME = 'AMP-Ad-Template-Extension'; +export const DEPRECATED_AMP_TEMPLATED_CREATIVE_HEADER_NAME = + 'AMP-template-amp-creative'; + +/** @type {?AmpAdTemplateHelper} */ +let ampAdTemplateHelper; + +/** + * Returns the global template helper. + * @param {!Window} win + * @return {!AmpAdTemplateHelper} + */ +export function getAmpAdTemplateHelper(win) { + return ampAdTemplateHelper || + (ampAdTemplateHelper = new AmpAdTemplateHelper(win)); +} + +/** + * Validator for Template ads. + */ +export class TemplateValidator extends Validator { + /** @override */ + validate(context, unvalidatedBytes, headers) { + + const body = utf8Decode(/** @type {!ArrayBuffer} */ (unvalidatedBytes)); + const parsedResponseBody = + /** @type {./amp-ad-type-defs.AmpTemplateCreativeDef} */ ( + tryParseJson(body)); + + // If we're missing the relevant header, or headers altogether, we cannot + // proceed. In this case, we return a NON_AMP response, since we cannot + // ensure this template will be valid AMP. We will pass the body of the + // response as the creative, and downstream renderers may attempt to render + // it as a non-AMP creative within a cross-domain iframe. + if (!parsedResponseBody || !headers || + (headers.get(AMP_TEMPLATED_CREATIVE_HEADER_NAME) !== 'amp-mustache' && + headers.get(DEPRECATED_AMP_TEMPLATED_CREATIVE_HEADER_NAME) !== + 'amp-mustache')) { + return Promise.resolve( + /** @type {!./amp-ad-type-defs.ValidatorOutput} */ ({ + creativeData: { + creative: body, + }, + adResponseType: AdResponseType.TEMPLATE, + type: ValidatorResult.NON_AMP, + })); + } + + return getAmpAdTemplateHelper(context.win) + .fetch(parsedResponseBody.templateUrl) + .then(template => { + const creativeMetadata = getAmpAdMetadata(template); + if (parsedResponseBody.analytics) { + pushIfNotExist( + creativeMetadata['customElementExtensions'], 'amp-analytics'); + } + pushIfNotExist( + creativeMetadata['customElementExtensions'], 'amp-mustache'); + + const extensions = Services.extensionsFor(context.win); + creativeMetadata.customElementExtensions.forEach( + extensionId => extensions./*OK*/preloadExtension(extensionId)); + // TODO(levitzky) Add preload logic for fonts / images. + return Promise.resolve( + /** @type {!./amp-ad-type-defs.ValidatorOutput} */ ({ + creativeData: { + templateData: parsedResponseBody, + creativeMetadata, + }, + adResponseType: AdResponseType.TEMPLATE, + type: ValidatorResult.AMP, + })); + }); + } +} diff --git a/extensions/amp-a4a/0.1/test/fetch-mock.js b/extensions/amp-a4a/0.1/test/fetch-mock.js new file mode 100644 index 000000000000..1a07d00822a6 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/fetch-mock.js @@ -0,0 +1,140 @@ +/** + * 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. + */ + +/** + * @fileoverview + * @deprecated Do not use this in new code. Use env.fetchMock instead. Its API + * is a superset of this one. TODO(@taymonbeal, #11066): Migrate all + * existing users and then delete this file. + */ + +/** + * @typedef {(?string|{ + * body: ?string, + * status: (number|undefined), + * headers: (!Object|undefined), + * })} + */ +export let MockResponseData; + +/** @typedef {(!MockResponseData|!Promise)} */ +export let MockResponseTiming; + +/** @typedef {(!MockResponseTiming|function(): !MockResponseTiming)} */ +export let MockResponse; + +/** + * A stub for `window.fetch`, facilitating hermetic testing of code that uses + * it. The window is stubbed when this class's constructor is called. + */ +export class FetchMock { + + /** @param {!Window} win */ + constructor(win) { + /** @private {!Window} */ + this.win_ = win; + /** @private {function(!RequestInfo, !RequestInit=): !Promise} */ + this.realFetch_ = win.fetch; + /** @private {!Object} */ + this.routes_ = {}; + /** @private {!Object} */ + this.names_ = {}; + + win.fetch = (input, init = undefined) => this.fetch_(input, init); + } + + /** + * Unstubs the window object and restores the real `window.fetch`. + */ + restore() { + this.win_.fetch = this.realFetch_; + this.routes_ = {}; + } + + /** + * Specifies that up to one GET request may be made to the given URL during + * the current test, and defines the response to return. + * + * @param {string} url the URL that the request is made to + * @param {!MockResponse} response the response to return + * @param {{name: string}=} options if provided, specifies a name that the + * caller may later use with the `called` method + */ + getOnce(url, response, options) { + if (url in this.routes_) { + throw new Error('route already defined for ' + url); + } + this.routes_[url] = {response, called: false}; + if (options) { + this.names_[options.name] = this.routes_[url]; + } + } + + /** + * Returns whether a particular URL specified by an earlier call to `getOnce` + * was ever actually used for a GET request. + * + * @param {string} name the name of the route passed in `options` + * @return {boolean} whether a request has been made to the given route. + */ + called(name) { + if (!(name in this.names_)) { + throw new Error('no route named ' + name); + } + return this.names_[name].called; + } + + /** + * Imitates the functionality of `window.fetch`. + * + * @param {!RequestInfo} input + * @param {(!RequestInit|undefined)} init + * @return {!Promise} + * @private + */ + fetch_(input, init) { + const {url} = new Request(input, init); + const route = this.routes_[url]; + if (!route) { + throw new Error('no route defined for ' + url); + } + if (route.called) { + throw new Error('route called twice for ' + url); + } + route.called = true; + return Promise.resolve( + typeof route.response == 'function' ? + route.response() : route.response) + .then(data => { + if (data === null || typeof data == 'string') { + return new Response(data); + } else { + const {body, status, headers} = data; + return new Response(body, /** @type {!ResponseInit} */ ( + {status, headers})); + } + }); + } +} + +/** + * Simulates a network connectivity or CORS failure. + * + * @return {!Error} an object that can be used as a rejection value + */ +export function networkFailure() { + return new TypeError('Failed to fetch'); +} 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..10c413ea1349 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/test-a4a-integration.js @@ -0,0 +1,221 @@ +/** + * 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. + */ + +// Need the following side-effect import because in actual production code, +// Fast Fetch impls are always loaded via an AmpAd tag, which means AmpAd is +// always available for them. However, when we test an impl in isolation, +// AmpAd is not loaded already, so we need to load it separately. +import '../../../amp-ad/0.1/amp-ad'; +import '../../../amp-ad/0.1/amp-ad-xorigin-iframe-handler'; +import {AMP_SIGNATURE_HEADER} from '../signature-verifier'; +import {FetchMock, networkFailure} from './fetch-mock'; +import {MockA4AImpl, TEST_URL} from './utils'; +import {createIframePromise} from '../../../../testing/iframe'; +import {getA4ARegistry, signingServerURLs} from '../../../../ads/_a4a-config'; +import {installCryptoService} from '../../../../src/service/crypto-impl'; +import {installDocService} from '../../../../src/service/ampdoc-impl'; +import {loadPromise} from '../../../../src/event-helper'; +import { + resetScheduledElementForTesting, + upgradeOrRegisterElement, +} from '../../../../src/service/custom-element-registry'; +import { + data as validCSSAmp, +} from './testdata/valid_css_at_rules_amp.reserialized'; + +// 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 fixture; + let fetchMock; + let adResponse; + let a4aElement; + let a4aRegistry; + beforeEach(() => { + sandbox = sinon.sandbox; + a4aRegistry = getA4ARegistry(); + a4aRegistry['mock'] = () => {return true;}; + return createIframePromise().then(f => { + fixture = f; + fetchMock = new FetchMock(fixture.win); + for (const serviceName in signingServerURLs) { + fetchMock.getOnce(signingServerURLs[serviceName], { + body: validCSSAmp.publicKeyset, + headers: {'Content-Type': 'application/jwk-set+json'}, + }); + } + fetchMock.getOnce( + TEST_URL + '&__amp_source_origin=about%3Asrcdoc', () => adResponse, + {name: 'ad'}); + adResponse = { + headers: {'AMP-Access-Control-Allow-Source-Origin': 'about:srcdoc'}, + body: validCSSAmp.reserialized, + }; + adResponse.headers[AMP_SIGNATURE_HEADER] = validCSSAmp.signatureHeader; + installDocService(fixture.win, /* isSingleDoc */ true); + installCryptoService(fixture.win); + upgradeOrRegisterElement(fixture.win, 'amp-a4a', MockA4AImpl); + const {doc} = fixture; + a4aElement = doc.createElement('amp-a4a'); + a4aElement.setAttribute('width', 200); + a4aElement.setAttribute('height', 50); + a4aElement.setAttribute('type', 'mock'); + }); + }); + + afterEach(() => { + fetchMock./*OK*/restore(); + sandbox.restore(); + resetScheduledElementForTesting(window, 'amp-a4a'); + 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 adResponse.headers[AMP_SIGNATURE_HEADER]; + return fixture.addElement(a4aElement).then(unusedElement => { + expectRenderedInXDomainIframe(a4aElement, TEST_URL); + }); + }); + + it('should not send request if display none', () => { + a4aElement.style.display = 'none'; + return fixture.addElement(a4aElement).then(element => { + expect(fetchMock.called('ad')).to.be.false; + expect(element.querySelector('iframe')).to.not.be.ok; + }); + }); + + it('should fall back to 3p when the XHR fails', () => { + adResponse = Promise.reject(networkFailure()); + // 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. + const forceCollapseStub = + sandbox.spy(MockA4AImpl.prototype, 'forceCollapse'); + 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); + expect(forceCollapseStub).to.not.be.called; + }); + }); + + it('should collapse slot when creative response has code 204', () => { + adResponse.status = 204; + adResponse.body = null; + const forceCollapseStub = + sandbox.spy(MockA4AImpl.prototype, 'forceCollapse'); + return fixture.addElement(a4aElement).then(() => { + expect(forceCollapseStub).to.be.calledOnce; + }); + }); + + it('should collapse slot when creative response.arrayBuffer() is empty', + () => { + adResponse.body = ''; + const forceCollapseStub = + sandbox.spy(MockA4AImpl.prototype, 'forceCollapse'); + return fixture.addElement(a4aElement).then(unusedElement => { + expect(forceCollapseStub).to.be.calledOnce; + }); + }); + + it('should continue to show old creative after refresh and no fill', () => { + return fixture.addElement(a4aElement).then(() => { + return expectRenderedInFriendlyIframe(a4aElement, 'Hello, world.') + .then(() => { + const a4a = new MockA4AImpl(a4aElement); + const initiateAdRequestMock = sandbox.stub( + MockA4AImpl.prototype, 'initiateAdRequest').callsFake( + () => { + a4a.adPromise_ = Promise.resolve(); + // This simulates calling forceCollapse, without tripping + // up any unrelated asserts. + a4a.isRefreshing = false; + }); + const tearDownSlotMock = + sandbox.stub(MockA4AImpl.prototype, 'tearDownSlot'); + tearDownSlotMock.returns(undefined); + const destroyFrameSpy = + sandbox.spy(MockA4AImpl.prototype, 'destroyFrame'); + const callback = sandbox.spy(); + return a4a.refresh(callback).then(() => { + expect(initiateAdRequestMock).to.be.called; + expect(tearDownSlotMock).to.be.called; + expect(destroyFrameSpy).to.not.be.called; + expect(callback).to.be.called; + }); + }); + }); + }); + + // 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..bf91df4d63dd --- /dev/null +++ b/extensions/amp-a4a/0.1/test/test-a4a-var-source.js @@ -0,0 +1,102 @@ +/** + * 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 {A4AVariableSource} from '../a4a-variable-source'; +import {createIframePromise} from '../../../../testing/iframe'; +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/); + }); + + it('should replace HTML_ATTR', () => { + expect(expandSync('HTML_ATTR', ['div', 'id'])).to.equal( + '[{\"id\":\"parent\"}]'); + }); + + it('should replace CLIENT_ID with null', () => { + return expect(expandSync('CLIENT_ID', ['a'])).to.be.null; + }); + + 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..158b78c173f6 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/test-amp-a4a.js @@ -0,0 +1,2583 @@ +/** + * 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 '../../../../extensions/amp-ad/0.1/amp-ad-xorigin-iframe-handler'; +// Need the following side-effect import because in actual production code, +// Fast Fetch impls are always loaded via an AmpAd tag, which means AmpAd is +// always available for them. However, when we test an impl in isolation, +// AmpAd is not loaded already, so we need to load it separately. +import '../../../amp-ad/0.1/amp-ad'; +// The following namespaces are imported so that we can stub and spy on certain +// methods in tests. +import * as analytics from '../../../../src/analytics'; +import * as analyticsExtension from '../../../../src/extension-analytics'; +import {AMP_SIGNATURE_HEADER, VerificationStatus} from '../signature-verifier'; +import { + AmpA4A, + CREATIVE_SIZE_HEADER, + DEFAULT_SAFEFRAME_VERSION, + EXPERIMENT_FEATURE_HEADER_NAME, + INVALID_SPSA_RESPONSE, + RENDERING_TYPE_HEADER, + SAFEFRAME_VERSION_HEADER, + assignAdUrlToError, + protectFunctionWrapper, +} from '../amp-a4a'; +import {CONSENT_POLICY_STATE} from '../../../../src/consent-state'; +import {Extensions} from '../../../../src/service/extensions-impl'; +import {FetchMock, networkFailure} from './fetch-mock'; +import {FriendlyIframeEmbed} from '../../../../src/friendly-iframe-embed'; +import {LayoutPriority} from '../../../../src/layout'; +import {MockA4AImpl, TEST_URL} from './utils'; +import { + RealTimeConfigManager, +} from '../real-time-config-manager'; +import {Services} from '../../../../src/services'; +import {Signals} from '../../../../src/utils/signals'; +import {Viewer} from '../../../../src/service/viewer-impl'; +import {cancellation} from '../../../../src/error'; +import {createElementWithAttributes} from '../../../../src/dom'; +import {createIframePromise} from '../../../../testing/iframe'; +import {dev, user} from '../../../../src/log'; +import { + incrementLoadingAds, + is3pThrottled, +} from '../../../amp-ad/0.1/concurrent-load'; +import {installDocService} from '../../../../src/service/ampdoc-impl'; +import {layoutRectLtwh} from '../../../../src/layout-rect'; +import { + resetScheduledElementForTesting, +} from '../../../../src/service/custom-element-registry'; +import {data as testFragments} from './testdata/test_fragments'; +import {toggleExperiment} from '../../../../src/experiments'; +import { + data as validCSSAmp, +} from './testdata/valid_css_at_rules_amp.reserialized'; + +describe('amp-a4a', () => { + const IFRAME_SANDBOXING_FLAGS = ['allow-forms', 'allow-modals', + 'allow-pointer-lock', 'allow-popups', 'allow-popups-to-escape-sandbox', + 'allow-same-origin', 'allow-scripts', + 'allow-top-navigation-by-user-activation']; + + let sandbox; + let fetchMock; + let getSigningServiceNamesMock; + let viewerWhenVisibleMock; + let adResponse; + let onCreativeRenderSpy; + let getResourceStub; + + beforeEach(() => { + sandbox = sinon.sandbox; + fetchMock = null; + getSigningServiceNamesMock = sandbox.stub(AmpA4A.prototype, + 'getSigningServiceNames'); + onCreativeRenderSpy = + sandbox.spy(AmpA4A.prototype, 'onCreativeRender'); + getSigningServiceNamesMock.returns(['google']); + viewerWhenVisibleMock = sandbox.stub(Viewer.prototype, 'whenFirstVisible'); + viewerWhenVisibleMock.returns(Promise.resolve()); + getResourceStub = sandbox.stub(AmpA4A.prototype, 'getResource'); + getResourceStub.returns({ + getUpgradeDelayMs: () => 12345, + }); + adResponse = { + headers: { + 'AMP-Access-Control-Allow-Source-Origin': 'about:srcdoc', + 'AMP-Fast-Fetch-Signature': validCSSAmp.signatureHeader, + }, + body: validCSSAmp.reserialized, + }; + adResponse.headers[AMP_SIGNATURE_HEADER] = validCSSAmp.signatureHeader; + }); + + afterEach(() => { + if (fetchMock) { + fetchMock./*OK*/restore(); + fetchMock = null; + } + sandbox.restore(); + resetScheduledElementForTesting(window, 'amp-a4a'); + }); + + /** + * Sets up testing by loading iframe within which test runs. + * @param {FixtureInterface} fixture + */ + function setupForAdTesting(fixture) { + expect(fetchMock).to.be.null; + fetchMock = new FetchMock(fixture.win); + fetchMock.getOnce( + 'https://cdn.ampproject.org/amp-ad-verifying-keyset.json', { + body: validCSSAmp.publicKeyset, + status: 200, + headers: {'Content-Type': 'application/jwk-set+json'}, + }); + installDocService(fixture.win, /* isSingleDoc */ true); + const {doc} = fixture; + // 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); + } + + /** + * @param {!Document} doc + * @param {Rect=} opt_rect + * @param {Element=} opt_body + */ + function createA4aElement(doc, opt_rect, opt_body) { + const element = createElementWithAttributes(doc, 'amp-a4a', { + 'width': opt_rect ? String(opt_rect.width) : '200', + 'height': opt_rect ? String(opt_rect.height) : '50', + 'type': 'adsense', + }); + element.getAmpDoc = () => { + const ampdocService = Services.ampdocServiceFor(doc.defaultView); + return ampdocService.getAmpDoc(element); + }; + element.isBuilt = () => {return true;}; + element.getLayoutBox = () => { + return opt_rect || layoutRectLtwh(0, 0, 200, 50); + }; + element.getPageLayoutBox = () => { + return element.getLayoutBox.apply(element, arguments); + }; + element.getIntersectionChangeEntry = () => {return null;}; + const signals = new Signals(); + element.signals = () => signals; + element.renderStarted = () => { + signals.signal('render-start'); + }; + (opt_body || doc.body).appendChild(element); + return element; + } + + /** + * @param {Object=} opt_additionalInfo + * @return {string} + */ + function buildCreativeString(opt_additionalInfo) { + const baseTestDoc = testFragments.minimalDocOneStyle; + const offsets = Object.assign({}, 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..1021b5cfb4cd --- /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..35b3248a664c --- /dev/null +++ b/extensions/amp-a4a/0.1/test/testdata/valid_css_at_rules_amp.reserialized.js @@ -0,0 +1,139 @@ +// Reserialized version of valid_css_at_rules_amp.original.html. +// Automatically generated by script. Do not edit directly. + +export const data = { + reserialized: `Hello, world. + +`, + + minifiedCreative: 'Hello, world.\n\n', + + + reserializedInvalidOffset: `Hello, world. + +`, + + reserializedMissingScriptTag: `Hello, world. + + + + + + + +`, + + minifiedTemplateCreative: '\n \n \n \n \n \n', + + original: ` + + + + + + + + + + + +Hello, world. +`, + + signatureHeader: 'google:1:vpiimZfnOmH+PjI8oTF9ujhrNyHYNrTW4nY8sP3CWtPIdlP/pxSJ/UeMFQznTCM12Cq56Qgz1mA0VQuPrgNBIp3olSkClQks5CpAuq43P/wXvs3yML8YhQRgUZMCdIpUSW75wW22fcXRtwfR30KRRj8kMjX/QvmWF3H3phah06HiCREl0ONGyxtgyjJkFkPHtfPGGK2vMYMHqHybnsIDsP+zsvyQBPsiMCku8H+i5KtHrjWvVXj/GdcnHL25wvJiyC4qHwkMEoW8ridsDYNQ3dWQD4bVNS1D7gIRvzAM9ZlCBpZcxVhoO5B+mMuIOqq0YTnfgXaT2QgVnHqavpbu9w==', + + publicKeyset: '{"keys":[{"alg":"RS256","e":"AQAB","ext":true,"kid":"1","kty":"RSA","n":"z43rjaJ9PLk1FHMEL31_ILXGtUTN03rxJ9amD9y3BRDpbTA-GkUKiQM07xAd8OXPUZRqcjvXQfc7b1RCEtwrcfx9oBRdF78QMA4tLLCqSHP0tSuqYF0fA7-GyTFWDcYzey90jRFNNWxjzKrvSazacE0TvJ8S_AVP4EV67VdbByCC1tpBzLhhy7RFHp2cXGTpWYUqZUAVUdJoeBuCho_zQz2au7c6sDaLiF-uYL9Td9MrZ6tSLo3MeMIZia4WgWqjTDICR0h-zlbHUd0K9CoXbGTt5nvkebXHmbKd99ma6zRYVlYNJTuSqsRCBNYtCTFVHIZeBlkjHKsQ46HTZPexZw"}]}', +}; 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..0c8e61d80d17 --- /dev/null +++ b/extensions/amp-a4a/0.1/test/utils.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. + */ + +import {AmpA4A} from '../amp-a4a'; +import {dict} from '../../../../src/utils/object'; + +/** @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); + } + + updateLayoutPriority() { + // Do nothing. + } + + getFallback() { + return null; + } + + toggleFallback() { + // Do nothing. + } + + mutateElement(callback) { + callback(); + } + + /** @override */ + getPreconnectUrls() { + return ['https://googleads.g.doubleclick.net']; + } + + /** @override */ + getA4aAnalyticsConfig() { + return dict(); + } +} 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/RTCDiagram.png b/extensions/amp-a4a/RTCDiagram.png new file mode 100644 index 000000000000..1c26cf5ac831 Binary files /dev/null and b/extensions/amp-a4a/RTCDiagram.png differ diff --git a/extensions/amp-a4a/RTCDiagram2.png b/extensions/amp-a4a/RTCDiagram2.png new file mode 100644 index 000000000000..e0d76d4b1be8 Binary files /dev/null and b/extensions/amp-a4a/RTCDiagram2.png differ diff --git a/extensions/amp-a4a/amp-a4a-format.md b/extensions/amp-a4a/amp-a4a-format.md new file mode 100644 index 000000000000..4d5fae900da1 --- /dev/null +++ b/extensions/amp-a4a/amp-a4a-format.md @@ -0,0 +1,458 @@ + + + +# AMPHTML Ad Creative Format + +_If you'd like to propose changes to the standard, please comment on the [Intent +to Implement](https://github.com/ampproject/amphtml/issues/4264)_. + +AMPHTML ads is a mechanism for rendering fast, +performant ads in AMP pages. To ensure that AMPHTML ad documents ("AMP +creatives") can be rendered quickly and smoothly in the browser and do +not degrade user experience, AMP creatives must obey a set of validation +rules. Similar in spirit to the +[AMP format rules](https://www.ampproject.org/docs/fundamentals/spec.html), AMPHTML ads have +access to a limited set of allowed tags, capabilities, and extensions. + +**Table of Contents** + +* [AMPHTML ad format rules](#amphtml-ad-format-rules) + * [Boilerplate](#boilerplate) + * [CSS](#css) + * [CSS animations and transitions](#css-animations-and-transitions) + * [Selectors](#selectors) + * [Transitionable and animatable properties](#transitionable-and-animatable-properties) + * [Allowed AMP extensions and builtins](#allowed-amp-extensions-and-builtins) + * [HTML tags](#html-tags) + +## AMPHTML ad format rules + +Unless otherwise specified below, the creative must obey all rules given by the +[AMP format rules](https://www.ampproject.org/docs/fundamentals/spec.html), +included here by reference. For example, the AMPHTML ad [Boilerplate](#boilerplate) deviates from the AMP standard boilerplate. + +In addition, creatives must obey the following rules: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RuleRationale
    Must use <html ⚡4ads> or <html amp4ads> as its enclosing tags.Allows validators to identify a creative document as either a general AMP doc or a restricted AMPHTML ad doc and to dispatch appropriately.
    Must include <script async src="https://cdn.ampproject.org/amp4ads-v0.js"></script> as the runtime script instead of https://cdn.ampproject.org/v0.js.Allows tailored runtime behaviors for AMPHTML ads served in cross-origin iframes.
    Must not include a <link rel="canonical"> tag.Ad creatives don't have a "non-AMP canonical version" and won't be independently search-indexed, so self-referencing would be useless.
    Can include optional meta tags in HTML head as identifiers, in the format of <meta name="amp4ads-id" content="vendor=${vendor},type=${type},id=${id}">. Those meta tags must be placed before the amp4ads-v0.js script. The value of vendor and id are strings containing only [0-9a-zA-Z_-]. The value of type is either creative-id or impression-id.Those custom identifiers can be used to identify the impression or the creative. They can be helpful for reporting and debugging.

    Example:

    +<meta name="amp4ads-id"
    +  content="vendor=adsense,type=creative-id,id=1283474">
    +<meta name="amp4ads-id"
    +  content="vendor=adsense,type=impression-id,id=xIsjdf921S">
    <amp-analytics> viewability tracking may only target the full-ad selector, via "visibilitySpec": { "selector": "amp-ad" } as defined in Issue #4018 and PR #4368. In particular, it may not target any selectors for elements within the ad creative.In some cases, AMPHTML ads 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:

    +
    +<amp-analytics id="nestedAnalytics">
    +  <script type="application/json">
    +  {
    +    "requests": {
    +      "visibility": "https://example.com/nestedAmpAnalytics"
    +    },
    +    "triggers": {
    +      "visibilitySpec": {
    +      "selector": "amp-ad",
    +      "visiblePercentageMin": 50,
    +      "continuousTimeMin": 1000
    +      }
    +    }
    +  }
    +  </script>
    +</amp-analytics>
    +
    +

    This configuration sends a request to the https://example.com/nestedAmpAnalytics URL when 50% of the enclosing ad has been continuously visible on the screen for 1 second.

    +
    + +### Boilerplate + +AMPHTML ad 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 AMPHTML ads, however, if Javascript is entirely +disabled, AMPHTML ads won't run and no ad will ever be shown, so there is no need for +the `