diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c13c5f6 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/.gitignore b/.gitignore index 123ae94..8d90772 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules + +lib diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..05375f6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +src +example diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..5da7ef4 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node example/app.js diff --git a/example/amp.css b/example/amp.css new file mode 100644 index 0000000..cc70404 --- /dev/null +++ b/example/amp.css @@ -0,0 +1,185 @@ +/*! + * Writ v1.0.2 + * + * Copyright © 2015, Curtis McEnroe + * + * https://cmcenroe.me/writ/LICENSE (ISC) + */ + +/* Fonts, sizes & vertical rhythm */ + +html { + font-family: Palatino, Georgia, Lucida Bright, Book Antiqua, serif; + font-size: 16px; + line-height: 1.5rem; +} + +code, pre, samp, kbd { + font-family: Consolas, Liberation Mono, Menlo, Courier, monospace; + font-size: 0.833rem; +} + +kbd { font-weight: bold; } +h1, h2, h3, h4, h5, h6, th { font-weight: normal; } + +/* Minor third */ +h1 { font-size: 2.488em; } +h2 { font-size: 2.074em; } +h3 { font-size: 1.728em; } +h4 { font-size: 1.44em; } +h5 { font-size: 1.2em; } +h6 { font-size: 1em; } +small { font-size: 0.833em; } + +h1, h2, h3 { line-height: 3rem; } + +p, ul, ol, dl, table, blockquote, pre, h1, h2, h3, h4, h5, h6 { + margin: 1.5rem 0 0; +} +ul ul, ol ol, ul ol, ol ul { margin: 0; } + +hr { + margin: 0; + border: none; + padding: 1.5rem 0 0; +} + +/* Accounting for borders */ +table { + line-height: calc(1.5rem - 1px); + margin-bottom: -1px; +} +pre { + margin-top: calc(1.5rem - 1px); + margin-bottom: -1px; +} + +/* Colors */ + +body { color: #222; } +code, pre, samp, kbd { color: #111; } +a, nav a:visited { color: #00e; } +a:visited { color: #60b; } +mark { color: inherit; } + +code, pre, samp, thead, tfoot { background-color: rgba(0, 0, 0, 0.05); } +mark { background-color: #fe0; } + +main aside, blockquote, ins { border: solid rgba(0, 0, 0, 0.05); } +pre, code, samp { border: solid rgba(0, 0, 0, 0.1); } +th, td { border: solid #dbdbdb; } + +/* Layout */ + +.wrapper-main { + width: 80%; + margin: auto; +} + +body { margin: 1.5rem 1ch; } + +body > header { text-align: center; } + +main, body > footer { + display: block; /* Just in case */ + max-width: 78ch; + margin: auto; +} + +main figure, main aside { + float: right; + margin: 1.5rem 0 0 1ch; +} + +main aside { + max-width: 26ch; + border-width: 0 0 0 0.5ch; + padding: 0 0 0 0.5ch; +} + +/* Copy blocks */ + +blockquote { + margin-right: 3ch; + margin-left: 1.5ch; + border-width: 0 0 0 0.5ch; + padding: 0 0 0 1ch; +} + +pre { + border-width: 1px; + border-radius: 2px; + padding: 0 0.5ch; + overflow-x: auto; +} +pre code { + border: none; + padding: 0; + background-color: transparent; + white-space: inherit; +} + +img { max-width: 100%; } + +/* Lists */ + +ul, ol, dd { padding: 0 0 0 3ch; } +dd { margin: 0; } + +ul > li { list-style-type: disc; } +li ul > li { list-style-type: circle; } +li li ul > li { list-style-type: square; } + +ol > li { list-style-type: decimal; } +li ol > li { list-style-type: lower-roman; } +li li ol > li { list-style-type: lower-alpha; } + +nav ul { + padding: 0; + list-style-type: none; +} +nav ul li { + display: inline; + padding-left: 1ch; + white-space: nowrap; +} +nav ul li:first-child { padding-left: 0; } + +/* Tables */ + +table { + width: 100%; + border-collapse: collapse; + overflow-x: auto; +} + +th, td { + border-width: 1px; + padding: 0 0.5ch; +} + +/* Copy inline */ + +a { text-decoration: none; } + +sup, sub { + font-size: 0.75em; + line-height: 1em; +} + +ins { + border-width: 1px; + padding: 1px; + text-decoration: none; +} + +mark { + padding: 1px; +} + +code, samp { + border-width: 1px; + border-radius: 2px; + padding: 0.1em 0.2em; + white-space: nowrap; +} diff --git a/example/app.js b/example/app.js new file mode 100644 index 0000000..778dad1 --- /dev/null +++ b/example/app.js @@ -0,0 +1,40 @@ +var fs = require('fs'); +var path = require('path'); + +var Hapi = require('hapi'); +var Inert = require('inert'); + +var ampl = require('ampl'); + +var css = fs.readFileSync(path.join(__dirname, './amp.css')); + +var port = process.env.PORT || 3000; +var server = new Hapi.Server(); + +server.connection({ port: port }); + +server.register(Inert, function (err) { + server.route( { + method: 'GET', + path: '/{param*}', + handler: { + directory: { + path: path.join(__dirname, 'public'), + listing: true + } + } + }); + server.route({ + method: 'POST', + path: '/convert', + handler: function(request, reply) { + ampl.parse(request.payload.md, css, function(ampHtml) { + return reply(ampHtml); + }); + } + }) +}); + +server.start(function(){ + console.log('server listening on port', port); +}); diff --git a/example/public/custom.css b/example/public/custom.css new file mode 100644 index 0000000..c423dfb --- /dev/null +++ b/example/public/custom.css @@ -0,0 +1,5 @@ +.big { + width: 80%; + min-height: 50vh; + text-align: left; +} diff --git a/example/public/index.html b/example/public/index.html new file mode 100644 index 0000000..8385ad5 --- /dev/null +++ b/example/public/index.html @@ -0,0 +1,16 @@ + + + + + ampl demo + + + +

ampl Demo

+
+ + +
+ + + diff --git a/package.json b/package.json index de3a1e5..c49291d 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "ampl", - "version": "1.0.0", + "version": "1.0.1", "description": "ampl converts your markdown files into AMP-compliant html so they can be cached by google for the fastest possible page load", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "babel-node test", + "watch-build": "babel src -d lib -w", + "build": "babel src -d lib", + "prepublish" : "npm run build" }, "repository": { "type": "git", @@ -20,5 +23,18 @@ "bugs": { "url": "https://github.com/nelsonic/ampl/issues" }, - "homepage": "https://github.com/nelsonic/ampl#readme" + "homepage": "https://github.com/nelsonic/ampl#readme", + "devDependencies": { + "babel-cli": "^6.4.0", + "babel-preset-es2015": "^6.3.13", + "tape": "^4.4.0" + }, + "dependencies": { + "ampl": "^1.0.0", + "hapi": "^12.1.0", + "inert": "^3.2.0", + "htmlparser2": "^3.9.0", + "image-size": "^0.4.0", + "remarkable": "^1.6.1" + } } diff --git a/src/ampl.js b/src/ampl.js new file mode 100644 index 0000000..9aa99bd --- /dev/null +++ b/src/ampl.js @@ -0,0 +1,99 @@ +var Remarkable = require('remarkable'); +var htmlparser = require("htmlparser2"); + +var remarkable = new Remarkable('full'); + +import { getDims } from './imageDims.js'; +import { html2Amp } from './templates.js'; + +export function parse(mdString, css, callback) { + var htmlRaw = remarkable.render(mdString); + var htmlAmp = html2Amp(css, htmlRaw) + parseHtml(htmlAmp, function(htmlAmp, data) { + getDims(data.urls, function(dimensions) { + // todo remove while loop + var i = 0; + var imageTagRegex = /( Object.keys(attribs).map(attribKey => ( + attribs[attribKey].length === 0 ? + attribKey : + `${attribKey}='${attribs[attribKey]}'` +)).join(' '); + +var parseRules = [ + (urls => ({ + label: "urls", + target: "img", + onOpenTag: attribs => urls.push(attribs.src), + getResults: () => urls + }))([]), + { + label: "wrapper-main", + target: "body", + onCloseTag: text => ` +
+ ${text} +
+ ` + } +] + +var parseHtml = function(html, callback) { + var urls = []; + var tagStack = [{text: ""}]; + var parser = new htmlparser.Parser({ + onopentag: function(name, attribs) { + tagStack.push({name, attribs, + text: "" + }); + parseRules.forEach(rule => { + if (rule.onOpenTag && !(rule.target && rule.target !== name)) { + rule.onOpenTag(attribs); + } + }); + if (name === 'img') { + urls.push(attribs.src); + } + }, + ontext: function(text) { + tagStack[tagStack.length-1].text += text; + }, + onclosetag: function(name) { + var tag = tagStack.pop(); + var text = + `<${tag.name} ${attribStr(tag.attribs)}>${tag.text}`; + parseRules.forEach(rule => { + if (rule.onCloseTag && !(rule.target && rule.target !== name)) { + text = rule.onCloseTag(text); + } + }); + tagStack[tagStack.length-1].text += text; + }, + onend: function() { + var ruleOutput = parseRules.reduce((data, rule) => { + if (typeof rule.getResults === 'function') { + data[rule.label] = rule.getResults(); + } + return data; + }, {}); + callback(tagStack[0].text, ruleOutput); + } + }); + parser.write(html); + parser.end(); +}; diff --git a/src/imageDims.js b/src/imageDims.js new file mode 100644 index 0000000..732d2a8 --- /dev/null +++ b/src/imageDims.js @@ -0,0 +1,45 @@ +var assert = require('assert'); +var sizeOf = require('image-size'); +var http = require('http'); +var https = require('https'); +var url = require('url'); + +export var getDims = (imageUrls, callback) => { + var totalLinks = imageUrls.length; + if (totalLinks === 0) { + setTimeout(function() { + callback([]); + }, 0); + } else { + var dimsFetched = 0; + var dimsArray = []; + imageUrls.forEach(function(imageUrl, index) { + var options = url.parse(imageUrl); + var proto = options.protocol === 'https:' ? https : http; + // if (options.protocol === 'https:') options.protocol = 'http:'; + var request = proto.request(options, function(response) { + getBody(response, function(body) { + next(sizeOf(body), index); + }); + }); + request.end(); + }); + } + var next = function(dims, index) { + dimsFetched += 1; + dimsArray[index] = dims; + if (dimsFetched === totalLinks) { + callback(dimsArray); + } + } +} + +var getBody = function(response, callback) { + var chunks = []; + response.on('data', function(chunk) { + chunks.push(chunk); + }); + response.on('end', function() { + callback(Buffer.concat(chunks)); + }); +}; diff --git a/src/templates.js b/src/templates.js new file mode 100644 index 0000000..5f90494 --- /dev/null +++ b/src/templates.js @@ -0,0 +1,35 @@ +var createAmpHeader = style => ` + + + + + + + + + + Index + +`; + +var createLinkHtml = link => ` +${link} +`; + +export var html2Amp = (style, contentHTML) => ` + ${createAmpHeader(style)} + + ${contentHTML} + + +`; +// +// export var buildIndex = (style, linksArr) => ` +// ${createAmpHeader(style)} +// +// ${contentHTMLArr.map(createLinkHtml)} +// +// +// `; diff --git a/test/imageDims.test.js b/test/imageDims.test.js new file mode 100644 index 0000000..ac0d505 --- /dev/null +++ b/test/imageDims.test.js @@ -0,0 +1,15 @@ +var test = require('tape'); + +import { parse } from '../src/ampl.js'; + +test('image dimensions found', t=> { + var testMd = '![image](https://cloud.githubusercontent.com/assets/12845233/12449931/68f11832-bf78-11e5-87ff-d34e6a3487c6.png)' + parse(testMd, '', function(ampHtml) { + t.ok(ampHtml.indexOf(`width="439"`) !== -1, 'image has correct width'); + t.ok(ampHtml.indexOf(`height="20"`) !== -1, 'image has correct height'); + t.end(); + }); +}) + + +// this is so meta ... diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..0698604 --- /dev/null +++ b/test/index.js @@ -0,0 +1,2 @@ +require('./templates.test.js'); +require('./imageDims.test.js'); diff --git a/test/templates.test.js b/test/templates.test.js new file mode 100644 index 0000000..e5e9565 --- /dev/null +++ b/test/templates.test.js @@ -0,0 +1,19 @@ +var test = require('tape'); + + +import { html2Amp } from '../src/templates.js' + +test("template.js", t => { + var style = "here is some styling"; + var html = ""; + var ampHtml = html2Amp(style, html); + t.ok(ampHtml.indexOf(html) !== -1, "html inserted into amp template"); + t.ok(ampHtml.indexOf(style) !== -1, "style inserted into amp template"); + t.ok(ampHtml.split('\n')[3].split(' ')[3].indexOf('amp') === 0, + 'html tag contains amp keyword'); + t.end(); +}); + +// test("template contains html amp tag" t => { +// +// });