├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── renovate.json ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── vmap-js-node.js └── vmap-js.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── spec ├── adbreak.spec.js ├── adsource.spec.js ├── parser_utils.spec.js ├── samples │ ├── correct-vmap-with-extension.xml │ ├── correct-vmap.xml │ ├── incorrect-vmap.xml │ └── parsing-example.xml ├── support │ └── jasmine.json ├── utils.js └── vmap.spec.js └── src ├── adbreak.js ├── adsource.js ├── index.js ├── parser_utils.js └── vmap.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "ie": "9", 8 | "safari": "8" 9 | }, 10 | "useBuiltIns": false 11 | } 12 | ] 13 | ], 14 | "exclude": ["node_modules/**"] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | !.eslintrc.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | jasmine: true, 7 | }, 8 | parser: '@babel/eslint-parser', 9 | parserOptions: { 10 | sourceType: 'module', 11 | }, 12 | globals: { 13 | readFixtures: false, 14 | }, 15 | plugins: ['import', 'no-for-of-loops'], 16 | settings: { 17 | 'import/extensions': ['.js'], 18 | }, 19 | root: true, 20 | rules: { 21 | 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], 22 | 'import/named': ['error'], 23 | 'import/no-unresolved': ['error'], 24 | 'import/newline-after-import': ['warn'], 25 | 'linebreak-style': ['warn', 'unix'], 26 | 'eol-last': ['warn', 'always'], 27 | 'no-console': ['error'], 28 | 'no-else-return': ['warn'], 29 | 'no-for-of-loops/no-for-of-loops': ['error'], 30 | 'no-multi-spaces': [ 31 | 'warn', 32 | { 33 | exceptions: { VariableDeclarator: true, ImportDeclaration: true }, 34 | }, 35 | ], 36 | 'no-trailing-spaces': ['warn'], 37 | 'no-var': ['warn'], 38 | 'no-undef': ['error'], 39 | 'no-unused-vars': [ 40 | 'error', 41 | { argsIgnorePattern: '^_', ignoreRestSiblings: true, args: 'after-used' }, 42 | ], 43 | 'object-shorthand': ['warn'], 44 | 'prefer-template': ['warn'], 45 | 'prefer-const': ['warn'], 46 | 'prefer-rest-params': ['error'], 47 | 'prefer-spread': ['warn'], 48 | 'space-before-function-paren': ['warn', 'never'], 49 | 'no-debugger': ['error'], 50 | 'no-shadow': ['error'], 51 | 'getter-return': ['error'], 52 | 'no-extra-semi': ['warn'], 53 | 'no-unreachable': ['error'], 54 | 'valid-typeof': ['error'], 55 | eqeqeq: ['error'], 56 | 'no-alert': ['error'], 57 | 'no-self-assign': ['error'], 58 | 'no-self-compare': ['error'], 59 | 'no-useless-return': ['warn'], 60 | 'no-const-assign': ['error'], 61 | 'no-duplicate-imports': ['error'], 62 | 'no-multiple-empty-lines': ['warn'], 63 | 'no-restricted-globals': ['error', 'fdescribe', 'fit', 'xdescribe', 'xit'], 64 | 'no-constant-condition': ['error'], 65 | 'no-func-assign': ['error'], 66 | 'no-unexpected-multiline': ['error'], 67 | 'no-unsafe-finally': ['error'], 68 | 'no-unsafe-negation': ['error'], 69 | 'no-dupe-args': ['error'], 70 | 'no-dupe-keys': ['error'], 71 | 'no-duplicate-case': ['error'], 72 | 'no-ex-assign': ['error'], 73 | 'no-cond-assign': ['error'], 74 | 'no-compare-neg-zero': ['error'], 75 | 'no-control-regex': ['error'], 76 | 'no-invalid-regexp': ['error'], 77 | 'no-obj-calls': ['error'], 78 | 'no-sparse-arrays': ['error'], 79 | 'no-global-assign': ['error'], 80 | 'no-new-func': ['error'], 81 | 'no-new-wrappers': ['error'], 82 | 'no-param-reassign': ['error', { props: true }], 83 | 'no-proto': ['error'], 84 | 'no-redeclare': ['error'], 85 | 'no-return-assign': ['error'], 86 | 'no-octal': ['error'], 87 | 'no-sequences': ['error'], 88 | 'no-unmodified-loop-condition': ['error'], 89 | 'no-unused-expressions': ['error'], 90 | 'no-unused-labels': ['error'], 91 | 'no-useless-call': ['error'], 92 | 'no-useless-escape': ['error'], 93 | 'no-void': ['error'], 94 | 'no-with': ['error'], 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>dailymotion/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/* 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.md 3 | *.json 4 | .travis.yml 5 | .babelrc 6 | .prettierrc 7 | .gitignore 8 | test/*.xml 9 | vmap-js.js 10 | dist/* 11 | LICENSE 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | parser: 'flow', 4 | tabWidth: 2, 5 | singleQuote: true, 6 | bracketSpacing: true, 7 | arrowParens: 'always', 8 | trailingComma: 'es5', 9 | semi: true, 10 | }; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8.9.4 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Olivier Poitrey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VMAP Javascript Library 2 | 3 | [![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://travis-ci.org/dailymotion/vmap-js) 4 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![npm version](https://badge.fury.io/js/vmap.svg)](https://badge.fury.io/js/vmap) 7 | 8 | Parse a VMAP XML document to Javascript object. Complies with [VMAP 1.0.1 spec](http://www.iab.net/media/file/VMAP.pdf). 9 | 10 | ## Installation 11 | 12 | Install with npm 13 | ``` 14 | npm install @dailymotion/vmap 15 | ``` 16 | 17 | ## Usage 18 | 19 | Provide the `VMAP` constructor an XML in order to have a parsed version of it. 20 | 21 | Access `VMAP` properties using the APIs documented below. 22 | 23 | ``` javascript 24 | import VMAP from '@dailymotion/vmap'; 25 | 26 | // Fetch VMAP as XML 27 | fetch(vmapURL) 28 | .then(response => response.text()) 29 | .then(xmlText => { 30 | // Get a parsed VMAP object 31 | const parser = new DOMParser(); 32 | const xmlDoc = parser.parseFromString(xmlText, 'text/xml'); 33 | const vmap = new VMAP(xmlDoc); 34 | }) 35 | .catch(error => { 36 | console.error('Error fetching VMAP:', error); 37 | }); 38 | ``` 39 | 40 | ## API 41 | 42 | ### VMAP 43 | 44 | #### Properties 45 | 46 | * `version`: The VMAP version (should be 1.0). 47 | * `adBreaks`: An array of `VMAPAdBreak` objects. 48 | * `extensions`: An array of `Object` with 49 | * `children`: `Object` containing all this extension children and their name as the key 50 | * `attribute`: `Object` containing all this extension attributes and their name as the key 51 | * `value`: `Object` parsed from CDATA or as a fallback all of the text nodes of this extension concatenated 52 | 53 | ### VMAPAdBreak 54 | 55 | Provides information about an ad break. 56 | 57 | #### Properties 58 | 59 | * `timeOffset`: Represents the timing of the ad break. 60 | * `breakType`: Identifies whether the ad break allows "linear", "nonlinear" or "display" ads. 61 | * `breakId`: An optional string identifier for the ad break. 62 | * `repeatAfter`: An option used to distribute ad breaks equally spaced apart from one another along a linear timeline. 63 | * `adSource`: A `VMAPAdSource` object. 64 | * `trackingEvents`: An array of `Object` with tracking URLs 65 | * `event`: The name of the event to track for the element. Can be one of breakStart, breakEnd or error. 66 | * `uri`: The URI of the tracker. 67 | * `extensions`: An array of `Object` with 68 | * `children`: `Object` containing all this extension children and their name as the key 69 | * `attribute`: `Object` containing all this extension attributes and their name as the key 70 | * `value`: `Object` parsed from CDATA or as a fallback all of the text nodes of this extension concatenated 71 | 72 | #### Methods 73 | 74 | * `track(event, errorCode)`: Call the trackers for the given event with an option error code parameter for `error` events. 75 | 76 | ### VMAPAdSource 77 | 78 | Provides the player with either an inline ad response or a reference to an ad response. 79 | 80 | #### Properties 81 | 82 | * `id`: Ad identifier for the ad source. 83 | * `allowMultipleAds`: Indicates whether a VAST ad pod or multple buffet of ads can be served into an ad break. 84 | * `followRedirects`: Indicates whether the video player should honor the redirects within an ad response. 85 | * `vastAdData`: Contains an embedded VAST response. 86 | * `adTagURI`: Contains a URI to the VAST. 87 | * `customData`: Contains custom ad data. 88 | 89 | ## Support and compatibility 90 | The library is 100% written in JavaScript and the source code uses modern features like `modules`, `classes`, ecc... . Make sure your environment supports these features, or transpile the library when bundling your project. 91 | 92 | ### Pre-bundled versions 93 | We provide several pre-bundled versions of the library (see [`dist` directory](dist/)) 94 | 95 | #### Browser 96 | A pre-bundled version of VMAP-jsis available: [`vmap-js.js`](dist/vmap-js.js). 97 | 98 | You can add the script directly to your page and access the library through the `VMAP` constructor. 99 | 100 | ```html 101 | 102 | ``` 103 | 104 | ```javascript 105 | var vmap = new VMAP(vmapXML); 106 | ``` 107 | 108 | #### Node 109 | A pre-bundled version for node is available too: [`vmap-js-node.js`](dist/vmap-js-node.js). 110 | 111 | ```javascript 112 | const VMAP = require('@dailymotion/vmap') 113 | 114 | const vmap = new VMAP(vmapXML); 115 | ``` 116 | 117 | ## Build and tests 118 | 119 | Install dependencies with: 120 | 121 | ``` 122 | npm install 123 | ``` 124 | 125 | The project is bundled using [Rollup](https://rollupjs.org/guide/en). Build with: 126 | 127 | ``` 128 | npm run-script build 129 | ``` 130 | 131 | Run tests with: 132 | 133 | ``` 134 | npm test 135 | ``` 136 | -------------------------------------------------------------------------------- /dist/vmap-js-node.js: -------------------------------------------------------------------------------- 1 | "use strict";function t(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function e(t,e){for(var a=0;a0)try{return JSON.parse(a[0].data)}catch(t){}var r="";for(var s in e){var o=e[s];switch(o.nodeName){case"#text":r+=o.textContent.trim();break;case"#cdata-section":r+=o.data}}return r}(t);var a=t.attributes;if(a)for(var n in a){var i=a[n];i.nodeName&&void 0!==i.nodeValue&&null!==i.nodeValue&&(e.attributes[i.nodeName]=i.nodeValue)}var s=t.childNodes;if(s)for(var o in s){var c=s[o];c.nodeName&&"#"!==c.nodeName.substring(0,1)&&(e.children[c.nodeName]=r(c))}return e}var s=function(){function e(a){for(var s in t(this,e),this.timeOffset=a.getAttribute("timeOffset"),this.breakType=a.getAttribute("breakType"),this.breakId=a.getAttribute("breakId"),this.repeatAfter=a.getAttribute("repeatAfter"),this.adSource=null,this.trackingEvents=[],this.extensions=[],a.childNodes){var o=a.childNodes[s];switch(o.localName){case"AdSource":this.adSource=new n(o);break;case"TrackingEvents":for(var c in o.childNodes){var d=o.childNodes[c];"Tracking"===d.localName&&this.trackingEvents.push({event:d.getAttribute("event"),uri:(d.textContent||d.text||"").trim()})}break;case"Extensions":this.extensions=i(o,"Extension").map((function(t){return r(t)}))}}}return a(e,[{key:"track",value:function(t,e){for(var a in this.trackingEvents){var n=this.trackingEvents[a];if(n.event===t){var i=n.uri;"error"===n.event&&(i=i.replace("[ERRORCODE]",e)),this.tracker(i)}}}},{key:"tracker",value:function(t){"undefined"!=typeof window&&null!==window&&((new Image).src=t)}}]),e}(),o=a((function e(a){if(t(this,e),!a||!a.documentElement||"VMAP"!==a.documentElement.localName)throw new Error("Not a VMAP document");for(var n in this.version=a.documentElement.getAttribute("version"),this.adBreaks=[],this.extensions=[],a.documentElement.childNodes){var o=a.documentElement.childNodes[n];switch(o.localName){case"AdBreak":this.adBreaks.push(new s(o));break;case"Extensions":this.extensions=i(o,"Extension").map((function(t){return r(t)}))}}}));module.exports=o; 2 | -------------------------------------------------------------------------------- /dist/vmap-js.js: -------------------------------------------------------------------------------- 1 | var VMAP=function(){"use strict";function t(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function e(t,e){for(var a=0;a0)try{return JSON.parse(a[0].data)}catch(t){}var i="";for(var s in e){var o=e[s];switch(o.nodeName){case"#text":i+=o.textContent.trim();break;case"#cdata-section":i+=o.data}}return i}(t);var a=t.attributes;if(a)for(var n in a){var r=a[n];r.nodeName&&void 0!==r.nodeValue&&null!==r.nodeValue&&(e.attributes[r.nodeName]=r.nodeValue)}var s=t.childNodes;if(s)for(var o in s){var c=s[o];c.nodeName&&"#"!==c.nodeName.substring(0,1)&&(e.children[c.nodeName]=i(c))}return e}var s=function(){function e(a){for(var s in t(this,e),this.timeOffset=a.getAttribute("timeOffset"),this.breakType=a.getAttribute("breakType"),this.breakId=a.getAttribute("breakId"),this.repeatAfter=a.getAttribute("repeatAfter"),this.adSource=null,this.trackingEvents=[],this.extensions=[],a.childNodes){var o=a.childNodes[s];switch(o.localName){case"AdSource":this.adSource=new n(o);break;case"TrackingEvents":for(var c in o.childNodes){var d=o.childNodes[c];"Tracking"===d.localName&&this.trackingEvents.push({event:d.getAttribute("event"),uri:(d.textContent||d.text||"").trim()})}break;case"Extensions":this.extensions=r(o,"Extension").map((function(t){return i(t)}))}}}return a(e,[{key:"track",value:function(t,e){for(var a in this.trackingEvents){var n=this.trackingEvents[a];if(n.event===t){var r=n.uri;"error"===n.event&&(r=r.replace("[ERRORCODE]",e)),this.tracker(r)}}}},{key:"tracker",value:function(t){"undefined"!=typeof window&&null!==window&&((new Image).src=t)}}]),e}();return a((function e(a){if(t(this,e),!a||!a.documentElement||"VMAP"!==a.documentElement.localName)throw new Error("Not a VMAP document");for(var n in this.version=a.documentElement.getAttribute("version"),this.adBreaks=[],this.extensions=[],a.documentElement.childNodes){var o=a.documentElement.childNodes[n];switch(o.localName){case"AdBreak":this.adBreaks.push(new s(o));break;case"Extensions":this.extensions=r(o,"Extension").map((function(t){return i(t)}))}}}))}(); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dailymotion/vmap", 3 | "author": "Dailymotion (https://developer.dailymotion.com/)", 4 | "version": "3.3.2", 5 | "description": "Javascript VMAP Parser", 6 | "keywords": [ 7 | "vmap", 8 | "ad", 9 | "advertising", 10 | "iab", 11 | "in-stream", 12 | "video" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/dailymotion/vmap-js" 17 | }, 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">=12.22.1" 21 | }, 22 | "main": "dist/vmap-js-node.js", 23 | "browser": "dist/vmap-js.js", 24 | "scripts": { 25 | "test": "jest", 26 | "build": "rollup -c rollup.config.js", 27 | "precommit": "eslint --fix . && pretty-quick --staged", 28 | "dev": "rollup -c rollup.config.js --watch", 29 | "lint": "eslint --fix . && prettier --write \"**/*.js\"" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.19.3", 33 | "@babel/eslint-parser": "^7.19.1", 34 | "@babel/preset-env": "^7.12.11", 35 | "@babel/register": "^7.12.10", 36 | "@rollup/plugin-babel": "^6.0.0", 37 | "eslint": "^7.32.0", 38 | "eslint-plugin-import": "^2.22.1", 39 | "eslint-plugin-no-for-of-loops": "^1.0.1", 40 | "husky": "^8.0.3", 41 | "jest": "^29.1.2", 42 | "path": "^0.12.7", 43 | "prettier": "2.2.1", 44 | "pretty-quick": "^3.1.0", 45 | "rollup": "^2.35.1", 46 | "rollup-plugin-terser": "^7.0.2", 47 | "xmldom": "github:xmldom/xmldom#0.7.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | import path from 'path'; 4 | 5 | const plugins = [ 6 | babel({ 7 | babelrc: false, 8 | presets: [['@babel/preset-env', { modules: false }]], 9 | exclude: ['node_modules/**'], 10 | }), 11 | terser(), 12 | ]; 13 | 14 | export default [ 15 | { 16 | input: 'src/index.js', 17 | output: { 18 | file: path.resolve(__dirname, 'dist', 'vmap-js-node.js'), 19 | format: 'cjs', 20 | }, 21 | plugins, 22 | }, 23 | { 24 | input: 'src/index.js', 25 | output: { 26 | file: path.resolve(__dirname, 'dist', 'vmap-js.js'), 27 | format: 'iife', 28 | name: 'VMAP', 29 | }, 30 | plugins, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /spec/adbreak.spec.js: -------------------------------------------------------------------------------- 1 | import VMAP from '../src/vmap'; 2 | import { readXMLFile } from './utils'; 3 | 4 | describe('AdBreaks', () => { 5 | const xml = readXMLFile('samples/correct-vmap.xml'); 6 | const vmap = new VMAP(xml); 7 | 8 | describe('#1 ad break', () => { 9 | const adbreak = vmap.adBreaks[0]; 10 | let tracked = []; 11 | 12 | adbreak.tracker = (uri) => tracked.push(uri); 13 | 14 | beforeEach(() => (tracked = [])); 15 | 16 | it('should have time offset set to start', () => { 17 | expect(adbreak.timeOffset).toBe('start'); 18 | }); 19 | 20 | it('should have break type linear', () => { 21 | expect(adbreak.breakType).toBe('linear'); 22 | }); 23 | 24 | it('should have break id set to mypre', () => { 25 | expect(adbreak.breakId).toBe('mypre'); 26 | }); 27 | 28 | it('should have one tracking event', () => { 29 | const trackingEvents = adbreak.trackingEvents; 30 | 31 | expect(trackingEvents.length).toBe(4); 32 | 33 | expect(trackingEvents[0].event).toBe('breakStart'); 34 | expect(trackingEvents[0].uri).toBe('http://server.com/breakstart'); 35 | 36 | expect(trackingEvents[1].event).toBe('breakEnd'); 37 | expect(trackingEvents[1].uri).toBe('http://server.com/breakend'); 38 | 39 | expect(trackingEvents[2].event).toBe('breakEnd'); 40 | expect(trackingEvents[2].uri).toBe('http://server.com/breakend2'); 41 | 42 | expect(trackingEvents[3].event).toBe('error'); 43 | expect(trackingEvents[3].uri).toBe('http://server.com/error?[ERRORCODE]'); 44 | }); 45 | 46 | it('should call start trackers', () => { 47 | adbreak.track('breakStart'); 48 | 49 | expect(tracked).toEqual(['http://server.com/breakstart']); 50 | }); 51 | 52 | it('should call end trackers', () => { 53 | adbreak.track('breakEnd'); 54 | 55 | expect(tracked).toEqual(['http://server.com/breakend', 'http://server.com/breakend2']); 56 | }); 57 | 58 | it('should call error trackers with variable', () => { 59 | adbreak.track('error', 402); 60 | 61 | expect(tracked).toEqual(['http://server.com/error?402']); 62 | }); 63 | }); 64 | 65 | describe('#2 ad break', () => { 66 | const adbreak = vmap.adBreaks[1]; 67 | 68 | it('should have time offset set to 00:10:23.125', () => { 69 | expect(adbreak.timeOffset).toBe('00:10:23.125'); 70 | }); 71 | 72 | it('should have break type linear', () => { 73 | expect(adbreak.breakType).toBe('linear'); 74 | }); 75 | 76 | it('should have break id set to myid', () => { 77 | expect(adbreak.breakId).toBe('myid'); 78 | }); 79 | 80 | it('should have one tracking event', () => { 81 | const trackingEvents = adbreak.trackingEvents; 82 | 83 | expect(trackingEvents.length).toBe(1); 84 | 85 | expect(trackingEvents[0].event).toBe('breakStart'); 86 | expect(trackingEvents[0].uri).toBe('http://server.com/breakstart'); 87 | }); 88 | }); 89 | 90 | describe('#3 ad break', () => { 91 | const adbreak = vmap.adBreaks[2]; 92 | 93 | it('should have time offset set to end', () => { 94 | expect(adbreak.timeOffset).toBe('end'); 95 | }); 96 | 97 | it('should have break type linear', () => { 98 | expect(adbreak.breakType).toBe('linear'); 99 | }); 100 | 101 | it('should have break id set to mypost', () => { 102 | expect(adbreak.breakId).toBe('mypost'); 103 | }); 104 | 105 | it('should have one tracking event', () => { 106 | const trackingEvents = adbreak.trackingEvents; 107 | 108 | expect(trackingEvents.length).toBe(1); 109 | 110 | expect(trackingEvents[0].event).toBe('breakStart'); 111 | expect(trackingEvents[0].uri).toBe('http://server.com/breakstart'); 112 | }); 113 | }); 114 | 115 | describe('Ad break with extensions', () => { 116 | const xmlWithExtensions = readXMLFile('samples/correct-vmap-with-extension.xml'); 117 | const vmapWithExtensions = new VMAP(xmlWithExtensions); 118 | const adbreak = vmapWithExtensions.adBreaks[0]; 119 | 120 | it('should parse extensions', () => { 121 | expect(adbreak.extensions).toEqual([ 122 | { 123 | attributes: { 124 | extAttribute: 'extAttribute content', 125 | extAttribute2: 'extAttribute2 content', 126 | }, 127 | children: { 128 | 'vmap:Test': { 129 | attributes: { 130 | testAttribute: 'testAttribute content', 131 | testAttribute2: 'testAttribute2 content', 132 | }, 133 | children: {}, 134 | value: 'Test value', 135 | }, 136 | 'vmap:Test2': { 137 | attributes: { 138 | test2Attribute: 'test2Attribute content', 139 | test2Attribute2: 'test2Attribute2 content', 140 | }, 141 | children: {}, 142 | value: 'Test2 value', 143 | }, 144 | }, 145 | value: 'Extension value', 146 | }, 147 | { 148 | attributes: {}, 149 | children: { 150 | 'vmap:Child': { 151 | attributes: { 152 | childAttribute: 'childAttribute content', 153 | childAttribute2: 'childAttribute2 content', 154 | }, 155 | children: {}, 156 | value: 'Child value', 157 | }, 158 | }, 159 | value: 'Extension2 value', 160 | }, 161 | ]); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /spec/adsource.spec.js: -------------------------------------------------------------------------------- 1 | import VMAP from '../src/vmap'; 2 | import { readXMLFile } from './utils'; 3 | 4 | describe('AdSources', () => { 5 | const xml = readXMLFile('samples/correct-vmap.xml'); 6 | const vmap = new VMAP(xml); 7 | 8 | describe('#1 ad break, ad source', () => { 9 | const adsource = vmap.adBreaks[0].adSource; 10 | 11 | it('should have ad source id 1', () => { 12 | expect(adsource.id).toBe('1'); 13 | }); 14 | 15 | it('should have only vast data set', () => { 16 | expect(adsource.vastData).not.toBeNull(); 17 | expect(adsource.adTagURI).toBeNull(); 18 | expect(adsource.customData).toBeNull(); 19 | }); 20 | }); 21 | 22 | describe('#2 ad break, ad source', () => { 23 | const adsource = vmap.adBreaks[1].adSource; 24 | 25 | it('should have ad source id 2', () => { 26 | expect(adsource.id).toBe('2'); 27 | }); 28 | 29 | it('should have only vast data set', () => { 30 | expect(adsource.vastData).not.toBeNull(); 31 | expect(adsource.adTagURI).toBeNull(); 32 | expect(adsource.customData).toBeNull(); 33 | }); 34 | }); 35 | 36 | describe('#3 ad break, ad source', () => { 37 | const adsource = vmap.adBreaks[2].adSource; 38 | 39 | it('should have ad source id 3', () => { 40 | expect(adsource.id).toBe('3'); 41 | }); 42 | 43 | it('should have only vast data set', () => { 44 | expect(adsource.vastData).not.toBeNull(); 45 | expect(adsource.adTagURI.uri).toBe('http://server.com/vast.xml'); 46 | expect(adsource.customData).toBeNull(); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /spec/parser_utils.spec.js: -------------------------------------------------------------------------------- 1 | import { childrenByName, parseNodeValue, parseXMLNode } from '../src/parser_utils'; 2 | import { readXMLFile } from './utils'; 3 | 4 | describe('ParserUtils', () => { 5 | describe('childrenByName function', () => { 6 | const testNode = { 7 | childNodes: [ 8 | { 9 | nodeName: 'Test', 10 | }, 11 | { 12 | nodeName: 'Test2', 13 | }, 14 | { 15 | nodeName: 'vmap:Test', 16 | }, 17 | { 18 | nodeName: 'vmap:Test2', 19 | }, 20 | { 21 | nodeName: 'Test:Test', 22 | }, 23 | { 24 | nodeName: 'Test:Test2', 25 | }, 26 | { 27 | nodeName: 'vmap:Test:Test', 28 | }, 29 | { 30 | nodeName: 'vmap:', 31 | }, 32 | { 33 | nodeName: '', 34 | }, 35 | { 36 | nodeName: undefined, 37 | }, 38 | { 39 | nodeName: null, 40 | }, 41 | ], 42 | }; 43 | 44 | it('should select children named Test', () => { 45 | const result = childrenByName(testNode, 'Test'); 46 | 47 | expect(result).toEqual([ 48 | { 49 | nodeName: 'Test', 50 | }, 51 | { 52 | nodeName: 'vmap:Test', 53 | }, 54 | ]); 55 | }); 56 | 57 | it('should select children named vmap:Test', () => { 58 | const result = childrenByName(testNode, 'vmap:Test'); 59 | 60 | expect(result).toEqual([ 61 | { 62 | nodeName: 'Test', 63 | }, 64 | { 65 | nodeName: 'vmap:Test', 66 | }, 67 | ]); 68 | }); 69 | 70 | it('should select children named Test:Test', () => { 71 | const result = childrenByName(testNode, 'Test:Test'); 72 | 73 | expect(result).toEqual([ 74 | { 75 | nodeName: 'Test:Test', 76 | }, 77 | { 78 | nodeName: 'vmap:Test:Test', 79 | }, 80 | ]); 81 | }); 82 | 83 | it('should select children named vmap:Test:Test', () => { 84 | const result = childrenByName(testNode, 'vmap:Test:Test'); 85 | 86 | expect(result).toEqual([ 87 | { 88 | nodeName: 'Test:Test', 89 | }, 90 | { 91 | nodeName: 'vmap:Test:Test', 92 | }, 93 | ]); 94 | }); 95 | }); 96 | 97 | describe('parseNodeValue function', () => { 98 | const testNode = { 99 | childNodes: [ 100 | { 101 | nodeName: 'Test', 102 | textContent: 'Wrong', 103 | }, 104 | { 105 | nodeName: '#text', 106 | textContent: ' Blabla ', 107 | }, 108 | { 109 | nodeName: '', 110 | }, 111 | { 112 | nodeName: undefined, 113 | }, 114 | { 115 | nodeName: null, 116 | }, 117 | { 118 | nodeName: '#text', 119 | textContent: ' Blobloblo ', 120 | }, 121 | ], 122 | }; 123 | 124 | it('should correctly extract text', () => { 125 | const result = parseNodeValue(testNode); 126 | 127 | expect(result).toEqual('BlablaBlobloblo'); 128 | }); 129 | }); 130 | 131 | describe('parseXMLNode function', () => { 132 | const testXML = readXMLFile('samples/parsing-example.xml'); 133 | 134 | const result = parseXMLNode(testXML); 135 | 136 | it('should correctly parse XML', () => { 137 | expect(result).toEqual({ 138 | attributes: {}, 139 | children: { 140 | 'vmap:Extension': { 141 | attributes: { 142 | extAttribute: 'extAttribute content', 143 | extAttribute2: 'extAttribute2 content', 144 | }, 145 | children: { 146 | 'vmap:Test': { 147 | attributes: { 148 | testAttribute: 'testAttribute content', 149 | testAttribute2: 'testAttribute2 content', 150 | }, 151 | children: {}, 152 | value: 'Test value', 153 | }, 154 | 'vmap:Test2': { 155 | attributes: { 156 | test2Attribute: 'test2Attribute content', 157 | test2Attribute2: 'test2Attribute2 content', 158 | }, 159 | children: {}, 160 | value: 'Test2 value', 161 | }, 162 | }, 163 | value: { 164 | example: { 165 | property1: 1234, 166 | property2: 'abcd', 167 | }, 168 | another: 'qwerty', 169 | }, 170 | }, 171 | }, 172 | value: '', 173 | }); 174 | }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /spec/samples/correct-vmap-with-extension.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Extension value 6 | 7 | Test value 8 | 9 | 10 | Test2 value 11 | 12 | 13 | 14 | Extension2 value 15 | 16 | Child value 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | http://server.com/breakstart 29 | http://server.com/breakend 30 | http://server.com/breakend2 31 | http://server.com/error?[ERRORCODE] 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /spec/samples/correct-vmap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | http://server.com/breakstart 11 | http://server.com/breakend 12 | http://server.com/breakend2 13 | http://server.com/error?[ERRORCODE] 14 | 15 | 16 | 17 | 18 | 19 | ... 20 | 21 | 22 | 23 | 24 | http://server.com/breakstart 25 | 26 | 27 | 28 | 29 | http://server.com/vast.xml 30 | 31 | 32 | http://server.com/breakstart 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /spec/samples/incorrect-vmap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | http://server.com/breakstart 11 | http://server.com/breakend 12 | http://server.com/breakend2 13 | http://server.com/error?[ERRORCODE] 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/samples/parsing-example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test value 5 | 6 | 7 | Test2 value 8 | 9 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": ["../node_modules/@babel/register/lib/node.js"], 7 | "stopSpecOnExpectationFailure": false, 8 | "random": true 9 | } 10 | -------------------------------------------------------------------------------- /spec/utils.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { DOMParser } from 'xmldom'; 4 | 5 | export const readXMLFile = (file) => { 6 | const url = path.resolve(path.dirname(module.filename), file).replace(/\\/g, '/'); 7 | const data = fs.readFileSync(url, 'utf8'); 8 | const xml = new DOMParser().parseFromString(data); 9 | 10 | return xml; 11 | }; 12 | -------------------------------------------------------------------------------- /spec/vmap.spec.js: -------------------------------------------------------------------------------- 1 | import VMAP from '../src/vmap'; 2 | import { readXMLFile } from './utils'; 3 | 4 | describe('A correct VMAP', () => { 5 | const xml = readXMLFile('samples/correct-vmap.xml'); 6 | const vmap = new VMAP(xml); 7 | 8 | it('should have version 1.0', () => { 9 | expect(vmap.version).toBe('1.0'); 10 | }); 11 | 12 | it('should have 3 ad breaks', () => { 13 | expect(vmap.adBreaks.length).toBe(3); 14 | }); 15 | 16 | it('should have no extensions', () => { 17 | expect(vmap.extensions.length).toBe(0); 18 | }); 19 | }); 20 | 21 | describe('An incorrect VMAP', () => { 22 | const xml = readXMLFile('samples/incorrect-vmap.xml'); 23 | 24 | it('should throw an error in the constructor', () => { 25 | expect(() => new VMAP(xml)).toThrow(new Error('Not a VMAP document')); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/adbreak.js: -------------------------------------------------------------------------------- 1 | import VMAPAdSource from './adsource'; 2 | import { childrenByName, parseXMLNode } from './parser_utils'; 3 | 4 | class VMAPAdBreak { 5 | constructor(xml) { 6 | this.timeOffset = xml.getAttribute('timeOffset'); 7 | this.breakType = xml.getAttribute('breakType'); 8 | this.breakId = xml.getAttribute('breakId'); 9 | this.repeatAfter = xml.getAttribute('repeatAfter'); 10 | this.adSource = null; 11 | this.trackingEvents = []; 12 | this.extensions = []; 13 | 14 | for (const nodeKey in xml.childNodes) { 15 | const node = xml.childNodes[nodeKey]; 16 | 17 | switch (node.localName) { 18 | case 'AdSource': 19 | this.adSource = new VMAPAdSource(node); 20 | break; 21 | case 'TrackingEvents': 22 | for (const subnodeKey in node.childNodes) { 23 | const subnode = node.childNodes[subnodeKey]; 24 | 25 | if (subnode.localName === 'Tracking') { 26 | this.trackingEvents.push({ 27 | event: subnode.getAttribute('event'), 28 | uri: (subnode.textContent || subnode.text || '').trim(), 29 | }); 30 | } 31 | } 32 | break; 33 | case 'Extensions': 34 | this.extensions = childrenByName(node, 'Extension').map((extension) => 35 | parseXMLNode(extension) 36 | ); 37 | break; 38 | } 39 | } 40 | } 41 | 42 | track(event, errorCode) { 43 | for (const trackerKey in this.trackingEvents) { 44 | const tracker = this.trackingEvents[trackerKey]; 45 | 46 | if (tracker.event === event) { 47 | let { uri } = tracker; 48 | if (tracker.event === 'error') { 49 | uri = uri.replace('[ERRORCODE]', errorCode); 50 | } 51 | this.tracker(uri); 52 | } 53 | } 54 | } 55 | 56 | // Easy to overwrite tracker client for unit testing 57 | tracker(uri) { 58 | if (typeof window !== 'undefined' && window !== null) { 59 | const i = new Image(); 60 | i.src = uri; 61 | } 62 | } 63 | } 64 | 65 | export default VMAPAdBreak; 66 | -------------------------------------------------------------------------------- /src/adsource.js: -------------------------------------------------------------------------------- 1 | class VMAPAdSource { 2 | constructor(xml) { 3 | this.id = xml.getAttribute('id'); 4 | this.allowMultipleAds = xml.getAttribute('allowMultipleAds'); 5 | this.followRedirects = xml.getAttribute('followRedirects'); 6 | this.vastAdData = null; 7 | this.adTagURI = null; 8 | this.customData = null; 9 | 10 | for (const nodeKey in xml.childNodes) { 11 | const node = xml.childNodes[nodeKey]; 12 | 13 | switch (node.localName) { 14 | case 'AdTagURI': 15 | this.adTagURI = { 16 | templateType: node.getAttribute('templateType'), 17 | uri: (node.textContent || node.text || '').trim(), 18 | }; 19 | break; 20 | case 'VASTAdData': 21 | this.vastAdData = node.firstChild; 22 | // Some browsers treats empty white-spaces or new lines as text nodes. 23 | // Ensure we get the first element node 24 | while (this.vastAdData && this.vastAdData.nodeType !== 1) { 25 | this.vastAdData = this.vastAdData.nextSibling; 26 | } 27 | break; 28 | case 'CustomAdData': 29 | this.customData = node; 30 | break; 31 | } 32 | } 33 | } 34 | } 35 | 36 | export default VMAPAdSource; 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import VMAP from './vmap'; 2 | 3 | export default VMAP; 4 | -------------------------------------------------------------------------------- /src/parser_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns all the elements of the given node which nodeName match the given name. 3 | * @param {Node} node - The node to use to find the matches. 4 | * @param {String} name - The name to look for. 5 | * @return {Array} 6 | */ 7 | function childrenByName(node, name) { 8 | const children = []; 9 | for (const childKey in node.childNodes) { 10 | const child = node.childNodes[childKey]; 11 | 12 | if ( 13 | child.nodeName === name || 14 | name === `vmap:${child.nodeName}` || 15 | child.nodeName === `vmap:${name}` 16 | ) { 17 | children.push(child); 18 | } 19 | } 20 | return children; 21 | } 22 | 23 | /** 24 | * Parses a node value giving priority to CDATA as a JSON over text, if CDATA is not a valid JSON it is converted to text 25 | * @param {Node} node - The node to parse the value from. 26 | * @return {String|Object} 27 | */ 28 | function parseNodeValue(node) { 29 | if (!node || !node.childNodes) { 30 | return {}; 31 | } 32 | const childNodes = node.childNodes; 33 | 34 | // Trying to find and parse CDATA as JSON 35 | const cdatas = []; 36 | for (const childKey in childNodes) { 37 | const childNode = childNodes[childKey]; 38 | 39 | if (childNode.nodeName === '#cdata-section') { 40 | cdatas.push(childNode); 41 | } 42 | } 43 | 44 | if (cdatas && cdatas.length > 0) { 45 | try { 46 | return JSON.parse(cdatas[0].data); 47 | } catch (e) {} 48 | } 49 | 50 | // Didn't find any CDATA or failed to parse it as JSON 51 | let nodeText = ''; 52 | for (const childKey in childNodes) { 53 | const childNode = childNodes[childKey]; 54 | 55 | switch (childNode.nodeName) { 56 | case '#text': 57 | nodeText += childNode.textContent.trim(); 58 | break; 59 | case '#cdata-section': 60 | nodeText += childNode.data; 61 | break; 62 | } 63 | } 64 | return nodeText; 65 | } 66 | 67 | /** 68 | * Parses an XML node recursively. 69 | * @param {Node} node - The node to parse. 70 | * @return {Object} 71 | */ 72 | function parseXMLNode(node) { 73 | const parsedNode = { 74 | attributes: {}, 75 | children: {}, 76 | value: {}, 77 | }; 78 | 79 | parsedNode.value = parseNodeValue(node); 80 | 81 | const attributes = node.attributes; 82 | if (attributes) { 83 | for (const attrKey in attributes) { 84 | const nodeAttr = attributes[attrKey]; 85 | 86 | if (nodeAttr.nodeName && nodeAttr.nodeValue !== undefined && nodeAttr.nodeValue !== null) { 87 | parsedNode.attributes[nodeAttr.nodeName] = nodeAttr.nodeValue; 88 | } 89 | } 90 | } 91 | 92 | const childNodes = node.childNodes; 93 | if (childNodes) { 94 | for (const childKey in childNodes) { 95 | const childNode = childNodes[childKey]; 96 | if (childNode.nodeName && childNode.nodeName.substring(0, 1) !== '#') { 97 | parsedNode.children[childNode.nodeName] = parseXMLNode(childNode); 98 | } 99 | } 100 | } 101 | 102 | return parsedNode; 103 | } 104 | 105 | export { childrenByName, parseNodeValue, parseXMLNode }; 106 | -------------------------------------------------------------------------------- /src/vmap.js: -------------------------------------------------------------------------------- 1 | import VMAPAdBreak from './adbreak'; 2 | import { childrenByName, parseXMLNode } from './parser_utils'; 3 | 4 | class VMAP { 5 | constructor(xml) { 6 | if (!xml || !xml.documentElement || xml.documentElement.localName !== 'VMAP') { 7 | throw new Error('Not a VMAP document'); 8 | } 9 | 10 | this.version = xml.documentElement.getAttribute('version'); 11 | this.adBreaks = []; 12 | this.extensions = []; 13 | 14 | for (const nodeKey in xml.documentElement.childNodes) { 15 | const node = xml.documentElement.childNodes[nodeKey]; 16 | 17 | switch (node.localName) { 18 | case 'AdBreak': 19 | this.adBreaks.push(new VMAPAdBreak(node)); 20 | break; 21 | case 'Extensions': 22 | this.extensions = childrenByName(node, 'Extension').map((extension) => 23 | parseXMLNode(extension) 24 | ); 25 | break; 26 | } 27 | } 28 | } 29 | } 30 | 31 | export default VMAP; 32 | --------------------------------------------------------------------------------