├── .npmignore
├── lib
├── twitter.js
├── utils.js
├── router.js
├── ogp.js
├── oembed.js
├── tags.js
└── index.js
├── .travis.yml
├── .gitignore
├── package.json
├── LICENSE
├── bench.js
├── test
├── tags.js
├── oembed.js
├── ogp.js
├── twitter.js
└── index.js
├── README.md
└── providers.json
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !lib/**
3 | !.npmignore
4 | !providers.json
5 |
--------------------------------------------------------------------------------
/lib/twitter.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/outmoded/metaphor/HEAD/lib/twitter.js
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "4"
5 | - "6"
6 | - "8"
7 | - "node"
8 |
9 | sudo: false
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | npm-debug.log
4 | dump.rdb
5 | node_modules
6 | results.tap
7 | results.xml
8 | config.json
9 | .DS_Store
10 | */.DS_Store
11 | */*/.DS_Store
12 | ._*
13 | */._*
14 | */*/._*
15 | coverage.*
16 | .settings
17 | package-lock.json
18 |
19 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 |
6 | // Declare internals
7 |
8 | const internals = {};
9 |
10 |
11 | exports.parse = function (payload) {
12 |
13 | try {
14 | return JSON.parse(payload.toString());
15 | }
16 | catch (err) {
17 | return null;
18 | }
19 | };
20 |
21 |
22 | exports.copy = function (from, to, keys, source) {
23 |
24 | to = to || {};
25 | let used = false;
26 | keys.forEach((key) => {
27 |
28 | if (from[key]) {
29 | to[key] = from[key];
30 | used = true;
31 | }
32 | });
33 |
34 | if (used &&
35 | source) {
36 |
37 | to.sources.push(source);
38 | }
39 |
40 | return to;
41 | };
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "metaphor",
3 | "description": "Open Graph, Twitter Card, and oEmbed Metadata Collector",
4 | "version": "3.8.3",
5 | "repository": "git://github.com/hueniverse/metaphor",
6 | "main": "lib/index.js",
7 | "keywords": [
8 | "oembed",
9 | "ogp",
10 | "open graph",
11 | "twitter card",
12 | "description",
13 | "embed"
14 | ],
15 | "engines": {
16 | "node": ">=4.x.x"
17 | },
18 | "dependencies": {
19 | "content": "3.x.x",
20 | "entities": "1.x.x",
21 | "hoek": "4.x.x",
22 | "htmlparser2": "3.x.x",
23 | "items": "2.x.x",
24 | "joi": "10.x.x",
25 | "wreck": "12.x.x"
26 | },
27 | "devDependencies": {
28 | "code": "4.x.x",
29 | "lab": "14.x.x"
30 | },
31 | "scripts": {
32 | "test": "node node_modules/lab/bin/lab -a code -t 100 -L -m 15000",
33 | "test-cov-html": "node node_modules/lab/bin/lab -a code -r html -o coverage.html -m 15000"
34 | },
35 | "license": "BSD-3-Clause"
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016-2017, Eran Hammer and Project contributors
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * The names of any contributors may not be used to endorse or promote
12 | products derived from this software without specific prior written
13 | permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
26 | * * *
27 |
28 | The complete list of contributors can be found at: https://github.com/hueniverse/metaphor/graphs/contributors
29 |
--------------------------------------------------------------------------------
/bench.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Bench = require('bench');
4 | const Metaphor = require('.');
5 | const Wreck = require('wreck');
6 |
7 |
8 | const parse = function (document) {
9 |
10 | // Grab the head
11 |
12 | const head = document.match(/
]*>([\s\S]*)<\/head\s*>/);
13 | if (!head) {
14 | return [];
15 | }
16 |
17 | // Remove scripts
18 |
19 | const scripts = head[1].split(''); // 'something' -> ['",
53 | "provider_name": "Twitter",
54 | "provider_url": "https://twitter.com",
55 | "type": "rich",
56 | "url": "https://twitter.com/sideway/status/626158822705401856",
57 | "version": "1.0",
58 | "width": 550
59 | }
60 | */
61 |
62 | if (url) {
63 | const uri = Url.parse(url, true);
64 | delete uri.href;
65 | delete uri.path;
66 | delete uri.search;
67 | uri.query.format = 'json';
68 | if (options.maxHeight) {
69 | uri.query.maxheight = options.maxHeight;
70 | }
71 |
72 | if (options.maxWidth) {
73 | uri.query.maxwidth = options.maxWidth;
74 | }
75 |
76 | url = Url.format(uri);
77 | }
78 | else if (options.router) {
79 | url = options.router.match(resource, options);
80 | }
81 |
82 | if (!url) {
83 | return next({});
84 | }
85 |
86 | Wreck.get(url, { redirects: 1 }, (err, res, payload) => {
87 |
88 | if (err ||
89 | res.statusCode !== 200) {
90 |
91 | return next({});
92 | }
93 |
94 | const raw = Utils.parse(payload);
95 | if (!raw) {
96 | return next({});
97 | }
98 |
99 | internals.oembedSchema.validate(raw, (err, oembed) => {
100 |
101 | if (err) {
102 | return next({});
103 | }
104 |
105 | const thumbnail = (!oembed.thumbnail_url ? null : {
106 | url: oembed.thumbnail_url,
107 | width: oembed.thumbnail_width,
108 | height: oembed.thumbnail_height
109 | });
110 |
111 | const description = {
112 | site_name: oembed.site_name,
113 | thumbnail
114 | };
115 |
116 | if (oembed.type === 'link') {
117 | description.url = oembed.url;
118 | }
119 | else {
120 | description.embed = Utils.copy(oembed, null, ['type', 'height', 'width', 'url', 'html']);
121 | }
122 |
123 | return next(description);
124 | });
125 | });
126 | };
127 |
128 |
129 | exports.providers = function (providers) {
130 |
131 | return new internals.Router(providers);
132 | };
133 |
134 |
135 | internals.Router = class extends Router {
136 | constructor(providers) {
137 |
138 | super();
139 |
140 | providers.forEach((provider) => {
141 |
142 | /*
143 | {
144 | "provider_name": "Alpha App Net",
145 | "provider_url": "https:\/\/alpha.app.net\/browse\/posts\/",
146 | "endpoints": [
147 | {
148 | "schemes": [
149 | "https:\/\/alpha.app.net\/*\/post\/*",
150 | "https:\/\/photos.app.net\/*\/*"
151 | ],
152 | "url": "https:\/\/alpha-api.app.net\/oembed",
153 | "formats": [
154 | "json"
155 | ]
156 | }
157 | ]
158 | }
159 | */
160 |
161 | provider.endpoints.forEach((endpoint) => {
162 |
163 | const url = endpoint.url.replace('{format}', 'json');
164 |
165 | if (!endpoint.schemes) {
166 | return this.add(provider.provider_url, url);
167 | }
168 |
169 | endpoint.schemes.forEach((scheme) => this.add(scheme, url));
170 | });
171 | });
172 | }
173 |
174 | match(url, options) {
175 |
176 | options = options || {};
177 |
178 | const service = this.lookup(url);
179 | if (!service) {
180 | return null;
181 | }
182 |
183 | const query = { url, format: 'json' };
184 | if (options.maxHeight) {
185 | query.maxheight = options.maxHeight;
186 | }
187 |
188 | if (options.maxWidth) {
189 | query.maxwidth = options.maxWidth;
190 | }
191 |
192 | return `${service}?${Querystring.stringify(query)}`;
193 | }
194 | };
195 |
--------------------------------------------------------------------------------
/test/tags.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Code = require('code');
6 | const Lab = require('lab');
7 | const Metaphor = require('..');
8 |
9 |
10 | // Declare internals
11 |
12 | const internals = {};
13 |
14 |
15 | // Test shortcuts
16 |
17 | const lab = exports.lab = Lab.script();
18 | const describe = lab.describe;
19 | const it = lab.it;
20 | const expect = Code.expect;
21 |
22 |
23 | describe('Metaphor', () => {
24 |
25 | describe('parse()', () => {
26 |
27 | it('ignores tags in scripts and body', (done) => {
28 |
29 | const html = `
30 |
31 | The Rock (1996)
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | `;
42 |
43 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
44 |
45 | expect(description).to.equal({
46 | title: 'The Rock',
47 | type: 'video.movie',
48 | url: 'http://www.imdb.com/title/tt0117500/',
49 | image: { url: 'http://ia.media-imdb.com/images/rock.jpg' },
50 | sources: ['ogp']
51 | });
52 |
53 | done();
54 | });
55 | });
56 |
57 | it('handles name/value attributes', (done) => {
58 |
59 | const html = `
60 |
61 | The Rock (1996)
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | `;
70 |
71 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
72 |
73 | expect(description).to.equal({
74 | title: 'The Rock',
75 | type: 'video.movie',
76 | url: 'http://www.imdb.com/title/tt0117500/',
77 | image: { url: 'http://ia.media-imdb.com/images/rock.jpg' },
78 | sources: ['ogp']
79 | });
80 |
81 | done();
82 | });
83 | });
84 |
85 | it('handles missing icon link href attributes', (done) => {
86 |
87 | const html = `
88 |
89 | The Rock (1996)
90 |
91 |
92 |
93 |
94 | `;
95 |
96 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
97 |
98 | expect(description).to.equal({
99 | type: 'website',
100 | url: 'http://www.imdb.com/title/tt0117500/'
101 | });
102 |
103 | done();
104 | });
105 | });
106 |
107 | it('sets default icon link sizes attribute', (done) => {
108 |
109 | const html = `
110 |
111 | The Rock (1996)
112 |
113 |
114 |
115 |
116 | `;
117 |
118 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
119 |
120 | expect(description).to.equal({
121 | type: 'website',
122 | url: 'http://www.imdb.com/title/tt0117500/',
123 | icon: { any: 'http://example.com/', smallest: 'http://example.com/' },
124 | sources: ['resource']
125 | });
126 |
127 | done();
128 | });
129 | });
130 |
131 | it('sets any icon link sizes attribute', (done) => {
132 |
133 | const html = `
134 |
135 | The Rock (1996)
136 |
137 |
138 |
139 |
140 | `;
141 |
142 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
143 |
144 | expect(description).to.equal({
145 | type: 'website',
146 | url: 'http://www.imdb.com/title/tt0117500/',
147 | icon: { any: 'http://example.com/', smallest: 'http://example.com/' },
148 | sources: ['resource']
149 | });
150 |
151 | done();
152 | });
153 | });
154 |
155 | it('ignores second icon link with same size', (done) => {
156 |
157 | const html = `
158 |
159 | The Rock (1996)
160 |
161 |
162 |
163 |
164 |
165 | `;
166 |
167 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
168 |
169 | expect(description).to.equal({
170 | type: 'website',
171 | url: 'http://www.imdb.com/title/tt0117500/',
172 | icon: { any: 'http://example.com/', smallest: 'http://example.com/' },
173 | sources: ['resource']
174 | });
175 |
176 | done();
177 | });
178 | });
179 | });
180 | });
181 |
--------------------------------------------------------------------------------
/lib/tags.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Url = require('url');
6 | const HtmlParser2 = require('htmlparser2');
7 | const Twitter = require('./twitter');
8 |
9 |
10 | // Declare internals
11 |
12 | const internals = {};
13 |
14 |
15 | exports.parse = function (document, base, next) {
16 |
17 | /*
18 |
19 |
20 | The Rock (1996)
21 | ...
22 |
23 |
24 |
25 |
26 | ...
27 |
28 |
29 |
30 |
31 | ...
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ...
44 |
45 |
46 |
47 |
48 |
49 |
50 | */
51 |
52 | const tweet = Twitter.isTweet(base);
53 |
54 | const tags = { og: [], twitter: [], meta: [] };
55 | let oembedLink = null;
56 | let smallestIcon = Infinity;
57 |
58 | const parser = new HtmlParser2.Parser({
59 | onopentag: function (name, attributes) {
60 |
61 | if (name === 'meta') {
62 | const property = attributes.property || attributes.name;
63 | const value = attributes.content || attributes.value;
64 | if (!property ||
65 | !value) {
66 |
67 | return;
68 | }
69 |
70 | if (['author', 'description'].indexOf(property) !== -1) {
71 | tags.meta[property] = value;
72 | return;
73 | }
74 |
75 | const parsed = property.match(/^(og|twitter):([^:]*)(?:\:(.*))?$/);
76 | if (parsed) {
77 | tags[parsed[1]].push({
78 | key: parsed[2],
79 | sub: parsed[3],
80 | value
81 | });
82 | }
83 |
84 | return;
85 | }
86 |
87 | if (name === 'link' &&
88 | attributes.href &&
89 | attributes.rel) {
90 |
91 | const href = Url.resolve(base, attributes.href);
92 | const rels = attributes.rel.split(' ');
93 | for (let i = 0; i < rels.length; ++i) {
94 | let match = true;
95 | switch (rels[i]) {
96 | case 'alternate':
97 | case 'alternative':
98 | if (attributes.type === 'application/json+oembed') {
99 | oembedLink = href;
100 | }
101 | break;
102 |
103 | case 'icon':
104 | if (!attributes.sizes ||
105 | attributes.sizes === 'any' ||
106 | attributes.sizes.match(/^\d+x\d+$/)) {
107 |
108 | tags.meta.icon = tags.meta.icon || {};
109 | let sizes = attributes.sizes || 'any';
110 | if (sizes !== 'any') {
111 | sizes = parseInt(sizes.split('x')[0], 10);
112 | if (sizes < smallestIcon) {
113 | smallestIcon = sizes;
114 | }
115 | }
116 |
117 | if (!tags.meta.icon[sizes]) {
118 | tags.meta.icon[sizes] = href;
119 | }
120 | }
121 | break;
122 | default:
123 | match = false;
124 | break;
125 | }
126 |
127 | if (match) {
128 | break;
129 | }
130 | }
131 | }
132 |
133 | if (tweet &&
134 | name === 'img' &&
135 | attributes.class &&
136 | attributes.class.indexOf('ProfileAvatar-image') !== -1) {
137 |
138 | tags.meta.avatar = attributes.src;
139 | parser.reset();
140 | return;
141 | }
142 |
143 | if (name === 'body' &&
144 | !tweet) {
145 |
146 | parser.reset();
147 | return;
148 | }
149 | },
150 | onend: function () {
151 |
152 | if (tags.meta.icon) {
153 | tags.meta.icon.smallest = tags.meta.icon[smallestIcon !== Infinity ? smallestIcon : 'any'];
154 | }
155 |
156 | return next(tags, oembedLink);
157 | }
158 | }, { decodeEntities: true });
159 |
160 | parser.write(document);
161 | parser.end();
162 | };
163 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # metaphor
2 |
3 | Open Graph, Twitter Card, and oEmbed Metadata Collector
4 |
5 | [](http://travis-ci.org/hueniverse/metaphor)
6 |
7 | **metaphor** uses three web protocols to obtain information about web resources for the purpose of embedding smaller
8 | versions of those resources in other web resources or applications. It is very common for applications to expand
9 | links into a formatted preview of the link destination. However, obtaining this information requires using multiple
10 | protocols to ensure maximum coverage.
11 |
12 | This module uses the [Open Graph protocol](http://ogp.me/), [Twitter Cards](https://dev.twitter.com/cards/overview),
13 | the [oEmbed protocol](http://oembed.com/), and information gathered from the resource HTML markup and HTTP headers.
14 | It takes an optimistic approach, trying to gather information from as many sources as possible.
15 |
16 | ## Usage
17 |
18 | ```js
19 | const Metaphor = require('..');
20 |
21 | const engine = new Metaphor.Engine();
22 | engine.describe('https://www.youtube.com/watch?v=cWDdd5KKhts', (description) => {
23 |
24 | /*
25 | {
26 | site_name: 'YouTube',
27 | url: 'https://www.youtube.com/watch?v=cWDdd5KKhts',
28 | title: 'Cheese Shop Sketch - Monty Python\'s Flying Circus',
29 | image: { url: 'https://i.ytimg.com/vi/cWDdd5KKhts/maxresdefault.jpg' },
30 | description: 'Subscribe to the Official Monty Python Channel here - http://smarturl.it/SubscribeToPython Cleese plays an erudite customer attempting to purchase some chees...',
31 | type: 'video',
32 | video: [
33 | {
34 | url: 'https://www.youtube.com/embed/cWDdd5KKhts',
35 | type: 'text/html',
36 | width: '480',
37 | height: '360'
38 | },
39 | {
40 | url: 'https://www.youtube.com/v/cWDdd5KKhts?version=3&autohide=1',
41 | type: 'application/x-shockwave-flash',
42 | width: '480',
43 | height: '360',
44 | tag: ['Monty Python', 'Python (Monty) Pictures Limited', 'Comedy', 'flying circus', 'monty pythons flying circus', 'john cleese', 'micael palin', 'eric idle', 'terry jones', 'graham chapman', 'terry gilliam', 'funny', 'comedy', 'animation', '60s animation', 'humor', 'humour', 'sketch show', 'british comedy', 'cheese shop', 'monty python cheese', 'cheese shop sketch', 'cleese cheese', 'cheese']
45 | }
46 | ],
47 | thumbnail: {
48 | url: 'https://i.ytimg.com/vi/cWDdd5KKhts/hqdefault.jpg',
49 | width: 480,
50 | height: 360
51 | },
52 | embed: {
53 | type: 'video',
54 | height: 344,
55 | width: 459,
56 | html: 'VIDEO '
57 | },
58 | app: {
59 | iphone: {
60 | name: 'YouTube',
61 | id: '544007664',
62 | url: 'vnd.youtube://www.youtube.com/watch?v=cWDdd5KKhts&feature=applinks'
63 | },
64 | ipad: {
65 | name: 'YouTube',
66 | id: '544007664',
67 | url: 'vnd.youtube://www.youtube.com/watch?v=cWDdd5KKhts&feature=applinks'
68 | },
69 | googleplay: {
70 | name: 'YouTube',
71 | id: 'com.google.android.youtube',
72 | url: 'https://www.youtube.com/watch?v=cWDdd5KKhts'
73 | }
74 | },
75 | player: {
76 | url: 'https://www.youtube.com/embed/cWDdd5KKhts',
77 | width: '480',
78 | height: '360'
79 | },
80 | twitter: { site_username: '@youtube' },
81 | icon: {
82 | '32': 'https://s.ytimg.com/yts/img/favicon_32-vfl8NGn4k.png',
83 | '48': 'https://s.ytimg.com/yts/img/favicon_48-vfl1s0rGh.png',
84 | '96': 'https://s.ytimg.com/yts/img/favicon_96-vfldSA3ca.png',
85 | '144': 'https://s.ytimg.com/yts/img/favicon_144-vflWmzoXw.png',
86 | smallest: 'https://s.ytimg.com/yts/img/favicon_32-vfl8NGn4k.png'
87 | },
88 | preview: 'Cheese Shop Sketch - Monty Python\'s Flying Circus ',
89 | sources: ['ogp', 'resource', 'oembed', 'twitter']
90 | }
91 | */
92 | });
93 | ```
94 |
95 | ## API
96 |
97 | ### `new Metaphor.Engine([options])`
98 |
99 | A reusable engine used to set global processing settings for each description where:
100 | - `options` - optional settings where:
101 | - `providers` - if `true`, the [oEmbed](http://oembed.com/) providers list file is used to look up
102 | resources when oEmbed [discovery](http://oembed.com/#section4) doesn't work. The module ships with
103 | a copy of the providers.json file. To use a different provider list, pass an array compatible with
104 | the providers.json format. If `false`, oEmbed usage will be limited to discovery only. Defaults to
105 | `true`.
106 | - `whitelist` - an optional array of HTTP or HTTPS URLs allowed to be described. The URLs use the
107 | format `{scheme}://{domain}/{path}` where:
108 | - `scheme` - must be `'http'` or `'https'`. The module will ignore which protocol is specified
109 | and will describe both schemes.
110 | - `domain` - the domain name with an optional `*.` prefix. The `'www.'` prefix is automatically
111 | removed and ignored.
112 | - `path` - if specified, but be `'*'` or a path where at least one segment is `'*'`.
113 | - `maxWidth` - an optional integer passed to the oEmbed endpoint to limit the maximum width of elements
114 | in the description. While the protocol requires providers to comply, many do not so this is at best
115 | a recommendation.
116 | - `maxHeight` - an optional integer passed to the oEmbed endpoint to limit the maximum height of elements
117 | in the description. While the protocol requires providers to comply, many do not so this is at best
118 | a recommendation.
119 | - `preview` - can be set to:
120 | - `true` - a HTML preview is generated and returned in `description.preview`. This is the default.
121 | - `false` - no HTML preview is generated.
122 | - a function with the signature `function(description, options, callback)` where:
123 | - `description` - the resource description document.
124 | - `options` - the engine settings.
125 | - `callback` - the callback method using the signature `function(preview)` where:
126 | - `preview` - the HTML preview string or `null` to skip setting a preview.
127 | - `summary` - if `true`, a `summary` object is include with the description which contains the same
128 | data provided by the `preview`. Defaults to `false`.
129 | - `tweet` - if `true`, tweets are parsed to include specific information required to show a proper
130 | embeded tweet without the use of the Twitter scripts. Defaults to `false`.
131 | - `maxSize` - the maximum image size in bytes allowed to be included in an image preview. The limit
132 | is only enforced when creating a `preview` (ignored when `preview` is disabled). When set, an HTTP
133 | HEAD request is made to each image URL to obtain its size. Defaults to `false`.
134 | - `css` - if set to a URL, it is used as a style sheet link in the `preview`. Defaults to `false` (no link).
135 | - `script` - if set to a URL, it is used as a script link in the `preview`. Defaults to `false` (no link).
136 |
137 | ### `engine.describe(url, callback)`
138 |
139 | Described a web resource where:
140 | - `url` - the resource HTTP or HTTPS URL.
141 | - `callback` - the callback function using the signature `function(description)` where:
142 | - `description` - the **metaphor** description object.
143 |
144 | Note that the `describe()` method does not return errors. Errors are optimistically ignored and best effort
145 | is made to use as many sources for describing the `url`. The `description` always includes the following
146 | two properties:
147 | - `type` - set to `'website'` when the type is unknown.
148 | - `url` - the resource canonical URL, set to the requested `url` when no source can be utilized.
149 |
150 | When resources are successfully used, the `description` includes the `sources` property set to an array
151 | of one or more of:
152 | - `'resource'` - information was gathered from the resource HTML page.
153 | - `'ogp'` - Open Graph protocol tags (`og:`) used.
154 | - `'twitter'` - Twitter Card tags (`twitter:`) used.
155 | - `'oembed'` - oEmbed service used.
156 |
--------------------------------------------------------------------------------
/test/oembed.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Code = require('code');
6 | const Lab = require('lab');
7 | const Metaphor = require('..');
8 | const Wreck = require('wreck');
9 | const Providers = require('../providers.json');
10 |
11 |
12 | // Declare internals
13 |
14 | const internals = {};
15 |
16 |
17 | // Test shortcuts
18 |
19 | const lab = exports.lab = Lab.script();
20 | const describe = lab.describe;
21 | const it = lab.it;
22 | const expect = Code.expect;
23 |
24 |
25 | describe('OEmbed', () => {
26 |
27 | describe('describe()', () => {
28 |
29 | it('ignores invalid oembed response (request error)', { parallel: false }, (done) => {
30 |
31 | const html = `
32 |
33 |
34 |
35 | `;
36 |
37 | const orig = Wreck.get;
38 | Wreck.get = (url, options, next) => {
39 |
40 | Wreck.get = orig;
41 | next(null, { statusCode: 400 }, '');
42 | };
43 |
44 | Metaphor.parse(html, 'https://twitter.com/dalmaer/status/726624422237364226', {}, (description) => {
45 |
46 | expect(description).to.equal({
47 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
48 | type: 'website'
49 | });
50 |
51 | done();
52 | });
53 | });
54 |
55 | it('ignores invalid oembed response (network error)', { parallel: false }, (done) => {
56 |
57 | const html = `
58 |
59 |
60 |
61 | `;
62 |
63 | const orig = Wreck.get;
64 | Wreck.get = (url, options, next) => {
65 |
66 | Wreck.get = orig;
67 | next(new Error('Cannot reach host'));
68 | };
69 |
70 | Metaphor.parse(html, 'https://twitter.com/dalmaer/status/726624422237364226', {}, (description) => {
71 |
72 | expect(description).to.equal({
73 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
74 | type: 'website'
75 | });
76 |
77 | done();
78 | });
79 | });
80 |
81 | it('ignores invalid oembed response (wrong version)', { parallel: false }, (done) => {
82 |
83 | const html = `
84 |
85 |
86 |
87 | `;
88 |
89 | const oembed = {
90 | type: 'link',
91 | version: '2.0',
92 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
93 | provider_name: 'Twitter'
94 | };
95 |
96 | const orig = Wreck.get;
97 | Wreck.get = (url, options, next) => {
98 |
99 | Wreck.get = orig;
100 | next(null, { statusCode: 200 }, JSON.stringify(oembed));
101 | };
102 |
103 | Metaphor.parse(html, 'https://twitter.com/dalmaer/status/726624422237364226', {}, (description) => {
104 |
105 | expect(description).to.equal({
106 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
107 | type: 'website'
108 | });
109 |
110 | done();
111 | });
112 | });
113 |
114 | it('ignores invalid oembed response (invalid payload)', { parallel: false }, (done) => {
115 |
116 | const html = `
117 |
118 |
119 |
120 | `;
121 |
122 | const orig = Wreck.get;
123 | Wreck.get = (url, options, next) => {
124 |
125 | Wreck.get = orig;
126 | next(null, { statusCode: 200 }, '{');
127 | };
128 |
129 | Metaphor.parse(html, 'https://twitter.com/dalmaer/status/726624422237364226', {}, (description) => {
130 |
131 | expect(description).to.equal({
132 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
133 | type: 'website'
134 | });
135 |
136 | done();
137 | });
138 | });
139 | });
140 |
141 | describe('match()', () => {
142 |
143 | it('returns a full service endpoint', (done) => {
144 |
145 | const router = Metaphor.oembed.providers(Providers);
146 | const resource = 'http://nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html?rref=collection%252Fnewseventcollection%252FPresidential+Election+2016&contentId=&mediaId=&referrer=http%3A%2F%2Fwww.nytimes.com%2F%3Faction%3Dclick%26contentCollection%3DPolitics%26region%3DTopBar%26module%3DHomePage-Button%26pgtype%3Darticle%26WT.z_jog%3D1%26hF%3Dt%26vS%3Dundefined&priority=true&action=click&contentCollection=Politics&module=Collection®ion=Marginalia&src=me&version=newsevent&pgtype=article';
147 | const url = router.match(resource);
148 | expect(url).to.equal(`https://www.nytimes.com/svc/oembed/json/?url=${encodeURIComponent(resource)}&format=json`);
149 | done();
150 | });
151 |
152 | it('returns a full service endpoint (options)', (done) => {
153 |
154 | const router = Metaphor.oembed.providers(Providers);
155 | const resource = 'http://nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html?rref=collection%252Fnewseventcollection%252FPresidential+Election+2016&contentId=&mediaId=&referrer=http%3A%2F%2Fwww.nytimes.com%2F%3Faction%3Dclick%26contentCollection%3DPolitics%26region%3DTopBar%26module%3DHomePage-Button%26pgtype%3Darticle%26WT.z_jog%3D1%26hF%3Dt%26vS%3Dundefined&priority=true&action=click&contentCollection=Politics&module=Collection®ion=Marginalia&src=me&version=newsevent&pgtype=article';
156 | const url = router.match(resource, { maxWidth: 250, maxHeight: 120 });
157 | expect(url).to.equal(`https://www.nytimes.com/svc/oembed/json/?url=${encodeURIComponent(resource)}&format=json&maxheight=120&maxwidth=250`);
158 | done();
159 | });
160 |
161 | it('returns a null on mismatching url', (done) => {
162 |
163 | const router = Metaphor.oembed.providers(Providers);
164 | const resource = 'http://example.com';
165 | const url = router.match(resource, { maxWidth: 250, maxHeight: 120 });
166 | expect(url).to.be.null();
167 | done();
168 | });
169 | });
170 |
171 | describe('lookup()', () => {
172 |
173 | it('parses oembed.com providers json file', (done) => {
174 |
175 | const router = Metaphor.oembed.providers(Providers);
176 | const url = router.lookup('http://nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html?rref=collection%252Fnewseventcollection%252FPresidential+Election+2016&contentId=&mediaId=&referrer=http%3A%2F%2Fwww.nytimes.com%2F%3Faction%3Dclick%26contentCollection%3DPolitics%26region%3DTopBar%26module%3DHomePage-Button%26pgtype%3Darticle%26WT.z_jog%3D1%26hF%3Dt%26vS%3Dundefined&priority=true&action=click&contentCollection=Politics&module=Collection®ion=Marginalia&src=me&version=newsevent&pgtype=article');
177 | expect(url).to.equal('https://www.nytimes.com/svc/oembed/json/');
178 | done();
179 | });
180 |
181 | it('matches resource (www)', (done) => {
182 |
183 | const router = Metaphor.oembed.providers(Providers);
184 | const url = router.lookup('http://www.nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html?rref=collection%252Fnewseventcollection%252FPresidential+Election+2016&contentId=&mediaId=&referrer=http%3A%2F%2Fwww.nytimes.com%2F%3Faction%3Dclick%26contentCollection%3DPolitics%26region%3DTopBar%26module%3DHomePage-Button%26pgtype%3Darticle%26WT.z_jog%3D1%26hF%3Dt%26vS%3Dundefined&priority=true&action=click&contentCollection=Politics&module=Collection®ion=Marginalia&src=me&version=newsevent&pgtype=article');
185 | expect(url).to.equal('https://www.nytimes.com/svc/oembed/json/');
186 | done();
187 | });
188 |
189 | it('matches resource (wildcard)', (done) => {
190 |
191 | const router = Metaphor.oembed.providers(Providers);
192 | const url = router.lookup('http://hammer-family.smugmug.com/Scotch/Bruichladdich/i-LLqFWHM');
193 | expect(url).to.equal('http://api.smugmug.com/services/oembed/');
194 | done();
195 | });
196 |
197 | it('matches resource (path)', (done) => {
198 |
199 | const router = Metaphor.oembed.providers(Providers);
200 | const url = router.lookup('https://photos.app.net/z/y');
201 | expect(url).to.equal('https://alpha-api.app.net/oembed');
202 | done();
203 | });
204 |
205 | it('fails to find a match (path)', (done) => {
206 |
207 | const router = Metaphor.oembed.providers(Providers);
208 | const url = router.lookup('https://alpha.app.net/z/y');
209 | expect(url).to.be.null();
210 | done();
211 | });
212 |
213 | it('fails to find a match (short)', (done) => {
214 |
215 | const router = Metaphor.oembed.providers(Providers);
216 | const url = router.lookup('http://streamonecloud.net/embed/x');
217 | expect(url).to.be.null();
218 | done();
219 | });
220 |
221 | it('fails to find a match (long)', (done) => {
222 |
223 | const router = Metaphor.oembed.providers(Providers);
224 | const url = router.lookup('http://x.content.streamonecloud.net/embed/x');
225 | expect(url).to.be.null();
226 | done();
227 | });
228 |
229 | it('fails to find a match (longer)', (done) => {
230 |
231 | const router = Metaphor.oembed.providers(Providers);
232 | const url = router.lookup('http://y.x.content.streamonecloud.net/embed/x');
233 | expect(url).to.be.null();
234 | done();
235 | });
236 |
237 | it('ignores invalid endpoint scheme', (done) => {
238 |
239 | const router = Metaphor.oembed.providers([
240 | {
241 | provider_name: 'Test provider',
242 | provider_url: 'https:\/\/example.com\/',
243 | endpoints: [
244 | {
245 | schemes: ['ftp:\/\/example.com\/*\/post\/*'],
246 | url: 'https:\/\/example.com\/oembed'
247 | }
248 | ]
249 | }
250 | ]);
251 |
252 | expect(router._domains.subs).to.be.empty();
253 | done();
254 | });
255 | });
256 | });
257 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Url = require('url');
6 | const Content = require('content');
7 | const Hoek = require('hoek');
8 | const Items = require('items');
9 | const Joi = require('joi');
10 | const Wreck = require('wreck');
11 | const Oembed = require('./oembed');
12 | const Ogp = require('./ogp');
13 | const Providers = require('../providers.json');
14 | const Router = require('./router');
15 | const Tags = require('./tags');
16 | const Twitter = require('./twitter');
17 | const Utils = require('./utils');
18 |
19 |
20 | // Declare internals
21 |
22 | const internals = {};
23 |
24 |
25 | exports.oembed = { providers: Oembed.providers };
26 |
27 |
28 | internals.schema = Joi.object({
29 | maxWidth: Joi.number().integer().min(1),
30 | maxHeight: Joi.number().integer().min(1),
31 | maxSize: Joi.number().integer().min(1).allow(false).default(false),
32 | providers: Joi.array().allow(true, false).default(true),
33 | whitelist: Joi.array().items(Joi.string()).min(1),
34 | preview: Joi.func().allow(true, false).default(true),
35 | css: Joi.string().allow(false),
36 | script: Joi.string().allow(false),
37 | redirect: Joi.string(),
38 | summary: Joi.boolean().default(false),
39 | tweet: Joi.boolean().default(false)
40 | });
41 |
42 |
43 | exports.Engine = class {
44 | constructor(options) {
45 |
46 | this.settings = Joi.attempt(options || {}, internals.schema);
47 | if (this.settings.providers === true) {
48 | this.settings.providers = Providers;
49 | }
50 |
51 | if (this.settings.providers) {
52 | this.settings.router = Oembed.providers(this.settings.providers);
53 | }
54 |
55 | if (this.settings.whitelist) {
56 | this._whitelist = new Router();
57 | this.settings.whitelist.forEach((url) => this._whitelist.add(url, true));
58 | }
59 |
60 | if (this.settings.preview === true) {
61 | this.settings.preview = internals.preview;
62 | }
63 | }
64 |
65 | describe(url, callback) {
66 |
67 | if (!this._whitelist ||
68 | this._whitelist.lookup(url)) {
69 |
70 | return this._describe(url, callback);
71 | }
72 |
73 | return this._preview({ type: 'website', url }, Hoek.nextTick(callback));
74 | }
75 |
76 | _describe(url, callback) {
77 |
78 | let req = null;
79 | const jar = {};
80 |
81 | const setup = {
82 | headers: {
83 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'
84 | },
85 | redirects: 5,
86 | redirect303: true,
87 | redirected: (statusCode, location, redirectionReq) => {
88 |
89 | req = redirectionReq;
90 | },
91 | beforeRedirect: (method, code, location, resHeaders, redirectOptions, next) => {
92 |
93 | const formatCookies = () => {
94 |
95 | let header = '';
96 | Object.keys(jar).forEach((name) => {
97 |
98 | header += `${header ? '; ' : ''}${name}=${jar[name]}`;
99 | });
100 |
101 | redirectOptions.headers = redirectOptions.headers || {};
102 | redirectOptions.headers.cookie = header;
103 | return next();
104 | };
105 |
106 | const cookies = resHeaders['set-cookie'];
107 | if (!cookies) {
108 | return formatCookies();
109 | }
110 |
111 | cookies.forEach((cookie) => {
112 |
113 | const parts = cookie.split(';', 1)[0].split('=', 2);
114 | jar[parts[0]] = parts[1];
115 | return formatCookies();
116 | });
117 | }
118 | };
119 |
120 | req = Wreck.request('GET', url, setup, (err, res) => {
121 |
122 | if (err ||
123 | res.statusCode !== 200 ||
124 | !res.headers['content-type']) {
125 |
126 | req.abort();
127 |
128 | if (this.settings.router) {
129 | Oembed.describe(url, null, this.settings, (oembed) => {
130 |
131 | const description = { type: 'website', url };
132 | internals.fill(description, oembed, ['site_name', 'thumbnail', 'embed'], 'oembed');
133 | return this._preview(description, callback);
134 | });
135 |
136 | return;
137 | }
138 |
139 | return this._preview({ type: 'website', url }, callback);
140 | }
141 |
142 | const type = Content.type(res.headers['content-type']);
143 | if (type.isBoom) {
144 | return this._preview({ type: 'website', url }, callback);
145 | }
146 |
147 | if (type.mime === 'text/html') {
148 | Wreck.read(res, {}, (err, payload) => {
149 |
150 | if (err) {
151 | return this._preview({ type: 'website', url }, callback);
152 | }
153 |
154 | return exports.parse(payload.toString(), url, this.settings, (description) => this._preview(description, callback));
155 | });
156 |
157 | return;
158 | }
159 |
160 | req.abort();
161 |
162 | if (type.mime.match(/^image\/\w+$/)) {
163 | const description = {
164 | type: 'website',
165 | url,
166 | site_name: 'Image',
167 | embed: {
168 | type: 'photo',
169 | url
170 | },
171 | sources: ['resource']
172 | };
173 |
174 | const contentLength = res.headers['content-length'];
175 | if (contentLength) {
176 | description.embed.size = parseInt(contentLength, 10);
177 | }
178 |
179 | return this._preview(description, callback);
180 | }
181 |
182 | return this._preview({ type: 'website', url }, callback);
183 | });
184 | }
185 |
186 | _preview(description, callback) {
187 |
188 | if (!description.site_name) {
189 | const uri = Url.parse(description.url);
190 | const parts = uri.hostname.split('.');
191 | description.site_name = (parts.length >= 2 && parts[parts.length - 1] === 'com' ? parts[parts.length - 2].replace(/^\w/, ($0) => $0.toUpperCase()) : uri.hostname);
192 | }
193 |
194 | if (!this.settings.preview &&
195 | !this.settings.summary &&
196 | !this.settings.tweet) {
197 |
198 | return callback(description);
199 | }
200 |
201 | internals.sizes(description, () => {
202 |
203 | description.summary = internals.summary(description, this.settings);
204 |
205 | const preview = (next) => {
206 |
207 | if (!this.settings.preview) {
208 | return next();
209 | }
210 |
211 | this.settings.preview(description, this.settings, (result) => {
212 |
213 | if (result) {
214 | description.preview = result;
215 | }
216 |
217 | return next();
218 | });
219 | };
220 |
221 | const tweet = (next) => {
222 |
223 | if (!this.settings.tweet) {
224 | return next();
225 | }
226 |
227 | Twitter.tweet(description, (result) => {
228 |
229 | if (result) {
230 | description.tweet = result;
231 | }
232 |
233 | return next();
234 | });
235 | };
236 |
237 | Items.parallel.execute([preview, tweet], (errIgnore, result) => {
238 |
239 | if (!this.settings.summary) {
240 | delete description.summary;
241 | }
242 |
243 | return callback(description);
244 | });
245 | });
246 | }
247 | };
248 |
249 |
250 | exports.parse = function (document, url, options, next) {
251 |
252 | Tags.parse(document, url, (tags, oembedLink) => {
253 |
254 | // Parse tags
255 |
256 | const description = Ogp.describe(tags.og); // Use Open Graph as base
257 | const twitter = Twitter.describe(tags.twitter);
258 |
259 | // Obtain and parse OEmbed description
260 |
261 | Oembed.describe(url, oembedLink, options, (oembed) => {
262 |
263 | // Combine descriptions
264 |
265 | description.url = internals.url(description.url) || internals.url(oembed.url) || url;
266 |
267 | internals.fill(description, oembed, ['site_name'], 'oembed');
268 | internals.fill(description, twitter, ['description', 'title', 'image'], 'twitter');
269 | internals.fill(description, tags.meta, ['description', 'author', 'icon', 'avatar'], 'resource');
270 |
271 | Utils.copy(oembed, description, ['thumbnail', 'embed'], 'oembed');
272 | Utils.copy(twitter, description, ['app', 'player', 'twitter'], 'twitter');
273 |
274 | if (description.sources.length) {
275 | description.sources = Hoek.unique(description.sources);
276 | }
277 | else {
278 | delete description.sources;
279 | }
280 |
281 | return next(description);
282 | });
283 | });
284 | };
285 |
286 |
287 | internals.urlRx = /^https?\:\/\/.+/;
288 |
289 | internals.url = function (url) {
290 |
291 | if (!url ||
292 | !url.match(internals.urlRx)) {
293 |
294 | return null;
295 | }
296 |
297 | return url;
298 | };
299 |
300 |
301 | internals.fill = function (description, from, fields, source) {
302 |
303 | let used = false;
304 | fields.forEach((field) => {
305 |
306 | if (!description[field] &&
307 | from[field]) {
308 |
309 | description[field] = from[field];
310 | used = true;
311 | }
312 | });
313 |
314 | if (used) {
315 | description.sources = description.sources || [];
316 | description.sources.push(source);
317 | }
318 | };
319 |
320 |
321 | internals.summary = function (description, options) {
322 |
323 | const summary = {
324 | url: (options.redirect ? `${options.redirect}${encodeURIComponent(description.url)}` : description.url),
325 | title: description.title || description.url,
326 | description: description.description,
327 | icon: description.icon ? description.icon.smallest : undefined
328 | };
329 |
330 | if (description.site_name !== 'Image') {
331 | summary.site = description.site_name;
332 | }
333 |
334 | const image = internals.image(description, options);
335 | if (image) {
336 | summary.image = image;
337 | }
338 |
339 | return summary;
340 | };
341 |
342 |
343 | internals.preview = function (description, options, callback) {
344 |
345 | const summary = description.summary;
346 | const html = `
347 |
348 |
349 |
350 | ${description.title ? '' + description.title + ' ' : ''}
351 | ${options.css ? ' ' : ''}
352 | ${options.script ? '' : ''}
353 |
354 |
355 |
370 |
371 | `;
372 |
373 | return callback(html.replace(/\n\s+/g, ''));
374 | };
375 |
376 |
377 | internals.image = function (description, options) {
378 |
379 | const images = internals.images(description);
380 | if (!images.length) {
381 | return '';
382 | }
383 |
384 | if (!options.maxSize) {
385 | return images[0].url;
386 | }
387 |
388 | for (let i = 0; i < images.length; ++i) {
389 | const image = images[i];
390 | if (image.size &&
391 | image.size <= options.maxSize) {
392 |
393 | return image.url;
394 | }
395 | }
396 |
397 | return '';
398 | };
399 |
400 |
401 | internals.images = function (description) {
402 |
403 | let images = [];
404 |
405 | if (description.thumbnail) {
406 | images.push(description.thumbnail);
407 | }
408 |
409 | if (description.embed &&
410 | description.embed.type === 'photo') {
411 |
412 | images.push(description.embed);
413 | }
414 |
415 | if (description.image) {
416 | images = images.concat(description.image);
417 | }
418 |
419 | return images;
420 | };
421 |
422 |
423 | internals.sizes = function (description, callback) {
424 |
425 | const each = (image, next) => {
426 |
427 | if (image.size) {
428 | return next();
429 | }
430 |
431 | Wreck.request('HEAD', image.url, {}, (err, res) => {
432 |
433 | if (err) {
434 | return next();
435 | }
436 |
437 | const contentLength = res.headers['content-length'];
438 | if (contentLength) {
439 | image.size = parseInt(contentLength, 10);
440 | }
441 |
442 | Wreck.read(res, null, next); // Flush out any payload
443 | });
444 | };
445 |
446 | const images = internals.images(description);
447 | Items.parallel(images, each, callback);
448 | };
449 |
--------------------------------------------------------------------------------
/test/ogp.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Code = require('code');
6 | const Lab = require('lab');
7 | const Metaphor = require('..');
8 |
9 |
10 | // Declare internals
11 |
12 | const internals = {};
13 |
14 |
15 | // Test shortcuts
16 |
17 | const lab = exports.lab = Lab.script();
18 | const describe = lab.describe;
19 | const it = lab.it;
20 | const expect = Code.expect;
21 |
22 |
23 | describe('Open Graph', () => {
24 |
25 | describe('describe()', () => {
26 |
27 | it('supports multiple images', (done) => {
28 |
29 | const html = `
30 |
31 | The Rock (1996)
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | `;
47 |
48 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
49 |
50 | expect(description).to.equal({
51 | title: 'The Rock',
52 | type: 'video.movie',
53 | url: 'http://www.imdb.com/title/tt0117500/',
54 | image: [
55 | { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
56 | { url: 'http://ia.media-imdb.com/images/rock2.jpg' }
57 | ],
58 | sources: ['ogp']
59 | });
60 |
61 | done();
62 | });
63 | });
64 |
65 | it('supports multiple images with sub attributes', (done) => {
66 |
67 | const html = `
68 |
69 | The Rock (1996)
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | `;
87 |
88 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
89 |
90 | expect(description).to.equal({
91 | title: 'The Rock',
92 | type: 'video.movie',
93 | url: 'http://www.imdb.com/title/tt0117500/',
94 | image: [
95 | { url: 'http://ia.media-imdb.com/images/rock1.jpg', width: 500, height: 330 },
96 | { url: 'https://ia.media-imdb.com/images/rock2.jpg' }
97 | ],
98 | locale: {
99 | primary: 'en_GB',
100 | alternate: ['fr_FR', 'es_ES']
101 | },
102 | sources: ['ogp']
103 | });
104 |
105 | done();
106 | });
107 | });
108 |
109 | it('sets default type', (done) => {
110 |
111 | const html = `
112 |
113 |
114 |
115 |
116 |
117 | `;
118 |
119 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
120 |
121 | expect(description).to.equal({
122 | title: 'The Rock',
123 | type: 'website',
124 | url: 'http://www.imdb.com/title/tt0117500/',
125 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
126 | sources: ['ogp']
127 | });
128 |
129 | done();
130 | });
131 | });
132 |
133 | it('uses custom sub type', (done) => {
134 |
135 | const html = `
136 |
137 |
138 |
139 |
140 |
141 |
142 | `;
143 |
144 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
145 |
146 | expect(description).to.equal({
147 | title: 'The Rock',
148 | type: 'photo',
149 | custom_type: 'custom:photo',
150 | url: 'http://www.imdb.com/title/tt0117500/',
151 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
152 | sources: ['ogp']
153 | });
154 |
155 | done();
156 | });
157 | });
158 |
159 | it('ignores unknown custom sub type', (done) => {
160 |
161 | const html = `
162 |
163 |
164 |
165 |
166 |
167 |
168 | `;
169 |
170 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
171 |
172 | expect(description).to.equal({
173 | title: 'The Rock',
174 | type: 'custom',
175 | custom_type: 'custom:unknown',
176 | url: 'http://www.imdb.com/title/tt0117500/',
177 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
178 | sources: ['ogp']
179 | });
180 |
181 | done();
182 | });
183 | });
184 |
185 | it('ignore sub properties in the wrong place', (done) => {
186 |
187 | const html = `
188 |
189 | The Rock (1996)
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 | `;
203 |
204 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
205 |
206 | expect(description).to.equal({
207 | title: 'The Rock',
208 | type: 'video.movie',
209 | url: 'http://www.imdb.com/title/tt0117500/',
210 | image: [
211 | { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
212 | { url: 'http://ia.media-imdb.com/images/rock2.jpg' }
213 | ],
214 | sources: ['ogp']
215 | });
216 |
217 | done();
218 | });
219 | });
220 |
221 | it('ignores invalid url', (done) => {
222 |
223 | const html = `
224 |
225 |
226 |
227 |
228 |
229 | `;
230 |
231 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
232 |
233 | expect(description).to.equal({
234 | title: 'The Rock',
235 | type: 'website',
236 | url: 'http://www.imdb.com/title/tt0117500/',
237 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
238 | sources: ['ogp']
239 | });
240 |
241 | done();
242 | });
243 | });
244 |
245 | it('handles missing image', (done) => {
246 |
247 | const html = `
248 |
249 |
250 |
251 |
252 | `;
253 |
254 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
255 |
256 | expect(description).to.equal({
257 | title: 'The Rock',
258 | type: 'website',
259 | url: 'http://www.imdb.com/title/tt0117500/',
260 | sources: ['ogp']
261 | });
262 |
263 | done();
264 | });
265 | });
266 |
267 | it('handles missing url', (done) => {
268 |
269 | const html = `
270 |
271 |
272 |
273 |
274 | `;
275 |
276 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
277 |
278 | expect(description).to.equal({
279 | title: 'The Rock',
280 | type: 'website',
281 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
282 | url: 'http://www.imdb.com/title/tt0117500/',
283 | sources: ['ogp']
284 | });
285 |
286 | done();
287 | });
288 | });
289 |
290 | it('handles missing title', (done) => {
291 |
292 | const html = `
293 |
294 |
295 |
296 |
297 | `;
298 |
299 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
300 |
301 | expect(description).to.equal({
302 | type: 'website',
303 | url: 'http://www.imdb.com/title/tt0117500/',
304 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
305 | sources: ['ogp']
306 | });
307 |
308 | done();
309 | });
310 | });
311 |
312 | it('handles duplicate image url', (done) => {
313 |
314 | const html = `
315 |
316 |
317 |
318 |
319 |
320 |
321 | `;
322 |
323 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
324 |
325 | expect(description).to.equal({
326 | type: 'website',
327 | url: 'http://www.imdb.com/title/tt0117500/',
328 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
329 | sources: ['ogp']
330 | });
331 |
332 | done();
333 | });
334 | });
335 |
336 | it('handles multiple subs with missing root', (done) => {
337 |
338 | const html = `
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 | `;
348 |
349 | Metaphor.parse(html, 'http://www.imdb.com/title/tt0117500/', {}, (description) => {
350 |
351 | expect(description).to.equal({
352 | title: 'The Rock',
353 | type: 'video',
354 | url: 'http://www.imdb.com/title/tt0117500/',
355 | image: { url: 'http://ia.media-imdb.com/images/rock1.jpg' },
356 | sources: ['ogp']
357 | });
358 |
359 | done();
360 | });
361 | });
362 | });
363 | });
364 |
--------------------------------------------------------------------------------
/test/twitter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Code = require('code');
6 | const Lab = require('lab');
7 | const Metaphor = require('..');
8 | const Wreck = require('wreck');
9 | const Twitter = require('../lib/twitter');
10 |
11 |
12 | // Declare internals
13 |
14 | const internals = {};
15 |
16 |
17 | // Test shortcuts
18 |
19 | const lab = exports.lab = Lab.script();
20 | const describe = lab.describe;
21 | const it = lab.it;
22 | const expect = Code.expect;
23 |
24 |
25 | describe('Twitter', () => {
26 |
27 | describe('describe()', () => {
28 |
29 | it('handles Twitter account id value', (done) => {
30 |
31 | const html = `
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | `;
44 |
45 | Metaphor.parse(html, 'https://example.com', {}, (description) => {
46 |
47 | expect(description).to.equal({
48 | url: 'https://example.com',
49 | type: 'website',
50 | description: 'The House energy and water bill failed after conservatives voted against their own legislation rather than acquiesce to a bipartisan amendment.',
51 | title: 'G.O.P. Opposition to Gay Rights Provision Derails Spending Bill',
52 | twitter: {
53 | site_username: '@nytimes',
54 | creator_id: '261289053'
55 | },
56 | sources: ['twitter']
57 | });
58 |
59 | done();
60 | });
61 | });
62 |
63 | it('handles missing image url', (done) => {
64 |
65 | const html = `
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | `;
76 |
77 | Metaphor.parse(html, 'https://example.com', {}, (description) => {
78 |
79 | expect(description).to.equal({
80 | url: 'https://example.com',
81 | type: 'website',
82 | twitter: {
83 | site_username: '@nytimes'
84 | },
85 | sources: ['twitter']
86 | });
87 |
88 | done();
89 | });
90 | });
91 |
92 | it('ignores unknown app', (done) => {
93 |
94 | const html = `
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | `;
106 |
107 | Metaphor.parse(html, 'https://example.com', {}, (description) => {
108 |
109 | expect(description).to.equal({
110 | url: 'https://example.com',
111 | type: 'website',
112 | description: 'The House energy and water bill failed after conservatives voted against their own legislation rather than acquiesce to a bipartisan amendment.',
113 | title: 'G.O.P. Opposition to Gay Rights Provision Derails Spending Bill',
114 | twitter: { site_username: '@nytimes' },
115 | sources: ['twitter']
116 | });
117 |
118 | done();
119 | });
120 | });
121 |
122 | it('ignores missing app sub key', (done) => {
123 |
124 | const html = `
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | `;
136 |
137 | Metaphor.parse(html, 'https://example.com', {}, (description) => {
138 |
139 | expect(description).to.equal({
140 | url: 'https://example.com',
141 | type: 'website',
142 | description: 'The House energy and water bill failed after conservatives voted against their own legislation rather than acquiesce to a bipartisan amendment.',
143 | title: 'G.O.P. Opposition to Gay Rights Provision Derails Spending Bill',
144 | twitter: { site_username: '@nytimes' },
145 | sources: ['twitter']
146 | });
147 |
148 | done();
149 | });
150 | });
151 | });
152 |
153 | describe('tweet()', () => {
154 |
155 | it('describes a tweet with image', (done) => {
156 |
157 | const engine = new Metaphor.Engine({ tweet: true });
158 | engine.describe('https://twitter.com/ardnahoe/status/892833709438754816', (description) => {
159 |
160 | expect(description).to.equal({
161 | type: 'article',
162 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
163 | title: 'Ardnahoe Distillery on Twitter',
164 | image: {
165 | url: 'https://pbs.twimg.com/media/DGP7fj7XUAE5aZE.jpg:large',
166 | size: 273453
167 | },
168 | description: '\u201cA misty but peaceful evening at Loch Ardnahoe tonight. #Islay\u201d',
169 | site_name: 'Twitter',
170 | sources: ['ogp', 'resource', 'oembed'],
171 | icon: {
172 | any: 'https://abs.twimg.com/favicons/favicon.ico',
173 | smallest: 'https://abs.twimg.com/favicons/favicon.ico'
174 | },
175 | avatar: 'https://pbs.twimg.com/profile_images/836605178744832001/iNcUgrE-_400x400.jpg',
176 | embed: {
177 | type: 'rich',
178 | width: 550,
179 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
180 | html: '\n'
181 | },
182 | preview: 'Ardnahoe Distillery on Twitter ',
183 | tweet: {
184 | name: 'Ardnahoe Distillery',
185 | username: 'ardnahoe',
186 | content: 'A misty but peaceful evening at Loch Ardnahoe tonight. #Islay pic.twitter.com/HXc38uYxa0',
187 | date: 'August 2, 2017',
188 | links: {},
189 | avatar: 'https://pbs.twimg.com/profile_images/836605178744832001/iNcUgrE-_400x400.jpg',
190 | image: 'https://pbs.twimg.com/media/DGP7fj7XUAE5aZE.jpg:large'
191 | }
192 | });
193 |
194 | done();
195 | });
196 | });
197 |
198 | it('describes a tweet with new lines', (done) => {
199 |
200 | const engine = new Metaphor.Engine({ tweet: true });
201 | engine.describe('https://twitter.com/LagavulinWhisky/status/769958225571557376', (description) => {
202 |
203 | expect(description).to.equal({
204 | type: 'article',
205 | url: 'https://twitter.com/LagavulinWhisky/status/769958225571557376',
206 | title: 'Lagavulin on Twitter',
207 | image: {
208 | url: 'https://pbs.twimg.com/profile_images/855018870419189760/MWCzc21G_400x400.jpg',
209 | size: description.image.size
210 | },
211 | description: '\u201cThe Islay Jazz festival will take place from the 9th - 11th September this year!\n\nTickets: https://t.co/XmgYyXnWrg #Lagavulin200 #LagaJazz\u201d',
212 | site_name: 'Twitter',
213 | sources: ['ogp', 'resource', 'oembed'],
214 | icon: {
215 | any: 'https://abs.twimg.com/favicons/favicon.ico',
216 | smallest: 'https://abs.twimg.com/favicons/favicon.ico'
217 | },
218 | avatar: 'https://pbs.twimg.com/profile_images/855018870419189760/MWCzc21G_400x400.jpg',
219 | embed: {
220 | type: 'rich',
221 | width: 550,
222 | url: 'https://twitter.com/LagavulinWhisky/status/769958225571557376',
223 | html: '\n'
224 | },
225 | preview: 'Lagavulin on Twitter ',
226 | tweet: {
227 | name: 'Lagavulin',
228 | username: 'LagavulinWhisky',
229 | content: 'The Islay Jazz festival will take place from the 9th - 11th September this year!\n\nTickets: http://www.islayjazzfestival.co.uk/programme.html #Lagavulin200 #LagaJazz',
230 | date: 'August 28, 2016',
231 | links: { 'http://www.islayjazzfestival.co.uk/programme.html': 'https://t.co/XmgYyXnWrg' },
232 | avatar: 'https://pbs.twimg.com/profile_images/855018870419189760/MWCzc21G_400x400.jpg'
233 | }
234 | });
235 |
236 | done();
237 | });
238 | });
239 |
240 | it('ignores documents without embed', (done) => {
241 |
242 | Twitter.tweet({ url: 'https://twitter.com/sideway/status/626158822705401856' }, (result) => {
243 |
244 | expect(result).to.not.exist();
245 | done();
246 | });
247 | });
248 |
249 | it('ignores documents with invalid embed html', (done) => {
250 |
251 | Twitter.tweet({ url: 'https://twitter.com/sideway/status/626158822705401856', embed: { html: 'something' } }, (result) => {
252 |
253 | expect(result).to.not.exist();
254 | done();
255 | });
256 | });
257 |
258 | it('parses message without links', (done) => {
259 |
260 | const description = {
261 | url: 'https://twitter.com/sideway/status/626158822705401856',
262 | embed: {
263 | html: '\n'
264 | }
265 | };
266 |
267 | Twitter.tweet(description, (result) => {
268 |
269 | expect(result).to.equal({
270 | name: 'Sideway',
271 | username: 'sideway',
272 | content: 'First & steps',
273 | date: 'July 28, 2015',
274 | links: {}
275 | });
276 |
277 | done();
278 | });
279 | });
280 |
281 | it('uses image for avatar', (done) => {
282 |
283 | const description = {
284 | url: 'https://twitter.com/sideway/status/626158822705401856',
285 | embed: {
286 | html: '\n'
287 | },
288 | image: {
289 | url: 'https://example.com/image'
290 | }
291 | };
292 |
293 | Twitter.tweet(description, (result) => {
294 |
295 | expect(result).to.equal({
296 | name: 'Sideway',
297 | username: 'sideway',
298 | avatar: 'https://example.com/image',
299 | content: 'First & steps',
300 | date: 'July 28, 2015',
301 | links: {}
302 | });
303 |
304 | done();
305 | });
306 | });
307 |
308 | it('skips image for avatar', (done) => {
309 |
310 | const description = {
311 | url: 'https://twitter.com/sideway/status/626158822705401856',
312 | embed: {
313 | html: '\n'
314 | },
315 | image: {}
316 | };
317 |
318 | Twitter.tweet(description, (result) => {
319 |
320 | expect(result).to.equal({
321 | name: 'Sideway',
322 | username: 'sideway',
323 | avatar: 'https://example.com/image',
324 | content: 'First & steps',
325 | date: 'July 28, 2015',
326 | links: {}
327 | });
328 |
329 | done();
330 | });
331 | });
332 |
333 | it('parses message with multiple links', (done) => {
334 |
335 | const description = {
336 | url: 'https://twitter.com/sideway/status/626158822705401856',
337 | embed: {
338 | html: '\n'
339 | }
340 | };
341 |
342 | Twitter.tweet(description, (result) => {
343 |
344 | expect(result).to.equal({
345 | name: 'Sideway',
346 | username: 'sideway',
347 | content: 'First & steps https://sideway.com https://example.com https://t.co/',
348 | date: 'July 28, 2015',
349 | links: {
350 | 'https://sideway.com': 'https://t.co/XvSn7XSI2G'
351 | }
352 | });
353 |
354 | done();
355 | });
356 | });
357 | });
358 |
359 | describe('long()', () => {
360 |
361 | it('handles request error', { parallel: false }, (done) => {
362 |
363 | const description = {
364 | url: 'https://twitter.com/sideway/status/626158822705401856',
365 | embed: {
366 | html: '\n'
367 | }
368 | };
369 |
370 | const orig = Wreck.request;
371 | Wreck.request = (method, url, options, next) => next(new Error());
372 |
373 | Twitter.tweet(description, (result) => {
374 |
375 | Wreck.request = orig;
376 |
377 | expect(result).to.equal({
378 | name: 'Sideway',
379 | username: 'sideway',
380 | content: 'First & steps https://t.co/XvSn7XSI2G',
381 | date: 'July 28, 2015',
382 | links: {}
383 | });
384 |
385 | done();
386 | });
387 | });
388 | });
389 | });
390 |
--------------------------------------------------------------------------------
/providers.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "provider_name": "23HQ",
4 | "provider_url": "http:\/\/www.23hq.com",
5 | "endpoints": [
6 | {
7 | "schemes": [
8 | "http:\/\/www.23hq.com\/*\/photo\/*"
9 | ],
10 | "url": "http:\/\/www.23hq.com\/23\/oembed"
11 | }
12 | ]
13 | },
14 | {
15 | "provider_name": "Alpha App Net",
16 | "provider_url": "https:\/\/alpha.app.net\/browse\/posts\/",
17 | "endpoints": [
18 | {
19 | "schemes": [
20 | "https:\/\/alpha.app.net\/*\/post\/*",
21 | "https:\/\/photos.app.net\/*\/*"
22 | ],
23 | "url": "https:\/\/alpha-api.app.net\/oembed",
24 | "formats": [
25 | "json"
26 | ]
27 | }
28 | ]
29 | },
30 | {
31 | "provider_name": "amCharts Live Editor",
32 | "provider_url": "http:\/\/live.amcharts.com\/",
33 | "endpoints": [
34 | {
35 | "schemes": [
36 | "http:\/\/live.amcharts.com\/*"
37 | ],
38 | "url": "http:\/\/live.amcharts.com\/oembed"
39 | }
40 | ]
41 | },
42 | {
43 | "provider_name": "Animatron",
44 | "provider_url": "https:\/\/www.animatron.com\/",
45 | "endpoints": [
46 | {
47 | "schemes": [
48 | "https:\/\/www.animatron.com\/project\/*",
49 | "https:\/\/animatron.com\/project\/*"
50 | ],
51 | "url": "https:\/\/animatron.com\/oembed\/json",
52 | "discovery": true
53 | }
54 | ]
55 | },
56 | {
57 | "provider_name": "Animoto",
58 | "provider_url": "http:\/\/animoto.com\/",
59 | "endpoints": [
60 | {
61 | "schemes": [
62 | "http:\/\/animoto.com\/play\/*"
63 | ],
64 | "url": "http:\/\/animoto.com\/oembeds\/create"
65 | }
66 | ]
67 | },
68 | {
69 | "provider_name": "AudioSnaps",
70 | "provider_url": "http:\/\/audiosnaps.com",
71 | "endpoints": [
72 | {
73 | "schemes": [
74 | "http:\/\/audiosnaps.com\/k\/*"
75 | ],
76 | "url": "http:\/\/audiosnaps.com\/service\/oembed",
77 | "discovery": true
78 | }
79 | ]
80 | },
81 | {
82 | "provider_name": "Blackfire.io",
83 | "provider_url": "https:\/\/blackfire.io",
84 | "endpoints": [
85 | {
86 | "schemes": [
87 | "https:\/\/blackfire.io\/profiles\/*\/graph",
88 | "https:\/\/blackfire.io\/profiles\/compare\/*\/graph"
89 | ],
90 | "url": "https:\/\/blackfire.io\/oembed",
91 | "discovery": true
92 | }
93 | ]
94 | },
95 | {
96 | "provider_name": "Cacoo",
97 | "provider_url": "https:\/\/cacoo.com",
98 | "endpoints": [
99 | {
100 | "schemes": [
101 | "https:\/\/cacoo.com\/diagrams\/*"
102 | ],
103 | "url": "http:\/\/cacoo.com\/oembed.{format}"
104 | }
105 | ]
106 | },
107 | {
108 | "provider_name": "CatBoat",
109 | "provider_url": "http:\/\/img.catbo.at\/",
110 | "endpoints": [
111 | {
112 | "schemes": [
113 | "http:\/\/img.catbo.at\/*"
114 | ],
115 | "url": "http:\/\/img.catbo.at\/oembed.json",
116 | "formats": [
117 | "json"
118 | ]
119 | }
120 | ]
121 | },
122 | {
123 | "provider_name": "ChartBlocks",
124 | "provider_url": "http:\/\/www.chartblocks.com\/",
125 | "endpoints": [
126 | {
127 | "schemes": [
128 | "http:\/\/public.chartblocks.com\/c\/*"
129 | ],
130 | "url": "http:\/\/embed.chartblocks.com\/1.0\/oembed"
131 | }
132 | ]
133 | },
134 | {
135 | "provider_name": "chirbit.com",
136 | "provider_url": "http:\/\/www.chirbit.com\/",
137 | "endpoints": [
138 | {
139 | "schemes": [
140 | "http:\/\/chirb.it\/*"
141 | ],
142 | "url": "http:\/\/chirb.it\/oembed.{format}",
143 | "discovery": true
144 | }
145 | ]
146 | },
147 | {
148 | "provider_name": "CircuitLab",
149 | "provider_url": "https:\/\/www.circuitlab.com\/",
150 | "endpoints": [
151 | {
152 | "schemes": [
153 | "https:\/\/www.circuitlab.com\/circuit\/*"
154 | ],
155 | "url": "https:\/\/www.circuitlab.com\/circuit\/oembed\/",
156 | "discovery": true
157 | }
158 | ]
159 | },
160 | {
161 | "provider_name": "Clyp",
162 | "provider_url": "http:\/\/clyp.it\/",
163 | "endpoints": [
164 | {
165 | "schemes": [
166 | "http:\/\/clyp.it\/*",
167 | "http:\/\/clyp.it\/playlist\/*"
168 | ],
169 | "url": "http:\/\/api.clyp.it\/oembed\/",
170 | "discovery": true
171 | }
172 | ]
173 | },
174 | {
175 | "provider_name": "Codepen",
176 | "provider_url": "https:\/\/codepen.io",
177 | "endpoints": [
178 | {
179 | "schemes": [
180 | "http:\/\/codepen.io\/*",
181 | "https:\/\/codepen.io\/*"
182 | ],
183 | "url": "http:\/\/codepen.io\/api\/oembed"
184 | }
185 | ]
186 | },
187 | {
188 | "provider_name": "Codepoints",
189 | "provider_url": "https:\/\/codepoints.net",
190 | "endpoints": [
191 | {
192 | "schemes": [
193 | "http:\/\/codepoints.net\/*",
194 | "https:\/\/codepoints.net\/*",
195 | "http:\/\/www.codepoints.net\/*",
196 | "https:\/\/www.codepoints.net\/*"
197 | ],
198 | "url": "https:\/\/codepoints.net\/api\/v1\/oembed",
199 | "discovery": true
200 | }
201 | ]
202 | },
203 | {
204 | "provider_name": "CollegeHumor",
205 | "provider_url": "http:\/\/www.collegehumor.com\/",
206 | "endpoints": [
207 | {
208 | "schemes": [
209 | "http:\/\/www.collegehumor.com\/video\/*"
210 | ],
211 | "url": "http:\/\/www.collegehumor.com\/oembed.{format}",
212 | "discovery": true
213 | }
214 | ]
215 | },
216 | {
217 | "provider_name": "Coub",
218 | "provider_url": "http:\/\/coub.com\/",
219 | "endpoints": [
220 | {
221 | "schemes": [
222 | "http:\/\/coub.com\/view\/*",
223 | "http:\/\/coub.com\/embed\/*"
224 | ],
225 | "url": "http:\/\/coub.com\/api\/oembed.{format}"
226 | }
227 | ]
228 | },
229 | {
230 | "provider_name": "Crowd Ranking",
231 | "provider_url": "http:\/\/crowdranking.com",
232 | "endpoints": [
233 | {
234 | "schemes": [
235 | "http:\/\/crowdranking.com\/*\/*"
236 | ],
237 | "url": "http:\/\/crowdranking.com\/api\/oembed.{format}"
238 | }
239 | ]
240 | },
241 | {
242 | "provider_name": "Daily Mile",
243 | "provider_url": "http:\/\/www.dailymile.com",
244 | "endpoints": [
245 | {
246 | "schemes": [
247 | "http:\/\/www.dailymile.com\/people\/*\/entries\/*"
248 | ],
249 | "url": "http:\/\/api.dailymile.com\/oembed?format=json",
250 | "formats": [
251 | "json"
252 | ]
253 | }
254 | ]
255 | },
256 | {
257 | "provider_name": "Dailymotion",
258 | "provider_url": "http:\/\/www.dailymotion.com",
259 | "endpoints": [
260 | {
261 | "schemes": [
262 | "http:\/\/www.dailymotion.com\/video\/*"
263 | ],
264 | "url": "http:\/\/www.dailymotion.com\/services\/oembed",
265 | "discovery": true
266 | }
267 | ]
268 | },
269 | {
270 | "provider_name": "Deviantart.com",
271 | "provider_url": "http:\/\/www.deviantart.com",
272 | "endpoints": [
273 | {
274 | "schemes": [
275 | "http:\/\/*.deviantart.com\/art\/*",
276 | "http:\/\/*.deviantart.com\/*#\/d*",
277 | "http:\/\/fav.me\/*",
278 | "http:\/\/sta.sh\/*"
279 | ],
280 | "url": "http:\/\/backend.deviantart.com\/oembed"
281 | }
282 | ]
283 | },
284 | {
285 | "provider_name": "Didacte",
286 | "provider_url": "https:\/\/www.didacte.com\/",
287 | "endpoints": [
288 | {
289 | "schemes": [
290 | "https:\/\/*.didacte.com\/a\/course\/*"
291 | ],
292 | "url": "https:\/\/*.didacte.com\/cards\/oembed'",
293 | "discovery": true,
294 | "formats": [
295 | "json"
296 | ]
297 | }
298 | ]
299 | },
300 | {
301 | "provider_name": "Dipity",
302 | "provider_url": "http:\/\/www.dipity.com",
303 | "endpoints": [
304 | {
305 | "schemes": [
306 | "http:\/\/www.dipity.com\/*\/*\/"
307 | ],
308 | "url": "http:\/\/www.dipity.com\/oembed\/timeline\/"
309 | }
310 | ]
311 | },
312 | {
313 | "provider_name": "Docs",
314 | "provider_url": "https:\/\/www.docs.com",
315 | "endpoints": [
316 | {
317 | "schemes": [
318 | "https:\/\/docs.com\/*",
319 | "https:\/\/www.docs.com\/*"
320 | ],
321 | "url": "https:\/\/docs.com\/api\/oembed",
322 | "discovery": true
323 | }
324 | ]
325 | },
326 | {
327 | "provider_name": "Dotsub",
328 | "provider_url": "http:\/\/dotsub.com\/",
329 | "endpoints": [
330 | {
331 | "schemes": [
332 | "http:\/\/dotsub.com\/view\/*"
333 | ],
334 | "url": "http:\/\/dotsub.com\/services\/oembed"
335 | }
336 | ]
337 | },
338 | {
339 | "provider_name": "edocr",
340 | "provider_url": "http:\/\/www.edocr.com",
341 | "endpoints": [
342 | {
343 | "schemes": [
344 | "http:\/\/edocr.com\/docs\/*"
345 | ],
346 | "url": "http:\/\/edocr.com\/api\/oembed"
347 | }
348 | ]
349 | },
350 | {
351 | "provider_name": "EgliseInfo",
352 | "provider_url": "http:\/\/egliseinfo.catholique.fr\/",
353 | "endpoints": [
354 | {
355 | "schemes": [
356 | "http:\/\/egliseinfo.catholique.fr\/*"
357 | ],
358 | "url": "http:\/\/egliseinfo.catholique.fr\/api\/oembed",
359 | "discovery": true
360 | }
361 | ]
362 | },
363 | {
364 | "provider_name": "Embed Articles",
365 | "provider_url": "http:\/\/embedarticles.com\/",
366 | "endpoints": [
367 | {
368 | "schemes": [
369 | "http:\/\/embedarticles.com\/*"
370 | ],
371 | "url": "http:\/\/embedarticles.com\/oembed\/"
372 | }
373 | ]
374 | },
375 | {
376 | "provider_name": "Embedly",
377 | "provider_url": "http:\/\/api.embed.ly\/",
378 | "endpoints": [
379 | {
380 | "url": "http:\/\/api.embed.ly\/1\/oembed"
381 | }
382 | ]
383 | },
384 | {
385 | "provider_name": "Flickr",
386 | "provider_url": "http:\/\/www.flickr.com\/",
387 | "endpoints": [
388 | {
389 | "schemes": [
390 | "http:\/\/*.flickr.com\/photos\/*",
391 | "http:\/\/flic.kr\/p\/*"
392 | ],
393 | "url": "http:\/\/www.flickr.com\/services\/oembed\/",
394 | "discovery": true
395 | }
396 | ]
397 | },
398 | {
399 | "provider_name": "FOX SPORTS Australia",
400 | "provider_url": "http:\/\/www.foxsports.com.au",
401 | "endpoints": [
402 | {
403 | "schemes": [
404 | "http:\/\/fiso.foxsports.com.au\/isomorphic-widget\/*",
405 | "https:\/\/fiso.foxsports.com.au\/isomorphic-widget\/*"
406 | ],
407 | "url": "https:\/\/fiso.foxsports.com.au\/oembed"
408 | }
409 | ]
410 | },
411 | {
412 | "provider_name": "FunnyOrDie",
413 | "provider_url": "http:\/\/www.funnyordie.com\/",
414 | "endpoints": [
415 | {
416 | "schemes": [
417 | "http:\/\/www.funnyordie.com\/videos\/*"
418 | ],
419 | "url": "http:\/\/www.funnyordie.com\/oembed.{format}"
420 | }
421 | ]
422 | },
423 | {
424 | "provider_name": "Geograph Britain and Ireland",
425 | "provider_url": "https:\/\/www.geograph.org.uk\/",
426 | "endpoints": [
427 | {
428 | "schemes": [
429 | "http:\/\/*.geograph.org.uk\/*",
430 | "http:\/\/*.geograph.co.uk\/*",
431 | "http:\/\/*.geograph.ie\/*",
432 | "http:\/\/*.wikimedia.org\/*_geograph.org.uk_*"
433 | ],
434 | "url": "http:\/\/api.geograph.org.uk\/api\/oembed"
435 | }
436 | ]
437 | },
438 | {
439 | "provider_name": "Geograph Channel Islands",
440 | "provider_url": "http:\/\/channel-islands.geograph.org\/",
441 | "endpoints": [
442 | {
443 | "schemes": [
444 | "http:\/\/*.geograph.org.gg\/*",
445 | "http:\/\/*.geograph.org.je\/*",
446 | "http:\/\/channel-islands.geograph.org\/*",
447 | "http:\/\/channel-islands.geographs.org\/*",
448 | "http:\/\/*.channel.geographs.org\/*"
449 | ],
450 | "url": "http:\/\/www.geograph.org.gg\/api\/oembed"
451 | }
452 | ]
453 | },
454 | {
455 | "provider_name": "Geograph Germany",
456 | "provider_url": "http:\/\/geo-en.hlipp.de\/",
457 | "endpoints": [
458 | {
459 | "schemes": [
460 | "http:\/\/geo-en.hlipp.de\/*",
461 | "http:\/\/geo.hlipp.de\/*",
462 | "http:\/\/germany.geograph.org\/*"
463 | ],
464 | "url": "http:\/\/geo.hlipp.de\/restapi.php\/api\/oembed"
465 | }
466 | ]
467 | },
468 | {
469 | "provider_name": "Getty Images",
470 | "provider_url": "http:\/\/www.gettyimages.com\/",
471 | "endpoints": [
472 | {
473 | "schemes": [
474 | "http:\/\/gty.im\/*"
475 | ],
476 | "url": "http:\/\/embed.gettyimages.com\/oembed",
477 | "formats": [
478 | "json"
479 | ]
480 | }
481 | ]
482 | },
483 | {
484 | "provider_name": "Gfycat",
485 | "provider_url": "https:\/\/gfycat.com\/",
486 | "endpoints": [
487 | {
488 | "schemes": [
489 | "http:\/\/gfycat.com\/*",
490 | "http:\/\/www.gfycat.com\/*",
491 | "https:\/\/gfycat.com\/*",
492 | "https:\/\/www.gfycat.com\/*"
493 | ],
494 | "url": "https:\/\/api.gfycat.com\/v1\/oembed",
495 | "discovery": true
496 | }
497 | ]
498 | },
499 | {
500 | "provider_name": "HuffDuffer",
501 | "provider_url": "http:\/\/huffduffer.com",
502 | "endpoints": [
503 | {
504 | "schemes": [
505 | "http:\/\/huffduffer.com\/*\/*"
506 | ],
507 | "url": "http:\/\/huffduffer.com\/oembed"
508 | }
509 | ]
510 | },
511 | {
512 | "provider_name": "Hulu",
513 | "provider_url": "http:\/\/www.hulu.com\/",
514 | "endpoints": [
515 | {
516 | "schemes": [
517 | "http:\/\/www.hulu.com\/watch\/*"
518 | ],
519 | "url": "http:\/\/www.hulu.com\/api\/oembed.{format}"
520 | }
521 | ]
522 | },
523 | {
524 | "provider_name": "iFixit",
525 | "provider_url": "http:\/\/www.iFixit.com",
526 | "endpoints": [
527 | {
528 | "schemes": [
529 | "http:\/\/www.ifixit.com\/Guide\/View\/*"
530 | ],
531 | "url": "http:\/\/www.ifixit.com\/Embed"
532 | }
533 | ]
534 | },
535 | {
536 | "provider_name": "IFTTT",
537 | "provider_url": "http:\/\/www.ifttt.com\/",
538 | "endpoints": [
539 | {
540 | "schemes": [
541 | "http:\/\/ifttt.com\/recipes\/*"
542 | ],
543 | "url": "http:\/\/www.ifttt.com\/oembed\/",
544 | "discovery": true
545 | }
546 | ]
547 | },
548 | {
549 | "provider_name": "Infogram",
550 | "provider_url": "https:\/\/infogr.am\/",
551 | "endpoints": [
552 | {
553 | "schemes": [
554 | "https:\/\/infogr.am\/*"
555 | ],
556 | "url": "https:\/\/infogr.am\/oembed"
557 | }
558 | ]
559 | },
560 | {
561 | "provider_name": "Instagram",
562 | "provider_url": "https:\/\/instagram.com",
563 | "endpoints": [
564 | {
565 | "schemes": [
566 | "http:\/\/instagram.com\/p\/*",
567 | "http:\/\/instagr.am\/p\/*",
568 | "https:\/\/instagram.com\/p\/*",
569 | "https:\/\/instagr.am\/p\/*"
570 | ],
571 | "url": "http:\/\/api.instagram.com\/oembed",
572 | "formats": [
573 | "json"
574 | ]
575 | }
576 | ]
577 | },
578 | {
579 | "provider_name": "iSnare Articles",
580 | "provider_url": "https:\/\/www.isnare.com\/",
581 | "endpoints": [
582 | {
583 | "schemes": [
584 | "https:\/\/www.isnare.com\/*"
585 | ],
586 | "url": "https:\/\/www.isnare.com\/oembed\/"
587 | }
588 | ]
589 | },
590 | {
591 | "provider_name": "Kickstarter",
592 | "provider_url": "http:\/\/www.kickstarter.com",
593 | "endpoints": [
594 | {
595 | "schemes": [
596 | "http:\/\/www.kickstarter.com\/projects\/*"
597 | ],
598 | "url": "http:\/\/www.kickstarter.com\/services\/oembed"
599 | }
600 | ]
601 | },
602 | {
603 | "provider_name": "Kitchenbowl",
604 | "provider_url": "http:\/\/www.kitchenbowl.com",
605 | "endpoints": [
606 | {
607 | "schemes": [
608 | "http:\/\/www.kitchenbowl.com\/recipe\/*"
609 | ],
610 | "url": "http:\/\/www.kitchenbowl.com\/oembed",
611 | "discovery": true
612 | }
613 | ]
614 | },
615 | {
616 | "provider_name": "LearningApps.org",
617 | "provider_url": "http:\/\/learningapps.org\/",
618 | "endpoints": [
619 | {
620 | "schemes": [
621 | "http:\/\/learningapps.org\/*"
622 | ],
623 | "url": "http:\/\/learningapps.org\/oembed.php",
624 | "discovery": true
625 | }
626 | ]
627 | },
628 | {
629 | "provider_name": "Meetup",
630 | "provider_url": "http:\/\/www.meetup.com",
631 | "endpoints": [
632 | {
633 | "schemes": [
634 | "http:\/\/meetup.com\/*",
635 | "http:\/\/meetu.ps\/*"
636 | ],
637 | "url": "https:\/\/api.meetup.com\/oembed",
638 | "formats": [
639 | "json"
640 | ]
641 | }
642 | ]
643 | },
644 | {
645 | "provider_name": "MixCloud",
646 | "provider_url": "http:\/\/mixcloud.com\/",
647 | "endpoints": [
648 | {
649 | "schemes": [
650 | "http:\/\/www.mixcloud.com\/*\/*\/"
651 | ],
652 | "url": "http:\/\/www.mixcloud.com\/oembed\/"
653 | }
654 | ]
655 | },
656 | {
657 | "provider_name": "Moby Picture",
658 | "provider_url": "http:\/\/www.mobypicture.com",
659 | "endpoints": [
660 | {
661 | "schemes": [
662 | "http:\/\/www.mobypicture.com\/user\/*\/view\/*",
663 | "http:\/\/moby.to\/*"
664 | ],
665 | "url": "http:\/\/api.mobypicture.com\/oEmbed"
666 | }
667 | ]
668 | },
669 | {
670 | "provider_name": "nfb.ca",
671 | "provider_url": "http:\/\/www.nfb.ca\/",
672 | "endpoints": [
673 | {
674 | "schemes": [
675 | "http:\/\/*.nfb.ca\/film\/*"
676 | ],
677 | "url": "http:\/\/www.nfb.ca\/remote\/services\/oembed\/",
678 | "discovery": true
679 | }
680 | ]
681 | },
682 | {
683 | "provider_name": "Office Mix",
684 | "provider_url": "http:\/\/mix.office.com\/",
685 | "endpoints": [
686 | {
687 | "schemes": [
688 | "https:\/\/mix.office.com\/watch\/*",
689 | "https:\/\/mix.office.com\/embed\/*"
690 | ],
691 | "url": "https:\/\/mix.office.com\/oembed",
692 | "discovery": true
693 | }
694 | ]
695 | },
696 | {
697 | "provider_name": "Official FM",
698 | "provider_url": "http:\/\/official.fm",
699 | "endpoints": [
700 | {
701 | "schemes": [
702 | "http:\/\/official.fm\/tracks\/*",
703 | "http:\/\/official.fm\/playlists\/*"
704 | ],
705 | "url": "http:\/\/official.fm\/services\/oembed.{format}"
706 | }
707 | ]
708 | },
709 | {
710 | "provider_name": "On Aol",
711 | "provider_url": "http:\/\/on.aol.com\/",
712 | "endpoints": [
713 | {
714 | "schemes": [
715 | "http:\/\/on.aol.com\/video\/*"
716 | ],
717 | "url": "http:\/\/on.aol.com\/api"
718 | }
719 | ]
720 | },
721 | {
722 | "provider_name": "Ora TV",
723 | "provider_url": "http:\/\/www.ora.tv\/",
724 | "endpoints": [
725 | {
726 | "discovery": true,
727 | "url": "https:\/\/www.ora.tv\/oembed\/*?format={format}"
728 | }
729 | ]
730 | },
731 | {
732 | "provider_name": "Oumy",
733 | "provider_url": "https:\/\/www.oumy.com\/",
734 | "endpoints": [
735 | {
736 | "schemes": [
737 | "https:\/\/www.oumy.com\/v\/*"
738 | ],
739 | "url": "https:\/\/www.oumy.com\/oembed",
740 | "discovery": true
741 | }
742 | ]
743 | },
744 | {
745 | "provider_name": "Pastery",
746 | "provider_url": "https:\/\/www.pastery.net",
747 | "endpoints": [
748 | {
749 | "schemes": [
750 | "http:\/\/pastery.net\/*",
751 | "https:\/\/pastery.net\/*",
752 | "http:\/\/www.pastery.net\/*",
753 | "https:\/\/www.pastery.net\/*"
754 | ],
755 | "url": "https:\/\/www.pastery.net\/oembed",
756 | "discovery": true
757 | }
758 | ]
759 | },
760 | {
761 | "provider_name": "Poll Daddy",
762 | "provider_url": "http:\/\/polldaddy.com",
763 | "endpoints": [
764 | {
765 | "schemes": [
766 | "http:\/\/*.polldaddy.com\/s\/*",
767 | "http:\/\/*.polldaddy.com\/poll\/*",
768 | "http:\/\/*.polldaddy.com\/ratings\/*"
769 | ],
770 | "url": "http:\/\/polldaddy.com\/oembed\/"
771 | }
772 | ]
773 | },
774 | {
775 | "provider_name": "Portfolium",
776 | "provider_url": "https:\/\/portfolium.com",
777 | "endpoints": [
778 | {
779 | "schemes": [
780 | "https:\/\/portfolium.com\/entry\/*"
781 | ],
782 | "url": "https:\/\/api.portfolium.com\/oembed"
783 | }
784 | ]
785 | },
786 | {
787 | "provider_name": "Quiz.biz",
788 | "provider_url": "http:\/\/www.quiz.biz\/",
789 | "endpoints": [
790 | {
791 | "schemes": [
792 | "http:\/\/www.quiz.biz\/quizz-*.html"
793 | ],
794 | "url": "http:\/\/www.quiz.biz\/api\/oembed",
795 | "discovery": true
796 | }
797 | ]
798 | },
799 | {
800 | "provider_name": "Quizz.biz",
801 | "provider_url": "http:\/\/www.quizz.biz\/",
802 | "endpoints": [
803 | {
804 | "schemes": [
805 | "http:\/\/www.quizz.biz\/quizz-*.html"
806 | ],
807 | "url": "http:\/\/www.quizz.biz\/api\/oembed",
808 | "discovery": true
809 | }
810 | ]
811 | },
812 | {
813 | "provider_name": "RapidEngage",
814 | "provider_url": "https:\/\/rapidengage.com",
815 | "endpoints": [
816 | {
817 | "schemes": [
818 | "https:\/\/rapidengage.com\/s\/*"
819 | ],
820 | "url": "https:\/\/rapidengage.com\/api\/oembed"
821 | }
822 | ]
823 | },
824 | {
825 | "provider_name": "Rdio",
826 | "provider_url": "http:\/\/rdio.com\/",
827 | "endpoints": [
828 | {
829 | "schemes": [
830 | "http:\/\/*.rdio.com\/artist\/*",
831 | "http:\/\/*.rdio.com\/people\/*"
832 | ],
833 | "url": "http:\/\/www.rdio.com\/api\/oembed\/"
834 | }
835 | ]
836 | },
837 | {
838 | "provider_name": "ReleaseWire",
839 | "provider_url": "http:\/\/www.releasewire.com\/",
840 | "endpoints": [
841 | {
842 | "schemes": [
843 | "http:\/\/rwire.com\/*"
844 | ],
845 | "url": "http:\/\/publisher.releasewire.com\/oembed\/",
846 | "discovery": true
847 | }
848 | ]
849 | },
850 | {
851 | "provider_name": "RepubHub",
852 | "provider_url": "http:\/\/repubhub.icopyright.net\/",
853 | "endpoints": [
854 | {
855 | "schemes": [
856 | "http:\/\/repubhub.icopyright.net\/freePost.act?*"
857 | ],
858 | "url": "http:\/\/repubhub.icopyright.net\/oembed.act",
859 | "discovery": true
860 | }
861 | ]
862 | },
863 | {
864 | "provider_name": "ReverbNation",
865 | "provider_url": "https:\/\/www.reverbnation.com\/",
866 | "endpoints": [
867 | {
868 | "schemes": [
869 | "https:\/\/www.reverbnation.com\/*",
870 | "https:\/\/www.reverbnation.com\/*\/songs\/*"
871 | ],
872 | "url": "https:\/\/www.reverbnation.com\/oembed",
873 | "discovery": true
874 | }
875 | ]
876 | },
877 | {
878 | "provider_name": "Roomshare",
879 | "provider_url": "http:\/\/roomshare.jp",
880 | "endpoints": [
881 | {
882 | "schemes": [
883 | "http:\/\/roomshare.jp\/post\/*",
884 | "http:\/\/roomshare.jp\/en\/post\/*"
885 | ],
886 | "url": "http:\/\/roomshare.jp\/en\/oembed.{format}"
887 | }
888 | ]
889 | },
890 | {
891 | "provider_name": "Sapo Videos",
892 | "provider_url": "http:\/\/videos.sapo.pt",
893 | "endpoints": [
894 | {
895 | "schemes": [
896 | "http:\/\/videos.sapo.pt\/*"
897 | ],
898 | "url": "http:\/\/videos.sapo.pt\/oembed"
899 | }
900 | ]
901 | },
902 | {
903 | "provider_name": "Screenr",
904 | "provider_url": "http:\/\/www.screenr.com\/",
905 | "endpoints": [
906 | {
907 | "schemes": [
908 | "http:\/\/www.screenr.com\/*\/"
909 | ],
910 | "url": "http:\/\/www.screenr.com\/api\/oembed.{format}"
911 | }
912 | ]
913 | },
914 | {
915 | "provider_name": "Scribd",
916 | "provider_url": "http:\/\/www.scribd.com\/",
917 | "endpoints": [
918 | {
919 | "schemes": [
920 | "http:\/\/www.scribd.com\/doc\/*"
921 | ],
922 | "url": "http:\/\/www.scribd.com\/services\/oembed\/"
923 | }
924 | ]
925 | },
926 | {
927 | "provider_name": "ShortNote",
928 | "provider_url": "https:\/\/www.shortnote.jp\/",
929 | "endpoints": [
930 | {
931 | "schemes": [
932 | "https:\/\/www.shortnote.jp\/view\/notes\/*"
933 | ],
934 | "url": "https:\/\/www.shortnote.jp\/oembed\/",
935 | "discovery": true
936 | }
937 | ]
938 | },
939 | {
940 | "provider_name": "Shoudio",
941 | "provider_url": "http:\/\/shoudio.com",
942 | "endpoints": [
943 | {
944 | "schemes": [
945 | "http:\/\/shoudio.com\/*",
946 | "http:\/\/shoud.io\/*"
947 | ],
948 | "url": "http:\/\/shoudio.com\/api\/oembed"
949 | }
950 | ]
951 | },
952 | {
953 | "provider_name": "Show the Way, actionable location info",
954 | "provider_url": "https:\/\/showtheway.io",
955 | "endpoints": [
956 | {
957 | "schemes": [
958 | "https:\/\/showtheway.io\/to\/*"
959 | ],
960 | "url": "https:\/\/showtheway.io\/oembed",
961 | "discovery": true
962 | }
963 | ]
964 | },
965 | {
966 | "provider_name": "Silk",
967 | "provider_url": "http:\/\/www.silk.co\/",
968 | "endpoints": [
969 | {
970 | "schemes": [
971 | "http:\/\/*.silk.co\/explore\/*",
972 | "https:\/\/*.silk.co\/explore\/*",
973 | "http:\/\/*.silk.co\/s\/embed\/*",
974 | "https:\/\/*.silk.co\/s\/embed\/*"
975 | ],
976 | "url": "http:\/\/www.silk.co\/oembed\/",
977 | "discovery": true
978 | }
979 | ]
980 | },
981 | {
982 | "provider_name": "Sketchfab",
983 | "provider_url": "http:\/\/sketchfab.com",
984 | "endpoints": [
985 | {
986 | "schemes": [
987 | "http:\/\/sketchfab.com\/models\/*",
988 | "https:\/\/sketchfab.com\/models\/*",
989 | "https:\/\/sketchfab.com\/*\/folders\/*"
990 | ],
991 | "url": "http:\/\/sketchfab.com\/oembed",
992 | "formats": [
993 | "json"
994 | ]
995 | }
996 | ]
997 | },
998 | {
999 | "provider_name": "SlideShare",
1000 | "provider_url": "http:\/\/www.slideshare.net\/",
1001 | "endpoints": [
1002 | {
1003 | "schemes": [
1004 | "http:\/\/www.slideshare.net\/*\/*",
1005 | "http:\/\/fr.slideshare.net\/*\/*",
1006 | "http:\/\/de.slideshare.net\/*\/*",
1007 | "http:\/\/es.slideshare.net\/*\/*",
1008 | "http:\/\/pt.slideshare.net\/*\/*"
1009 | ],
1010 | "url": "http:\/\/www.slideshare.net\/api\/oembed\/2",
1011 | "discovery": true
1012 | }
1013 | ]
1014 | },
1015 | {
1016 | "provider_name": "SmugMug",
1017 | "provider_url": "http:\/\/www.smugmug.com\/",
1018 | "endpoints": [
1019 | {
1020 | "schemes": [
1021 | "http:\/\/*.smugmug.com\/*"
1022 | ],
1023 | "url": "http:\/\/api.smugmug.com\/services\/oembed\/",
1024 | "discovery": true
1025 | }
1026 | ]
1027 | },
1028 | {
1029 | "provider_name": "SoundCloud",
1030 | "provider_url": "http:\/\/soundcloud.com\/",
1031 | "endpoints": [
1032 | {
1033 | "schemes": [
1034 | "http:\/\/soundcloud.com\/*"
1035 | ],
1036 | "url": "https:\/\/soundcloud.com\/oembed"
1037 | }
1038 | ]
1039 | },
1040 | {
1041 | "provider_name": "SpeakerDeck",
1042 | "provider_url": "https:\/\/speakerdeck.com",
1043 | "endpoints": [
1044 | {
1045 | "schemes": [
1046 | "http:\/\/speakerdeck.com\/*\/*",
1047 | "https:\/\/speakerdeck.com\/*\/*"
1048 | ],
1049 | "url": "https:\/\/speakerdeck.com\/oembed.json",
1050 | "discovery": true,
1051 | "formats": [
1052 | "json"
1053 | ]
1054 | }
1055 | ]
1056 | },
1057 | {
1058 | "provider_name": "Streamable",
1059 | "provider_url": "https:\/\/streamable.com\/",
1060 | "endpoints": [
1061 | {
1062 | "schemes": [
1063 | "http:\/\/streamable.com\/*",
1064 | "https:\/\/streamable.com\/*"
1065 | ],
1066 | "url": "https:\/\/api.streamable.com\/oembed.json",
1067 | "discovery": true
1068 | }
1069 | ]
1070 | },
1071 | {
1072 | "provider_name": "StreamOneCloud",
1073 | "provider_url": "https:\/\/www.streamone.nl",
1074 | "endpoints": [
1075 | {
1076 | "schemes": [
1077 | "https:\/\/content.streamonecloud.net\/embed\/*"
1078 | ],
1079 | "url": "https:\/\/content.streamonecloud.net\/oembed",
1080 | "discovery": true
1081 | }
1082 | ]
1083 | },
1084 | {
1085 | "provider_name": "Sway",
1086 | "provider_url": "https:\/\/www.sway.com",
1087 | "endpoints": [
1088 | {
1089 | "schemes": [
1090 | "https:\/\/sway.com\/*",
1091 | "https:\/\/www.sway.com\/*"
1092 | ],
1093 | "url": "https:\/\/sway.com\/api\/v1.0\/oembed",
1094 | "discovery": true
1095 | }
1096 | ]
1097 | },
1098 | {
1099 | "provider_name": "Ted",
1100 | "provider_url": "http:\/\/ted.com",
1101 | "endpoints": [
1102 | {
1103 | "schemes": [
1104 | "http:\/\/ted.com\/talks\/*"
1105 | ],
1106 | "url": "http:\/\/www.ted.com\/talks\/oembed.{format}"
1107 | }
1108 | ]
1109 | },
1110 | {
1111 | "provider_name": "The New York Times",
1112 | "provider_url": "https:\/\/www.nytimes.com",
1113 | "endpoints": [
1114 | {
1115 | "url": "https:\/\/www.nytimes.com\/svc\/oembed\/{format}\/",
1116 | "discovery": true
1117 | }
1118 | ]
1119 | },
1120 | {
1121 | "provider_name": "They Said So",
1122 | "provider_url": "https:\/\/theysaidso.com\/",
1123 | "endpoints": [
1124 | {
1125 | "schemes": [
1126 | "https:\/\/theysaidso.com\/image\/*"
1127 | ],
1128 | "url": "https:\/\/theysaidso.com\/extensions\/oembed\/",
1129 | "discovery": true
1130 | }
1131 | ]
1132 | },
1133 | {
1134 | "provider_name": "Topy",
1135 | "provider_url": "http:\/\/www.topy.se\/",
1136 | "endpoints": [
1137 | {
1138 | "schemes": [
1139 | "http:\/\/www.topy.se\/image\/*"
1140 | ],
1141 | "url": "http:\/\/www.topy.se\/oembed\/",
1142 | "discovery": true
1143 | }
1144 | ]
1145 | },
1146 | {
1147 | "provider_name": "Ustream",
1148 | "provider_url": "http:\/\/www.ustream.tv",
1149 | "endpoints": [
1150 | {
1151 | "schemes": [
1152 | "http:\/\/*.ustream.tv\/*",
1153 | "http:\/\/*.ustream.com\/*"
1154 | ],
1155 | "url": "http:\/\/www.ustream.tv\/oembed",
1156 | "formats": [
1157 | "json"
1158 | ]
1159 | }
1160 | ]
1161 | },
1162 | {
1163 | "provider_name": "Uttles",
1164 | "provider_url": "http:\/\/uttles.com",
1165 | "endpoints": [
1166 | {
1167 | "schemes": [
1168 | "http:\/\/uttles.com\/uttle\/*"
1169 | ],
1170 | "url": "http:\/\/uttles.com\/api\/reply\/oembed",
1171 | "discovery": true
1172 | }
1173 | ]
1174 | },
1175 | {
1176 | "provider_name": "Verse",
1177 | "provider_url": "http:\/\/verse.media\/",
1178 | "endpoints": [
1179 | {
1180 | "url": "http:\/\/verse.media\/services\/oembed\/"
1181 | }
1182 | ]
1183 | },
1184 | {
1185 | "provider_name": "Viddler",
1186 | "provider_url": "http:\/\/www.viddler.com\/",
1187 | "endpoints": [
1188 | {
1189 | "schemes": [
1190 | "http:\/\/www.viddler.com\/v\/*"
1191 | ],
1192 | "url": "http:\/\/www.viddler.com\/oembed\/"
1193 | }
1194 | ]
1195 | },
1196 | {
1197 | "provider_name": "VideoJug",
1198 | "provider_url": "http:\/\/www.videojug.com",
1199 | "endpoints": [
1200 | {
1201 | "schemes": [
1202 | "http:\/\/www.videojug.com\/film\/*",
1203 | "http:\/\/www.videojug.com\/interview\/*"
1204 | ],
1205 | "url": "http:\/\/www.videojug.com\/oembed.{format}"
1206 | }
1207 | ]
1208 | },
1209 | {
1210 | "provider_name": "Vimeo",
1211 | "provider_url": "https:\/\/vimeo.com\/",
1212 | "endpoints": [
1213 | {
1214 | "schemes": [
1215 | "https:\/\/vimeo.com\/*",
1216 | "https:\/\/vimeo.com\/album\/*\/video\/*",
1217 | "https:\/\/vimeo.com\/channels\/*\/*",
1218 | "https:\/\/vimeo.com\/groups\/*\/videos\/*",
1219 | "https:\/\/vimeo.com\/ondemand\/*\/*",
1220 | "https:\/\/player.vimeo.com\/video\/*"
1221 | ],
1222 | "url": "https:\/\/vimeo.com\/api\/oembed.{format}",
1223 | "discovery": true
1224 | }
1225 | ]
1226 | },
1227 | {
1228 | "provider_name": "Vine",
1229 | "provider_url": "https:\/\/vine.co\/",
1230 | "endpoints": [
1231 | {
1232 | "schemes": [
1233 | "http:\/\/vine.co\/v\/*",
1234 | "https:\/\/vine.co\/v\/*"
1235 | ],
1236 | "url": "https:\/\/vine.co\/oembed.json",
1237 | "discovery": true
1238 | }
1239 | ]
1240 | },
1241 | {
1242 | "provider_name": "Wiredrive",
1243 | "provider_url": "https:\/\/www.wiredrive.com\/",
1244 | "endpoints": [
1245 | {
1246 | "schemes": [
1247 | "https:\/\/*.wiredrive.com\/*"
1248 | ],
1249 | "url": "http:\/\/*.wiredrive.com\/present-oembed\/",
1250 | "formats": [
1251 | "json"
1252 | ],
1253 | "discovery": true
1254 | }
1255 | ]
1256 | },
1257 | {
1258 | "provider_name": "WordPress.com",
1259 | "provider_url": "http:\/\/wordpress.com\/",
1260 | "endpoints": [
1261 | {
1262 | "url": "http:\/\/public-api.wordpress.com\/oembed\/",
1263 | "discovery": true
1264 | }
1265 | ]
1266 | },
1267 | {
1268 | "provider_name": "YFrog",
1269 | "provider_url": "http:\/\/yfrog.com\/",
1270 | "endpoints": [
1271 | {
1272 | "schemes": [
1273 | "http:\/\/*.yfrog.com\/*",
1274 | "http:\/\/yfrog.us\/*"
1275 | ],
1276 | "url": "http:\/\/www.yfrog.com\/api\/oembed",
1277 | "formats": [
1278 | "json"
1279 | ]
1280 | }
1281 | ]
1282 | },
1283 | {
1284 | "provider_name": "YouTube",
1285 | "provider_url": "http:\/\/www.youtube.com\/",
1286 | "endpoints": [
1287 | {
1288 | "url": "http:\/\/www.youtube.com\/oembed",
1289 | "discovery": true
1290 | }
1291 | ]
1292 | }
1293 | ]
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Load modules
4 |
5 | const Code = require('code');
6 | const Lab = require('lab');
7 | const Metaphor = require('..');
8 | const Wreck = require('wreck');
9 | const Providers = require('../providers.json');
10 |
11 |
12 | // Declare internals
13 |
14 | const internals = {};
15 |
16 |
17 | // Test shortcuts
18 |
19 | const lab = exports.lab = Lab.script();
20 | const describe = lab.describe;
21 | const it = lab.it;
22 | const expect = Code.expect;
23 |
24 |
25 | describe('Metaphor', () => {
26 |
27 | describe('Engine', () => {
28 |
29 | describe('describe()', () => {
30 |
31 | it('describes a NY Times article', (done) => {
32 |
33 | const engine = new Metaphor.Engine({ css: '/embed.css', script: '/script.js', providers: Providers, redirect: 'https://example.com/redirect=', tweet: true });
34 | const resource = 'https://www.nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html?rref=collection%252Fnewseventcollection%252FPresidential+Election+2016&contentId=&mediaId=&referrer=https%3A%2F%2Fwww.nytimes.com%2F%3Faction%3Dclick%26contentCollection%3DPolitics%26region%3DTopBar%26module%3DHomePage-Button%26pgtype%3Darticle%26WT.z_jog%3D1%26hF%3Dt%26vS%3Dundefined&priority=true&action=click&contentCollection=Politics&module=Collection®ion=Marginalia&src=me&version=newsevent&pgtype=article';
35 | engine.describe(resource, (description) => {
36 |
37 | expect(description).to.equal({
38 | url: 'https://www.nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html',
39 | type: 'article',
40 | title: 'Rise of Donald Trump Tracks Growing Debate Over Global Fascism',
41 | description: 'Mr. Trump\u2019s campaign has engendered impassioned discussion about the nature of his appeal and warnings from critics on the left and the right.',
42 | image: {
43 | url: 'https://static01.nyt.com/images/2016/05/29/world/JP-FASCISM1/JP-FASCISM1-facebookJumbo.jpg',
44 | size: 122475
45 | },
46 | sources: ['ogp', 'oembed', 'resource', 'twitter'],
47 | site_name: 'The New York Times',
48 | author: 'Peter Baker',
49 | icon: {
50 | any: 'https://static01.nyt.com/favicon.ico',
51 | smallest: 'https://static01.nyt.com/favicon.ico'
52 | },
53 | thumbnail: {
54 | url: 'https://static01.nyt.com/images/2016/05/29/world/JP-FASCISM1/JP-FASCISM1-mediumThreeByTwo440.jpg',
55 | width: 440,
56 | height: 293,
57 | size: 34445
58 | },
59 | embed: {
60 | type: 'rich',
61 | height: 550,
62 | width: 300,
63 | url: 'https://www.nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html',
64 | html: ''
65 | },
66 | app: {
67 | googleplay: {
68 | name: 'NYTimes',
69 | id: 'com.nytimes.android',
70 | url: 'nytimes://reader/id/100000004437909'
71 | }
72 | },
73 | twitter: { site_username: '@nytimes', creator_username: 'peterbakernyt' },
74 | preview: 'Rise of Donald Trump Tracks Growing Debate Over Global Fascism '
75 | });
76 |
77 | done();
78 | });
79 | });
80 |
81 | it('uses the providers list', (done) => {
82 |
83 | const engine = new Metaphor.Engine({ preview: false });
84 | const resource = 'http://www.deviantart.com/art/Who-are-you-612604046';
85 | engine.describe(resource, (description) => {
86 |
87 | expect(description).to.equal({
88 | title: 'Who are you?',
89 | image: {
90 | url: 'http://orig15.deviantart.net/7150/f/2016/153/4/0/img_2814_kopie_2_by_maaira-da4q8b2.jpg',
91 | width: 675,
92 | height: 450
93 | },
94 | url: 'http://maaira.deviantart.com/art/Who-are-you-612604046',
95 | description: 'Nova meets cows.',
96 | type: 'website',
97 | sources: ['ogp', 'resource', 'oembed', 'twitter'],
98 | site_name: 'DeviantArt',
99 | icon: {
100 | '48': 'http://st.deviantart.net/minish/touch-icons/android-48.png',
101 | '96': 'http://st.deviantart.net/minish/touch-icons/android-96.png',
102 | '144': 'http://st.deviantart.net/minish/touch-icons/android-144.png',
103 | '192': 'http://st.deviantart.net/minish/touch-icons/android-192.png',
104 | any: 'http://i.deviantart.net/icons/da_favicon.ico',
105 | smallest: 'http://st.deviantart.net/minish/touch-icons/android-48.png'
106 | },
107 | thumbnail: {
108 | url: 'http://t01.deviantart.net/GRpFefgpAK8ZU15icNW3ZcgOrGE=/fit-in/300x900/filters:no_upscale():origin()/pre14/566b/th/pre/f/2016/153/4/0/img_2814_kopie_2_by_maaira-da4q8b2.jpg',
109 | width: 300,
110 | height: 200
111 | },
112 | embed: {
113 | type: 'photo',
114 | height: 450,
115 | width: 675,
116 | url: 'http://orig15.deviantart.net/7150/f/2016/153/4/0/img_2814_kopie_2_by_maaira-da4q8b2.jpg'
117 | },
118 | twitter: { site_username: '@deviantart' }
119 | });
120 |
121 | done();
122 | });
123 | });
124 |
125 | it('skips using a providers list', (done) => {
126 |
127 | const engine = new Metaphor.Engine({ providers: false, preview: false });
128 | const resource = 'http://www.deviantart.com/art/Who-are-you-612604046';
129 | engine.describe(resource, (description) => {
130 |
131 | expect(description).to.equal({
132 | title: 'Who are you?',
133 | image: {
134 | url: 'http://orig15.deviantart.net/7150/f/2016/153/4/0/img_2814_kopie_2_by_maaira-da4q8b2.jpg',
135 | width: 675,
136 | height: 450
137 | },
138 | url: 'http://maaira.deviantart.com/art/Who-are-you-612604046',
139 | description: 'Nova meets cows.',
140 | type: 'website',
141 | site_name: 'DeviantArt',
142 | sources: ['ogp', 'resource', 'twitter'],
143 | icon: {
144 | '48': 'http://st.deviantart.net/minish/touch-icons/android-48.png',
145 | '96': 'http://st.deviantart.net/minish/touch-icons/android-96.png',
146 | '144': 'http://st.deviantart.net/minish/touch-icons/android-144.png',
147 | '192': 'http://st.deviantart.net/minish/touch-icons/android-192.png',
148 | any: 'http://i.deviantart.net/icons/da_favicon.ico',
149 | smallest: 'http://st.deviantart.net/minish/touch-icons/android-48.png'
150 | },
151 | twitter: { site_username: '@deviantart' }
152 | });
153 |
154 | done();
155 | });
156 | });
157 |
158 | it('describes a whitelisted resource', (done) => {
159 |
160 | const engine = new Metaphor.Engine({ whitelist: ['https://twitter.com/*'], preview: false });
161 | engine.describe('https://twitter.com/ardnahoe/status/892833709438754816', (description) => {
162 |
163 | expect(description).to.equal({
164 | type: 'article',
165 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
166 | title: 'Ardnahoe Distillery on Twitter',
167 | image: { url: 'https://pbs.twimg.com/media/DGP7fj7XUAE5aZE.jpg:large' },
168 | description: '\u201cA misty but peaceful evening at Loch Ardnahoe tonight. #Islay\u201d',
169 | site_name: 'Twitter',
170 | sources: ['ogp', 'resource', 'oembed'],
171 | icon: {
172 | any: 'https://abs.twimg.com/favicons/favicon.ico',
173 | smallest: 'https://abs.twimg.com/favicons/favicon.ico'
174 | },
175 | avatar: 'https://pbs.twimg.com/profile_images/836605178744832001/iNcUgrE-_400x400.jpg',
176 | embed: {
177 | type: 'rich',
178 | width: 550,
179 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
180 | html: '\n'
181 | }
182 | });
183 |
184 | done();
185 | });
186 | });
187 |
188 | it('block when non whitelisted', (done) => {
189 |
190 | const engine = new Metaphor.Engine({ whitelist: ['https://example.com/*'] });
191 | const resource = 'http://www.nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html';
192 | engine.describe(resource, (description) => {
193 |
194 | expect(description).to.equal({
195 | type: 'website',
196 | site_name: 'Nytimes',
197 | url: 'http://www.nytimes.com/2016/05/29/world/europe/rise-of-donald-trump-tracks-growing-debate-over-global-fascism.html',
198 | preview: ''
199 | });
200 |
201 | done();
202 | });
203 | });
204 |
205 | it('describes a tweet', (done) => {
206 |
207 | const engine = new Metaphor.Engine({ tweet: true });
208 | engine.describe('https://twitter.com/ardnahoe/status/892833709438754816', (description) => {
209 |
210 | expect(description).to.equal({
211 | type: 'article',
212 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
213 | title: 'Ardnahoe Distillery on Twitter',
214 | image: {
215 | url: 'https://pbs.twimg.com/media/DGP7fj7XUAE5aZE.jpg:large',
216 | size: 273453
217 | },
218 | description: '\u201cA misty but peaceful evening at Loch Ardnahoe tonight. #Islay\u201d',
219 | site_name: 'Twitter',
220 | sources: ['ogp', 'resource', 'oembed'],
221 | icon: {
222 | any: 'https://abs.twimg.com/favicons/favicon.ico',
223 | smallest: 'https://abs.twimg.com/favicons/favicon.ico'
224 | },
225 | avatar: 'https://pbs.twimg.com/profile_images/836605178744832001/iNcUgrE-_400x400.jpg',
226 | embed: {
227 | type: 'rich',
228 | width: 550,
229 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
230 | html: '\n'
231 | },
232 | preview: 'Ardnahoe Distillery on Twitter ',
233 | tweet: {
234 | name: 'Ardnahoe Distillery',
235 | username: 'ardnahoe',
236 | content: 'A misty but peaceful evening at Loch Ardnahoe tonight. #Islay pic.twitter.com/HXc38uYxa0',
237 | date: 'August 2, 2017',
238 | links: {},
239 | avatar: 'https://pbs.twimg.com/profile_images/836605178744832001/iNcUgrE-_400x400.jpg',
240 | image: 'https://pbs.twimg.com/media/DGP7fj7XUAE5aZE.jpg:large'
241 | }
242 | });
243 |
244 | done();
245 | });
246 | });
247 |
248 | it('describes a private tweet', (done) => {
249 |
250 | const engine = new Metaphor.Engine();
251 | engine.describe('https://twitter.com/halfbee/status/683408044386959360', (description) => {
252 |
253 | expect(description).to.equal({
254 | type: 'website',
255 | sources: ['resource'],
256 | url: 'https://twitter.com/halfbee/status/683408044386959360',
257 | description: 'The latest Tweets and replies from Half Bee (@halfbee). The unpublishable brain farts of @eranhammer',
258 | icon: {
259 | any: 'https://abs.twimg.com/favicons/favicon.ico',
260 | smallest: 'https://abs.twimg.com/favicons/favicon.ico'
261 | },
262 | site_name: 'Twitter',
263 | avatar: 'https://pbs.twimg.com/profile_images/680600062896902145/jVPdsOot_400x400.png',
264 | preview: ''
265 | });
266 |
267 | done();
268 | });
269 | });
270 |
271 | it('describes a flickr photo', (done) => {
272 |
273 | const engine = new Metaphor.Engine({ maxWidth: 400, maxHeight: 200 });
274 | engine.describe('https://www.flickr.com/photos/kent-macdonald/19455364653/', (description) => {
275 |
276 | expect(description).to.equal({
277 | site_name: 'Flickr',
278 | updated_time: description.updated_time,
279 | title: '300/365 "The Lonely Gold Rush"',
280 | description: '27.07.15 So this is it, day 300. The real count down begins now I guess. Also found a pickaxe at my house moment before I even shot this. I seem to have strange and worrisome objects at my house. The first one I was looking for was a spear. And I\'m still in need of another deadly prop for this series. A lot has been said with very few words. Don\'t worry I\'m not a murderer. HOnestly I was searching for the spear first as I had a stronger concept, well it has a stronger meaning to it for me, bur alas I couldn\'t find it in time. I have seince then loaceted it after I\'ve shot this. But time was of the essence. In other news I\'m planning a new photographic series and have been doing some research and sketching. On the downside I don\'t think I\'ll be shooting any of them until this project is over.',
281 | type: 'article',
282 | url: 'https://www.flickr.com/photos/kent-macdonald/19455364653/',
283 | image: {
284 | url: 'https://c1.staticflickr.com/1/259/19455364653_201bdfd31b_b.jpg',
285 | width: 1024,
286 | height: 576,
287 | size: 278195
288 | },
289 | sources: ['ogp', 'resource', 'oembed', 'twitter'],
290 | icon: {
291 | any: 'https://s.yimg.com/pw/favicon.ico',
292 | smallest: 'https://s.yimg.com/pw/favicon.ico'
293 | },
294 | thumbnail: {
295 | url: 'https://farm1.staticflickr.com/259/19455364653_201bdfd31b_q.jpg',
296 | width: 150,
297 | height: 150,
298 | size: 15476
299 | },
300 | embed: {
301 | type: 'photo',
302 | height: 180,
303 | width: 320,
304 | url: 'https://farm1.staticflickr.com/259/19455364653_201bdfd31b_n.jpg',
305 | html: ' ',
306 | size: 21677
307 | },
308 | app: {
309 | iphone: {
310 | name: 'Flickr',
311 | id: '328407587',
312 | url: 'flickr://flickr.com/photos/kent-macdonald/19455364653/'
313 | }
314 | },
315 | twitter: { site_username: '@flickr' },
316 | preview: '300/365 "The Lonely Gold Rush" '
317 | });
318 |
319 | done();
320 | });
321 | });
322 |
323 | it('describes an image', (done) => {
324 |
325 | const engine = new Metaphor.Engine();
326 | engine.describe('https://www.sideway.com/sideway.png', (description) => {
327 |
328 | expect(description).to.equal({
329 | url: 'https://www.sideway.com/sideway.png',
330 | type: 'website',
331 | site_name: 'Image',
332 | embed: {
333 | url: 'https://www.sideway.com/sideway.png',
334 | type: 'photo',
335 | size: 17014
336 | },
337 | preview: '',
338 | sources: ['resource']
339 | });
340 |
341 | done();
342 | });
343 | });
344 |
345 | it('describes an image (with summary)', (done) => {
346 |
347 | const engine = new Metaphor.Engine({ summary: true });
348 | engine.describe('https://www.sideway.com/sideway.png', (description) => {
349 |
350 | expect(description).to.equal({
351 | url: 'https://www.sideway.com/sideway.png',
352 | type: 'website',
353 | site_name: 'Image',
354 | embed: {
355 | url: 'https://www.sideway.com/sideway.png',
356 | type: 'photo',
357 | size: 17014
358 | },
359 | summary: {
360 | url: 'https://www.sideway.com/sideway.png',
361 | title: 'https://www.sideway.com/sideway.png',
362 | description: undefined,
363 | icon: undefined,
364 | image: 'https://www.sideway.com/sideway.png'
365 | },
366 | preview: '',
367 | sources: ['resource']
368 | });
369 |
370 | done();
371 | });
372 | });
373 |
374 | it('describes an image (with summary without preview)', (done) => {
375 |
376 | const engine = new Metaphor.Engine({ summary: true, preview: false });
377 | engine.describe('https://www.sideway.com/sideway.png', (description) => {
378 |
379 | expect(description).to.equal({
380 | url: 'https://www.sideway.com/sideway.png',
381 | type: 'website',
382 | site_name: 'Image',
383 | embed: {
384 | url: 'https://www.sideway.com/sideway.png',
385 | type: 'photo',
386 | size: 17014
387 | },
388 | summary: {
389 | url: 'https://www.sideway.com/sideway.png',
390 | title: 'https://www.sideway.com/sideway.png',
391 | description: undefined,
392 | icon: undefined,
393 | image: 'https://www.sideway.com/sideway.png'
394 | },
395 | sources: ['resource']
396 | });
397 |
398 | done();
399 | });
400 | });
401 |
402 | it('describes an image (max size)', (done) => {
403 |
404 | const engine = new Metaphor.Engine({ maxSize: 1024 });
405 | engine.describe('https://www.sideway.com/sideway.png', (description) => {
406 |
407 | expect(description).to.equal({
408 | url: 'https://www.sideway.com/sideway.png',
409 | type: 'website',
410 | site_name: 'Image',
411 | embed: {
412 | url: 'https://www.sideway.com/sideway.png',
413 | type: 'photo',
414 | size: 17014
415 | },
416 | preview: '',
417 | sources: ['resource']
418 | });
419 |
420 | done();
421 | });
422 | });
423 |
424 | it('describes an image (large max size)', (done) => {
425 |
426 | const engine = new Metaphor.Engine({ maxSize: 18000 });
427 | engine.describe('https://www.sideway.com/sideway.png', (description) => {
428 |
429 | expect(description).to.equal({
430 | url: 'https://www.sideway.com/sideway.png',
431 | type: 'website',
432 | site_name: 'Image',
433 | embed: {
434 | url: 'https://www.sideway.com/sideway.png',
435 | type: 'photo',
436 | size: 17014
437 | },
438 | preview: '',
439 | sources: ['resource']
440 | });
441 |
442 | done();
443 | });
444 | });
445 |
446 | it('describes an article', (done) => {
447 |
448 | const engine = new Metaphor.Engine();
449 | engine.describe('http://www.wired.com/2016/05/google-doesnt-owe-oracle-cent-using-java-android-jury-finds/', (description) => {
450 |
451 | expect(description).to.equal({
452 | type: 'article',
453 | title: 'Google Doesn\u2019t Owe Oracle a Cent for Using Java in Android, Jury Finds',
454 | image: {
455 | url: 'https://www.wired.com/wp-content/uploads/2016/05/android-1200x630-e1464301027666.jpg',
456 | width: 1200,
457 | height: 630,
458 | size: 233720
459 | },
460 | description: 'The verdict could have major implications for the future of software developments.',
461 | locale: { primary: 'en_US' },
462 | url: 'https://www.wired.com/2016/05/google-doesnt-owe-oracle-cent-using-java-android-jury-finds/',
463 | site_name: 'WIRED',
464 | sources: ['ogp', 'resource', 'oembed', 'twitter'],
465 | icon: {
466 | any: 'https://www.wired.com/wp-content/themes/Phoenix/assets/images/favicon.ico',
467 | smallest: 'https://www.wired.com/wp-content/themes/Phoenix/assets/images/favicon.ico'
468 | },
469 | thumbnail: {
470 | url: 'https://www.wired.com/wp-content/uploads/2016/05/android.jpg',
471 | width: 600,
472 | height: 450,
473 | size: 424398
474 | },
475 | embed: {
476 | type: 'rich',
477 | height: 338,
478 | width: 600,
479 | html: 'Google Doesn’t Owe Oracle a Cent for Using Java in Android, Jury Finds \n'
480 | },
481 | twitter: { site_username: '@wired', creator_username: '@wired' },
482 | preview: 'Google Doesn\u2019t Owe Oracle a Cent for Using Java in Android, Jury Finds '
483 | });
484 |
485 | done();
486 | });
487 | });
488 |
489 | it('described a YouTube video', (done) => {
490 |
491 | const engine = new Metaphor.Engine();
492 | engine.describe('https://www.youtube.com/watch?v=cWDdd5KKhts', (description) => {
493 |
494 | expect(description).to.equal({
495 | site_name: 'YouTube',
496 | url: 'https://www.youtube.com/watch?v=cWDdd5KKhts',
497 | title: 'Cheese Shop Sketch - Monty Python\'s Flying Circus',
498 | image: {
499 | url: 'https://i.ytimg.com/vi/cWDdd5KKhts/maxresdefault.jpg',
500 | size: 106445
501 | },
502 | description: 'Subscribe to the Official Monty Python Channel here - http://smarturl.it/SubscribeToPython Cleese plays an erudite customer attempting to purchase some chees...',
503 | type: 'video',
504 | video: [
505 | {
506 | url: 'https://www.youtube.com/embed/cWDdd5KKhts',
507 | type: 'text/html',
508 | width: 480,
509 | height: 360
510 | },
511 | {
512 | url: 'https://www.youtube.com/v/cWDdd5KKhts?version=3&autohide=1',
513 | type: 'application/x-shockwave-flash',
514 | width: 480,
515 | height: 360,
516 | tag: ['Monty Python', 'Python (Monty) Pictures Limited', 'Comedy', 'flying circus', 'monty pythons flying circus', 'john cleese', 'micael palin', 'eric idle', 'terry jones', 'graham chapman', 'terry gilliam', 'funny', 'comedy', 'animation', '60s animation', 'humor', 'humour', 'sketch show', 'british comedy', 'cheese shop', 'monty python cheese', 'cheese shop sketch', 'cleese cheese', 'cheese']
517 | }
518 | ],
519 | sources: ['ogp', 'resource', 'oembed', 'twitter'],
520 | icon: {
521 | '32': 'https://www.youtube.com/yts/img/favicon_32-vfl8NGn4k.png',
522 | '48': 'https://www.youtube.com/yts/img/favicon_48-vfl1s0rGh.png',
523 | '96': 'https://www.youtube.com/yts/img/favicon_96-vfldSA3ca.png',
524 | '144': 'https://www.youtube.com/yts/img/favicon_144-vflWmzoXw.png',
525 | any: 'https://s.ytimg.com/yts/img/favicon-vflz7uhzw.ico',
526 | smallest: 'https://www.youtube.com/yts/img/favicon_32-vfl8NGn4k.png'
527 | },
528 | thumbnail: {
529 | url: 'https://i.ytimg.com/vi/cWDdd5KKhts/hqdefault.jpg',
530 | width: 480,
531 | height: 360,
532 | size: 30519
533 | },
534 | embed: {
535 | type: 'video',
536 | height: 344,
537 | width: 459,
538 | html: 'VIDEO '
539 | },
540 | app: {
541 | iphone: {
542 | name: 'YouTube',
543 | id: '544007664',
544 | url: 'vnd.youtube://www.youtube.com/watch?v=cWDdd5KKhts&feature=applinks'
545 | },
546 | ipad: {
547 | name: 'YouTube',
548 | id: '544007664',
549 | url: 'vnd.youtube://www.youtube.com/watch?v=cWDdd5KKhts&feature=applinks'
550 | },
551 | googleplay: {
552 | name: 'YouTube',
553 | id: 'com.google.android.youtube',
554 | url: 'https://www.youtube.com/watch?v=cWDdd5KKhts'
555 | }
556 | },
557 | player: {
558 | url: 'https://www.youtube.com/embed/cWDdd5KKhts',
559 | width: 480,
560 | height: 360
561 | },
562 | twitter: { site_username: '@youtube' },
563 | preview: 'Cheese Shop Sketch - Monty Python\'s Flying Circus '
564 | });
565 |
566 | done();
567 | });
568 | });
569 |
570 | it('describes a resource with redirection and no cookies', { parallel: false }, (done, onCleanup) => {
571 |
572 | const orig = Wreck.request;
573 | onCleanup((next) => {
574 |
575 | Wreck.request = orig;
576 | return next();
577 | });
578 |
579 | Wreck.request = (method, url, options, next) => {
580 |
581 | setImmediate(() => {
582 |
583 | options.beforeRedirect('GET', 301, 'http://example.com/something', {}, {}, () => next(null, { statusCode: 301, headers: {} }));
584 | });
585 |
586 | return { abort: () => null };
587 | };
588 |
589 | const engine = new Metaphor.Engine({ preview: false });
590 | engine.describe('http://example.com/something', (description) => {
591 |
592 | expect(description).to.equal({ type: 'website', url: 'http://example.com/something', site_name: 'Example' });
593 |
594 | done();
595 | });
596 | });
597 |
598 | it('uses non-discovery oembed when resource request fails', { parallel: false }, (done) => {
599 |
600 | const orig = Wreck.request;
601 | Wreck.request = (method, url, options, next) => {
602 |
603 | Wreck.request = orig;
604 | setImmediate(() => next(new Error('failed')));
605 | return { abort: () => null };
606 | };
607 |
608 | const engine = new Metaphor.Engine({ preview: false });
609 | const resource = 'http://www.deviantart.com/art/Who-are-you-612604046';
610 | engine.describe(resource, (description) => {
611 |
612 | expect(description).to.equal({
613 | type: 'website',
614 | url: 'http://www.deviantart.com/art/Who-are-you-612604046',
615 | site_name: 'DeviantArt',
616 | thumbnail: {
617 | url: 'http://t01.deviantart.net/GRpFefgpAK8ZU15icNW3ZcgOrGE=/fit-in/300x900/filters:no_upscale():origin()/pre14/566b/th/pre/f/2016/153/4/0/img_2814_kopie_2_by_maaira-da4q8b2.jpg',
618 | width: 300,
619 | height: 200
620 | },
621 | embed: {
622 | type: 'photo',
623 | height: 450,
624 | width: 675,
625 | url: 'http://orig15.deviantart.net/7150/f/2016/153/4/0/img_2814_kopie_2_by_maaira-da4q8b2.jpg'
626 | },
627 | sources: ['oembed']
628 | });
629 |
630 | done();
631 | });
632 | });
633 |
634 | it('skips non-discovery oembed when resource request fails', { parallel: false }, (done) => {
635 |
636 | const orig = Wreck.request;
637 | Wreck.request = (method, url, options, next) => {
638 |
639 | Wreck.request = orig;
640 | setImmediate(() => next(new Error('failed')));
641 | return { abort: () => null };
642 | };
643 |
644 | const engine = new Metaphor.Engine({ preview: false, providers: false });
645 | const resource = 'http://www.deviantart.com/art/Who-are-you-612604046';
646 | engine.describe(resource, (description) => {
647 |
648 | expect(description).to.equal({
649 | type: 'website',
650 | url: 'http://www.deviantart.com/art/Who-are-you-612604046',
651 | site_name: 'Deviantart'
652 | });
653 |
654 | done();
655 | });
656 | });
657 |
658 | it('overrides preview function', (done) => {
659 |
660 | const engine = new Metaphor.Engine({ preview: (description, options, next) => next('yay!') });
661 | engine.describe('https://twitter.com/sideway/status/1', (description) => {
662 |
663 | expect(description).to.equal({
664 | type: 'website',
665 | url: 'https://twitter.com/sideway/status/1',
666 | preview: 'yay!',
667 | site_name: 'Twitter'
668 | });
669 |
670 | done();
671 | });
672 | });
673 |
674 | it('overrides preview function (empty preview)', (done) => {
675 |
676 | const engine = new Metaphor.Engine({ preview: (description, options, next) => next() });
677 | engine.describe('https://twitter.com/sideway/status/1', (description) => {
678 |
679 | expect(description).to.equal({
680 | type: 'website',
681 | url: 'https://twitter.com/sideway/status/1',
682 | site_name: 'Twitter'
683 | });
684 |
685 | done();
686 | });
687 | });
688 |
689 | it('handles missing document', (done) => {
690 |
691 | const engine = new Metaphor.Engine();
692 | engine.describe('https://twitter.com/sideway/status/1', (description) => {
693 |
694 | expect(description).to.equal({
695 | type: 'website',
696 | url: 'https://twitter.com/sideway/status/1',
697 | site_name: 'Twitter',
698 | preview: ''
699 | });
700 |
701 | done();
702 | });
703 | });
704 |
705 | it('handles invalid domain', (done) => {
706 |
707 | const engine = new Metaphor.Engine({ preview: false });
708 | engine.describe('http://0/1', (description) => {
709 |
710 | expect(description).to.equal({ type: 'website', url: 'http://0/1', site_name: '0' });
711 | done();
712 | });
713 | });
714 |
715 | it('handles unknown content type', { parallel: false }, (done) => {
716 |
717 | const orig = Wreck.request;
718 | Wreck.request = (method, url, options, next) => {
719 |
720 | Wreck.request = orig;
721 | setImmediate(() => next(null, { statusCode: 200, headers: { 'content-type': 'x/y' } }));
722 | return { abort: () => { } };
723 | };
724 |
725 | const engine = new Metaphor.Engine({ preview: false });
726 | engine.describe('https://example.com/invalid', (description) => {
727 |
728 | expect(description).to.equal({ type: 'website', url: 'https://example.com/invalid', site_name: 'Example' });
729 | done();
730 | });
731 | });
732 |
733 | it('handles missing content-length', { parallel: false }, (done) => {
734 |
735 | const orig = Wreck.request;
736 | Wreck.request = (method, url, options, next) => {
737 |
738 | Wreck.request = orig;
739 | return Wreck.request(method, url, options, (err, res) => {
740 |
741 | delete res.headers['content-length'];
742 | return next(err, res);
743 | });
744 | };
745 |
746 | const engine = new Metaphor.Engine({ preview: false });
747 | engine.describe('https://www.sideway.com/sideway.png', (description) => {
748 |
749 | expect(description).to.equal({
750 | url: 'https://www.sideway.com/sideway.png',
751 | type: 'website',
752 | site_name: 'Image',
753 | embed: {
754 | url: 'https://www.sideway.com/sideway.png',
755 | type: 'photo'
756 | },
757 | sources: ['resource']
758 | });
759 |
760 | done();
761 | });
762 | });
763 |
764 | it('handles missing content-type', { parallel: false }, (done) => {
765 |
766 | const orig = Wreck.request;
767 | Wreck.request = (method, url, options, next) => {
768 |
769 | Wreck.request = orig;
770 | setImmediate(() => next(null, { statusCode: 200, headers: {} }));
771 | return { abort: () => { } };
772 | };
773 |
774 | const engine = new Metaphor.Engine({ preview: false });
775 | engine.describe('https://example.com/invalid', (description) => {
776 |
777 | expect(description).to.equal({ type: 'website', url: 'https://example.com/invalid', site_name: 'Example' });
778 | done();
779 | });
780 | });
781 |
782 | it('handles on invalid content-type', { parallel: false }, (done) => {
783 |
784 | const orig = Wreck.request;
785 | Wreck.request = (method, url, options, next) => {
786 |
787 | Wreck.request = orig;
788 | setImmediate(() => next(null, { statusCode: 200, headers: { 'content-type': 'x' } }));
789 | return { abort: () => { } };
790 | };
791 |
792 | const engine = new Metaphor.Engine({ preview: false });
793 | engine.describe('https://example.com/invalid', (description) => {
794 |
795 | expect(description).to.equal({ type: 'website', url: 'https://example.com/invalid', site_name: 'Example' });
796 | done();
797 | });
798 | });
799 |
800 | it('handles on invalid response object', { parallel: false }, (done) => {
801 |
802 | const origRequest = Wreck.request;
803 | Wreck.request = (method, url, options, next) => {
804 |
805 | Wreck.request = origRequest;
806 | setImmediate(() => next(null, { statusCode: 200, headers: { 'content-type': 'text/html' } }));
807 | return { abort: () => { } };
808 | };
809 |
810 | const origRead = Wreck.read;
811 | Wreck.read = (res, options, next) => {
812 |
813 | Wreck.read = origRead;
814 | return next(new Error('Invalid'));
815 | };
816 |
817 | const engine = new Metaphor.Engine({ preview: false });
818 | engine.describe('https://example.com/invalid', (description) => {
819 |
820 | expect(description).to.equal({ type: 'website', url: 'https://example.com/invalid', site_name: 'Example' });
821 | done();
822 | });
823 | });
824 |
825 | it('handles failed image size request', { parallel: false }, (done, onCleanup) => {
826 |
827 | const orig = Wreck.request;
828 | onCleanup((next) => {
829 |
830 | Wreck.request = orig;
831 | return next();
832 | });
833 |
834 | Wreck.request = (method, url, options, next) => {
835 |
836 | if (method === 'HEAD') {
837 | return next(new Error('failed'));
838 | }
839 |
840 | return orig.call(Wreck, method, url, options, next);
841 | };
842 |
843 | const engine = new Metaphor.Engine({ maxSize: 1024 * 1024 });
844 | engine.describe('https://twitter.com/ardnahoe/status/892833709438754816', (description) => {
845 |
846 | expect(description).to.equal({
847 | type: 'article',
848 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
849 | title: 'Ardnahoe Distillery on Twitter',
850 | image: { url: 'https://pbs.twimg.com/media/DGP7fj7XUAE5aZE.jpg:large' },
851 | description: '\u201cA misty but peaceful evening at Loch Ardnahoe tonight. #Islay\u201d',
852 | site_name: 'Twitter',
853 | sources: ['ogp', 'resource', 'oembed'],
854 | icon: {
855 | any: 'https://abs.twimg.com/favicons/favicon.ico',
856 | smallest: 'https://abs.twimg.com/favicons/favicon.ico'
857 | },
858 | avatar: 'https://pbs.twimg.com/profile_images/836605178744832001/iNcUgrE-_400x400.jpg',
859 | embed: {
860 | type: 'rich',
861 | width: 550,
862 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
863 | html: '\n'
864 | },
865 | preview: 'Ardnahoe Distillery on Twitter '
866 | });
867 |
868 | done();
869 | });
870 | });
871 |
872 | it('handles failed image size request (missing length)', { parallel: false }, (done, onCleanup) => {
873 |
874 | const orig = Wreck.request;
875 | onCleanup((next) => {
876 |
877 | Wreck.request = orig;
878 | return next();
879 | });
880 |
881 | Wreck.request = (method, url, options, next) => {
882 |
883 | if (method === 'HEAD') {
884 | return orig.call(Wreck, method, url, options, (err, res) => {
885 |
886 | delete res.headers['content-length'];
887 | return next(err, res);
888 | });
889 | }
890 |
891 | return orig.call(Wreck, method, url, options, next);
892 | };
893 |
894 | const engine = new Metaphor.Engine({ maxSize: 1024 * 1024 });
895 | engine.describe('https://twitter.com/ardnahoe/status/892833709438754816', (description) => {
896 |
897 | expect(description).to.equal({
898 | type: 'article',
899 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
900 | title: 'Ardnahoe Distillery on Twitter',
901 | image: { url: 'https://pbs.twimg.com/media/DGP7fj7XUAE5aZE.jpg:large' },
902 | description: '\u201cA misty but peaceful evening at Loch Ardnahoe tonight. #Islay\u201d',
903 | site_name: 'Twitter',
904 | sources: ['ogp', 'resource', 'oembed'],
905 | icon: {
906 | any: 'https://abs.twimg.com/favicons/favicon.ico',
907 | smallest: 'https://abs.twimg.com/favicons/favicon.ico'
908 | },
909 | avatar: 'https://pbs.twimg.com/profile_images/836605178744832001/iNcUgrE-_400x400.jpg',
910 | embed: {
911 | type: 'rich',
912 | width: 550,
913 | url: 'https://twitter.com/ardnahoe/status/892833709438754816',
914 | html: '\n'
915 | },
916 | preview: 'Ardnahoe Distillery on Twitter '
917 | });
918 |
919 | done();
920 | });
921 | });
922 | });
923 | });
924 |
925 | describe('parse()', () => {
926 |
927 | it('uses oembed site_name if og is missing', (done) => {
928 |
929 | const html = `
930 |
931 |
932 |
933 | `;
934 |
935 | Metaphor.parse(html, 'https://twitter.com/dalmaer/status/726624422237364226', {}, (description) => {
936 |
937 | expect(description).to.equal({
938 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
939 | type: 'website',
940 | site_name: 'Twitter',
941 | embed: {
942 | type: 'rich',
943 | width: 550,
944 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
945 | html: '\n'
946 | },
947 | sources: ['oembed']
948 | });
949 |
950 | done();
951 | });
952 | });
953 |
954 | it('uses oembed link url if og is missing', { parallel: false }, (done) => {
955 |
956 | const html = `
957 |
958 |
959 |
960 | `;
961 |
962 | const oembed = {
963 | type: 'link',
964 | version: '1.0',
965 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
966 | provider_name: 'Twitter'
967 | };
968 |
969 | const orig = Wreck.get;
970 | Wreck.get = (url, options, next) => {
971 |
972 | Wreck.get = orig;
973 | next(null, { statusCode: 200 }, JSON.stringify(oembed));
974 | };
975 |
976 | Metaphor.parse(html, 'https://twitter.com/dalmaer/status/726624422237364226', {}, (description) => {
977 |
978 | expect(description).to.equal({
979 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
980 | type: 'website',
981 | site_name: 'Twitter',
982 | sources: ['oembed']
983 | });
984 |
985 | done();
986 | });
987 | });
988 |
989 | it('uses input link url if oembed is invalid', { parallel: false }, (done) => {
990 |
991 | const html = `
992 |
993 |
994 |
995 | `;
996 |
997 | const oembed = {
998 | type: 'link',
999 | version: '1.0',
1000 | url: 'https//twitter.com/dalmaer/status/726624422237364226',
1001 | provider_name: 'Twitter'
1002 | };
1003 |
1004 | const orig = Wreck.get;
1005 | Wreck.get = (url, options, next) => {
1006 |
1007 | Wreck.get = orig;
1008 | next(null, { statusCode: 200 }, JSON.stringify(oembed));
1009 | };
1010 |
1011 | Metaphor.parse(html, 'https://twitter.com/dalmaer/status/726624422237364226', {}, (description) => {
1012 |
1013 | expect(description).to.equal({
1014 | url: 'https://twitter.com/dalmaer/status/726624422237364226',
1015 | type: 'website'
1016 | });
1017 |
1018 | done();
1019 | });
1020 | });
1021 | });
1022 | });
1023 |
--------------------------------------------------------------------------------