diff --git a/README.md b/README.md index 4840522..1e9b8fb 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ html-metadata # MetaData html scraper and parser for Node.js (supports Promises and callback style) -The aim of this library is to be a comprehensive source for extracting all html embedded metadata. Currently it supports Schema.org microdata using a third party library, a native BEPress, Dublin Core, Highwire Press, Open Graph, EPrints, and COinS implementation, and some general metadata that doesn't belong to a particular standard (for instance, the content of the title tag, or meta description tags). +The aim of this library is to be a comprehensive source for extracting all html embedded metadata. Currently it supports Schema.org microdata using a third party library, a native BEPress, Dublin Core, Highwire Press, Open Graph, Twitter, EPrints, and COinS implementation, and some general metadata that doesn't belong to a particular standard (for instance, the content of the title tag, or meta description tags). -Planned is support for RDFa, Twitter, AGLS, and other yet unheard of metadata types. Contributions and requests for other metadata types welcome! +Planned is support for RDFa, AGLS, and other yet unheard of metadata types. Contributions and requests for other metadata types welcome! ## Install diff --git a/index.js b/index.js index 2ca1005..84dd8bf 100644 --- a/index.js +++ b/index.js @@ -143,6 +143,17 @@ exports.parseSchemaOrgMicrodata = function(chtml, callback){ return index.parseSchemaOrgMicrodata(chtml).nodeify(callback); }; +/** + * Scrapes Twitter data given html object + * + * @param {Object} chtml html Cheerio object + * @param {Function} [callback] optional callback function + * @return {Object} BBPromise for metadata + */ +exports.parseTwitter = function(chtml, callback){ + return index.parseTwitter(chtml).nodeify(callback); +}; + /** * Global exportable list of scraping promises with string keys * @type {Object} diff --git a/lib/index.js b/lib/index.js index 05f00fd..33d7254 100644 --- a/lib/index.js +++ b/lib/index.js @@ -448,6 +448,102 @@ exports.parseSchemaOrgMicrodata = BBPromise.method(function(chtml){ return meta; }); + +/** + * Scrapes twitter microdata given Cheerio html object + * @param {Object} chtml html Cheerio object + * @return {Object} promise of twitter metadata object + */ +exports.parseTwitter = BBPromise.method(function(chtml) { + if (!chtml) { + throw new Error('Undefined argument'); + } + + var meta = {}; + var metaTags = chtml('meta'); + + // These properties can either be strings or objects + var dualStateSubProperties = { + image : 'url', + player : 'url', + creator : '@username' + }; + + metaTags.each(function() { + var element = chtml(this); + var propertyValue = element.attr('name'); + + var property; + var content = element.attr('content'); + var node; + + // Exit if not a twitter tag + if (!propertyValue){ + return; + } else { + propertyValue = propertyValue.toLowerCase().split(':'); + property = propertyValue[1]; + } + + // Exit if tag not twitter metadata + if(propertyValue[0] !== 'twitter') { + return; + } + + // Handle nested properties + if(propertyValue.length > 2) { + var subProperty = propertyValue[2]; + + // Upgrade the property to an object if it needs to be + if(property in dualStateSubProperties && !(meta[property] instanceof Object)) { + node = {}; + node[dualStateSubProperties[property]] = meta[property]; + meta[property] = []; // Clear out the existing string as we just placed it into our new node + }else { + node = meta[property] ? meta[property] : {}; // Either create a new node or ammend the existing one + } + + // Differentiate betweeen twice and thrice nested properties + // Not the prettiest solution, but twitter metadata guidelines are fairly strict so it's not nessesary + // to anticipate strange data. + if(propertyValue.length === 3) { + node[subProperty] = content; + } else if (propertyValue.length === 4) { + // Solve the very specific twitter:player:stream:content_type case where stream needs to be upgraded to an object + if(subProperty.toLowerCase() === "stream"){ + node[subProperty] = {url: node[subProperty] }; + }else { + node[subProperty] = node[subProperty] ? node[subProperty] : {}; //Either create a new subnode or ammend the existing one + } + node[subProperty][propertyValue[3]] = content; + } else { + // Something is malformed, so exit + return; + } + }else { + node = content; + } + + // Create array if property exists and is not a nested object + if(meta[property] && !(meta[property] instanceof Object)) { + if (meta[property] instanceof Array) { + meta[property].push(node); + } else { + meta[property] = [meta[property], node]; + } + }else { + meta[property] = node; + } + }); + + if(Object.keys(meta).length === 0) { + throw new Error("No twitter metadata found on this page"); + } + + return meta; +}); + + /** * Global exportable list of scraping promises with string keys * @type {Object} @@ -460,5 +556,6 @@ exports.metadataFunctions = { 'general': exports.parseGeneral, 'highwirePress': exports.parseHighwirePress, 'openGraph': exports.parseOpenGraph, - 'schemaOrg': exports.parseSchemaOrgMicrodata + 'schemaOrg': exports.parseSchemaOrgMicrodata, + 'twitter': exports.parseTwitter }; diff --git a/package.json b/package.json index b0a3a20..9298175 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-metadata", - "version": "1.4.4", + "version": "1.5.0", "description": "Scrapes metadata of several different standards", "main": "index.js", "dependencies": { diff --git a/test/errors.js b/test/errors.js index baab186..33c0211 100644 --- a/test/errors.js +++ b/test/errors.js @@ -88,6 +88,16 @@ describe('errors', function() { }); }); + it('should not find twitter metadata, reject promise', function() { + var url = 'http://example.com'; + return preq.get(url) + .then(function(callRes) { + var $ = cheerio.load(callRes.body); + var prom = meta.parseTwitter($); + return assert.fails(prom); + }); + }); + //TODO: Add test for lacking general metadata //TODO: Add test for lacking any metadata diff --git a/test/scraping.js b/test/scraping.js index efc02e7..edd1411 100644 --- a/test/scraping.js +++ b/test/scraping.js @@ -16,7 +16,7 @@ var cheerio = require('cheerio'); describe('scraping', function() { - this.timeout(40000); + this.timeout(50000); var url; @@ -144,4 +144,29 @@ describe('scraping', function() { }); }); + describe('twitter tests', function() { + it('should get most basic twitter info', function() { + url = 'http://www.aftenposten.no/kultur/Pinlig-for-Skaber-555558b.html'; + return meta(url) + .catch(function(e){throw e;}) + .then(function(res) { + ['card', 'site', 'description', 'title', 'image'].forEach(function(key) { + if(!res.twitter[key]) { + throw new Error('Expected to find the ' + key + ' key in the response!'); + } + }); + }); + }); + + it('should get twitter nested data correctly', function() { + url = 'http://www.theguardian.com/us'; + return meta(url) + .catch(function(e){throw e;}) + .then(function(res) { + var expected = '{"app":{"id":{"iphone":"409128287","ipad":"409128287","googleplay":"com.guardian"},"name":{"googleplay":"The Guardian","ipad":"The Guardian","iphone":"The Guardian"},"url":{"ipad":"gnmguardian://us?contenttype=front&source=twitter","iphone":"gnmguardian://us?contenttype=front&source=twitter"}},"site":"@guardian","card":"summary","url":"https://www.theguardian.com/us"}'; + assert.deepEqual(JSON.stringify(res.twitter), expected); + }); + }); + }); + }); diff --git a/test/static.js b/test/static.js index bbdacae..f3425b1 100644 --- a/test/static.js +++ b/test/static.js @@ -17,7 +17,7 @@ describe('static files', function() { var expected; it('should get correct info from turtle movie file', function() { - expected = {"dublinCore":{"title":"Turtles of the Jungle","creator":"http://www.example.com/turtlelvr","description":"A 2008 film about jungle turtles.","date":"2012-02-04 12:00:00","type":"Image.Moving"},"general":{"author":"Turtle Lvr","authorlink":"http://examples.com/turtlelvr","canonical":"http://example.com/turtles","description":"Exposition on the awesomeness of turtles","publisher":"https://mediawiki.org","robots":"we welcome our robot overlords","shortlink":"http://example.com/c","title":"Turtles are AWESOME!!1 | Awesome Turtles Website", "lang":"en"},"openGraph":{"locale":"en_US","type":"video.movie","title":"Turtles of the Jungle","description":"A 2008 film about jungle turtles.","url":"http://example.com","site_name":"Awesome Turtle Movies Website","image":[{"url":"http://example.com/turtle.jpg"},{"url":"http://example.com/shell.jpg"}],"tag":["turtle","movie","awesome"],"director":"http://www.example.com/PhilTheTurtle","actor":["http://www.example.com/PatTheTurtle","http://www.example.com/SaminaTheTurtle"],"writer":"http://www.example.com/TinaTheTurtle","release_date":"2015-01-14T19:14:27+00:00","duration":"1000000"}}; + expected = {"dublinCore":{"title":"Turtles of the Jungle","creator":"http://www.example.com/turtlelvr","description":"A 2008 film about jungle turtles.","date":"2012-02-04 12:00:00","type":"Image.Moving"},"general":{"author":"Turtle Lvr","authorlink":"http://examples.com/turtlelvr","canonical":"http://example.com/turtles","description":"Exposition on the awesomeness of turtles","publisher":"https://mediawiki.org","robots":"we welcome our robot overlords","shortlink":"http://example.com/c","title":"Turtles are AWESOME!!1 | Awesome Turtles Website", "lang":"en"},"openGraph":{"locale":"en_US","type":"video.movie","title":"Turtles of the Jungle","description":"A 2008 film about jungle turtles.","url":"http://example.com","site_name":"Awesome Turtle Movies Website","image":[{"url":"http://example.com/turtle.jpg"},{"url":"http://example.com/shell.jpg"}],"tag":["turtle","movie","awesome"],"director":"http://www.example.com/PhilTheTurtle","actor":["http://www.example.com/PatTheTurtle","http://www.example.com/SaminaTheTurtle"],"writer":"http://www.example.com/TinaTheTurtle","release_date":"2015-01-14T19:14:27+00:00","duration":"1000000"},"twitter":{"card":"summary","site":"@Turtlessssssssss","creator":"@Turtlessssssssss","url":"http://www.example.com/turtles","title":"Turtles of the Jungle","description":"A 2008 film about jungle turtles.","player":{"url":"http://www.example.com/turtles/player","width":"400","height":"400","stream":{"url":"http://www.example.com/turtles/turtle.mp4","content_type":"video/mp4"}}}}; $ = cheerio.load(fs.readFileSync('./test/static/turtle_movie.html')); return meta.parseAll($).then(function(results){ assert.deepEqual(results, expected); @@ -25,7 +25,7 @@ describe('static files', function() { }); it('should get correct info from turtle article file', function() { - expected = {"bePress":{"series_title":"Turtles","author":"Turtle Lvr","author_institution":"Mediawiki","title":"Turtles are AWESOME!!1","date":"2012","pdf_url":"http://www.example.com/turtlelvr/pdf","abstract_html_url":"http://www.example.com/turtlelvr","publisher":"Turtles Society","online_date":"2012/02/04"},"coins":[{"ctx_ver":"Z39.88-2004","rft_id":"info:doi/http://dx.doi.org/10.5555/12345678","rfr_id":"info:sid/crossref.org:search","rft_val_fmt":"info:ofi/fmt:kev:mtx:journal","rft":{"atitle":"Toward a Unified Theory of High-Energy Metaphysics: Silly String Theory","jtitle":"Journal of Psychoceramics","date":"2008","volume":"5","issue":"11","spage":"1","epage":"3","aufirst":"Josiah","aulast":"Carberry","genre":"article","au":["Josiah Carberry"]}}],"dublinCore":{"title":"Turtles are AWESOME!!1","creator":"http://www.example.com/turtlelvr","description":"Exposition on the awesomeness of turtles","date":"2012-02-04 12:00:00","type":"Text.Article"},"general":{"author":"Turtle Lvr","authorlink":"http://examples.com/turtlelvr","canonical":"http://example.com/turtles","description":"Exposition on the awesomeness of turtles","publisher":"https://mediawiki.org","robots":"we welcome our robot overlords","shortlink":"http://example.com/c","title":"Turtles are AWESOME!!1 | Awesome Turtles Website", "lang":"en"},"highwirePress":{"journal_title":"Turtles","issn":"1234-5678","doi":"10.1000/123","publication_date":"2012-02-04","title":"Turtles are AWESOME!!1","author":"Turtle Lvr","author_institution":"Mediawiki","volume":"150","issue":"1","firstpage":"123","lastpage":"456","publisher":"Turtles Society","abstract":"Exposition on the awesomeness of turtles."},"openGraph":{"locale":"en_US","type":"article","title":"Turtles are AWESOME!!1","description":"Exposition on the awesomeness of turtles","url":"http://example.com","site_name":"Awesome Turtles Website","image":[{"url":"http://example.com/turtle.jpg","secure_url":"https://secure.example.com/turtle.jpg","type":"image/jpeg","width":"400","height":"300"},{"url":"http://example.com/shell.jpg","width":"200","height":"150"}],"audio":{"url":"http://example.com/sound.mp3","secure_url":"https://secure.example.com/sound.mp3","type":"audio/mpeg"},"tag":["turtles","are","awesome"],"section":["Turtles are tough","Turtles are flawless","Turtles are cute"],"published_time":"2012-02-04T12:00:00+00:00","modified_time":"2015-01-14T19:14:27+00:00","author":"http://examples.com/turtlelvr","publisher":"http://mediawiki.org"},"eprints":{"title":"Turtles are AWESOME!!1","creators_name":"http://www.example.com/turtlelvr","abstract":"Exposition on the awesomeness of turtles","datestamp":"2012-02-04 12:00:00","type":"article"}}; + expected = {"bePress":{"series_title":"Turtles","author":"Turtle Lvr","author_institution":"Mediawiki","title":"Turtles are AWESOME!!1","date":"2012","pdf_url":"http://www.example.com/turtlelvr/pdf","abstract_html_url":"http://www.example.com/turtlelvr","publisher":"Turtles Society","online_date":"2012/02/04"},"coins":[{"ctx_ver":"Z39.88-2004","rft_id":"info:doi/http://dx.doi.org/10.5555/12345678","rfr_id":"info:sid/crossref.org:search","rft_val_fmt":"info:ofi/fmt:kev:mtx:journal","rft":{"atitle":"Toward a Unified Theory of High-Energy Metaphysics: Silly String Theory","jtitle":"Journal of Psychoceramics","date":"2008","volume":"5","issue":"11","spage":"1","epage":"3","aufirst":"Josiah","aulast":"Carberry","genre":"article","au":["Josiah Carberry"]}}],"dublinCore":{"title":"Turtles are AWESOME!!1","creator":"http://www.example.com/turtlelvr","description":"Exposition on the awesomeness of turtles","date":"2012-02-04 12:00:00","type":"Text.Article"},"general":{"author":"Turtle Lvr","authorlink":"http://examples.com/turtlelvr","canonical":"http://example.com/turtles","description":"Exposition on the awesomeness of turtles","publisher":"https://mediawiki.org","robots":"we welcome our robot overlords","shortlink":"http://example.com/c","title":"Turtles are AWESOME!!1 | Awesome Turtles Website", "lang":"en"},"highwirePress":{"journal_title":"Turtles","issn":"1234-5678","doi":"10.1000/123","publication_date":"2012-02-04","title":"Turtles are AWESOME!!1","author":"Turtle Lvr","author_institution":"Mediawiki","volume":"150","issue":"1","firstpage":"123","lastpage":"456","publisher":"Turtles Society","abstract":"Exposition on the awesomeness of turtles."},"openGraph":{"locale":"en_US","type":"article","title":"Turtles are AWESOME!!1","description":"Exposition on the awesomeness of turtles","url":"http://example.com","site_name":"Awesome Turtles Website","image":[{"url":"http://example.com/turtle.jpg","secure_url":"https://secure.example.com/turtle.jpg","type":"image/jpeg","width":"400","height":"300"},{"url":"http://example.com/shell.jpg","width":"200","height":"150"}],"audio":{"url":"http://example.com/sound.mp3","secure_url":"https://secure.example.com/sound.mp3","type":"audio/mpeg"},"tag":["turtles","are","awesome"],"section":["Turtles are tough","Turtles are flawless","Turtles are cute"],"published_time":"2012-02-04T12:00:00+00:00","modified_time":"2015-01-14T19:14:27+00:00","author":"http://examples.com/turtlelvr","publisher":"http://mediawiki.org"},"eprints":{"title":"Turtles are AWESOME!!1","creators_name":"http://www.example.com/turtlelvr","abstract":"Exposition on the awesomeness of turtles","datestamp":"2012-02-04 12:00:00","type":"article"},"twitter":{"card":"summary","site":"@Turtlessssssssss","creator":["@Turtlessssssssss","@Turtlezzzzzzzzzz"],"url":"http://www.example.com/turtles","title":"Turtles are AWESOME!!1","description":"Exposition on the awesomeness of turtles","image":{"url":"http://example.com/turtles.jpg","alt":"It's a bunch of turtles!"},"app":{"url":{"iphone":"turtle://","googleplay":"turtle://"},"id":{"iphone":"000","googleplay":"superturtlearticle.androidapp"}}}}; $ = cheerio.load(fs.readFileSync('./test/static/turtle_article.html')); return meta.parseAll($).then(function(results){ assert.deepEqual(results, expected); @@ -33,7 +33,7 @@ describe('static files', function() { }); it('should be case insensitive on Turtle Article file', function() { - expected = {"bePress":{"series_title":"Turtles","author":"Turtle Lvr","author_institution":"Mediawiki","title":"Turtles are AWESOME!!1","date":"2012","pdf_url":"http://www.example.com/turtlelvr/pdf","abstract_html_url":"http://www.example.com/turtlelvr","publisher":"Turtles Society","online_date":"2012/02/04"},"coins":[{"ctx_ver":"Z39.88-2004","rft_id":"info:doi/http://dx.doi.org/10.5555/12345678","rfr_id":"info:sid/crossref.org:search","rft_val_fmt":"info:ofi/fmt:kev:mtx:journal","rft":{"atitle":"Toward a Unified Theory of High-Energy Metaphysics: Silly String Theory","jtitle":"Journal of Psychoceramics","date":"2008","volume":"5","issue":"11","spage":"1","epage":"3","aufirst":"Josiah","aulast":"Carberry","genre":"article","au":["Josiah Carberry"]}}],"dublinCore":{"title":"Turtles are AWESOME!!1","creator":"http://www.example.com/turtlelvr","description":"Exposition on the awesomeness of turtles","date":"2012-02-04 12:00:00","type":"Text.Article"},"general":{"author":"Turtle Lvr","authorlink":"http://examples.com/turtlelvr","canonical":"http://example.com/turtles","description":"Exposition on the awesomeness of turtles","publisher":"https://mediawiki.org","robots":"we welcome our robot overlords","shortlink":"http://example.com/c","title":"Turtles are AWESOME!!1 | Awesome Turtles Website", "lang":"en"},"highwirePress":{"journal_title":"Turtles","issn":"1234-5678","doi":"10.1000/123","publication_date":"2012-02-04","title":"Turtles are AWESOME!!1","author":"Turtle Lvr","author_institution":"Mediawiki","volume":"150","issue":"1","firstpage":"123","lastpage":"456","publisher":"Turtles Society","abstract":"Exposition on the awesomeness of turtles."},"openGraph":{"locale":"en_US","type":"article","title":"Turtles are AWESOME!!1","description":"Exposition on the awesomeness of turtles","url":"http://example.com","site_name":"Awesome Turtles Website","image":[{"url":"http://example.com/turtle.jpg","secure_url":"https://secure.example.com/turtle.jpg","type":"image/jpeg","width":"400","height":"300"},{"url":"http://example.com/shell.jpg","width":"200","height":"150"}],"audio":{"url":"http://example.com/sound.mp3","secure_url":"https://secure.example.com/sound.mp3","type":"audio/mpeg"},"tag":["turtles","are","awesome"],"section":["Turtles are tough","Turtles are flawless","Turtles are cute"],"published_time":"2012-02-04T12:00:00+00:00","modified_time":"2015-01-14T19:14:27+00:00","author":"http://examples.com/turtlelvr","publisher":"http://mediawiki.org"},"eprints":{"title":"Turtles are AWESOME!!1","creators_name":"http://www.example.com/turtlelvr","abstract":"Exposition on the awesomeness of turtles","datestamp":"2012-02-04 12:00:00","type":"article"}}; + expected = {"bePress":{"series_title":"Turtles","author":"Turtle Lvr","author_institution":"Mediawiki","title":"Turtles are AWESOME!!1","date":"2012","pdf_url":"http://www.example.com/turtlelvr/pdf","abstract_html_url":"http://www.example.com/turtlelvr","publisher":"Turtles Society","online_date":"2012/02/04"},"coins":[{"ctx_ver":"Z39.88-2004","rft_id":"info:doi/http://dx.doi.org/10.5555/12345678","rfr_id":"info:sid/crossref.org:search","rft_val_fmt":"info:ofi/fmt:kev:mtx:journal","rft":{"atitle":"Toward a Unified Theory of High-Energy Metaphysics: Silly String Theory","jtitle":"Journal of Psychoceramics","date":"2008","volume":"5","issue":"11","spage":"1","epage":"3","aufirst":"Josiah","aulast":"Carberry","genre":"article","au":["Josiah Carberry"]}}],"dublinCore":{"title":"Turtles are AWESOME!!1","creator":"http://www.example.com/turtlelvr","description":"Exposition on the awesomeness of turtles","date":"2012-02-04 12:00:00","type":"Text.Article"},"general":{"author":"Turtle Lvr","authorlink":"http://examples.com/turtlelvr","canonical":"http://example.com/turtles","description":"Exposition on the awesomeness of turtles","publisher":"https://mediawiki.org","robots":"we welcome our robot overlords","shortlink":"http://example.com/c","title":"Turtles are AWESOME!!1 | Awesome Turtles Website", "lang":"en"},"highwirePress":{"journal_title":"Turtles","issn":"1234-5678","doi":"10.1000/123","publication_date":"2012-02-04","title":"Turtles are AWESOME!!1","author":"Turtle Lvr","author_institution":"Mediawiki","volume":"150","issue":"1","firstpage":"123","lastpage":"456","publisher":"Turtles Society","abstract":"Exposition on the awesomeness of turtles."},"openGraph":{"locale":"en_US","type":"article","title":"Turtles are AWESOME!!1","description":"Exposition on the awesomeness of turtles","url":"http://example.com","site_name":"Awesome Turtles Website","image":[{"url":"http://example.com/turtle.jpg","secure_url":"https://secure.example.com/turtle.jpg","type":"image/jpeg","width":"400","height":"300"},{"url":"http://example.com/shell.jpg","width":"200","height":"150"}],"audio":{"url":"http://example.com/sound.mp3","secure_url":"https://secure.example.com/sound.mp3","type":"audio/mpeg"},"tag":["turtles","are","awesome"],"section":["Turtles are tough","Turtles are flawless","Turtles are cute"],"published_time":"2012-02-04T12:00:00+00:00","modified_time":"2015-01-14T19:14:27+00:00","author":"http://examples.com/turtlelvr","publisher":"http://mediawiki.org"},"eprints":{"title":"Turtles are AWESOME!!1","creators_name":"http://www.example.com/turtlelvr","abstract":"Exposition on the awesomeness of turtles","datestamp":"2012-02-04 12:00:00","type":"article"},"twitter":{"card":"summary","site":"@Turtlessssssssss","creator":["@Turtlessssssssss","@Turtlezzzzzzzzzz"],"url":"http://www.example.com/turtles","title":"Turtles are AWESOME!!1","description":"Exposition on the awesomeness of turtles","image":{"url":"http://example.com/turtles.jpg","alt":"It's a bunch of turtles!"},"app":{"url":{"iphone":"turtle://","googleplay":"turtle://"},"id":{"iphone":"000","googleplay":"superturtlearticle.androidapp"}}}}; $ = cheerio.load(fs.readFileSync('./test/static/Turtle_Article.html')); return meta.parseAll($).then(function(results){ assert.deepEqual(results, expected); diff --git a/test/static/Turtle_Article.html b/test/static/Turtle_Article.html index ff433f1..56ceb53 100644 --- a/test/static/Turtle_Article.html +++ b/test/static/Turtle_Article.html @@ -71,9 +71,16 @@ + + + + + + + diff --git a/test/static/turtle_article.html b/test/static/turtle_article.html index 20f4ab2..4b71765 100644 --- a/test/static/turtle_article.html +++ b/test/static/turtle_article.html @@ -69,9 +69,16 @@ + + + + + + + diff --git a/test/static/turtle_movie.html b/test/static/turtle_movie.html index de2b9c4..0a598b4 100644 --- a/test/static/turtle_movie.html +++ b/test/static/turtle_movie.html @@ -55,6 +55,11 @@ + + + + +