├── .gitignore ├── Gruntfile.js ├── Makefile ├── README.md ├── bin ├── swaggerOptions.js └── webmentions.js ├── config.js ├── index.js ├── lib ├── expand.js ├── handlers.js ├── interface.js ├── routes.js ├── utilities.js └── webmentions.js ├── license.txt ├── package-lock.json ├── package.json ├── public ├── css │ ├── examples.css │ ├── readme.css │ ├── site.css │ └── styles.css └── javascript │ └── site.js ├── templates ├── error.html ├── example.html ├── helpers │ └── numberformat.js ├── swagger.html └── withPartials │ ├── footer.html │ ├── head.html │ └── header.html └── test ├── maths-test.js └── sums-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | node_modules 11 | npm-debug.log 12 | .DS_Store -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | grunt.loadNpmTasks('grunt-mocha-test'); 3 | grunt.loadNpmTasks('grunt-contrib-jshint'); 4 | grunt.initConfig({ 5 | jshint: { 6 | files: ['grunt.js', 'lib/*.js'], 7 | options: { 8 | curly: true, 9 | eqeqeq: true, 10 | immed: true, 11 | latedef: false, 12 | newcap: true, 13 | noarg: true, 14 | sub: true, 15 | undef: true, 16 | boss: true, 17 | eqnull: true, 18 | browser: false, 19 | node: true, 20 | strict: false, 21 | quotmark: 'single' 22 | } 23 | }, 24 | mochaTest: { 25 | files: ['test/*-test.js'] 26 | }, 27 | watch: { 28 | files: 'lib/*.js', 29 | tasks: 'mochaTest' 30 | } 31 | }); 32 | // Default task. 33 | grunt.registerTask( 'default', 'mochaTest', 'jshint' ); 34 | }; -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = list 2 | test: 3 | ./node_modules/.bin/mocha \ 4 | --reporter $(REPORTER) 5 | 6 | .PHONY: test 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## An API of helper functions for consuming webmentions 2 | 3 | __This is a 9-year-old historical project - It is left up as an archive.__ 4 | 5 | 6 | 7 | [Webmentions](http://indiewebcamp.com/webmention) are an interesting method of notify another site that a comment/post on your own site is written in response to a post on their site. The site receiving the webmention notification can then verify the request and gather the message adding into a conversation flow in their post. You can read more about webmention on the [indiewebcamp](http://indiewebcamp.com/) site. 8 | 9 | This node module provides a number of functions that will help you add the ability to consumes webmentions to your site 10 | 11 | 12 | ## Install 13 | Add once its mature enough to put on NPM. 14 | 15 | or 16 | 17 | git clone http://github.com/glennjones/webmentions.git 18 | 19 | 20 | ## Run 21 | 22 | 1. Move into the project directory `$ cd webmentions` 23 | 2. Run `$ npm install` 24 | 3. Run `$ node bin/webmentions.js` 25 | 4. Connect to the server using `http://localhost:3008` 26 | 27 | 28 | ## discoverEndpoint 29 | Makes a request to the URL provided and trys to discover the webmentions API endpoint, the module will parse both the HTML or HTTP header in its search for the endpoint url. 30 | 31 | var webmentions = require('webmentions'), 32 | options = {'url': 'http://example.com/'}; 33 | 34 | webmentions.discoverEndpoint( options, function( err, data ){ 35 | if(!err && data.endpoint){ 36 | // do something with data.endpoint 37 | } 38 | }) 39 | 40 | If the request is completed without error the response is returned in the following format: 41 | 42 | { 43 | "endpoint": "http://example.com/webmention/" 44 | } 45 | 46 | 47 | 48 | ## proxyMention 49 | Allow you enter both the source and target URLs for webmention request. The method discovers the correct webmention request API endpoint and fires the webmention request on your behave. 50 | 51 | var webmentions = require('webmentions'), 52 | options = { 53 | 'source': 'http://example.com/comment', 54 | 'target': 'http://example.com/post', 55 | }; 56 | 57 | webmentions.proxyMention( options, function( err, data ){ 58 | if(!err){ 59 | // do something 60 | } 61 | }) 62 | 63 | If the request is completed without error the response is returned in the following format: 64 | 65 | { 66 | "statusCode": 200, 67 | "message": "forwarded successfully to: http://example.com/webmention/" 68 | } 69 | 70 | 71 | 72 | ## validateMention 73 | Makes a request to the source and target URLs validates the webmention by checking the pages are linked. The JSON also returns the first h-entry found on the source page. This method should give you everything you need to consume webmention request. 74 | 75 | var webmentions = require('webmentions'), 76 | options = { 77 | 'source': 'http://example.com/comment', 78 | 'target': 'http://example.com/post', 79 | }; 80 | 81 | webmentions.validateMention( options, function( err, data ){ 82 | if(!err && data){ 83 | // do something with data 84 | } 85 | }) 86 | 87 | 88 | If the request is completed without error it should contain four top level properties: 89 | 90 | 1. `isValid` - weather pages are interlinked correctly 91 | 2. `matchedWith` - how the URLs where matched 92 | 3. `target` 93 | 1. `url` - the `target` URL passed in the options object 94 | 2. `endpoint` - a webmention API endpoint if found 95 | 4. `source` 96 | 1. `url` - the `source` URL passed in the options object 97 | 2. `links` - all the links fron within the source page 98 | 3. `entry` - the first [h-entry](http://microformats.org/wiki/h-entry) found in the source page 99 | 100 | 101 | The response is returned in the following format: 102 | 103 | { 104 | "isValid": true, 105 | "matchedWith": "in-reply-to string", 106 | "target": { 107 | "url": "http://example.com/notes/2014-02-02-1", 108 | "endpoint": "http://example.com/webmention" 109 | }, 110 | "source": { 111 | "url": "http://example.com/notes/2014-02-03-1", 112 | "links": [ 113 | "http://example.com/articles", 114 | "http://example.com/notes", 115 | "http://example.com/events", 116 | "http://example.com/about", 117 | "http://example.com/projects", 118 | "http://example.com/tools", 119 | "http://example.com/", 120 | "http://example.com/notes/2014-02-03-1", 121 | "http://example.com/notes/2014-02-02-1", 122 | "https://twitter.com/example/status/430360177393799168", 123 | "https://twitter.com/intent/favorite?tweet_id=430360177393799168", 124 | "https://twitter.com/intent/retweet?tweet_id=430360177393799168", 125 | "https://twitter.com/intent/tweet?in_reply_to=430360177393799168", 126 | "https://twitter.com/example", 127 | "https://github.com/example", 128 | "http://delicious.com/example/", 129 | "http://lanyrd.com/people/example/", 130 | "https://plus.google.com/u/0/example/about", 131 | "http://www.linkedin.com/in/example", 132 | "http://www.slideshare.net/example/presentations", 133 | "http://creativecommons.org/licenses/by-nc/3.0/deed.en_US" 134 | ], 135 | "entry": { 136 | "type": [ 137 | "h-entry", 138 | "h-as-note" 139 | ], 140 | "properties": { 141 | "name": [ 142 | "test the input stream" 143 | ], 144 | "content": [ 145 | { 146 | "value": "test posting a webmention", 147 | "html": "test posting a webmention" 148 | } 149 | ], 150 | "url": [ 151 | "http://example.com/notes/2014-02-03-1" 152 | ], 153 | "in-reply-to": [ 154 | "http://example.com/notes/2014-02-02-1" 155 | ], 156 | "published": [ 157 | "2014-02-03T15:20:32.120Z" 158 | ], 159 | "syndication": [ 160 | "https://twitter.com/example/status/430360177393799168" 161 | ], 162 | "author": [ 163 | { 164 | "value": "Glenn Jones Exploring semantic mark-up and data portability. twitter (glennjones) github (glennjones) delicious (glennjonesnet) lanyrd (glennjones) google+ (105161464208920272734) linkedin (glennjones) slideshare (glennjones) © 2013 Glenn Jones. The text and photo's in this blog are licensed under a Creative Commons Attribution-NonCommercial 3.0 Unported License. The code examples are licensed under a MIT License.", 165 | "type": [ 166 | "h-card" 167 | ], 168 | "properties": { 169 | "photo": [ 170 | "http://example.com/images/photo-small.png" 171 | ], 172 | "url": [ 173 | "https://twitter.com/example", 174 | "https://github.com/example", 175 | "http://delicious.com/example/", 176 | "http://lanyrd.com/people/example/", 177 | "https://plus.google.com/u/0/example/about", 178 | "http://www.linkedin.com/in/example", 179 | "http://www.slideshare.net/example/presentations" 180 | ], 181 | "name": [ 182 | "Glenn Jones" 183 | ], 184 | "summary": [ 185 | "Exploring semantic mark-up and data portability." 186 | ] 187 | } 188 | } 189 | ] 190 | } 191 | } 192 | } 193 | } 194 | 195 | 196 | ## Errors 197 | 198 | The error format can have any combination of 4 properties; code, error, message and validation. The optional fourth property validation, is added to HTTP request if a input value is in the incorrect format. 199 | 200 | { 201 | "statusCode": 400, 202 | "error": "Bad Request", 203 | "message": "the value of url must be a string", 204 | "validation": { 205 | "source": "query", 206 | "keys": [ 207 | "url" 208 | ] 209 | } 210 | } 211 | 212 | 213 | 214 | ## Mocha integration test 215 | The project has a number integration and unit tests. To run the test, `cd` to project directory type the following command 216 | 217 | $ mocha --reporter list 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /bin/swaggerOptions.js: -------------------------------------------------------------------------------- 1 | const { version } = require('../package.json'); 2 | 3 | module.exports = { 4 | swaggerOptions: { 5 | info: { 6 | title: 'Webmentions', 7 | version: version, 8 | }, 9 | } 10 | }; -------------------------------------------------------------------------------- /bin/webmentions.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('@hapi/hapi'); 2 | const Inert = require('@hapi/inert'); 3 | const Vision = require('@hapi/vision'); 4 | const Blipp = require('blipp'); 5 | const HapiSwagger = require('hapi-swagger'); 6 | const { swaggerOptions } = require('./swaggerOptions.js'); 7 | const { routes } = require('../lib/routes.js'); 8 | const utils = require('../lib/utilities.js'); 9 | config = require('../config.js'); 10 | 11 | const host = process.env.HOST || 'localhost'; 12 | const port = process.env.PORT || 3051; 13 | 14 | // refines configure using server context 15 | 16 | config = utils.processConfig( config ) 17 | if( config.proxy.url ){ 18 | console.log(['start'],'using proxy',config.proxy.url); 19 | } 20 | 21 | const serverOptions = { 22 | host, 23 | port, 24 | debug: { request: ['error'] }, 25 | routes: { 26 | response: { 27 | modify: true, 28 | }, 29 | } 30 | }; 31 | 32 | 33 | 34 | (async () => { 35 | // create server 36 | const server = new Hapi.Server(serverOptions); 37 | // add swagger UI plugin 38 | await server.register([ 39 | Inert, 40 | Vision, 41 | Blipp, 42 | { plugin: HapiSwagger, options: swaggerOptions }, 43 | ]); 44 | 45 | server.views({ 46 | engines: { 47 | html: require('handlebars') 48 | }, 49 | relativeTo: __dirname.replace('/bin', ''), 50 | path: 'templates', 51 | partialsPath: 'templates/withPartials', 52 | helpersPath: 'templates/helpers', 53 | isCached: false 54 | }); 55 | 56 | // register routes 57 | server.route(routes); 58 | // add basic logger 59 | server.events.on( 60 | 'response', 61 | ({ info, method, path }) => { 62 | console.info(`[${info.remoteAddress}] ${method.toUpperCase()}: ${path}`); 63 | } 64 | ); 65 | 66 | server.ext('onPreResponse', (request, h) => { 67 | const response = request.response; 68 | console.log('request', request.info.host, request.path, request.payload, request.querystring); 69 | if (!response.isBoom) { 70 | return h.continue; 71 | } else { 72 | console.error('error', response); 73 | return h.continue; 74 | } 75 | /* 76 | // Replace error with friendly HTML 77 | const error = response; 78 | const ctx = { 79 | message: (error.output.statusCode === 404 ? 'page not found' : 'something went wrong') 80 | }; 81 | return h.view('error', ctx); 82 | */ 83 | }); 84 | 85 | // start server 86 | await server.start(); 87 | console.info('Server running at:', server.info.uri); 88 | })(); 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "site": { 3 | "name": "webmentions", 4 | }, 5 | "environments": { 6 | "development": { 7 | "server": { 8 | "host": "localhost", 9 | "port": 3008, 10 | }, 11 | "proxy" : { 12 | "port": 3001, 13 | "username": "3UWeT658816j46nB", 14 | "password": "5mz232a15NW264ax" 15 | } 16 | }, 17 | "production": { 18 | "server": { 19 | "host": "0.0.0.0", 20 | "port": 8000, 21 | }, 22 | "proxy" : { 23 | "port": 3001, 24 | "username": "3UWeT658816j46nB", 25 | "password": "5mz232a15NW264ax" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/interface.js'); -------------------------------------------------------------------------------- /lib/expand.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * urlexpand - index.js 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | var buffer = require('buffer-concat'), 9 | http = require('http'), 10 | https = require('https'), 11 | urlutil = require('url'), 12 | charset = require('charset'), 13 | iconv = require('iconv-lite'); 14 | 15 | 16 | 17 | // Glenn Jones added this to speed up requests 18 | // This can use a lot of memory and needs updating for each use case 19 | // ------------------------------------------- 20 | 21 | http.globalAgent.maxSockets = 10000; 22 | https.globalAgent.maxSockets = 10000; 23 | 24 | // ------------------------------------------- 25 | 26 | 27 | 28 | function handleCallback(err, url, callback) { 29 | if (callback.__called) { 30 | return; 31 | } 32 | callback.__called = true; 33 | callback(err, { 34 | url: url, 35 | title: callback.__title, 36 | count: callback.__redirectCounter, 37 | tracks: callback.__tracks 38 | }); 39 | } 40 | 41 | var TITLE_RE = /([^<]+)</i; 42 | 43 | function getTitle(data, cs) { 44 | cs = iconv.encodings[cs] ? cs : 'utf8'; 45 | var text = iconv.decode(data, cs); 46 | var m = TITLE_RE.exec(text); 47 | return m ? m[1].trim() : null; 48 | } 49 | 50 | /** 51 | * Expand a shorten url, return the original url and the redirect histories. 52 | * 53 | * @param {String} url, the url you want to expand. 54 | * @param {Object} [options] 55 | * - {Number} [redirects], max redirect times, default is `5`. 56 | * - {Boolean} [title], get title or not, default is `true`. 57 | * - {Number} [timeout], request timeout, default is `10000` ms. 58 | * @param {Function(err, data)} callback 59 | * - {Object} data { 60 | * {String} url: the last status 200 url. 61 | * {String} title: the last status 200 html page title, maybe empty. 62 | * {Number} count: need redirect times. 63 | * {Array} tracks: the handle tracks. `[{ url: $url, headers: $headers, statusCode: 301 }, ... ]` 64 | * } 65 | */ 66 | function expand(url, options, callback) { 67 | if (typeof options === 'function') { 68 | callback = options; 69 | options = null; 70 | } 71 | options = options || {}; 72 | options.redirects = options.redirects || 5; 73 | if (options.title === undefined) { 74 | options.title = true; 75 | } 76 | options.timeout = options.timeout || 10000; 77 | var info = urlutil.parse(url || ''); 78 | if (!info.hostname) { 79 | return callback(); 80 | } 81 | var reqOptions = { 82 | hostname: info.hostname, 83 | path: info.path, 84 | method: 'GET' 85 | }; 86 | if (info.port) { 87 | reqOptions.port = info.port; 88 | } 89 | if (callback.__redirectCounter === undefined) { 90 | callback.__redirectCounter = 0; 91 | callback.__tracks = []; 92 | } 93 | var request = http.request; 94 | if (info.protocol === 'https:') { 95 | request = https.request; 96 | } 97 | var req = request(reqOptions); 98 | var timer = null; 99 | req.on('response', function (res) { 100 | callback.__tracks.push({ 101 | url: url, 102 | headers: res.headers, 103 | statusCode: res.statusCode 104 | }); 105 | if (res.statusCode === 302 || res.statusCode === 301) { 106 | clearTimeout(timer); 107 | callback.__redirectCounter++; 108 | var location = urlutil.resolve(url, res.headers.location); 109 | if (callback.__redirectCounter > options.redirects) { 110 | return handleCallback(null, location, callback); 111 | } 112 | return expand(location, options, callback); 113 | } 114 | 115 | if (!options.title) { 116 | clearTimeout(timer); 117 | res.destroy(); 118 | return handleCallback(null, url, callback); 119 | } 120 | 121 | // get the title 122 | var buffers = []; 123 | var size = 0; 124 | res.on('data', function (chunk) { 125 | buffers.push(chunk); 126 | size += chunk.length; 127 | }); 128 | res.on('end', function () { 129 | clearTimeout(timer); 130 | var data = Buffer.concat(buffers, size); 131 | var cs = charset(res.headers, data) || 'utf8'; 132 | var title = getTitle(data, cs); 133 | callback.__title = title; 134 | handleCallback(null, url, callback); 135 | }); 136 | }); 137 | req.on('error', function (err) { 138 | callback.__tracks.push({ 139 | url: url, 140 | error: req.isTimeout ? 'request timeout' : err.message 141 | }); 142 | handleCallback(err, url, callback); 143 | }); 144 | req.end(); 145 | timer = setTimeout(function () { 146 | req.isTimeout = true; 147 | req.abort(); 148 | }, options.timeout); 149 | } 150 | 151 | module.exports = expand; 152 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const hapi = require('@hapi/hapi'); 4 | let config = require('../config.js'); 5 | const pack = require('../package'); 6 | const webmentions = require('../lib/webmentions.js'); 7 | const utils = require('../lib/utilities.js'); 8 | 9 | var captures = []; 10 | 11 | // refines configure using server context 12 | config = utils.processConfig( config ) 13 | 14 | 15 | 16 | async function index(request, h) { 17 | utils.getMarkDownHTML(__dirname.replace('/lib','') + '/README.md', function(err, data){ 18 | return h.view('swagger.html', { 19 | title: pack.name, 20 | markdown: data 21 | }); 22 | }); 23 | } 24 | 25 | async function example(request, h) { 26 | return h.view('example.html', { url: 'http://' + request.info.host + '/webmention/capture/' }); 27 | } 28 | 29 | async function capture(request, h) { 30 | var options = { 31 | target: request.payload.target, 32 | source: request.payload.source 33 | }; 34 | 35 | if(request.payload.proxy){ 36 | options.proxy = request.payload.proxy; 37 | } 38 | 39 | options.request = request.info; 40 | 41 | // add capture here 42 | captures.unshift( options ); 43 | if(captures.length > 5){ 44 | captures.pop() 45 | } 46 | renderJSON( request, h, null, options ); 47 | } 48 | 49 | async function displayCapture(request, h) { 50 | renderJSON( request, h, null, captures ); 51 | } 52 | 53 | // used to discover webmention API endpoint 54 | async function discoverEndpoint(request, h) { 55 | var options = { 56 | url: request.query.url 57 | }; 58 | 59 | // use node url parser to check format 60 | if( urlParser.parse( options.url ) ){ 61 | webmentions.discoverEndpoint( options, function( error, result ){ 62 | renderJSON( request, h, error, result ); 63 | }); 64 | } else { 65 | renderJSON( request, h, utils.buildError( 400, 'bad request', 'The url are not in the correct format'), {} ); 66 | } 67 | } 68 | 69 | // used to validate and then proxy webmention request 70 | async function proxyMention(request, h) { 71 | var options = { 72 | target: request.query.target, 73 | source: request.query.source 74 | }; 75 | 76 | if(request.query.proxy){ 77 | options.proxy = request.query.proxy; 78 | } 79 | 80 | webmentions.proxyMention( options, function( error, result ){ 81 | renderJSON( request, h, error, result ); 82 | }); 83 | } 84 | 85 | // used to validate a webmention request 86 | async function validateMention(request, h) { 87 | var options = { 88 | target: request.query.target, 89 | source: request.query.source 90 | }; 91 | 92 | if(request.query.proxy){ 93 | options.proxy = request.query.proxy; 94 | } 95 | 96 | webmentions.validateMention( options, function( error, result ){ 97 | renderJSON( request, h, error, result ); 98 | }); 99 | } 100 | 101 | // render json out to http stream 102 | async function renderJSON( request, h, err, result ){ 103 | if(err){ 104 | console.log(err) 105 | if(err.code === 404){ 106 | return h.response(new hapi.error.notFound(err.message)); 107 | } else { 108 | return h.response(new hapi.error.badRequest(err.message)); 109 | } 110 | } else { 111 | return h.response(result).type('application/json; charset=utf-8'); 112 | } 113 | } 114 | 115 | 116 | 117 | 118 | exports.index = index; 119 | exports.example = example; 120 | exports.discoverEndpoint = discoverEndpoint; 121 | exports.proxyMention = proxyMention; 122 | exports.validateMention = validateMention; 123 | exports.capture = capture; 124 | exports.displayCapture = displayCapture; 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /lib/interface.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | if(require.main === module) { 5 | // if they want the app 6 | var app = require('../bin/webmentions.js'); 7 | }else{ 8 | // if they want a module interface 9 | var routes = require('../lib/routes'), 10 | webmentions = require('../lib/webmentions.js'); 11 | 12 | module.exports = { 13 | 'routes': routes.routes, 14 | 'discoverEndpoint': function( options, callback ){ 15 | webmentions.discoverEndpoint( options, callback ); 16 | }, 17 | 'validateMention': function( options, callback ){ 18 | webmentions.validateMention( options, callback ); 19 | }, 20 | 'proxyMention': function( options, callback ){ 21 | webmentions.proxyMention( options, callback ); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Hapi = require('@hapi/hapi'); 3 | const Joi = require('joi'); 4 | const handlers = require('../lib/handlers.js'); 5 | let routes; 6 | 7 | 8 | // adds the routes and validation for api 9 | routes = [{ 10 | method: 'GET', 11 | path: '/', 12 | handler: handlers.index, 13 | options: {} 14 | }, { 15 | method: 'GET', 16 | path: '/example', 17 | handler: handlers.example, 18 | options: {} 19 | }, { 20 | method: 'GET', 21 | path: '/webmention/endpoint/', 22 | handler: handlers.discoverEndpoint, 23 | options: { 24 | description: 'Discovers URL for webmention API', 25 | notes: ['Discovers a webmention API from a given URL', 26 | 'Error status codes', 27 | '400, bad request', 28 | '404, not found', 29 | '500, internal server error' 30 | ], 31 | tags: ['api'], 32 | validate: { 33 | query: Joi.object({ 34 | url: Joi.string() 35 | .required() 36 | .description('the url on which the discovery is carried out on') 37 | }) 38 | } 39 | } 40 | },{ 41 | method: 'GET', 42 | path: '/webmention/mention/forward/', 43 | handler: handlers.proxyMention, 44 | options: { 45 | description: 'Post a webmention request', 46 | notes: ['Validates and then forwards on a webmention post request', 47 | 'Error status codes', 48 | '400, bad request', 49 | '404, not found', 50 | '500, internal server error' 51 | ], 52 | tags: ['api'], 53 | validate: { 54 | query: Joi.object({ 55 | 56 | source: Joi.string() 57 | .required() 58 | .description('the url of the comment'), 59 | 60 | target: Joi.string() 61 | .required() 62 | .description('the url of the entry been commented on'), 63 | 64 | proxy: Joi.string() 65 | .optional() 66 | .description('(Not part of the standard) a proxy url for the target where the webmention is pass on to another system') 67 | 68 | }) 69 | } 70 | } 71 | },{ 72 | method: 'GET', 73 | path: '/webmention/mention/validate/', 74 | handler: handlers.validateMention, 75 | options: { 76 | description: 'Validates webmention', 77 | notes: ['Requests webmention URLs and makes sure there is a valid link between them', 78 | 'Error status codes', 79 | '400, bad request', 80 | '404, not found', 81 | '500, internal server error' 82 | ], 83 | tags: ['api'], 84 | validate: { 85 | query: Joi.object({ 86 | source: Joi.string() 87 | .required() 88 | .description('the url of the comment'), 89 | 90 | target: Joi.string() 91 | .required() 92 | .description('the url of the entry been commented on'), 93 | 94 | proxy: Joi.string() 95 | .optional() 96 | .description('(Not part of the standard) a proxy url for the target where the webmention is pass on to another system') 97 | 98 | }) 99 | } 100 | } 101 | }, { 102 | method: 'POST', 103 | path: '/webmention/capture/', 104 | handler: handlers.capture, 105 | options: { 106 | description: 'Captures incoming webmention', 107 | notes: ['Captures incoming webmention request for later displays', 108 | 'Error status codes', 109 | '400, bad request', 110 | '404, not found', 111 | '500, internal server error' 112 | ], 113 | tags: ['api'], 114 | validate: { 115 | payload: Joi.object({ 116 | source: Joi.string() 117 | .required() 118 | .description('the url of the comment'), 119 | 120 | target: Joi.string() 121 | .required() 122 | .description('the url of the entry been commented on'), 123 | 124 | proxy: Joi.string() 125 | .optional() 126 | .description('(Not part of the standard) a proxy url for the target where the webmention is pass on to another system') 127 | 128 | }) 129 | } 130 | } 131 | }, { 132 | method: 'GET', 133 | path: '/webmention/capture/', 134 | handler: handlers.displayCapture, 135 | options: { 136 | description: 'Displays the captured webmention', 137 | notes: 'Displays the captured webmention request', 138 | tags: ['api'], 139 | } 140 | },{ 141 | method: 'GET', 142 | path: '/{path*}', 143 | handler: { 144 | directory: { 145 | path: __dirname.replace('/lib','') + '/public', 146 | listing: false, 147 | index: true 148 | } 149 | } 150 | }]; 151 | 152 | 153 | exports.routes = routes; -------------------------------------------------------------------------------- /lib/utilities.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | var marked = require('marked'), 4 | fs = require('fs'); 5 | 6 | 7 | module.exports = { 8 | 9 | 10 | // refines configure using server context 11 | processConfig: function( config ){ 12 | // get the options for the right server setup 13 | var out = {}, 14 | serverMode = (process.env.NODE_ENV) ? process.env.NODE_ENV : 'development'; 15 | 16 | // loop object properties and add them to root of out object 17 | for (var key in config.environments[serverMode]) { 18 | if (config.environments[serverMode].hasOwnProperty(key)) { 19 | out[key] = config.environments[serverMode][key]; 20 | } 21 | } 22 | 23 | if(process.env.HOST){ 24 | out.server.host = process.env.HOST; 25 | } 26 | if(process.env.PORT){ 27 | out.server.port = parseInt(process.env.PORT, 10); 28 | } 29 | if(process.env.PROXY_HOST){ 30 | out.proxy.host = process.env.PROXY_HOST; 31 | } 32 | if(process.env.PROXY_PORT){ 33 | out.proxy.port = parseInt(process.env.PROXY_PORT, 10); 34 | } 35 | if(process.env.PROXY_USERNAME){ 36 | out.proxy.username = process.env.PROXY_USERNAME; 37 | } 38 | if(process.env.PROXY_PASSWORD){ 39 | out.proxy.password = process.env.PROXY_PASSWORD; 40 | } 41 | 42 | // allows for custom port 43 | if(process.env.TO_MIRCOFORMATS_PORT){ 44 | out.server.port = parseInt(process.env.TO_MIRCOFORMATS_PORT, 10); 45 | } 46 | 47 | if(process.env.TO_MIRCOFORMATS_BASEPATH){ 48 | out.server.basepath = process.env.TO_MIRCOFORMATS_BASEPATH; 49 | } 50 | 51 | // add modulus information 52 | if (process.env.SERVO_ID && process.env.CLOUD_DIR) { 53 | this.host = this.host ? this.host : {}; 54 | this.host.clouddir = process.env.CLOUD_DIR; 55 | this.host.servoid = process.env.SERVO_ID; 56 | } 57 | 58 | if(out.proxy.host && out.proxy.port && out.proxy.username && out.proxy.password){ 59 | out.proxy.url = 'http://' 60 | + out.proxy.username + ':' 61 | + out.proxy.password + '@' 62 | + out.proxy.host + ':' 63 | + out.proxy.port + '/'; 64 | } 65 | 66 | return out; 67 | }, 68 | 69 | 70 | // read a file and converts the markdown to HTML 71 | getMarkDownHTML: function( path, callback ){ 72 | fs.readFile(path, 'utf8', function (err,data) { 73 | if (!err) { 74 | marked.setOptions({ 75 | gfm: true, 76 | tables: true, 77 | breaks: false, 78 | pedantic: false, 79 | sanitize: true, 80 | smartLists: true, 81 | smartypants: false, 82 | langPrefix: 'language-', 83 | highlight: function(code, lang) { 84 | return code; 85 | } 86 | }); 87 | data = marked(data); 88 | } 89 | callback( err, data ); 90 | }); 91 | }, 92 | 93 | 94 | // return error object 95 | buildError: function( code, err, message ){ 96 | code = (code || isNaN(code))? code : 500; 97 | err = (err)? err : ''; 98 | message = (message)? message : ''; 99 | 100 | return { 101 | 'code': code, 102 | 'error': err, 103 | 'message': message 104 | } 105 | }, 106 | 107 | 108 | generateID: function() { 109 | return ('0000' + (Math.random()*Math.pow(36,4) << 0).toString(36)).substr(-4); 110 | }, 111 | 112 | // is the object a string 113 | isString: function( obj ) { 114 | return typeof( obj ) === 'string'; 115 | }, 116 | 117 | 118 | // does a string start with the test 119 | startWith: function( str, test ) { 120 | return(str.indexOf(test) === 0); 121 | }, 122 | 123 | 124 | // remove spaces at front and back of string 125 | trim: function( str ) { 126 | if(this.isString(str)){ 127 | return str.replace(/^\s+|\s+$/g, ''); 128 | }else{ 129 | return ''; 130 | } 131 | }, 132 | 133 | 134 | // is a string only contain white space chars 135 | isOnlyWhiteSpace: function( str ){ 136 | return !(/[^\t\n\r ]/.test( str )); 137 | }, 138 | 139 | 140 | // removes white space from a string 141 | removeWhiteSpace: function( str ){ 142 | return str.replace(/[\t\n\r ]+/g, ' '); 143 | }, 144 | 145 | 146 | // is the object a array 147 | isArray: function( obj ) { 148 | return obj && !( obj.propertyIsEnumerable( 'length' ) ) && typeof obj === 'object' && typeof obj.length === 'number'; 149 | }, 150 | 151 | 152 | // simple function to find out if a object has any properties. 153 | hasProperties: function( obj ) { 154 | var key; 155 | for(key in obj) { 156 | if( obj.hasOwnProperty( key ) ) { 157 | return true; 158 | } 159 | } 160 | return false; 161 | }, 162 | 163 | 164 | // http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically 165 | merge: function() { 166 | var obj = {}, 167 | i = 0, 168 | il = arguments.length, 169 | key; 170 | for (; i < il; i++) { 171 | for (key in arguments[i]) { 172 | if (arguments[i].hasOwnProperty(key)) { 173 | obj[key] = arguments[i][key]; 174 | } 175 | } 176 | } 177 | return obj; 178 | }, 179 | 180 | 181 | dateNumber: function (n) { 182 | return n < 10 ? '0' + n : n.toString(); 183 | } 184 | 185 | 186 | }; -------------------------------------------------------------------------------- /lib/webmentions.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | var urlParser = require('url'), 4 | request = require('request'), 5 | cheerio = require('cheerio'), 6 | entities = require('entities'), 7 | microformats = require('microformat-node'), 8 | urlExpander = require('../lib/expand'), 9 | utils = require('../lib/utilities.js'); 10 | 11 | 12 | var httpHeaders = { 13 | 'Accept': '*/*', 14 | 'Accept-Charset': 'utf-8', 15 | 'Cache-Control': 'no-cache', 16 | 'Connection': 'keep-alive', 17 | 'Pragma': 'no-cache', 18 | 'User-Agent': 'transmat-webmentions', 19 | 'X-TransmatMaxAge' : 2000 20 | }; 21 | 22 | 23 | module.exports = { 24 | 25 | // discovers a webmention API endpoint from a given URL 26 | discoverEndpoint: function ( options, callback ){ 27 | getURLContent( options, function( err , data ){ 28 | if( err ){ 29 | callback( err, null ); 30 | }else{ 31 | var body = data.body, 32 | response = data.response, 33 | webmention = null, 34 | dom, 35 | node; 36 | 37 | dom = cheerio.load(body); 38 | node = dom.root(); 39 | parseEndpoint(options.url, response, body, dom, node, function( err, webmention ){ 40 | if( err ){ 41 | callback( err, null ); 42 | }else{ 43 | callback( null, {'endpoint': webmention} ); 44 | } 45 | }); 46 | } 47 | }) 48 | }, 49 | 50 | 51 | // validates the webmention request and returns the comment entry and author 52 | proxyMention: function( options, callback){ 53 | var context = this; 54 | validateMentionOptions( options, function( err, valid){ 55 | if( valid ){ 56 | 57 | // get target - this is the original entry to be commented on 58 | getURLContent( {'url': options.target}, function( err , data ){ 59 | if(!err){ 60 | var body = data.body, 61 | response = data.response, 62 | dom, 63 | node; 64 | 65 | dom = cheerio.load(body); 66 | node = dom.root(); 67 | // get webmention endpoint 68 | parseEndpoint(options.target, response, body, dom, node, function( err, webmention ){ 69 | if( !err ){ 70 | if(webmention){ 71 | console.log('webmention endpoint', webmention) 72 | var requestOptions = { 73 | uri: webmention, 74 | method: 'POST', 75 | pool: { 76 | maxSockets: 10000 77 | }, 78 | timeout: 10000, 79 | headers: httpHeaders, 80 | form: options 81 | } 82 | request(requestOptions, function(requestErrors, response, body) { 83 | if (!requestErrors && (response.statusCode >= 200 && response.statusCode < 300 ) ) { 84 | console.log('webmention successful:', options) 85 | callback( null, {'statusCode': response.statusCode, 'message': 'webmention forwarded successfully'} ); 86 | } else { 87 | console.log('webmention failed:', options) 88 | if(response && response.statusCode && response.statusCode > 399){ 89 | callback( utils.buildError( response.statusCode, null, null), null ); 90 | }else{ 91 | callback( utils.buildError( 400, 'bad request', requestErrors), null ); 92 | } 93 | } 94 | }); 95 | }else{ 96 | callback( utils.buildError( 400, 'bad request', 'could not find a webmention API endpoint for - ' + options.target), null ); 97 | } 98 | }else{ 99 | callback( err, null ); 100 | } 101 | }); 102 | }else{ 103 | callback( err, null ); 104 | } 105 | }); 106 | }else{ 107 | callback( err, null ); 108 | } 109 | }); 110 | }, 111 | 112 | 113 | // validates the webmention request 114 | validateMention: function ( options, callback ){ 115 | var context = this; 116 | validateMentionOptions( options, function( err, valid){ 117 | if( valid ){ 118 | context.getMentionData( options, function( err, data ){ 119 | if(!err && data){ 120 | var match = matchUrls( options.target, data.source ); 121 | if( match ){ 122 | data.isValid = match.matched; 123 | data.matchedWith = match.matchedWith; 124 | } 125 | } 126 | 127 | callback( err, data ); 128 | }); 129 | }else{ 130 | callback( err, null ); 131 | } 132 | }); 133 | }, 134 | 135 | 136 | // gets webmention data from source and target urls 137 | getMentionData: function ( options, callback ){ 138 | var out = { 139 | 'isValid': false, 140 | 'matchedWith': null, 141 | 'target': {}, 142 | 'source': {} 143 | }, 144 | count = 0; 145 | 146 | // outs for parallel processing of data 147 | function release(){ 148 | if(count === 2){ 149 | callback( null, out ); 150 | } 151 | } 152 | 153 | // get target - is the original entry to be commented on 154 | getURLContent( {'url': options.target}, function( err , data ){ 155 | if(!err){ 156 | var body = data.body, 157 | response = data.response, 158 | webmention = null, 159 | dom, 160 | node; 161 | 162 | dom = cheerio.load(body); 163 | node = dom.root(); 164 | // get webmention endpoint 165 | parseEndpoint(options.target, response, body, dom, node, function( err, webmention ){ 166 | if( !err ){ 167 | out.target = { 168 | 'url': options.target, 169 | 'endpoint': webmention 170 | } 171 | if(options.proxy){ 172 | out.target.proxy = options.proxy; 173 | } 174 | count++ 175 | release(); 176 | }else{ 177 | callback( err, null ); 178 | } 179 | }); 180 | }else{ 181 | callback( err, null ); 182 | } 183 | }); 184 | 185 | // get source - is the comment 186 | getURLContent( {'url': options.source}, function( err , data ){ 187 | if(!err){ 188 | var body = data.body, 189 | response = data.response, 190 | dom, 191 | node, 192 | links =[], 193 | urls = []; 194 | 195 | 196 | dom = cheerio.load(body); 197 | node = dom.root(); 198 | 199 | // get all urls within the page 200 | dom(node).find('a').each(function(i, elem) { 201 | var url = dom(this).attr('href'); 202 | if( url && utils.isString(url) ){ 203 | urls.push( absoluteUrl( options.source, url ) ); 204 | } 205 | }); 206 | 207 | // get microformats 208 | microformats.parseDom(dom, node, {baseUrl: options.source}, function( err, data ){ 209 | if(!err){ 210 | var entries = microformatsOfType( data, 'h-entry' ); 211 | out.source = { 212 | 'url': options.source, 213 | 'links': urls 214 | } 215 | if(entries && entries.length > -1){ 216 | out.source.entry = entries[0]; 217 | } 218 | }else{ 219 | console.log('microformats parser err ', err) 220 | } 221 | count++ 222 | release(); 223 | }); 224 | }else{ 225 | callback( err, null ); 226 | } 227 | }); 228 | 229 | } 230 | } 231 | 232 | 233 | 234 | // discovers a webmention API endpoint from request/cheerio objects 235 | function parseEndpoint(url, response, body, dom, node, callback){ 236 | var webmention = null; 237 | 238 | // process microformats to get rel="*" 239 | microformats.parseDom(dom, node, {}, function(err, data){ 240 | 241 | // find in rels="*" 242 | // <link rel="webmention" href="http://glennjones.net/webmention" /> 243 | // <link rel="http://webmention.org/" href="http://glennjones.net/webmention" /> 244 | if( data && data.rels ){ 245 | for (var prop in data.rels) { 246 | if(data.rels.hasOwnProperty(prop)){ 247 | if(prop === 'webmention'){ 248 | webmention = data.rels[prop][0]; 249 | break; 250 | } 251 | if(prop === 'http://webmention.org/'){ 252 | webmention = data.rels[prop][0]; 253 | break; 254 | } 255 | } 256 | } 257 | } 258 | 259 | 260 | // find in header 261 | // "link: "<http://glennjones.net/webmention/>; rel="webmention"" 262 | if(!webmention){ 263 | if( response && response.headers ){ 264 | for (var prop in response.headers) { 265 | if(response.headers.hasOwnProperty(prop)){ 266 | if(prop === 'link'){ 267 | var str = response.headers[prop]; 268 | if(str.indexOf('webmention') > -1 && str.indexOf(';') > -1){ 269 | var items = str.split(';'); 270 | if( utils.trim(items[1]) === 'rel="webmention"' ){ 271 | webmention = items[0].replace('<','').replace('>','') 272 | break; 273 | } 274 | } 275 | } 276 | } 277 | } 278 | } 279 | } 280 | 281 | if(webmention){ 282 | webmention = absoluteUrl( url, webmention ); 283 | } 284 | 285 | callback( err, webmention ); 286 | 287 | }); 288 | } 289 | 290 | 291 | // request the url content 292 | function getURLContent( options, callback ){ 293 | if(options.url){ 294 | 295 | if(options.url === undefined 296 | || utils.isString(options.url) === false 297 | || urlParser.parse( options.url ) === null){ 298 | callback( utils.buildError( 400, 'bad request', 'The url seems not to be in the correct formatted'), null ); 299 | } 300 | 301 | var requestOptions = { 302 | uri: options.url, 303 | pool: { 304 | maxSockets: 10000 305 | }, 306 | timeout: 10000, 307 | headers: httpHeaders 308 | } 309 | if(options.proxy){ 310 | requestOptions.proxy = options.proxy; 311 | } 312 | 313 | request(requestOptions, function(requestErrors, response, body) { 314 | if (!requestErrors && response.statusCode === 200) { 315 | callback( null, {'response': response, 'body': body} ); 316 | } else { 317 | console.log('failed:', options.url) 318 | if(response && response.statusCode && response.statusCode > 399){ 319 | callback( utils.buildError( response.statusCode, null, options.url), null ); 320 | }else{ 321 | callback( utils.buildError( 400, 'bad request', requestErrors + ' - ' + options.url), null ); 322 | } 323 | } 324 | }); 325 | 326 | }else{ 327 | callback( utils.buildError( 400, 'bad request', 'no url was given'), null ); 328 | } 329 | } 330 | 331 | 332 | function matchUrls( targetUrl, sourceData, callback ){ 333 | var matched = false, 334 | matchedWith = null, 335 | entry = sourceData.entry, 336 | links = sourceData.links, 337 | i, 338 | x; 339 | 340 | targetUrl = removeProtocols( targetUrl ); 341 | 342 | // does microformats data have a h-entry 343 | // we do this for speed rather than try match all urls frist 344 | if(entry && entry.properties && entry.properties['in-reply-to']){ 345 | var replyTo = entry.properties['in-reply-to']; 346 | // make sure its an array 347 | if( utils.isArray( replyTo ) ){ 348 | // loop array 349 | i = replyTo.length; 350 | while (i--) { 351 | // is 'in-reply-to' a string 352 | if( utils.isString( replyTo[i] ) ){ 353 | if( removeProtocols(replyTo[i]) === targetUrl ){ 354 | matched = true; 355 | matchedWith = 'in-reply-to string'; 356 | break 357 | } 358 | // is 'in-reply-to' a object 359 | }else{ 360 | if( replyTo && replyTo[i].properties && replyTo[i].properties.url ){ 361 | x = replyTo[i].properties.url.length; 362 | while (x--) { 363 | if( removeProtocols(replyTo[i].properties.url[x]) === targetUrl ){ 364 | matched = true; 365 | matchedWith = 'in-reply-to object property'; 366 | break 367 | } 368 | } 369 | } 370 | } 371 | } 372 | } 373 | } 374 | 375 | 376 | if(matched === false){ 377 | i = links.length; 378 | while (i--) { 379 | if( removeProtocols(links[i]) === targetUrl ){ 380 | matched = true; 381 | matchedWith = 'url'; 382 | break 383 | } 384 | } 385 | } 386 | 387 | return {'matched': matched, 'matchedWith': matchedWith}; 388 | 389 | } 390 | 391 | 392 | // get root level microformats of a given type 393 | function microformatsOfType( json, name ){ 394 | var out = [], 395 | x, 396 | i; 397 | 398 | if(json && json.items && name ){ 399 | i = json.items.length; 400 | x = 0; 401 | while (x < i) { 402 | if( json.items[x].type && json.items[x].type.indexOf(name) > -1 ){ 403 | out.push( json.items[x] ); 404 | } 405 | x++; 406 | } 407 | } 408 | return out; 409 | } 410 | 411 | 412 | // checks that the option object is valid 413 | function validateMentionOptions( options, callback ){ 414 | var err = null; 415 | 416 | if(testUrl( options.source, 'source' ) && testUrl( options.target, 'target' )){ 417 | if(removeProtocols( options.source ) !== removeProtocols( options.target )){ 418 | callback(null, true); 419 | }else{ 420 | callback( utils.buildError( 400, 'bad request', 'the target and source url should not be the same'), null ); 421 | } 422 | }else{ 423 | callback( err, null ); 424 | } 425 | 426 | function testUrl( url, name ){ 427 | if( url && utils.isString( url ) ){ 428 | if(urlParser.parse( url ) ){ 429 | return true; 430 | } else { 431 | err = utils.buildError( 400, 'bad request', name + ' url is not in the correct format - ' + url); 432 | return false; 433 | } 434 | }else{ 435 | err = utils.buildError( 400, 'bad request', name + ' url is not in the correct format - ' + url); 436 | return false; 437 | } 438 | } 439 | } 440 | 441 | 442 | // removes protocols for matching urls 443 | function removeProtocols ( url ){ 444 | return url.replace('http://','//').replace('https://','//'); 445 | } 446 | 447 | 448 | // make sure that url is absolute 449 | function absoluteUrl( url, urlFragment ){ 450 | if(utils.startWith(urlFragment, 'http')){ 451 | return urlFragment; 452 | }else{ 453 | return urlParser.resolve(url, urlFragment) 454 | } 455 | } 456 | 457 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License - webmentions 2 | 3 | Copyright (c) 2014-2023 Glenn Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmentions", 3 | "author": "Glenn Jones", 4 | "version": "0.0.8", 5 | "description": "An API of helper function for providing and consuming webmentions", 6 | "keywords": [ 7 | "webmention", 8 | "microformat", 9 | "h-entry", 10 | "h-card", 11 | "indieweb" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/transmat/webmentions.git" 16 | }, 17 | "scripts": { 18 | "test": "make test" 19 | }, 20 | "dependencies": { 21 | "@hapi/hapi": "21.3.2", 22 | "@hapi/inert": "^7.1.0", 23 | "@hapi/vision": "^7.0.3", 24 | "async": "3.2.5", 25 | "blipp": "^4.0.2", 26 | "buffer-concat": "1.0.0", 27 | "charset": "1.0.1", 28 | "cheerio": "1.0.0-rc.12", 29 | "entities": "4.5.0", 30 | "handlebars": "4.7.8", 31 | "hapi-swagger": "17.2.0", 32 | "iconv-lite": "0.6.3", 33 | "joi": "17.11.0", 34 | "marked": "11.0.0", 35 | "microformat-node": "2.0.1", 36 | "request": "2.88.2" 37 | }, 38 | "devDependencies": { 39 | "chai": "4.3.10", 40 | "grunt": "1.6.1", 41 | "grunt-contrib-jshint": "3.2.0", 42 | "grunt-mocha-test": "0.13.3", 43 | "mocha": "10.2.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/css/examples.css: -------------------------------------------------------------------------------- 1 | /* Glenn Jones - taken from ident engine */ 2 | 3 | .str { color: #85C5DC; } 4 | .kwd { color: #EDF0D1; } 5 | .com { color: #878989; } 6 | .typ { color: #F5896F; } 7 | .lit { color: #FFB17A; } 8 | .pun { color: #FFFFFF; } 9 | .pln { color: #FFFFFF; } 10 | .tag { color: #F5896F; } 11 | .atn { color: #F5896F; } 12 | .atv { color: #85C5DC; } 13 | .dec { color: #878989; } 14 | 15 | pre.prettyprint { 16 | background-color:#302F2D; 17 | border: none; 18 | line-height: normal; 19 | font-size: 100%; 20 | border-radius: 6px 6px 6px 6px; 21 | font-family: consolas,​'andale mono',​'courier new',​monospace; 22 | padding-top: 12px; 23 | overflow: hidden; 24 | } 25 | 26 | code{ 27 | font-size: 13px; 28 | line-height: normal; 29 | } 30 | 31 | /* Specify class=linenums on a pre to get line numbering */ 32 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ 33 | li.L0, 34 | li.L1, 35 | li.L2, 36 | li.L3, 37 | li.L5, 38 | li.L6, 39 | li.L7, 40 | li.L8 { list-style-type: none } 41 | /* Alternate shading for lines */ 42 | li.L1, 43 | li.L3, 44 | li.L5, 45 | li.L7, 46 | li.L9 { background: #eee } 47 | 48 | @media print { 49 | .str { color: #060; } 50 | .kwd { color: #006; font-weight: bold; } 51 | .com { color: #600; font-style: italic; } 52 | .typ { color: #404; font-weight: bold; } 53 | .lit { color: #044; } 54 | .pun { color: #440; } 55 | .pln { color: #000; } 56 | .tag { color: #006; font-weight: bold; } 57 | .atn { color: #404; } 58 | .atv { color: #060; } 59 | } 60 | 61 | 62 | header, section, footer { 63 | width: 100%; 64 | float: none; 65 | position: relative; 66 | } 67 | 68 | 69 | h1 { 70 | margin-top: 0; 71 | } 72 | 73 | .home-link{ 74 | margin: 0; 75 | } 76 | 77 | 78 | .services{ 79 | margin-top: 2em; 80 | list-style: none; 81 | margin-bottom: 60px; 82 | } 83 | 84 | .services li{ 85 | height: 24px; 86 | border-bottom: solid 1px #ccc; 87 | margin-bottom: 2px; 88 | background-size: 16px 16px; 89 | width: 100%; 90 | font-size: 14px; 91 | } 92 | 93 | .services li span{ 94 | display: inline-block; 95 | margin-right: 10px; 96 | color: #666; 97 | } 98 | 99 | .services li a{ 100 | 101 | } 102 | 103 | #form { 104 | margin-bottom: 20px; 105 | } 106 | 107 | .gray-button { 108 | background: #e5e5e5; 109 | background: -moz-linear-gradient(top, #e5e5e5 0%, #999999 100%); 110 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e5e5e5), color-stop(100%,#999999)); 111 | background: -webkit-linear-gradient(top, #e5e5e5 0%,#999999 100%); 112 | background: -o-linear-gradient(top, #e5e5e5 0%,#999999 100%); 113 | background: -ms-linear-gradient(top, #e5e5e5 0%,#999999 100%); 114 | background: linear-gradient(to bottom, #e5e5e5 0%,#999999 100%); 115 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e5e5e5', endColorstr='#999999',GradientType=0 ); 116 | border-color: #ababab #999999 #8c8c8c; 117 | border-style: solid; 118 | border-width: 1px; 119 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 1px 0 #f2f2f2 inset; 120 | } 121 | 122 | .gray-button:active, .button-down { 123 | background: #999999; 124 | background: -moz-linear-gradient(top, #999999 0%, #e5e5e5 100%); 125 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#999999), color-stop(100%,#e5e5e5)); 126 | background: -webkit-linear-gradient(top, #999999 0%,#e5e5e5 100%); 127 | background: -o-linear-gradient(top, #999999 0%,#e5e5e5 100%); 128 | background: -ms-linear-gradient(top, #999999 0%,#e5e5e5 100%); 129 | background: linear-gradient(to bottom, #999999 0%,#e5e5e5 100%); 130 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#999999', endColorstr='#e5e5e5',GradientType=0 ); 131 | } 132 | 133 | input[type="submit"].inline-button { 134 | margin: 0; 135 | display: inline-block; 136 | padding: 2px 8px; 137 | float: right; 138 | } 139 | 140 | footer{ 141 | margin-top: 10em; 142 | } 143 | 144 | footer p{ 145 | margin: 0; 146 | } 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /public/css/readme.css: -------------------------------------------------------------------------------- 1 | #readme h1, #readme h2, #readme h3, #readme h4{ 2 | margin-bottom: 0; 3 | text-transform: none; 4 | font-weight: bold; 5 | } 6 | 7 | #readme p, #readme li, #readme td{ 8 | font-family: arial; 9 | } 10 | 11 | #readme ol { 12 | list-style-type: decimal; 13 | } 14 | 15 | #readme ul { 16 | list-style: disc; 17 | } 18 | 19 | #readme li, #readme ol li, #readme ul li { 20 | padding: 0; 21 | margin: 0; 22 | font-size: 1em; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /public/css/site.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .stats-boxes{ 4 | margin-top: 1em; 5 | margin-bottom: 1em; 6 | } 7 | 8 | .stats-box{ 9 | width: 3em; 10 | font-size: 3rem; 11 | border: solid 1px #333; 12 | border-radius: 4px; 13 | padding: 10px; 14 | margin-right: 0.4em; 15 | float: left; 16 | 17 | } 18 | 19 | .stats-box span.stats-label{ 20 | font-size: 0.8rem; 21 | display: block; 22 | font-weight: 400; 23 | } 24 | 25 | 26 | table.chart-table{ 27 | margin: 0; 28 | } 29 | 30 | table.chart-table tbody td:first-child { 31 | text-align: right; 32 | padding-right: 8px; 33 | width: 30%; 34 | } 35 | 36 | table.chart-table tbody td:last-child { 37 | width: 10%; 38 | } 39 | 40 | table.chart-table tbody td div{ 41 | background-color: #c5bbd0; 42 | color: #c5bbd0; 43 | height: 10px; 44 | min-width: 1px; 45 | text-indent: -9999px 46 | } 47 | 48 | table.graphs-table tbody td:first-child { 49 | width: 20%; 50 | } 51 | 52 | table.graphs-table tbody td:last-child { 53 | width: 20%; 54 | } 55 | 56 | 57 | ul.profile-list{ 58 | margin-top: 2em; 59 | margin-bottom: 2em; 60 | text-transform: lowercase; 61 | 62 | } 63 | 64 | #tools .button{ 65 | margin-left: 0; 66 | } 67 | 68 | #profile table caption{ 69 | font-weight: bold; 70 | text-align: left; 71 | text-transform: uppercase; 72 | } 73 | 74 | #profile table{ 75 | margin: 4em 0 4em 0; 76 | } 77 | 78 | #profile table td { 79 | width: 20%; 80 | } 81 | 82 | #profile table td:last-child { 83 | width: 80%; 84 | } 85 | 86 | .blog-platforms-boxes{ 87 | margin-right: 0.4em; 88 | } 89 | 90 | .body-code{ 91 | width: 100%; 92 | height: 300px; 93 | } 94 | 95 | form.button-form{ 96 | display: inline-block; 97 | margin: 0; 98 | margin-right: 10px; 99 | } 100 | 101 | input[type="submit"]{ 102 | margin-top: 10px; 103 | } 104 | 105 | -------------------------------------------------------------------------------- /public/css/styles.css: -------------------------------------------------------------------------------- 1 | /* Glenn Jones - taken from ident engine */ 2 | 3 | @import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); 4 | 5 | body { 6 | padding:2em; 7 | padding-top: 0; 8 | margin: 0; 9 | font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color:#333; 11 | font-weight:300; 12 | font-size: 100%; 13 | } 14 | 15 | h1, h2, h3, h4, h5, h6 { 16 | color:#222; 17 | margin:0 0 20px; 18 | font-family: Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-weight:300; 20 | } 21 | 22 | p, ul, ol, table, pre, dl { 23 | margin:0 0 20px; 24 | } 25 | 26 | h1{ 27 | font-size: 1.5em; 28 | } 29 | 30 | h2, h3 { 31 | margin-top: 3em; 32 | margin-bottom: 0.8em; 33 | font-size: 1.17em; 34 | } 35 | 36 | h1, h2, h3 { 37 | line-height:1.1; 38 | clear: both; 39 | text-transform: uppercase; 40 | font-weight: normal; 41 | } 42 | 43 | 44 | ol{ 45 | list-style-type: lower-alpha; 46 | } 47 | 48 | ol li{ 49 | margin-bottom: 1em; 50 | } 51 | 52 | h1.home-link{ 53 | font-size: 1.8em; 54 | font-weight: 400; 55 | margin-bottom: 0.4em; 56 | background-image: url('../images/logo.png'); 57 | background-repeat: no-repeat; 58 | 59 | text-transform: uppercase; 60 | height: 145px; 61 | padding: 0; 62 | position: relative; 63 | } 64 | 65 | h1.home-link a{ 66 | color: #333; 67 | display: block; 68 | position: absolute; bottom: 0; left: 0; 69 | } 70 | 71 | h1.entry-title{ 72 | margin-top: 2em; 73 | margin-bottom: 1em; 74 | } 75 | 76 | 77 | 78 | header h2 { 79 | color:#ff6666; 80 | } 81 | 82 | h3, h4, h5, h6 { 83 | color:#494949; 84 | } 85 | 86 | a { 87 | color:#39c; 88 | text-decoration:none; 89 | } 90 | 91 | a:hover { 92 | text-decoration:underline; 93 | } 94 | 95 | a small { 96 | font-size:11px; 97 | color:#777; 98 | margin-top:-0.6em; 99 | display:block; 100 | } 101 | 102 | .wrapper { 103 | width:700px; 104 | margin:0 auto; 105 | } 106 | 107 | blockquote { 108 | border-left:1px solid #e5e5e5; 109 | margin:0; 110 | padding:0 0 0 20px; 111 | font-style:italic; 112 | } 113 | 114 | code, pre { 115 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; 116 | color:#333; 117 | font-size:12px; 118 | } 119 | 120 | pre { 121 | padding:8px 15px; 122 | background: #f8f8f8; 123 | border-radius:5px; 124 | border:1px solid #e5e5e5; 125 | overflow-x: auto; 126 | } 127 | 128 | table { 129 | width:100%; 130 | border-collapse:collapse; 131 | } 132 | 133 | th, td { 134 | text-align:left; 135 | padding:5px 10px; 136 | border-bottom:1px solid #e5e5e5; 137 | } 138 | 139 | dt { 140 | color:#444; 141 | font-weight:700; 142 | } 143 | 144 | th { 145 | color:#444; 146 | } 147 | 148 | img { 149 | max-width:100%; 150 | } 151 | 152 | nav{ 153 | border-top: 1px solid #e7e7e7; 154 | clear: both; 155 | padding-top: 0.5em; 156 | } 157 | 158 | .menu { 159 | list-style-image: none; 160 | list-style-position: outside; 161 | list-style-type: none; 162 | margin: 0 auto; 163 | padding: 0; 164 | font-weight: normal; 165 | } 166 | 167 | .menu li { 168 | display: block; 169 | float: left; 170 | margin: 0; 171 | padding: 0; 172 | } 173 | 174 | .menu li a { 175 | border-right: 1px solid #eee; 176 | display: block; 177 | padding-left: 1em; 178 | padding-right: 1em; 179 | text-decoration: none; 180 | text-transform: lowercase; 181 | color: #000; 182 | } 183 | 184 | .menu li a:hover { 185 | text-decoration:underline; 186 | } 187 | 188 | .menu .lastItem{ 189 | border: none; 190 | } 191 | 192 | .menu .firstItem{ 193 | padding-left: 0; 194 | } 195 | 196 | dl{ 197 | margin-left: 1.4em; 198 | } 199 | 200 | dd{ 201 | margin-left: 0; 202 | margin-bottom: 1em; 203 | } 204 | 205 | 206 | header { 207 | width:270px; 208 | float:left; 209 | position:fixed; 210 | } 211 | 212 | header .buttons { 213 | list-style:none; 214 | height:40px; 215 | 216 | padding:0; 217 | 218 | background: #eee; 219 | background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); 220 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); 221 | background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 222 | background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 223 | background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 224 | background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 225 | 226 | border-radius:5px; 227 | border:1px solid #d2d2d2; 228 | box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; 229 | width:270px; 230 | } 231 | 232 | header .buttons li { 233 | width:89px; 234 | float:left; 235 | border-right:1px solid #d2d2d2; 236 | height:40px; 237 | } 238 | 239 | header ul.buttons a { 240 | line-height:1; 241 | font-size:11px; 242 | color:#999; 243 | display:block; 244 | text-align:center; 245 | padding-top:6px; 246 | height:40px; 247 | } 248 | 249 | strong { 250 | color:#222; 251 | font-weight:700; 252 | } 253 | 254 | header ul.buttons li + li { 255 | width:88px; 256 | border-left:1px solid #fff; 257 | } 258 | 259 | header ul.buttons li + li + li { 260 | border-right:none; 261 | width:89px; 262 | } 263 | 264 | header ul.buttons a strong { 265 | font-size:14px; 266 | display:block; 267 | color:#222; 268 | } 269 | 270 | section { 271 | width:500px; 272 | float:right; 273 | padding-bottom:5px; 274 | } 275 | 276 | small { 277 | font-size:11px; 278 | } 279 | 280 | hr { 281 | border:0; 282 | background:#e5e5e5; 283 | height:1px; 284 | margin:0 0 20px; 285 | } 286 | 287 | footer { 288 | width:270px; 289 | float:left; 290 | position:fixed; 291 | bottom:50px; 292 | margin-top: 4em; 293 | } 294 | 295 | 296 | 297 | form{ 298 | margin-bottom: 80px; 299 | } 300 | 301 | form p{ 302 | margin: 0 0 8px 0; 303 | } 304 | 305 | input[type="text"]{ 306 | width: 325px; 307 | padding: 7px; 308 | border: 1px solid #999; 309 | border-radius: 3px 3px 3px 3px; 310 | font-size: 12px; 311 | color: #333; 312 | } 313 | 314 | input[type="submit"], input[type="button"]{ 315 | margin-top: 10px; 316 | font-size: 2em; 317 | } 318 | 319 | label{ 320 | display: inline-block; 321 | } 322 | 323 | textarea { 324 | width: 340px; 325 | height: 150px; 326 | padding: 7px; 327 | border: 1px solid #999; 328 | border-radius: 3px 3px 3px 3px; 329 | font-size: 12px; 330 | color: #333; 331 | } 332 | 333 | xxxul { 334 | float: left; 335 | } 336 | 337 | .button { 338 | background: #33a0e8; 339 | background: -moz-linear-gradient(top, #33a0e8 0%, #2180ce 100%); 340 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#33a0e8), color-stop(100%,#2180ce)); 341 | background: -webkit-linear-gradient(top, #33a0e8 0%,#2180ce 100%); 342 | background: -o-linear-gradient(top, #33a0e8 0%,#2180ce 100%); 343 | background: -ms-linear-gradient(top, #33a0e8 0%,#2180ce 100%); 344 | background: linear-gradient(to bottom, #33a0e8 0%,#2180ce 100%); 345 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#33a0e8', endColorstr='#2180ce',GradientType=0 ); 346 | border-color: #2270AB #18639A #0F568B; 347 | border-style: solid; 348 | border-width: 1px; 349 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 1px 0 #83C5F1 inset; 350 | color: #FFFFFF; 351 | border-radius: 3px 3px 3px 3px; 352 | cursor: pointer; 353 | font-size: 13px; 354 | font-weight: 600; 355 | overflow: visible; 356 | padding: 5px 16px; 357 | text-align: center; 358 | } 359 | 360 | .button:active, .button-down { 361 | background: #2180ce; 362 | background: -moz-linear-gradient(top, #2180ce 0%, #33a0e8 100%); 363 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2180ce), color-stop(100%,#33a0e8)); 364 | background: -webkit-linear-gradient(top, #2180ce 0%,#33a0e8 100%); 365 | background: -o-linear-gradient(top, #2180ce 0%,#33a0e8 100%); 366 | background: -ms-linear-gradient(top, #2180ce 0%,#33a0e8 100%); 367 | background: linear-gradient(to bottom, #2180ce 0%,#33a0e8 100%); 368 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2180ce', endColorstr='#33a0e8',GradientType=0 ); 369 | } 370 | 371 | .large-input{ 372 | padding: 7px; 373 | border: 1px solid #999; 374 | border-radius: 3px 3px 3px 3px; 375 | font-size: 14px; 376 | } 377 | 378 | .large-label{ 379 | display: block; 380 | width: 100%; 381 | margin-bottom: 16px; 382 | } 383 | 384 | input.large-button{ 385 | margin-left: 0; 386 | } 387 | 388 | #loading{ 389 | display: none; 390 | margin-left: 6px; 391 | vertical-align: top; 392 | } 393 | 394 | #demos{ 395 | margin-bottom: 60px; 396 | font-weight: bold; 397 | } 398 | 399 | .code-title{ 400 | margin-top: 2.2em; 401 | } 402 | 403 | .services{ 404 | margin-top: 2em; 405 | list-style: none; 406 | margin-bottom: 30px; 407 | width: 20em; 408 | } 409 | 410 | .services li{ 411 | height: 24px; 412 | margin-bottom: 2px; 413 | background-size: 16px 16px; 414 | font-size: 14px; 415 | } 416 | 417 | .services li span{ 418 | display: inline-block; 419 | margin-right: 10px; 420 | color: #666; 421 | } 422 | 423 | .services li a{ 424 | font-weight: bold; 425 | } 426 | 427 | ul.services{ 428 | float: none; 429 | } 430 | 431 | #support{ 432 | margin-bottom: 6em; 433 | } 434 | 435 | #support .services{ 436 | margin-bottom: 0; 437 | } 438 | 439 | #support-lists ul{ 440 | float: left; 441 | } 442 | 443 | #support-lists .services{ 444 | width: auto; 445 | } 446 | 447 | #support-lists .services li{ 448 | border: none; 449 | } 450 | 451 | 452 | .notes{ 453 | clear: both; 454 | margin-bottom: 60px; 455 | margin-left: 40px; 456 | } 457 | 458 | .error{ 459 | margin-bottom: 60px; 460 | } 461 | 462 | 463 | .paging{ 464 | list-style: none; 465 | padding: 0; 466 | margin: 0; 467 | } 468 | 469 | .paging li{ 470 | padding: 2px 8px; 471 | margin: 2px; 472 | background-color: white; 473 | border: solid 1px #39C; 474 | display: block; 475 | float: left; 476 | } 477 | 478 | .paging li.current-page{ 479 | border: solid 1px #ddd; 480 | color: #ddd; 481 | } 482 | 483 | 484 | .clearfix:after { 485 | visibility: hidden; 486 | display: block; 487 | font-size: 0; 488 | content: " "; 489 | clear: both; 490 | height: 0; 491 | } 492 | * html .clearfix { zoom: 1; } /* IE6 */ 493 | *:first-child+html .clearfix { zoom: 1; } /* IE7 */ 494 | 495 | 496 | 497 | 498 | 499 | 500 | div.swagger-ui-wrap h2, div.swagger-ui-wrap h3{ 501 | margin: 0; 502 | text-transform: none; 503 | } 504 | 505 | div.swagger-ui-wrap input[type="text"]{ 506 | padding: 2px; 507 | width: 220px; 508 | } 509 | 510 | div.swagger-ui-wrap form{ 511 | margin: 0; 512 | } 513 | 514 | div.swagger-ui-wrap ul{ 515 | margin: 0; 516 | } 517 | 518 | div.footer{ 519 | display: none; 520 | } 521 | 522 | #message-bar{ 523 | min-height: inherit; 524 | padding: 0; 525 | } 526 | 527 | 528 | 529 | @media print, screen and (max-width: 800px) { 530 | .wrapper { 531 | width: 100%; 532 | } 533 | } 534 | 535 | 536 | 537 | @media print, screen and (max-width: 720px) { 538 | body { 539 | word-wrap:break-word; 540 | } 541 | 542 | pre, code { 543 | word-wrap:normal; 544 | } 545 | } 546 | 547 | @media print, screen and (max-width: 480px) { 548 | body { 549 | padding:15px; 550 | } 551 | 552 | .menu li { 553 | float: none; 554 | } 555 | 556 | .menu li a { 557 | border: 0; 558 | padding-left: 0em; 559 | padding-top: 0.4em; 560 | } 561 | 562 | .menu .lastItem{ 563 | border: none; 564 | } 565 | 566 | .services { 567 | width: 15em; 568 | } 569 | 570 | input[type="text"]{ 571 | width: 200px; 572 | } 573 | 574 | 575 | } 576 | 577 | @media print { 578 | body { 579 | padding:0.4in; 580 | font-size:12pt; 581 | color:#444; 582 | } 583 | } -------------------------------------------------------------------------------- /public/javascript/site.js: -------------------------------------------------------------------------------- 1 | 2 | window.onload = function(){ 3 | //getJSON('../sitenames/?callback=displaySites') 4 | getJSON('../maps/?ownershiptype=individual&callback=displayIndividual'); 5 | getJSON('../maps/?ownershiptype=group&callback=displayGrouped'); 6 | } 7 | 8 | 9 | function displayIndividual( sites ){ 10 | displaySites( getNamesList(sites), 'support-single', 'Maps - for individual profile ownership'); 11 | } 12 | 13 | 14 | function displayGrouped( sites ){ 15 | displaySites( getNamesList(sites), 'support-group', 'Maps - for group profile ownership'); 16 | } 17 | 18 | 19 | 20 | function getNamesList( sites ){ 21 | var names = []; 22 | var i = sites.length; 23 | var x = 0; 24 | while (x < i) { 25 | names.push(sites[x].name) 26 | x++; 27 | } 28 | return names; 29 | } 30 | 31 | 32 | function displaySites( sites, elementID, title ){ 33 | var h1 = document.createElement('h1'); 34 | h1.className="entry-title"; 35 | h1.appendChild(document.createTextNode(sites.length + ' ' + title)) 36 | 37 | var ul1 = document.createElement('ul'); 38 | var ul2 = document.createElement('ul'); 39 | var ul3 = document.createElement('ul'); 40 | ul1.setAttribute('class', 'services'); 41 | ul2.setAttribute('class', 'services'); 42 | ul3.setAttribute('class', 'services'); 43 | 44 | var third = Math.round(sites.length / 3); 45 | 46 | for (var x = 0; x < sites.length; x++) { 47 | 48 | // lowercase replace spaces, dots and dashes 49 | sites[x] = sites[x].replace(/\s+/g,'').replace('.','').replace(/-/g,'').toLowerCase(); 50 | var li = document.createElement('li'); 51 | var span = document.createElement('span'); 52 | span.setAttribute('class', 'sprite-icons-' + sites[x]); 53 | li.appendChild(span); 54 | li.appendChild(document.createTextNode(sites[x])) 55 | if(x < third){ 56 | ul1.appendChild(li); 57 | }else if(x < (2 * third)){ 58 | ul2.appendChild(li); 59 | }else{ 60 | ul3.appendChild(li); 61 | } 62 | 63 | } 64 | var support = document.getElementById(elementID); 65 | support.appendChild(h1); 66 | 67 | support.appendChild(ul1); 68 | support.appendChild(ul2); 69 | support.appendChild(ul3); 70 | 71 | 72 | } 73 | 74 | 75 | function getJSON(url) { 76 | script = document.createElement("script"); 77 | script.setAttribute("type", "text/javascript"); 78 | /* if (url.indexOf('?') > -1) 79 | url += '&'; 80 | else 81 | url += '?'; 82 | url += 'rand=' + Math.random();*/ 83 | script.setAttribute("src", url); 84 | document.getElementsByTagName('head')[0].appendChild(script); 85 | }; 86 | 87 | 88 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | <!doctype html> 3 | <html lang="en-GB"> 4 | <head> 5 | <meta charset="UTF-8"> 6 | <title>error 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |

