diff --git a/Gemfile b/Gemfile index 3352c69..2cd3481 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,8 @@ gem 'rake' gem 'jekyll' gem 'jekyll-assets' +gem 'mini_magick' + gem 'octopress', '~> 3.0.0.rc.14' gem 'octopress-deploy', '~> 1.0.0.rc.11' diff --git a/Gemfile.lock b/Gemfile.lock index 85346bd..46e8a93 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,6 +57,8 @@ GEM rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) mercenary (0.3.4) + mini_magick (3.8.1) + subexec (~> 0.2.1) multi_json (1.10.1) octopress (3.0.0.rc.14) jekyll (~> 2.0) @@ -89,6 +91,7 @@ GEM sprockets-sass (1.2.0) sprockets (~> 2.0) tilt (~> 1.1) + subexec (0.2.3) thor (0.19.1) tilt (1.4.1) timers (4.0.1) @@ -110,6 +113,7 @@ DEPENDENCIES jekyll jekyll-assets kramdown (~> 1.3.3) + mini_magick octopress (~> 3.0.0.rc.14) octopress-deploy (~> 1.0.0.rc.11) rake diff --git a/_assets/images/pages/sale/DSCF1694.jpg b/_assets/images/pages/sale/DSCF1694.jpg new file mode 100644 index 0000000..a213479 Binary files /dev/null and b/_assets/images/pages/sale/DSCF1694.jpg differ diff --git a/_assets/javascripts/lib/picturefill.js b/_assets/javascripts/lib/picturefill.js new file mode 100644 index 0000000..9e5ccda --- /dev/null +++ b/_assets/javascripts/lib/picturefill.js @@ -0,0 +1,633 @@ +/*! Picturefill - v2.1.0 - 2014-08-20 +* http://scottjehl.github.io/picturefill +* Copyright (c) 2014 https://github.com/scottjehl/picturefill/blob/master/Authors.txt; Licensed MIT */ +/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. Dual MIT/BSD license */ + +window.matchMedia || (window.matchMedia = function() { + "use strict"; + + // For browsers that support matchMedium api such as IE 9 and webkit + var styleMedia = (window.styleMedia || window.media); + + // For those that don't support matchMedium + if (!styleMedia) { + var style = document.createElement('style'), + script = document.getElementsByTagName('script')[0], + info = null; + + style.type = 'text/css'; + style.id = 'matchmediajs-test'; + + script.parentNode.insertBefore(style, script); + + // 'style.currentStyle' is used by IE <= 8 and 'window.getComputedStyle' for all other browsers + info = ('getComputedStyle' in window) && window.getComputedStyle(style, null) || style.currentStyle; + + styleMedia = { + matchMedium: function(media) { + var text = '@media ' + media + '{ #matchmediajs-test { width: 1px; } }'; + + // 'style.styleSheet' is used by IE <= 8 and 'style.textContent' for all other browsers + if (style.styleSheet) { + style.styleSheet.cssText = text; + } else { + style.textContent = text; + } + + // Test if media query is true or false + return info.width === '1px'; + } + }; + } + + return function(media) { + return { + matches: styleMedia.matchMedium(media || 'all'), + media: media || 'all' + }; + }; +}()); +/*! Picturefill - Responsive Images that work today. +* Author: Scott Jehl, Filament Group, 2012 ( new proposal implemented by Shawn Jansepar ) +* License: MIT/GPLv2 +* Spec: http://picture.responsiveimages.org/ +*/ +(function( w, doc ) { + // Enable strict mode + "use strict"; + + // If picture is supported, well, that's awesome. Let's get outta here... + if ( w.HTMLPictureElement ) { + w.picturefill = function() { }; + return; + } + + // HTML shim|v it for old IE (IE9 will still need the HTML video tag workaround) + doc.createElement( "picture" ); + + // local object for method references and testing exposure + var pf = {}; + + // namespace + pf.ns = "picturefill"; + + // srcset support test + pf.srcsetSupported = "srcset" in doc.createElement( "img" ); + pf.sizesSupported = w.HTMLImageElement.sizes; + + // just a string trim workaround + pf.trim = function( str ) { + return str.trim ? str.trim() : str.replace( /^\s+|\s+$/g, "" ); + }; + + // just a string endsWith workaround + pf.endsWith = function( str, suffix ) { + return str.endsWith ? str.endsWith( suffix ) : str.indexOf( suffix, str.length - suffix.length ) !== -1; + }; + + /** + * Shortcut method for https://w3c.github.io/webappsec/specs/mixedcontent/#restricts-mixed-content ( for easy overriding in tests ) + */ + pf.restrictsMixedContent = function() { + return w.location.protocol === "https:"; + }; + /** + * Shortcut method for matchMedia ( for easy overriding in tests ) + */ + pf.matchesMedia = function( media ) { + return w.matchMedia && w.matchMedia( media ).matches; + }; + + /** + * Shortcut method for `devicePixelRatio` ( for easy overriding in tests ) + */ + pf.getDpr = function() { + return ( w.devicePixelRatio || 1 ); + }; + + /** + * Get width in css pixel value from a "length" value + * http://dev.w3.org/csswg/css-values-3/#length-value + */ + pf.getWidthFromLength = function( length ) { + // If a length is specified and doesn’t contain a percentage, and it is greater than 0 or using `calc`, use it. Else, use the `100vw` default. + length = length && length.indexOf( "%" ) > -1 === false && ( parseFloat( length ) > 0 || length.indexOf( "calc(" ) > -1 ) ? length : "100vw"; + /** + * If length is specified in `vw` units, use `%` instead since the div we’re measuring + * is injected at the top of the document. + * + * TODO: maybe we should put this behind a feature test for `vw`? + */ + length = length.replace( "vw", "%" ); + + // Create a cached element for getting length value widths + if ( !pf.lengthEl ) { + pf.lengthEl = doc.createElement( "div" ); + doc.documentElement.insertBefore( pf.lengthEl, doc.documentElement.firstChild ); + } + + // Positioning styles help prevent padding/margin/width on `html` from throwing calculations off. + pf.lengthEl.style.cssText = "position: absolute; left: 0; width: " + length + ";"; + + if ( pf.lengthEl.offsetWidth <= 0 ) { + // Something has gone wrong. `calc()` is in use and unsupported, most likely. Default to `100vw` (`100%`, for broader support.): + pf.lengthEl.style.cssText = "width: 100%;"; + } + + return pf.lengthEl.offsetWidth; + }; + + // container of supported mime types that one might need to qualify before using + pf.types = {}; + + // Add support for standard mime types. + pf.types["image/jpeg"] = true; + pf.types["image/gif"] = true; + pf.types["image/png"] = true; + + // test svg support + pf.types[ "image/svg+xml" ] = doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1"); + + // test webp support, only when the markup calls for it + pf.types[ "image/webp" ] = function() { + // based on Modernizr's lossless img-webp test + // note: asynchronous + var img = new w.Image(), + type = "image/webp"; + + img.onerror = function() { + pf.types[ type ] = false; + picturefill(); + }; + img.onload = function() { + pf.types[ type ] = img.width === 1; + picturefill(); + }; + img.src = "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="; + }; + + /** + * Takes a source element and checks if its type attribute is present and if so, supported + * Note: for type tests that require a async logic, + * you can define them as a function that'll run only if that type needs to be tested. Just make the test function call picturefill again when it is complete. + * see the async webp test above for example + */ + pf.verifyTypeSupport = function( source ) { + var type = source.getAttribute( "type" ); + // if type attribute exists, return test result, otherwise return true + if ( type === null || type === "" ) { + return true; + } else { + // if the type test is a function, run it and return "pending" status. The function will rerun picturefill on pending elements once finished. + if ( typeof( pf.types[ type ] ) === "function" ) { + pf.types[ type ](); + return "pending"; + } else { + return pf.types[ type ]; + } + } + }; + + /** + * Parses an individual `size` and returns the length, and optional media query + */ + pf.parseSize = function( sourceSizeStr ) { + var match = /(\([^)]+\))?\s*(.+)/g.exec( sourceSizeStr ); + return { + media: match && match[1], + length: match && match[2] + }; + }; + + /** + * Takes a string of sizes and returns the width in pixels as a number + */ + pf.findWidthFromSourceSize = function( sourceSizeListStr ) { + // Split up source size list, ie ( max-width: 30em ) 100%, ( max-width: 50em ) 50%, 33% + // or (min-width:30em) calc(30% - 15px) + var sourceSizeList = pf.trim( sourceSizeListStr ).split( /\s*,\s*/ ), + winningLength; + + for ( var i = 0, len = sourceSizeList.length; i < len; i++ ) { + // Match ? length, ie ( min-width: 50em ) 100% + var sourceSize = sourceSizeList[ i ], + // Split "( min-width: 50em ) 100%" into separate strings + parsedSize = pf.parseSize( sourceSize ), + length = parsedSize.length, + media = parsedSize.media; + + if ( !length ) { + continue; + } + if ( !media || pf.matchesMedia( media ) ) { + // if there is no media query or it matches, choose this as our winning length + // and end algorithm + winningLength = length; + break; + } + } + + // pass the length to a method that can properly determine length + // in pixels based on these formats: http://dev.w3.org/csswg/css-values-3/#length-value + return pf.getWidthFromLength( winningLength ); + }; + + pf.parseSrcset = function( srcset ) { + /** + * A lot of this was pulled from Boris Smus’ parser for the now-defunct WHATWG `srcset` + * https://github.com/borismus/srcset-polyfill/blob/master/js/srcset-info.js + * + * 1. Let input (`srcset`) be the value passed to this algorithm. + * 2. Let position be a pointer into input, initially pointing at the start of the string. + * 3. Let raw candidates be an initially empty ordered list of URLs with associated + * unparsed descriptors. The order of entries in the list is the order in which entries + * are added to the list. + */ + var candidates = []; + + while ( srcset !== "" ) { + srcset = srcset.replace(/^\s+/g,""); + + // 5. Collect a sequence of characters that are not space characters, and let that be url. + var pos = srcset.search(/\s/g), + url, descriptor = null; + + if ( pos !== -1 ) { + url = srcset.slice( 0, pos ); + + var last = url[ url.length - 1 ]; + + // 6. If url ends with a U+002C COMMA character (,), remove that character from url + // and let descriptors be the empty string. Otherwise, follow these substeps + // 6.1. If url is empty, then jump to the step labeled descriptor parser. + + if ( last === "," || url === "" ) { + url = url.replace(/,+$/, ""); + descriptor = ""; + } + srcset = srcset.slice( pos + 1 ); + + // 6.2. Collect a sequence of characters that are not U+002C COMMA characters (,), and + // let that be descriptors. + if ( descriptor === null ) { + var descpos = srcset.indexOf(","); + if ( descpos !== -1 ) { + descriptor = srcset.slice( 0, descpos ); + srcset = srcset.slice( descpos + 1 ); + } else { + descriptor = srcset; + srcset = ""; + } + } + } else { + url = srcset; + srcset = ""; + } + + // 7. Add url to raw candidates, associated with descriptors. + if ( url || descriptor ) { + candidates.push({ + url: url, + descriptor: descriptor + }); + } + } + return candidates; + }; + + pf.parseDescriptor = function( descriptor, sizesattr ) { + // 11. Descriptor parser: Let candidates be an initially empty source set. The order of entries in the list + // is the order in which entries are added to the list. + var sizes = sizesattr || "100vw", + sizeDescriptor = descriptor && descriptor.replace(/(^\s+|\s+$)/g, ""), + widthInCssPixels = pf.findWidthFromSourceSize( sizes ), + resCandidate; + + if ( sizeDescriptor ) { + var splitDescriptor = sizeDescriptor.split(" "); + + for (var i = splitDescriptor.length + 1; i >= 0; i--) { + if ( splitDescriptor[ i ] !== undefined ) { + var curr = splitDescriptor[ i ], + lastchar = curr && curr.slice( curr.length - 1 ); + + if ( ( lastchar === "h" || lastchar === "w" ) && !pf.sizesSupported ) { + resCandidate = parseFloat( ( parseInt( curr, 10 ) / widthInCssPixels ) ); + } else if ( lastchar === "x" ) { + var res = curr && parseFloat( curr, 10 ); + resCandidate = res && !isNaN( res ) ? res : 1; + } + } + } + } + return resCandidate || 1; + }; + + /** + * Takes a srcset in the form of url/ + * ex. "images/pic-medium.png 1x, images/pic-medium-2x.png 2x" or + * "images/pic-medium.png 400w, images/pic-medium-2x.png 800w" or + * "images/pic-small.png" + * Get an array of image candidates in the form of + * {url: "/foo/bar.png", resolution: 1} + * where resolution is http://dev.w3.org/csswg/css-values-3/#resolution-value + * If sizes is specified, resolution is calculated + */ + pf.getCandidatesFromSourceSet = function( srcset, sizes ) { + var candidates = pf.parseSrcset( srcset ), + formattedCandidates = []; + + for ( var i = 0, len = candidates.length; i < len; i++ ) { + var candidate = candidates[ i ]; + + formattedCandidates.push({ + url: candidate.url, + resolution: pf.parseDescriptor( candidate.descriptor, sizes ) + }); + } + return formattedCandidates; + }; + + /* + * if it's an img element and it has a srcset property, + * we need to remove the attribute so we can manipulate src + * (the property's existence infers native srcset support, and a srcset-supporting browser will prioritize srcset's value over our winning picture candidate) + * this moves srcset's value to memory for later use and removes the attr + */ + pf.dodgeSrcset = function( img ) { + if ( img.srcset ) { + img[ pf.ns ].srcset = img.srcset; + img.removeAttribute( "srcset" ); + } + }; + + /* + * Accept a source or img element and process its srcset and sizes attrs + */ + pf.processSourceSet = function( el ) { + var srcset = el.getAttribute( "srcset" ), + sizes = el.getAttribute( "sizes" ), + candidates = []; + + // if it's an img element, use the cached srcset property (defined or not) + if ( el.nodeName.toUpperCase() === "IMG" && el[ pf.ns ] && el[ pf.ns ].srcset ) { + srcset = el[ pf.ns ].srcset; + } + + if ( srcset ) { + candidates = pf.getCandidatesFromSourceSet( srcset, sizes ); + } + return candidates; + }; + + pf.applyBestCandidate = function( candidates, picImg ) { + var candidate, + length, + bestCandidate; + + candidates.sort( pf.ascendingSort ); + + length = candidates.length; + bestCandidate = candidates[ length - 1 ]; + + for ( var i = 0; i < length; i++ ) { + candidate = candidates[ i ]; + if ( candidate.resolution >= pf.getDpr() ) { + bestCandidate = candidate; + break; + } + } + + if ( bestCandidate && !pf.endsWith( picImg.src, bestCandidate.url ) ) { + if ( pf.restrictsMixedContent() && bestCandidate.url.substr(0, "http:".length).toLowerCase() === "http:" ) { + if ( typeof console !== undefined ) { + console.warn( "Blocked mixed content image " + bestCandidate.url ); + } + } else { + picImg.src = bestCandidate.url; + // currentSrc attribute and property to match + // http://picture.responsiveimages.org/#the-img-element + picImg.currentSrc = picImg.src; + } + } + }; + + pf.ascendingSort = function( a, b ) { + return a.resolution - b.resolution; + }; + + /* + * In IE9, elements get removed if they aren't children of + * video elements. Thus, we conditionally wrap source elements + * using + * and must account for that here by moving those source elements + * back into the picture element. + */ + pf.removeVideoShim = function( picture ) { + var videos = picture.getElementsByTagName( "video" ); + if ( videos.length ) { + var video = videos[ 0 ], + vsources = video.getElementsByTagName( "source" ); + while ( vsources.length ) { + picture.insertBefore( vsources[ 0 ], video ); + } + // Remove the video element once we're finished removing its children + video.parentNode.removeChild( video ); + } + }; + + /* + * Find all `img` elements, and add them to the candidate list if they have + * a `picture` parent, a `sizes` attribute in basic `srcset` supporting browsers, + * a `srcset` attribute at all, and they haven’t been evaluated already. + */ + pf.getAllElements = function() { + var elems = [], + imgs = doc.getElementsByTagName( "img" ); + + for ( var h = 0, len = imgs.length; h < len; h++ ) { + var currImg = imgs[ h ]; + + if ( currImg.parentNode.nodeName.toUpperCase() === "PICTURE" || + ( currImg.getAttribute( "srcset" ) !== null ) || currImg[ pf.ns ] && currImg[ pf.ns ].srcset !== null ) { + elems.push( currImg ); + } + } + return elems; + }; + + pf.getMatch = function( img, picture ) { + var sources = picture.childNodes, + match; + + // Go through each child, and if they have media queries, evaluate them + for ( var j = 0, slen = sources.length; j < slen; j++ ) { + var source = sources[ j ]; + + // ignore non-element nodes + if ( source.nodeType !== 1 ) { + continue; + } + + // Hitting the `img` element that started everything stops the search for `sources`. + // If no previous `source` matches, the `img` itself is evaluated later. + if ( source === img ) { + return match; + } + + // ignore non-`source` nodes + if ( source.nodeName.toUpperCase() !== "SOURCE" ) { + continue; + } + // if it's a source element that has the `src` property set, throw a warning in the console + if ( source.getAttribute( "src" ) !== null && typeof console !== undefined ){ + console.warn("The `src` attribute is invalid on `picture` `source` element; instead, use `srcset`."); + } + + var media = source.getAttribute( "media" ); + + // if source does not have a srcset attribute, skip + if ( !source.getAttribute( "srcset" ) ) { + continue; + } + + // if there's no media specified, OR w.matchMedia is supported + if ( ( !media || pf.matchesMedia( media ) ) ) { + var typeSupported = pf.verifyTypeSupport( source ); + + if ( typeSupported === true ) { + match = source; + break; + } else if ( typeSupported === "pending" ) { + return false; + } + } + } + + return match; + }; + + function picturefill( opt ) { + var elements, + element, + parent, + firstMatch, + candidates, + + options = opt || {}; + elements = options.elements || pf.getAllElements(); + + // Loop through all elements + for ( var i = 0, plen = elements.length; i < plen; i++ ) { + element = elements[ i ]; + parent = element.parentNode; + firstMatch = undefined; + candidates = undefined; + + // expando for caching data on the img + if ( !element[ pf.ns ] ) { + element[ pf.ns ] = {}; + } + + // if the element has already been evaluated, skip it + // unless `options.force` is set to true ( this, for example, + // is set to true when running `picturefill` on `resize` ). + if ( !options.reevaluate && element[ pf.ns ].evaluated ) { + continue; + } + + // if `img` is in a `picture` element + if ( parent.nodeName.toUpperCase() === "PICTURE" ) { + + // IE9 video workaround + pf.removeVideoShim( parent ); + + // return the first match which might undefined + // returns false if there is a pending source + // TODO the return type here is brutal, cleanup + firstMatch = pf.getMatch( element, parent ); + + // if any sources are pending in this picture due to async type test(s) + // remove the evaluated attr and skip for now ( the pending test will + // rerun picturefill on this element when complete) + if ( firstMatch === false ) { + continue; + } + } else { + firstMatch = undefined; + } + + // Cache and remove `srcset` if present and we’re going to be doing `picture`/`srcset`/`sizes` polyfilling to it. + if ( parent.nodeName.toUpperCase() === "PICTURE" || + ( element.srcset && !pf.srcsetSupported ) || + ( !pf.sizesSupported && ( element.srcset && element.srcset.indexOf("w") > -1 ) ) ) { + pf.dodgeSrcset( element ); + } + + if ( firstMatch ) { + candidates = pf.processSourceSet( firstMatch ); + pf.applyBestCandidate( candidates, element ); + } else { + // No sources matched, so we’re down to processing the inner `img` as a source. + candidates = pf.processSourceSet( element ); + + if ( element.srcset === undefined || element[ pf.ns ].srcset ) { + // Either `srcset` is completely unsupported, or we need to polyfill `sizes` functionality. + pf.applyBestCandidate( candidates, element ); + } // Else, resolution-only `srcset` is supported natively. + } + + // set evaluated to true to avoid unnecessary reparsing + element[ pf.ns ].evaluated = true; + } + } + + /** + * Sets up picture polyfill by polling the document and running + * the polyfill every 250ms until the document is ready. + * Also attaches picturefill on resize + */ + function runPicturefill() { + picturefill(); + var intervalId = setInterval( function() { + // When the document has finished loading, stop checking for new images + // https://github.com/ded/domready/blob/master/ready.js#L15 + picturefill(); + if ( /^loaded|^i|^c/.test( doc.readyState ) ) { + clearInterval( intervalId ); + return; + } + }, 250 ); + if ( w.addEventListener ) { + var resizeThrottle; + w.addEventListener( "resize", function() { + if (!w._picturefillWorking) { + w._picturefillWorking = true; + w.clearTimeout( resizeThrottle ); + resizeThrottle = w.setTimeout( function() { + picturefill({ reevaluate: true }); + w._picturefillWorking = false; + }, 60 ); + } + }, false ); + } + } + + runPicturefill(); + + /* expose methods for testing */ + picturefill._ = pf; + + /* expose picturefill */ + if ( typeof module === "object" && typeof module.exports === "object" ) { + // CommonJS, just export + module.exports = picturefill; + } else if ( typeof define === "function" && define.amd ){ + // AMD support + define( function() { return picturefill; } ); + } else if ( typeof w === "object" ) { + // If no AMD and we are in the browser, attach to window + w.picturefill = picturefill; + } + +} )( this, this.document ); diff --git a/_assets/javascripts/main.js b/_assets/javascripts/main.js new file mode 100644 index 0000000..b469236 --- /dev/null +++ b/_assets/javascripts/main.js @@ -0,0 +1 @@ +//= require lib/picturefill diff --git a/_config.yml b/_config.yml index b926701..664123b 100644 --- a/_config.yml +++ b/_config.yml @@ -26,15 +26,20 @@ assets: - _assets/images - _assets/fonts - _assets/icons + js_compressor: uglifier + css_compressor: sass -redcarpet: - extensions: - - hard_wrap - - no_intra_emphasis - - autolink - - strikethrough - - fenced_code_blocks - - smart +picture: + source: "_assets/images" + output: "pictures" + markup: "picture" + presets: + default: + ppi: [1, 2] + attr: + itemprop: "image" + source_default: + width: "800" kramdown: input: GFM diff --git a/_layouts/default.html b/_layouts/default.html index 6a3fe7c..17b48df 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -23,6 +23,7 @@ {{ 'styles' | stylesheet }} + {{ 'main' | javascript }} diff --git a/_plugins/picture_tag.rb b/_plugins/picture_tag.rb new file mode 100644 index 0000000..4e29b57 --- /dev/null +++ b/_plugins/picture_tag.rb @@ -0,0 +1,238 @@ +# Title: Jekyll Picture Tag +# Authors: Rob Wierzbowski : @robwierzbowski +# Justin Reese : @justinxreese +# Welch Canavan : @xiwcx +# +# Description: Easy responsive images for Jekyll. +# +# Download: https://github.com/robwierzbowski/jekyll-picture-tag +# Documentation: https://github.com/robwierzbowski/jekyll-picture-tag/readme.md +# Issues: https://github.com/robwierzbowski/jekyll-picture-tag/issues +# +# Syntax: {% picture [preset] path/to/img.jpg [source_key: path/to/alt-img.jpg] [attr="value"] %} +# Example: {% picture poster.jpg alt="The strange case of responsive images" %} +# {% picture gallery poster.jpg source_small: poster_closeup.jpg +# alt="The strange case of responsive images" class="gal-img" data-selected %} +# +# See the documentation for full configuration and usage instructions. + +require 'fileutils' +require 'pathname' +require 'digest/md5' +require 'mini_magick' + +module Jekyll + + class Picture < Liquid::Tag + + def initialize(tag_name, markup, tokens) + @markup = markup + super + end + + def render(context) + + # Render any liquid variables in tag arguments and unescape template code + render_markup = Liquid::Template.parse(@markup).render(context).gsub(/\\\{\\\{|\\\{\\%/, '\{\{' => '{{', '\{\%' => '{%') + + # Gather settings + site = context.registers[:site] + settings = site.config['picture'] + markup = /^(?:(?[^\s.:\/]+)\s+)?(?[^\s]+\.[a-zA-Z0-9]{3,4})\s*(?(?:(source_[^\s.:\/]+:\s+[^\s]+\.[a-zA-Z0-9]{3,4})\s*)+)?(?[\s\S]+)?$/.match(render_markup) + preset = settings['presets'][ markup[:preset] ] || settings['presets']['default'] + + raise "Picture Tag can't read this tag. Try {% picture [preset] path/to/img.jpg [source_key: path/to/alt-img.jpg] [attr=\"value\"] %}." unless markup + + # Assign defaults + settings['source'] ||= '.' + settings['output'] ||= 'generated' + settings['markup'] ||= 'picturefill' + + # Prevent Jekyll from erasing our generated files + site.config['keep_files'] << settings['output'] unless site.config['keep_files'].include?(settings['output']) + + # Deep copy preset for single instance manipulation + instance = Marshal.load(Marshal.dump(preset)) + + # Process alternate source images + source_src = if markup[:source_src] + Hash[ *markup[:source_src].gsub(/:/, '').split ] + else + {} + end + + # Process html attributes + html_attr = if markup[:html_attr] + Hash[ *markup[:html_attr].scan(/(?[^\s="]+)(?:="(?[^"]+)")?\s?/).flatten ] + else + {} + end + + if instance['attr'] + html_attr = instance.delete('attr').merge(html_attr) + end + + if settings['markup'] == 'picturefill' + html_attr['data-picture'] = nil + html_attr['data-alt'] = html_attr.delete('alt') + end + + html_attr_string = html_attr.inject('') { |string, attrs| + if attrs[1] + string << "#{attrs[0]}=\"#{attrs[1]}\" " + else + string << "#{attrs[0]} " + end + } + + # Prepare ppi variables + ppi = if instance['ppi'] then instance.delete('ppi').sort.reverse else nil end + # this might work??? ppi = instance.delete('ppi'){ |ppi| [nil] }.sort.reverse + ppi_sources = {} + + # Switch width and height keys to the symbols that generate_image() expects + instance.each { |key, source| + raise "Preset #{key} is missing a width or a height" if !source['width'] and !source['height'] + instance[key][:width] = instance[key].delete('width') if source['width'] + instance[key][:height] = instance[key].delete('height') if source['height'] + } + + # Store keys in an array for ordering the instance sources + source_keys = instance.keys + # used to escape markdown parsing rendering below + markdown_escape = "\ " + + # Raise some exceptions before we start expensive processing + raise "Picture Tag can't find the \"#{markup[:preset]}\" preset. Check picture: presets in _config.yml for a list of presets." unless preset + raise "Picture Tag can't find this preset source. Check picture: presets: #{markup[:preset]} in _config.yml for a list of sources." unless (source_src.keys - source_keys).empty? + + # Process instance + # Add image paths for each source + instance.each_key { |key| + instance[key][:src] = source_src[key] || markup[:image_src] + } + + # Construct ppi sources + # Generates -webkit-device-ratio and resolution: dpi media value for cross browser support + # Reference: http://www.brettjankord.com/2012/11/28/cross-browser-retinahigh-resolution-media-queries/ + if ppi + instance.each { |key, source| + ppi.each { |p| + if p != 1 + ppi_key = "#{key}-x#{p}" + + ppi_sources[ppi_key] = { + :width => if source[:width] then (source[:width].to_f * p).round else nil end, + :height => if source[:height] then (source[:height].to_f * p).round else nil end, + 'media' => if source['media'] + "#{source['media']} and (-webkit-min-device-pixel-ratio: #{p}), #{source['media']} and (min-resolution: #{(p * 96).round}dpi)" + else + "(-webkit-min-device-pixel-ratio: #{p}), (min-resolution: #{(p * 96).to_i}dpi)" + end, + :src => source[:src] + } + + # Add ppi_key to the source keys order + source_keys.insert(source_keys.index(key), ppi_key) + end + } + } + instance.merge!(ppi_sources) + end + + # Generate resized images + instance.each { |key, source| + instance[key][:generated_src] = generate_image(source, site.source, site.dest, settings['source'], settings['output'], site.config["baseurl"]) + } + + # Construct and return tag + if settings['markup'] == 'picture' + + source_tags = '' + source_keys.each { |source| + media = " media=\"#{instance[source]['media']}\"" unless source == 'source_default' + source_tags += "#{markdown_escape * 4}\n" + } + + # Note: we can't indent html output because markdown parsers will turn 4 spaces into code blocks + # Note: Added backslash+space escapes to bypass markdown parsing of indented code below -WD + picture_tag = "\n"\ + "#{source_tags}"\ + "#{markdown_escape * 4}\n"\ + "#{markdown_escape * 2}\n" + + elsif settings['markup'] == 'img' + # TODO implement + end + + # Return the markup! + picture_tag + end + + def generate_image(instance, site_source, site_dest, image_source, image_dest, baseurl) + image = MiniMagick::Image.open(File.join(site_source, image_source, instance[:src])) + digest = Digest::MD5.hexdigest(image.to_blob).slice!(0..5) + + image_dir = File.dirname(instance[:src]) + ext = File.extname(instance[:src]) + basename = File.basename(instance[:src], ext) + + orig_width = image[:width].to_f + orig_height = image[:height].to_f + orig_ratio = orig_width/orig_height + + gen_width = if instance[:width] + instance[:width].to_f + elsif instance[:height] + orig_ratio * instance[:height].to_f + else + orig_width + end + gen_height = if instance[:height] + instance[:height].to_f + elsif instance[:width] + instance[:width].to_f / orig_ratio + else + orig_height + end + gen_ratio = gen_width/gen_height + + # Don't allow upscaling. If the image is smaller than the requested dimensions, recalculate. + if orig_width < gen_width || orig_height < gen_height + undersize = true + gen_width = if orig_ratio < gen_ratio then orig_width else orig_height * gen_ratio end + gen_height = if orig_ratio > gen_ratio then orig_height else orig_width/gen_ratio end + end + + gen_name = "#{basename}-#{gen_width.round}by#{gen_height.round}-#{digest}#{ext}" + gen_dest_dir = File.join(site_dest, image_dest, image_dir) + gen_dest_file = File.join(gen_dest_dir, gen_name) + + # Generate resized files + unless File.exists?(gen_dest_file) + + warn "Warning:".yellow + " #{instance[:src]} is smaller than the requested output file. It will be resized without upscaling." if undersize + + # If the destination directory doesn't exist, create it + FileUtils.mkdir_p(gen_dest_dir) unless File.exist?(gen_dest_dir) + + # Let people know their images are being generated + puts "Generating #{gen_name}" + + # Scale and crop + image.combine_options do |i| + i.resize "#{gen_width}x#{gen_height}^" + i.gravity "center" + i.crop "#{gen_width}x#{gen_height}+0+0" + end + + image.write gen_dest_file + end + + # Return path relative to the site root for html + Pathname.new(File.join(baseurl, image_dest, image_dir, gen_name)).cleanpath + end + end +end + +Liquid::Template.register_tag('picture', Jekyll::Picture)