├── lib ├── jekyll-spark.rb └── jekyll │ ├── spark │ ├── version.rb │ ├── tag.rb │ ├── block.rb │ └── base.rb │ └── spark.rb ├── test ├── source │ ├── _posts │ │ └── 2016-01-01-base.md │ ├── _plugins │ │ ├── jekyll-spark.rb │ │ └── components │ │ │ ├── blocks │ │ │ ├── nested.rb │ │ │ ├── container.rb │ │ │ └── wistia_popover.rb │ │ │ └── tags │ │ │ ├── wistia.rb │ │ │ └── image.rb │ ├── _config.yml │ └── components │ │ ├── wistia-markdown.md │ │ ├── image-markdown.md │ │ ├── wistia.html │ │ ├── test-nested.md │ │ ├── wistia-popover.md │ │ ├── image.html │ │ └── container.md ├── test_component_props.rb ├── test_component_container.rb ├── test_component_test_nested.rb ├── test_unit_spark_base.rb ├── test_component_wistia_popover.rb ├── test_component_wistia.rb ├── helper.rb └── test_component_image.rb ├── Gemfile ├── .gitignore ├── .travis.yml ├── Rakefile ├── examples ├── tag_component.rb └── block_component.rb ├── LICENSE.txt ├── docs ├── introduction.md └── creating-a-component.md ├── README.md └── jekyll-spark.gemspec /lib/jekyll-spark.rb: -------------------------------------------------------------------------------- 1 | require "jekyll/spark" 2 | -------------------------------------------------------------------------------- /test/source/_posts/2016-01-01-base.md: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /test/source/_plugins/jekyll-spark.rb: -------------------------------------------------------------------------------- 1 | ../../../lib/jekyll-spark.rb -------------------------------------------------------------------------------- /lib/jekyll/spark/version.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Spark 3 | VERSION = "0.6.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/source/_config.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | - 3 | scope: 4 | path: "" 5 | values: 6 | layout: null 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'coveralls', require: false 4 | gem 'simplecov', require: false 5 | 6 | gemspec 7 | -------------------------------------------------------------------------------- /lib/jekyll/spark.rb: -------------------------------------------------------------------------------- 1 | require "jekyll" 2 | require "jekyll/spark/version" 3 | require "jekyll/spark/block" 4 | require "jekyll/spark/tag" 5 | -------------------------------------------------------------------------------- /test/source/components/wistia-markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Wistia Markdown" 3 | type: "Component" 4 | --- 5 | 6 | {% Wistia id: "123" %} 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.bundle/ 3 | /.yardoc 4 | /Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | -------------------------------------------------------------------------------- /lib/jekyll/spark/tag.rb: -------------------------------------------------------------------------------- 1 | require "jekyll" 2 | require_relative "./base" 3 | 4 | module Jekyll 5 | class ComponentTag < Liquid::Tag 6 | include Jekyll::ComponentBase 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/jekyll/spark/block.rb: -------------------------------------------------------------------------------- 1 | require "jekyll" 2 | require_relative "./base" 3 | 4 | module Jekyll 5 | class ComponentBlock < Liquid::Block 6 | include Jekyll::ComponentBase 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.3 5 | before_install: gem install bundler -v 1.13.7 6 | 7 | install: 8 | - bundle install 9 | 10 | script: 11 | - bundle exec rake test 12 | -------------------------------------------------------------------------------- /test/source/components/image-markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Image Markdown" 3 | type: "Component" 4 | --- 5 | 6 | {% img 7 | alt: "alt" 8 | class: "one" 9 | height: "200" 10 | src: "test.png" 11 | style: "background: red;" 12 | title: "title" 13 | width: "400" 14 | %} 15 | -------------------------------------------------------------------------------- /test/source/components/wistia.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Wistia" 3 | type: "Component" 4 | --- 5 | 6 |
7 | {% Wistia id: "123" %} 8 |
9 | 10 |
11 | {% Wistia %} 12 |
13 | 14 |
15 | {% Wistia id: "wistia_123" %} 16 |
17 | 18 |
19 | {% Wistia id: "wistia_123" %} 20 |
21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rubygems' 3 | require 'rake' 4 | require 'rdoc' 5 | require 'date' 6 | require 'yaml' 7 | require 'rake/testtask' 8 | 9 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w[lib])) 10 | require 'jekyll/version' 11 | 12 | Rake::TestTask.new(:test) do |test| 13 | test.libs << 'lib' << 'test' 14 | test.pattern = 'test/**/test_*.rb' 15 | test.verbose = true 16 | end 17 | -------------------------------------------------------------------------------- /test/source/components/test-nested.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Test Nested" 3 | type: "Component" 4 | --- 5 | 6 |
7 | {% TestNested %}{% endTestNested %} 8 |
9 | 10 |
11 | {% TestNested %} 12 | Hello 13 | {% endTestNested %} 14 |
15 | 16 |
17 | {% TestNested %} 18 | {% TestNested %} 19 | Yo dawg… 20 | {% endTestNested %} 21 | {% endTestNested %} 22 |
23 | -------------------------------------------------------------------------------- /examples/tag_component.rb: -------------------------------------------------------------------------------- 1 | require "jekyll-spark" 2 | 3 | module Jekyll 4 | class ExampleTagComponent < ComponentTag 5 | def template(context) 6 | # Declare props as variables here 7 | 8 | # Output rendered markup 9 | render = %Q[ 10 |
11 | Example 12 |
13 | ] 14 | end 15 | end 16 | end 17 | 18 | Liquid::Template.register_tag( 19 | "ExampleTag", 20 | Jekyll::ExampleTagComponent, 21 | ) 22 | -------------------------------------------------------------------------------- /examples/block_component.rb: -------------------------------------------------------------------------------- 1 | require "jekyll-spark" 2 | 3 | module Jekyll 4 | class ExampleBlockComponent < ComponentBlock 5 | def template(context) 6 | # Declare props as variables here 7 | content = @props["content"] 8 | 9 | # Output rendered markup 10 | render = %Q[ 11 |
12 | #{content} 13 |
14 | ] 15 | end 16 | end 17 | end 18 | 19 | Liquid::Template.register_tag( 20 | "ExampleBlock", 21 | Jekyll::ExampleBlockComponent, 22 | ) 23 | -------------------------------------------------------------------------------- /test/source/_plugins/components/blocks/nested.rb: -------------------------------------------------------------------------------- 1 | require "jekyll-spark" 2 | 3 | module Jekyll 4 | class TestNestedComponent < ComponentBlock 5 | def template(context) 6 | content = @props["content"] 7 | 8 | render = %Q[ 9 |
10 | {% img 11 | src: "test.jpg" 12 | %} 13 | #{content} 14 |
15 | ] 16 | end 17 | end 18 | end 19 | 20 | Liquid::Template.register_tag( 21 | "TestNested", 22 | Jekyll::TestNestedComponent, 23 | ) 24 | -------------------------------------------------------------------------------- /test/source/_plugins/components/blocks/container.rb: -------------------------------------------------------------------------------- 1 | require "jekyll-spark" 2 | 3 | module Jekyll 4 | class ContainerComponent < ComponentBlock 5 | def template(context) 6 | content = @props["content"] 7 | class_name = @props["class"] 8 | 9 | render = %Q[ 10 |
11 |
12 |
13 | #{content} 14 |
15 |
16 |
17 | ] 18 | end 19 | end 20 | end 21 | 22 | Liquid::Template.register_tag( 23 | "Container", 24 | Jekyll::ContainerComponent, 25 | ) 26 | -------------------------------------------------------------------------------- /test/source/components/wistia-popover.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Wistia Popover" 3 | type: "Component" 4 | text: "wut" 5 | --- 6 | 7 | {% WistiaPopover id: "hello" class: "first" %} 8 | Hello 9 | {% endWistiaPopover %} 10 | 11 | 12 | 13 | {% WistiaPopover id: "linky" class: "linky" %} 14 | Help Scout 15 | {% endWistiaPopover %} 16 | 17 | 18 | 19 | {% WistiaPopover id: "linky" class: "eval" %} 20 | {% if page.text %} 21 | {{ page.text }} 22 | {% else %} 23 | Nope! 24 | {% endif %} 25 | {% endWistiaPopover %} 26 | 27 | 28 | 29 | {% WistiaPopover 30 | id: "mov123" 31 | class: "stylin" 32 | style: "display: inline-block" 33 | %} 34 | Watch the video 35 | {% endWistiaPopover %} 36 | -------------------------------------------------------------------------------- /test/test_component_props.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class PropsComponent < JekyllUnitTest 4 | should "render with nil props" do 5 | @joule.site.data["props"] = Hash.new 6 | @joule.site.data["props"]["src"] = "hello.jpg" 7 | @joule.site.data["props"]["post"] = { 8 | "nope_title" => "Title" 9 | } 10 | 11 | @joule.render(%Q[ 12 | {% assign thumb = site.data.props.src %} 13 | {% img 14 | src: thumb 15 | alt: post.title 16 | height: 196 17 | title: post.title 18 | width: 350 19 | skip_lazy: true 20 | %} 21 | ]) 22 | 23 | el = @joule.find("img") 24 | 25 | assert_equal(el.prop("src"), "hello.jpg") 26 | assert_equal(el.prop("width"), "350") 27 | assert_equal(el.prop("height"), "196") 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/source/components/image.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Image" 3 | type: "Component" 4 | --- 5 | 6 | {% img 7 | alt: "alt" 8 | class: "one" 9 | height: "200" 10 | src: "test.png" 11 | style: "background: red;" 12 | title: "title" 13 | width: "400" 14 | %} 15 | 16 | {% Image class: "nope" %} 17 | 18 | {% img 19 | class: "default" 20 | alt: "alt" 21 | crossorigin: "crossorigin" 22 | id: "id" 23 | ismap: "ismap" 24 | longdesc: "longdesc" 25 | usemap: "usemap" 26 | %} 27 | 28 | {% img 29 | class: "data-attr" 30 | data_jared: "cupcake" 31 | %} 32 | 33 | {% img 34 | class: "no-lazy" 35 | src: "test.png" 36 | skip_lazy: true 37 | %} 38 | 39 | {% img 40 | class: "responsive" 41 | srcset: "test.png" 42 | width: "600" 43 | %} 44 | 45 | {% img 46 | class: "number-dimension" 47 | srcset: "test.png" 48 | width: 600 49 | height: 400 50 | %} 51 | -------------------------------------------------------------------------------- /test/source/components/container.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Container" 3 | type: "Component" 4 | --- 5 | 6 | {% Container class: "pink-hot" %} 7 | 8 | Hot Pink 9 | 10 |
11 | Alright 12 |
13 | 18 | 19 | {% endContainer %} 20 | 21 | {% Container class: "nested" %} 22 | 23 | {% Wistia id: "awesome" class: "awesome" %} 24 | 25 |
26 |
27 | {% img src: "001.png" class: "picture" %} 28 |
29 |
30 | {% img src: "002.png" %} 31 |
32 |
33 | {% img src: "003.png" %} 34 |
35 |
36 | 37 | {% Container class: "inner" %} 38 | {% WistiaPopover id: "pop" class: "pop" %} 39 | Popped! 40 | {% endWistiaPopover %} 41 | {% endContainer %} 42 | {% endContainer %} 43 | -------------------------------------------------------------------------------- /test/source/_plugins/components/tags/wistia.rb: -------------------------------------------------------------------------------- 1 | require "jekyll-spark" 2 | 3 | module Jekyll 4 | class WistiaComponent < ComponentTag 5 | def template(context) 6 | unless @props["id"] 7 | return context 8 | end 9 | 10 | id = @props["id"].gsub("wistia_", "").gsub("Wistia_", "") 11 | class_name = @props["class"] 12 | 13 | render = %Q[ 14 | 15 |
16 |
17 |
18 |   19 |
20 |
21 |
22 | ] 23 | end 24 | end 25 | end 26 | 27 | Liquid::Template.register_tag('Wistia', Jekyll::WistiaComponent) 28 | -------------------------------------------------------------------------------- /test/test_component_container.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class ContainerComponent < JekyllUnitTest 4 | should "render inner HTML tag(s) and content" do 5 | markup = get_component_page("Container") 6 | doc = Nokogiri::HTML(markup) 7 | o = doc.css(".pink-hot")[0] 8 | t = doc.css(".text")[0] 9 | script = doc.css("script")[0] 10 | style = doc.css("style")[0] 11 | 12 | assert(o) 13 | assert(t.text.downcase.include?("hot pink")) 14 | assert(script) 15 | assert(style) 16 | end 17 | 18 | should "render a series of inner components and HTML tags" do 19 | markup = get_component_page("Container") 20 | doc = Nokogiri::HTML(markup) 21 | o = doc.css(".nested")[0] 22 | wistia = doc.css(".awesome")[0] 23 | pop = doc.css(".pop")[0] 24 | inner = doc.css(".inner")[0] 25 | picture = doc.css(".picture")[0] 26 | img = doc.css("img") 27 | 28 | assert(o) 29 | assert(wistia) 30 | assert(pop) 31 | assert(inner) 32 | assert(picture) 33 | assert_equal(img.length, 6) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/source/_plugins/components/blocks/wistia_popover.rb: -------------------------------------------------------------------------------- 1 | require "jekyll-spark" 2 | 3 | module Jekyll 4 | class WistiaPopoverComponent < ComponentBlock 5 | def template(context) 6 | unless @props["id"] 7 | return context 8 | end 9 | 10 | id = @props["id"].gsub("wistia_", "").gsub("Wistia_", "") 11 | class_name = @props["class"] 12 | content = @props["content"] 13 | style = @props["style"] || "display:inline-block;height:80px;width:150px" 14 | 15 | render = %Q[ 16 | 17 | 18 | 26 | #{content} 27 | 28 | 29 | ] 30 | end 31 | end 32 | end 33 | 34 | Liquid::Template.register_tag('WistiaPopover', Jekyll::WistiaPopoverComponent) 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Help Scout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test_component_test_nested.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class TestNestedComponent < JekyllUnitTest 4 | should "render inner component tag and content" do 5 | markup = get_component_page("Test Nested") 6 | doc = Nokogiri::HTML(markup) 7 | div = doc.css(".basic div")[0] 8 | image = doc.css("img")[0] 9 | 10 | assert(div) 11 | assert(div.text.include?("Hello")) 12 | assert(image, "Renders inner {% img %} component") 13 | end 14 | 15 | should "render without content" do 16 | markup = get_component_page("Test Nested") 17 | doc = Nokogiri::HTML(markup) 18 | div = doc.css(".no-content div")[0] 19 | image = doc.css("img")[0] 20 | 21 | assert(div) 22 | assert(image, "Renders inner {% img %} component") 23 | end 24 | 25 | should "render inner recursive component block and content" do 26 | markup = get_component_page("Test Nested") 27 | doc = Nokogiri::HTML(markup) 28 | div = doc.css(".recursive")[0] 29 | img = doc.css("img")[0] 30 | repeat = doc.css(".c-test-nested .c-test-nested") 31 | 32 | assert(div) 33 | assert(img, "Renders inner {% img %} component") 34 | assert(repeat) 35 | assert(repeat.text.include?("Yo dawg")) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Components are `.rb` files that are added to your Jekyll project's default `_plugins/` directory: 4 | 5 | ```shell 6 | my-jekyll/ 7 | └── _plugins/ 8 | └── *Add components here* 9 | ``` 10 | 11 | There are two types are Components: 12 | 13 | **Tags** 14 | 15 | These components are created using Liquid [Tags](http://www.rubydoc.info/github/Shopify/liquid/Liquid/Tag), and they do not contain content when used. 16 | 17 | Example: 18 | ```html 19 | {% Napolean id: "skillz" class: "nunchucks bow staff computer-hacking" %} 20 | ``` 21 | 22 | **Blocks** 23 | 24 | These components are created using Liquid [Blocks](http://www.rubydoc.info/github/Shopify/liquid/Liquid/Block), and they **do** contain content when used. 25 | 26 | Example: 27 | ```html 28 | {% Napolean class: "chapstick" %} 29 | But my lips hurt real bad! 30 | {% endNapolean %} 31 | ``` 32 | 33 | Because of these types, we recommend you organize your components in your `_plugins/` directory into `tags` and `blocks` directories: 34 | 35 | ```shell 36 | my-jekyll/ 37 | └── _plugins/ 38 | ├── blocks/ 39 | └── tags/ 40 | ``` 41 | 42 | 43 | ### Up next 44 | 45 | Learn how to [create a component](creating-a-component.md). 46 | -------------------------------------------------------------------------------- /test/test_unit_spark_base.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | require "jekyll/spark" 3 | 4 | class SparkComponentBase < JekyllUnitTest 5 | should "have blank? return false for Liquid" do 6 | o = Object.new 7 | o.extend(Jekyll::ComponentBase) 8 | 9 | assert(!o.blank?) 10 | end 11 | 12 | class PropsMethod < JekyllUnitTest 13 | should "be able to set props" do 14 | o = Object.new 15 | o.extend(Jekyll::ComponentBase) 16 | 17 | h = Hash.new 18 | h["name"] = "Name" 19 | o.props = h 20 | 21 | assert_equal(o.props["name"], "Name") 22 | end 23 | 24 | should "handle null key/values" do 25 | o = Object.new 26 | o.extend(Jekyll::ComponentBase) 27 | o.props = Hash.new 28 | 29 | h = Hash.new 30 | h["name"] = "Name" 31 | h["nulls"] 32 | h["nil"] = nil 33 | 34 | o.set_props(h) 35 | 36 | assert_equal(o.props["name"], "Name") 37 | end 38 | end 39 | 40 | class TemplateMethod < JekyllUnitTest 41 | should "return context arg by default" do 42 | o= Struct.new(:foo).new 43 | class << o 44 | include Jekyll::ComponentBase 45 | end 46 | 47 | assert_equal(o.template("default"), "default") 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spark ✨ [![Build Status](https://travis-ci.org/helpscout/jekyll-spark.svg?branch=master)](https://travis-ci.org/helpscout/jekyll-spark) [![Gem Version](https://badge.fury.io/rb/jekyll-spark.svg)](https://badge.fury.io/rb/jekyll-spark) [![Coverage Status](https://coveralls.io/repos/github/helpscout/jekyll-spark/badge.svg?branch=master)](https://coveralls.io/github/helpscout/jekyll-spark?branch=master) 2 | 3 | A Jekyll library for building fast component-based UI. 4 | 5 | This library was heavily inspired by view/component creation from modern Javascript libraries like [React](https://facebook.github.io/react/) and [Vue](https://vuejs.org/). 6 | 7 | **Table of Contents** 8 | 9 | - [Install](#install) 10 | - [Documentation](#documenation) 11 | - [Examples](#examples) 12 | 13 | ## Install 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'jekyll-spark' 19 | ``` 20 | 21 | And then execute: 22 | ``` 23 | bundle 24 | ``` 25 | 26 | Or install it yourself as: 27 | ``` 28 | gem install jekyll-spark 29 | ``` 30 | 31 | 32 | 33 | ## Documentation 34 | 35 | **[View the docs](https://github.com/helpscout/jekyll-spark/blob/master/docs/introduction.md)** to get started with Jekyll Components! 36 | 37 | 38 | ## Examples 39 | 40 | **[View the starter](https://github.com/helpscout/jekyll-spark/tree/master/examples)** Component view files. 41 | -------------------------------------------------------------------------------- /jekyll-spark.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'jekyll/spark/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "jekyll-spark" 8 | spec.version = Jekyll::Spark::VERSION 9 | spec.authors = ["ItsJonQ"] 10 | spec.email = ["itsjonq@gmail.com"] 11 | 12 | spec.summary = "A Jekyll library for building component-based UI" 13 | spec.homepage = "https://github.com/helpscout/jekyll-spark" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 17 | f.match(%r{^(test|spec|features)/}) 18 | end 19 | spec.bindir = "exe" 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_runtime_dependency("jekyll", ">= 3.1.2") 24 | spec.add_runtime_dependency("htmlcompressor", "~> 0.3.1") 25 | 26 | spec.add_development_dependency "bundler", "~> 1.13" 27 | spec.add_development_dependency "rake", "~> 10.0" 28 | spec.add_development_dependency "minitest-reporters" 29 | spec.add_development_dependency "minitest-profile" 30 | spec.add_development_dependency "minitest", "~> 5.8" 31 | spec.add_development_dependency "rspec-mocks" 32 | spec.add_development_dependency "jekyll-joule", "~> 0.2.0" 33 | spec.add_development_dependency "shoulda" 34 | spec.add_development_dependency "kramdown" 35 | end 36 | -------------------------------------------------------------------------------- /test/test_component_wistia_popover.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class WistiaPopoverComponent < JekyllUnitTest 4 | should "generate an async Wistia popover embed code with an ID" do 5 | markup = get_component_page("Wistia Popover") 6 | doc = Nokogiri::HTML(markup) 7 | span = doc.css(".first")[0] 8 | 9 | assert(span) 10 | assert(span.text.downcase.include?("hello")) 11 | assert(span["class"].include?("wistia_async_hello")) 12 | end 13 | 14 | should "generate a block with markup-based content" do 15 | markup = get_component_page("Wistia Popover") 16 | doc = Nokogiri::HTML(markup) 17 | span = doc.css(".linky")[0] 18 | link = doc.css(".linky a")[0] 19 | 20 | assert(span) 21 | assert(span["class"].include?("wistia_async_linky")) 22 | assert(link) 23 | assert(link["href"].include?("helpscout.net")) 24 | assert(link.text.include?("Help Scout")) 25 | end 26 | 27 | should "render style prop" do 28 | markup = get_component_page("Wistia Popover") 29 | doc = Nokogiri::HTML(markup) 30 | span = doc.css(".stylin")[0] 31 | link = doc.css(".stylin a")[0] 32 | 33 | assert(span) 34 | assert(span["style"].include?("inline-block")) 35 | assert(link) 36 | assert(link["class"].include?("text-link")) 37 | assert(link["href"].include?("#")) 38 | assert(link.text.include?("video")) 39 | end 40 | 41 | should "evaluate variables within content" do 42 | markup = get_component_page("Wistia Popover") 43 | doc = Nokogiri::HTML(markup) 44 | span = doc.css(".eval")[0] 45 | 46 | assert(span) 47 | assert(span.text.include?("wut")) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/test_component_wistia.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class WistiaComponent < JekyllUnitTest 4 | should "generate an async Wistia embed code with an ID" do 5 | markup = get_component_page("Wistia") 6 | doc = Nokogiri::HTML(markup) 7 | script = doc.css(".default script")[0] 8 | embed = doc.css(".default .wistia_embed")[0] 9 | 10 | assert(script) 11 | assert(script.key?("async")) 12 | assert(script["src"].include?("jsonp"), "Has jsonp callback") 13 | assert(embed) 14 | end 15 | 16 | should "correctly be parsed by markdown" do 17 | markup = get_component_page("Wistia Markdown") 18 | doc = Nokogiri::HTML(markup) 19 | script = doc.css("script")[0] 20 | embed = doc.css(".wistia_embed")[0] 21 | 22 | assert(script) 23 | assert(embed) 24 | assert(!doc.css("code")[0]) 25 | assert(!doc.css(".highlighter-rouge")[0]) 26 | end 27 | 28 | should "not generate an embed without an ID" do 29 | markup = get_component_page("Wistia") 30 | doc = Nokogiri::HTML(markup) 31 | script = doc.css(".nope script")[0] 32 | embed = doc.css(".nope .wistia_embed")[0] 33 | 34 | assert(!script) 35 | assert(!embed) 36 | end 37 | 38 | should "strip wistia- (hyphen) from the ID" do 39 | markup = get_component_page("Wistia") 40 | doc = Nokogiri::HTML(markup) 41 | script = doc.css(".wistia-hyphen-id script")[0] 42 | 43 | assert(!script["src"].include?("wistia-")) 44 | end 45 | 46 | should "strip wistia_ (underscore) from the ID" do 47 | markup = get_component_page("Wistia") 48 | doc = Nokogiri::HTML(markup) 49 | script = doc.css(".wistia-underscore-id script")[0] 50 | 51 | assert(!script["src"].include?("wistia_")) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | require "coveralls" 3 | 4 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 5 | SimpleCov::Formatter::HTMLFormatter, 6 | Coveralls::SimpleCov::Formatter 7 | ] 8 | 9 | SimpleCov.start do 10 | add_filter "/source/" 11 | # add_filter "/test/" 12 | end 13 | 14 | require 'rubygems' 15 | require 'minitest/autorun' 16 | require 'minitest/reporters' 17 | require 'minitest/profile' 18 | require 'nokogiri' 19 | require 'ostruct' 20 | require 'rspec/mocks' 21 | 22 | require 'jekyll' 23 | require 'jekyll/joule' 24 | 25 | Jekyll.logger = Logger.new(StringIO.new) 26 | 27 | require 'kramdown' 28 | require 'shoulda' 29 | 30 | include Jekyll 31 | 32 | # Report with color. 33 | Minitest::Reporters.use! [ 34 | Minitest::Reporters::SpecReporter.new( 35 | :color => true 36 | ) 37 | ] 38 | 39 | class JekyllUnitTest < Minitest::Test 40 | include ::RSpec::Mocks::ExampleMethods 41 | 42 | def setup 43 | @site = Site.new(site_configuration) 44 | @site.read 45 | @site.generate 46 | @site.render 47 | @pages = @site.pages 48 | @joule = Jekyll::Joule::Site.new(@site) 49 | end 50 | 51 | def get_component_page(title) 52 | page = @pages.find { |p| 53 | p.data["title"].downcase === title.downcase and p.data["type"].downcase === "component" 54 | } 55 | 56 | return page ? page["content"] : false 57 | end 58 | 59 | def mocks_expect(*args) 60 | RSpec::Mocks::ExampleMethods::ExpectHost.instance_method(:expect).\ 61 | bind(self).call(*args) 62 | end 63 | 64 | def before_setup 65 | ::RSpec::Mocks.setup 66 | super 67 | end 68 | 69 | def after_teardown 70 | super 71 | ::RSpec::Mocks.verify 72 | ensure 73 | ::RSpec::Mocks.teardown 74 | end 75 | 76 | def fixture_site(overrides = {}) 77 | Jekyll::Site.new(site_configuration(overrides)) 78 | end 79 | 80 | def build_configs(overrides, base_hash = Jekyll::Configuration::DEFAULTS) 81 | Utils.deep_merge_hashes(base_hash, overrides) 82 | .fix_common_issues.backwards_compatibilize.add_default_collections 83 | end 84 | 85 | def site_configuration(overrides = {}) 86 | full_overrides = build_configs(overrides, build_configs({ 87 | "destination" => dest_dir, 88 | "incremental" => false 89 | })) 90 | build_configs({ 91 | "source" => source_dir 92 | }, full_overrides) 93 | end 94 | 95 | def dest_dir(*subdirs) 96 | root_dir('dest', *subdirs) 97 | end 98 | 99 | def source_dir(*subdirs) 100 | root_dir('source', *subdirs) 101 | end 102 | 103 | def clear_dest 104 | FileUtils.rm_rf(dest_dir) 105 | FileUtils.rm_rf(source_dir('.jekyll-metadata')) 106 | end 107 | 108 | def root_dir(*subdirs) 109 | File.join(File.dirname(__FILE__), *subdirs) 110 | end 111 | 112 | def directory_with_contents(path) 113 | FileUtils.rm_rf(path) 114 | FileUtils.mkdir(path) 115 | File.open("#{path}/index.html", "w"){ |f| f.write("I was previously generated.") } 116 | end 117 | 118 | def with_env(key, value) 119 | old_value = ENV[key] 120 | ENV[key] = value 121 | yield 122 | ENV[key] = old_value 123 | end 124 | 125 | def capture_output 126 | stderr = StringIO.new 127 | Jekyll.logger = Logger.new stderr 128 | yield 129 | stderr.rewind 130 | return stderr.string.to_s 131 | end 132 | alias_method :capture_stdout, :capture_output 133 | alias_method :capture_stderr, :capture_output 134 | end 135 | -------------------------------------------------------------------------------- /lib/jekyll/spark/base.rb: -------------------------------------------------------------------------------- 1 | require "htmlcompressor" 2 | require "jekyll" 3 | require "liquid" 4 | 5 | module Jekyll 6 | module ComponentBase 7 | attr_accessor :props, :content 8 | include Liquid::StandardFilters 9 | 10 | @@compressor = HtmlCompressor::Compressor.new({ 11 | :remove_comments => true, 12 | :remove_intertag_spaces => true, 13 | :preserve_line_breaks => false, 14 | }).freeze 15 | 16 | def initialize(tag_name, markup, tokens) 17 | super 18 | 19 | @attributes = {} 20 | @context = false 21 | @context_name = self.name.to_s.gsub("::", "_").downcase 22 | @content = '' 23 | @default_selector_attr = [] 24 | @props = Hash.new 25 | @site = false 26 | 27 | if markup =~ /(#{Liquid::QuotedFragment}+)?/ 28 | # Parse parameters 29 | # Source: https://gist.github.com/jgatjens/8925165 30 | markup.scan(Liquid::TagAttributes) do |key, value| 31 | @attributes[key] = Liquid::Expression.parse(value) 32 | end 33 | end 34 | end 35 | 36 | # blank? 37 | # Description: Override's Liquid's default blank checker. This allows 38 | # for templates to be used without passing inner content. 39 | def blank? 40 | false 41 | end 42 | 43 | def selector_default_props(attr = @default_selector_attr) 44 | template = "" 45 | attr.each { |prop| 46 | if @props.key?(prop) 47 | template += "#{prop}='#{@props[prop]}' " 48 | end 49 | } 50 | 51 | return template 52 | end 53 | 54 | def selector_data_props(attr = @attributes) 55 | template = "" 56 | attr.keys.each { |key| 57 | if key.include? "data" 58 | template += "#{key.gsub("_", "-")}='#{@props[key]}' " 59 | end 60 | } 61 | 62 | return template 63 | end 64 | 65 | def selector_props(attr = @default_selector_attr) 66 | template = "" 67 | template += selector_data_props 68 | template += selector_default_props(attr) 69 | 70 | return template 71 | end 72 | 73 | def set_props(props = Hash.new) 74 | @props = @props.merge(props) 75 | end 76 | 77 | def serialize_data 78 | data = Hash.new 79 | @attributes["children"] = @content 80 | @attributes["content"] = @content 81 | if @attributes.length 82 | @attributes.each do |key, value| 83 | if @context 84 | value = @context.evaluate(value) 85 | end 86 | data[key] = value 87 | end 88 | end 89 | 90 | return set_props(data) 91 | end 92 | 93 | def unindent(content) 94 | # Remove initial whitespace 95 | content.gsub!(/\A^\s*\n/, "") 96 | # Remove indentations 97 | if content =~ %r!^\s*!m 98 | indentation = Regexp.last_match(0).length 99 | content.gsub!(/^\ {#{indentation}}/, "") 100 | end 101 | 102 | return content 103 | end 104 | 105 | def render(context) 106 | @context = context 107 | @site = @context.registers[:site] 108 | @content = super 109 | serialize_data 110 | output = template(context) 111 | 112 | if (output.instance_of?(String)) 113 | output = Liquid::Template.parse(output).render() 114 | output = @@compressor.compress(unindent(output)) 115 | else 116 | output = "" 117 | end 118 | 119 | return output 120 | end 121 | 122 | def template(context = @context) 123 | return context 124 | end 125 | 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/source/_plugins/components/tags/image.rb: -------------------------------------------------------------------------------- 1 | require "jekyll-spark" 2 | 3 | module Jekyll 4 | class ImageComponent < ComponentTag 5 | DEFAULT_TAG_PROPS = [ 6 | "alt", 7 | "crossorigin", 8 | "id", 9 | "ismap", 10 | "longdesc", 11 | "style", 12 | "title", 13 | "usemap", 14 | ] 15 | BLANK_IMG = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" 16 | 17 | def template(context) 18 | prop = @props 19 | 20 | blazy_class_name = "b-lazy" 21 | class_name = prop["class"] 22 | height = prop["height"] 23 | data_src = BLANK_IMG 24 | src = prop["src"] 25 | srcset = prop["srcset"] 26 | title = prop["title"] 27 | width = prop["width"] 28 | responsive = "" 29 | default_props = selector_props(DEFAULT_TAG_PROPS) 30 | 31 | # Responsive 32 | # Adds new responsive-based image attributes: srcset and sizes. 33 | # The numbers are based on @attribute["width"]. 34 | # At the moment, this plugin only supports 1x and 2x pixel-density sizes. 35 | # 36 | # Learn more about srcset 37 | # https://ericportis.com/posts/2014/srcset-sizes/ 38 | # 39 | if srcset 40 | srcset_2x = srcset.to_s.gsub(/\.(?=[^.]*$)/, "@2x.") 41 | srcset_1x = srcset.to_s.gsub(/\.(?=[^.]*$)/, "@1x.") 42 | # Width-based srcset (Recommended) 43 | if (width) 44 | # Ensure that width is just a number (just in case) 45 | w = width.to_s.gsub("px", "").gsub("%", "").to_i 46 | responsive = " 47 | srcset=' 48 | #{srcset_1x} #{w}w, 49 | #{srcset_2x} #{w*2}w 50 | ' 51 | " 52 | responsive += " 53 | sizes=' 54 | (min-width: 40em) #{w}px, 55 | 100vw 56 | ' 57 | " 58 | # Fallback to pixel-density based (if width is not available) 59 | else 60 | responsive = " 61 | srcset=' 62 | #{srcset_1x} 1x, 63 | #{srcset_2x} 2x 64 | ' 65 | " 66 | end 67 | src = srcset_1x 68 | end 69 | 70 | # Set the src for