{{}}

15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @cackhanded forgot to turn off sync to twitter while while testing, put it down to cold - now swearing a lot at computer as if it is its fault 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 | Replied to a item on twitter 27 |
28 | 29 |
30 | 31 | 32 |
33 | 34 | 35 |

@cackhanded forgot to turn off sync to twitter while while testing, put it down to cold - now swearing a lot at computer as if it is its fault

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ‐ On twitter 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | 62 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /templates/helpers/numberformat.js: -------------------------------------------------------------------------------- 1 | // Handlebars helper 2 | // takes a number 3456 and return 3,456 3 | 4 | module.exports = function(number) { 5 | if(number){ 6 | number = number.toString(); 7 | return number.replace(/\B(?=(\d{3})+(?!\d))/g, ","); 8 | }else{ 9 | return number; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /templates/swagger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{{title}}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |

{{{title}}}

56 |
57 | 58 |
59 |

API

60 |
61 |
62 |
63 | 64 |
65 |

Documentation

66 | {{{markdown}}} 67 |
68 | 69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /templates/withPartials/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/withPartials/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/withPartials/header.html: -------------------------------------------------------------------------------- 1 |
2 |

{{{title}}}

3 | 11 |
-------------------------------------------------------------------------------- /test/maths-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | assert = chai.assert, 5 | maths = require('../lib/maths'); 6 | 7 | // units tests math.js 8 | 9 | describe('maths', function(){ 10 | 11 | 12 | it('should add numbers together', function(done){ 13 | var options = { 14 | a: 5, 15 | b: 5 16 | }; 17 | maths.add(options, function(error, result){ 18 | assert.equal(result, 10, '5 + 5 should = 10'); 19 | assert.equal(error, null, '5 + 5 should = 10 without error'); 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should capture type errors', function(done){ 25 | var options = { 26 | a: 'text', 27 | b: 5 28 | }; 29 | maths.add(options, function(error, result){ 30 | assert.equal(result, null, 'if input is not a number return null'); 31 | assert.equal(error, 'The one of the two numbers was not provided', 'if a input is not a number throw error'); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should capture missing input errors', function(done){ 37 | var options = { 38 | a: '5' 39 | }; 40 | maths.add(options, function(error, result){ 41 | assert.equal(result, null, 'if we are missing input is not a number return null'); 42 | assert.equal(error, 'The one of the two numbers was not provided', 'if we are missing input throw error'); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should subtract numbers', function(done){ 48 | var options = { 49 | a: 10, 50 | b: 5 51 | }; 52 | maths.subtract(options, function(error, result){ 53 | assert.equal(result, 5, '10 - 5 should = 5'); 54 | assert.equal(error, null, '10 - 5 should = 5 without error'); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('should divide numbers', function(done){ 60 | var options = { 61 | a: 10, 62 | b: 5 63 | }; 64 | maths.divide(options, function(error, result){ 65 | assert.equal(result, 2, '10 / 5 should = 2'); 66 | assert.equal(error, null, '10 / 5 should = 2 without error'); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should divide capture divide by zero errors', function(done){ 72 | var options = { 73 | a: 10, 74 | b: 0 75 | }; 76 | maths.divide(options, function(error, result){ 77 | assert.equal(result, null, 'should return null for dividing by zero error'); 78 | assert.equal(error, 'One of the supplied numbers is set zero. You cannot divide by zero.', 'should throw an error for dividing by zero'); 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should multiple numbers', function(done){ 84 | var options = { 85 | a: 10, 86 | b: 5 87 | }; 88 | maths.multiple(options, function(error, result){ 89 | assert.equal(result, 50, '10 * 5 should = 50'); 90 | assert.equal(error, null, '10 * 5 should = 50 without error'); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('should multiple numbers', function(done){ 96 | var options = { 97 | a: 10, 98 | b: 5 99 | }; 100 | maths.multiple(options, function(error, result){ 101 | assert.equal(result, 50, '10 * 5 should = 50'); 102 | assert.equal(error, null, '10 * 5 should = 50 without error'); 103 | done(); 104 | }); 105 | }); 106 | 107 | }); -------------------------------------------------------------------------------- /test/sums-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hapi = require('hapi'), 4 | assert = require('assert'), 5 | chai = require('chai'), 6 | assert = chai.assert, 7 | routes = require('../lib/routes.js'); 8 | 9 | 10 | // integration tests for API endpoint 11 | 12 | 13 | // setup server with firing up - use inject instead 14 | var server = new hapi.Server(); 15 | server.route(routes.routes); 16 | 17 | 18 | // parseurls endpoint test 19 | describe('add endpoint', function(){ 20 | it('add - should add two numbers together', function(done){ 21 | server.inject({method: 'GET', url: '/sum/add/5/5'}, function (res) { 22 | assert.deepEqual( 23 | { 24 | 'equals': 10 25 | }, JSON.parse(res.payload)); 26 | done(); 27 | }); 28 | }); 29 | 30 | 31 | it('add - should error if a string is passed', function(done){ 32 | server.inject({method: 'GET', url: '/sum/add/100/x'}, function (res) { 33 | assert.deepEqual( 34 | { 35 | 'code': 400, 36 | 'error': 'Bad Request', 37 | 'message': 'the value of b must be a number', 38 | 'validation': { 39 | 'source': 'path', 40 | 'keys': [ 41 | 'b' 42 | ] 43 | } 44 | }, JSON.parse(res.payload)); 45 | done(); 46 | }); 47 | }); 48 | 49 | }); --------------------------------------------------------------------------------