├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin └── jekyll_pages_api ├── gemfiles ├── jekyll_2.0.gemfile ├── jekyll_3.0.gemfile └── jekyll_3.1.gemfile ├── jekyll_pages_api.gemspec ├── lib ├── jekyll │ └── site.rb ├── jekyll_pages_api.rb └── jekyll_pages_api │ ├── filters.rb │ ├── generated_page.rb │ ├── generated_page_parser.rb │ ├── generated_site.rb │ ├── generator.rb │ ├── page.rb │ ├── page_without_a_file.rb │ └── version.rb └── spec ├── filters_spec.rb ├── generated_page_parser_spec.rb ├── generated_page_spec.rb ├── generated_site_spec.rb ├── integration_spec.rb ├── page_spec.rb ├── site ├── .gitignore ├── Gemfile ├── _config.yml ├── _includes │ ├── footer.html │ ├── head.html │ └── header.html ├── _layouts │ ├── default.html │ ├── page.html │ └── post.html ├── _posts │ ├── 2015-01-26-welcome-to-jekyll.markdown │ └── 2015-05-25-do-not-render-result.markdown ├── _sass │ ├── _base.scss │ ├── _layout.scss │ └── _syntax-highlighting.scss ├── about.md ├── css │ └── main.scss ├── feed.xml ├── index.html └── unicode.html ├── spec_helper.rb └── support ├── create_page.rb └── shell.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /gemfiles/*.lock 4 | /Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | *.bundle 12 | *.so 13 | *.o 14 | *.a 15 | mkmf.log 16 | .*.swp 17 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | gemfile: 5 | - gemfiles/jekyll_2.0.gemfile 6 | - gemfiles/jekyll_3.0.gemfile 7 | - gemfiles/jekyll_3.1.gemfile 8 | script: bundle exec rspec 9 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "jekyll-2.0" do 2 | gem 'jekyll', '~> 2.0' 3 | end 4 | 5 | appraise "jekyll-3.0" do 6 | gem 'jekyll', '~> 3.0' 7 | end 8 | 9 | appraise "jekyll-3.1" do 10 | gem 'jekyll', '~> 3.1' 11 | end 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | 3 | We're so glad you're thinking about contributing to an 18F open source project! If you're unsure or afraid of anything, just ask or submit the issue or pull request anyways. The worst that can happen is that you'll be politely asked to change something. We appreciate any sort of contribution, and don't want a wall of rules to get in the way of that. 4 | 5 | Before contributing, we encourage you to read our CONTRIBUTING policy (you are here), our LICENSE, and our README, all of which should be in this repository. If you have any questions, or want to read more about our underlying policies, you can consult the 18F Open Source Policy GitHub repository at https://github.com/18f/open-source-policy, or just shoot us an email/official government letterhead note to [18f@gsa.gov](mailto:18f@gsa.gov). 6 | 7 | ## Running tests 8 | 9 | ```bash 10 | bundle 11 | bundle exec rspec 12 | ``` 13 | 14 | ### Running against Jekyll 3 15 | 16 | ```bash 17 | BUNDLE_GEMFILE=gemfiles/jekyll_3.gemfile bundle exec rspec 18 | ``` 19 | 20 | ## Public domain 21 | 22 | This project is in the public domain within the United States, and 23 | copyright and related rights in the work worldwide are waived through 24 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 25 | 26 | All contributions to this project will be released under the CC0 27 | dedication. By submitting a pull request, you are agreeing to comply 28 | with this waiver of copyright interest. 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "appraisal" 4 | 5 | # Specify your gem's dependencies in jekyll_pages_api.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States Government, this project is in the 2 | public domain within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work 5 | worldwide through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal Summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 10 | 11 | ### No Copyright 12 | 13 | The person who associated a work with this deed has dedicated the work to 14 | the public domain by waiving all of his or her rights to the work worldwide 15 | under copyright law, including all related and neighboring rights, to the 16 | extent allowed by law. 17 | 18 | You can copy, modify, distribute and perform the work, even for commercial 19 | purposes, all without asking permission. 20 | 21 | ### Other Information 22 | 23 | In no way are the patent or trademark rights of any person affected by CC0, 24 | nor are the rights that other persons may have in the work or in how the 25 | work is used, such as publicity or privacy rights. 26 | 27 | Unless expressly stated otherwise, the person who associated a work with 28 | this deed makes no warranties about the work, and disclaims liability for 29 | all uses of the work, to the fullest extent permitted by applicable law. 30 | When using or citing the work, you should not imply endorsement by the 31 | author or the affirmer. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Pages API [![Build Status](https://travis-ci.org/18F/jekyll_pages_api.svg?branch=master)](https://travis-ci.org/18F/jekyll_pages_api) [![Code Climate](https://codeclimate.com/github/18F/jekyll_pages_api/badges/gpa.svg)](https://codeclimate.com/github/18F/jekyll_pages_api) 2 | 3 | [![Join the chat at https://gitter.im/18F/jekyll_pages_api](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/18F/jekyll_pages_api?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Jekyll Pages API is a [Jekyll Plugin](http://jekyllrb.com/docs/plugins/) gem that generates a JSON file with data for all the Pages, Posts, Documents (i.e. "collections") and StaticFiles in your Site. [Jekyll](http://jekyllrb.com), if you're not familiar, is a static website generator written in Ruby. 6 | 7 | ## Usage 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | group :jekyll_plugins do 13 | gem 'jekyll_pages_api' 14 | end 15 | ``` 16 | 17 | If you're creating a new Gemfile in your Jekyll project, make sure to include a source at the top: 18 | 19 | ```ruby 20 | source 'https://rubygems.org' 21 | ``` 22 | 23 | And then execute: 24 | 25 | ```bash 26 | bundle 27 | bundle exec jekyll serve 28 | ``` 29 | 30 | You can then see the generated JSON file at http://localhost:4000/api/v1/pages.json, which will look like this: 31 | 32 | ```javascript 33 | { 34 | "entries": [ 35 | { 36 | "title": "18F Hub", 37 | // the page path 38 | "url": "/", 39 | // the content of the page, with the HTML tags stripped and the whitespace condensed 40 | "body": "18F is a digital services team within GSA...", 41 | "meta": { 42 | // all the frontmatter for the page 43 | "title": "18F Hub", 44 | "url": "/", 45 | "layout": "page", 46 | "permalink": "/", 47 | } 48 | }, 49 | // ... 50 | ] 51 | } 52 | ``` 53 | 54 | This endpoint will be re-generated any time your site is rebuilt. 55 | 56 | ### Skipping the index 57 | 58 | The [Jekyll Pages API Search plugin](https://github.com/18F/jekyll_pages_api_search) uses this plugin to build a search index. Add `skip_index: true` to the front matter of any documents you wish to exclude from this index. 59 | 60 | ### Running standalone 61 | 62 | If you wish to generate a `pages.json` file when using a site generation tool other than Jekyll, you can run the `jekyll_pages_api` executable as a post-generation step. Run `jekyll_pages_api -h` for instructions. 63 | 64 | ## Developing 65 | 66 | > __Important Note:__ This gem uses the [Appraisal 67 | Gem](https://github.com/thoughtbot/appraisal) to ensure that it's tests pass against 68 | all supported versions of Jekyll. It uses the `appraisal` command - you can 69 | read more [in the 70 | documentation](https://github.com/thoughtbot/appraisal#usage). 71 | 72 | * Run `bundle` to install any necessary gems. 73 | * Run `bundle exec rake -T` to get a list of build commands and descriptions. 74 | * Run `bundle exec rake spec` to run the tests. 75 | * Run `appraisal install` to install the correct Jekyll dependencies. 76 | * Run `appraisal rake spec` to run all of the appraisals. 77 | * Run `bundle exec rake build` to ensure the entire gem can build. 78 | * Commit an update to bump the version number of 79 | `lib/jekyll_pages_api/version.rb` before running `bundle exec rake release`. 80 | 81 | While developing this gem, add this to the Gemfile of any project using the 82 | gem to try out your changes (presuming the project's working directory is a 83 | sibling of the gem's working directory): 84 | 85 | ```ruby 86 | group :jekyll_plugins do 87 | gem 'jekyll_pages_api', :path => '../jekyll_pages_api' 88 | end 89 | ``` 90 | 91 | ## See also 92 | 93 | Additional means of turning your site content into data: 94 | 95 | * [Jekyll's `jsonify` filter](http://jekyllrb.com/docs/templates/) 96 | * [jekyll-git_metadata](https://github.com/ivantsepp/jekyll-git_metadata) 97 | * [jekyll-rss](https://github.com/agelber/jekyll-rss) 98 | * [jekyll-sitemap](https://github.com/jekyll/jekyll-sitemap) 99 | * Reading the YAML frontmatter of the source files 100 | * Scraping the HTML pages themselves 101 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | require "bundler/gem_tasks" 4 | 5 | begin 6 | require 'rspec/core/rake_task' 7 | RSpec::Core::RakeTask.new(:spec) 8 | rescue LoadError 9 | end 10 | -------------------------------------------------------------------------------- /bin/jekyll_pages_api: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # Author: Mike Bland 3 | # Date: 2015-06-21 4 | 5 | require_relative '../lib/jekyll_pages_api' 6 | 7 | USAGE=< pages.json 12 | #{$0} -h 13 | 14 | Arguments: 15 | -h 16 | Print this help message 17 | baseurl 18 | URL prefix of every page of the generated site 19 | basedir 20 | Path to the generated site's root directory 21 | title_prefix 22 | Prefix to strip from page titles 23 | body_element_tag 24 | Tag (or tag prefix) identifying the main content element within the 25 | element of each document. Can be a complete tag (ending in '>'), or the 26 | prefix of a longer tag. Used to strip boilerplate out of the content 27 | exported via the API. 28 | END_USAGE 29 | 30 | if ARGV.length == 1 && ARGV[0] == '-h' 31 | puts USAGE 32 | exit 33 | end 34 | 35 | if ARGV.length != 4 36 | $stderr.puts USAGE 37 | exit 1 38 | end 39 | 40 | baseurl, basedir, title_prefix, body_element_tag = ARGV 41 | unless Dir.exist?(basedir) 42 | $stderr.puts "#{basedir} does not exist" 43 | exit 1 44 | end 45 | 46 | generator = ::JekyllPagesApi::Generator.new( 47 | ::JekyllPagesApi::GeneratedSite.new( 48 | baseurl, basedir, title_prefix, body_element_tag)) 49 | puts generator.page.output 50 | -------------------------------------------------------------------------------- /gemfiles/jekyll_2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "jekyll", "~> 2.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/jekyll_3.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "jekyll", "~> 3.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/jekyll_3.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "jekyll", "~> 3.1" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /jekyll_pages_api.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_pages_api/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "jekyll_pages_api" 8 | spec.version = JekyllPagesApi::VERSION 9 | spec.authors = ["Aidan Feldman"] 10 | spec.email = ["aidan.feldman@gsa.gov"] 11 | spec.summary = %q{A Jekyll Plugin that generates a JSON file with data for all the Pages in your Site.} 12 | spec.homepage = "https://github.com/18F/jekyll_pages_api" 13 | spec.license = "CC0-1.0" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "htmlentities", "~> 4.3" 21 | spec.add_dependency "jekyll", [">= 2.0", "< 4.0"] 22 | 23 | spec.add_development_dependency "bundler", "~> 1.7" 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "rspec", "~> 3.0" 26 | end 27 | -------------------------------------------------------------------------------- /lib/jekyll/site.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll/site' 2 | require_relative '../jekyll_pages_api/generator' 3 | 4 | module Jekyll 5 | class Site 6 | # monkey-patch to make an after_render hook 7 | alias_method :orig_render, :render 8 | def render 9 | orig_render 10 | after_render 11 | end 12 | 13 | def after_render 14 | JekyllPagesApi::Generator.new(self).generate 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll_pages_api/filters' 2 | require 'jekyll_pages_api/generated_site' 3 | require 'jekyll_pages_api/generator' 4 | require 'jekyll_pages_api/page' 5 | require 'jekyll_pages_api/page_without_a_file' 6 | require 'jekyll_pages_api/version' 7 | require_relative 'jekyll/site' 8 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/filters.rb: -------------------------------------------------------------------------------- 1 | require 'htmlentities' 2 | require 'liquid' 3 | 4 | module JekyllPagesApi 5 | # This is a hack to allow the module functions to be used 6 | class Filters 7 | include Liquid::StandardFilters 8 | 9 | def decode_html(str) 10 | html_decoder.decode(str) 11 | end 12 | 13 | # Slight tweak of 14 | # https://github.com/Shopify/liquid/blob/v2.6.1/lib/liquid/standardfilters.rb#L71-L74 15 | # to replace newlines with spaces. 16 | def condense(str) 17 | str.to_s.gsub(/\s+/m, ' '.freeze).strip 18 | end 19 | 20 | def text_only(str) 21 | # apply each filter in order 22 | [ 23 | :strip_html, 24 | :condense, 25 | :decode_html, 26 | :strip_html, 27 | :condense 28 | ].reduce(str) do |result, filter| 29 | self.send(filter, result) 30 | end 31 | end 32 | 33 | 34 | private 35 | 36 | def html_decoder 37 | @html_decoder = HTMLEntities.new 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/generated_page.rb: -------------------------------------------------------------------------------- 1 | # @author Mike Bland (michael.bland@gsa.gov) 2 | 3 | require_relative 'generated_page_parser' 4 | 5 | module JekyllPagesApi 6 | # Used by GeneratedSite to mimic a Jekyll page object when processing an 7 | # already-generated site using the Generator. 8 | class GeneratedPage 9 | attr_reader :path, :relative_path, :data, :content 10 | 11 | # @param path [String] full path to the generated page's file 12 | # @param basedir see {GeneratedSite#initialize} 13 | # @param title_prefix see {GeneratedSite#initialize} 14 | # @param body_element_tag see {GeneratedSite#initialize} 15 | # @param content [String] HTML content of the generated page's file 16 | # @raises [RuntimError] if path does not begin with basedir 17 | def initialize(path, basedir, title_prefix, body_element_tag, content) 18 | unless path.start_with? basedir 19 | raise "#{path} does not start with #{basedir}" 20 | end 21 | 22 | @path = path 23 | basedir_len = basedir.size 24 | basedir_len -= File::SEPARATOR.size if basedir.end_with? File::SEPARATOR 25 | 26 | end_path = path.size 27 | index_suffix = File.join "", "index.html" 28 | end_path -= index_suffix.size if path.end_with? index_suffix 29 | @relative_path = (path[basedir_len..end_path] || "") 30 | @data, @content = GeneratedPageParser.parse_generated_page( 31 | content, title_prefix, body_element_tag) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/generated_page_parser.rb: -------------------------------------------------------------------------------- 1 | # @author Mike Bland (michael.bland@gsa.gov) 2 | 3 | module JekyllPagesApi 4 | # Contains helper methods for parsing values from HTML. 5 | class GeneratedPageParser 6 | # Parses elements from a generated page's content needed by GeneratedPage. 7 | # @param content see {GeneratedPage#initialize} 8 | # @param title_prefix see {GeneratedSite#initialize} 9 | # @param body_element_tag see {GeneratedSite#initialize} 10 | # @return [Hash, String] the metadata hash containing the 11 | # `title`, `tags`, and `skip-index` elements; and the body content 12 | # stripped of boilerplate 13 | def self.parse_generated_page(content, title_prefix, body_element_tag) 14 | data = {} 15 | head_element = self.parse_basic_tag 'head', content 16 | return data, "" if head_element.nil? 17 | 18 | title = self.parse_basic_tag 'title', head_element 19 | if !title.nil? && title.start_with?(title_prefix) 20 | title = title[title_prefix.size..title.size] 21 | end 22 | data['title'] = title 23 | data.merge!(self.parse_meta_tags head_element) 24 | return data, self.parse_content_from_body(content, body_element_tag) 25 | end 26 | 27 | # Parses a value from content from between tags that cannot be nested, 28 | # e.g. , , . 29 | # @param tag_name [String] name of the tag to parse 30 | # @param content [String] HTML content from which to parse a value 31 | # @return [String] if a value is successfully parsed 32 | # @return [nil] if the tag isn't present in content, or is not well-formed 33 | def self.parse_basic_tag(tag_name, content) 34 | open_tag = "<#{tag_name}" 35 | close_tag = "</#{tag_name}>" 36 | open_i = content.index open_tag 37 | return nil if open_i.nil? 38 | open_i = content.index('>', open_i + open_tag.size) + 1 39 | close_i = content.index close_tag, open_i 40 | return nil if close_i.nil? 41 | content[open_i..close_i-1] 42 | end 43 | 44 | # Parses the (name, content) pairs from <meta> tags in the <head> element. 45 | # Note that it parses _only_ the `name` and `content` fields. 46 | # @param head_element [String] <head> element from an HTML document 47 | # @return [Hash<String, String>] a collection of (name, content) values 48 | def self.parse_meta_tags(head_element) 49 | open_tag = "<meta " 50 | open_i = head_element.index open_tag 51 | meta_tags = {} 52 | 53 | until open_i.nil? do 54 | # -1 to remove the space at the end. 55 | open_i += open_tag.size - 1 56 | close_i = head_element.index '>', open_i 57 | return meta_tags if close_i.nil? 58 | 59 | current = head_element[open_i..close_i] 60 | attrs = {'name' => nil, 'content' => nil} 61 | 62 | attrs.keys.each do |attr| 63 | attr_begin = " #{attr}=" 64 | attr_begin_i = current.index attr_begin 65 | unless attr_begin_i.nil? 66 | attr_begin_i += attr_begin.size + 1 67 | delim = current[attr_begin_i-1] 68 | attr_end_i = current.index delim, attr_begin_i 69 | next if attr_end_i.nil? 70 | attr_end_i -= 1 71 | attrs[attr] = current[attr_begin_i..attr_end_i] 72 | end 73 | end 74 | meta_name = attrs['name'] 75 | meta_tags[meta_name] = attrs['content'] unless meta_name.nil? 76 | close_i += 1 77 | open_i = head_element.index open_tag, close_i 78 | end 79 | meta_tags 80 | end 81 | 82 | # Parse actual content from an HTML page, leaving out boilerplate. 83 | # @param content [String] content of an HTML document 84 | # @param body_element_tag see {GeneratedSite#initialize} 85 | def self.parse_content_from_body(content, body_element_tag) 86 | body = parse_basic_tag 'body', content 87 | return content if body.nil? 88 | start_body = body.index body_element_tag unless body_element_tag.empty? 89 | return body if start_body.nil? 90 | 91 | start_body += 1 92 | end_name_i = body.index ' ', start_body 93 | bracket_i = body.index '>', start_body 94 | end_name_i = bracket_i if bracket_i < end_name_i 95 | tag_name = body[start_body..end_name_i-1] 96 | open_tag = "<#{tag_name}" 97 | end_tag = "</#{tag_name}>" 98 | 99 | start_body = bracket_i + 1 100 | search_i = start_body 101 | open_tag_i = body.index open_tag, search_i 102 | end_tag_i = body.index end_tag, search_i 103 | depth = 1 104 | until depth == 0 105 | if end_tag_i.nil? 106 | raise "End tag missing: #{end_tag}" 107 | end 108 | if !open_tag_i.nil? && open_tag_i < end_tag_i 109 | depth += 1 110 | search_i = open_tag_i + open_tag.size 111 | open_tag_i = body.index open_tag, search_i 112 | else 113 | depth -= 1 114 | search_i = end_tag_i + end_tag.size 115 | end_tag_i = body.index end_tag, search_i unless depth == 0 116 | end 117 | end 118 | body[start_body..end_tag_i-1] 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/generated_site.rb: -------------------------------------------------------------------------------- 1 | # @author Mike Bland (michael.bland@gsa.gov) 2 | 3 | require_relative 'generated_page' 4 | 5 | module JekyllPagesApi 6 | # Used by the standalone executable to mimic a Jekyll::Site when processing 7 | # an already-generated site using the Generator. 8 | class GeneratedSite 9 | # @see #initialize 10 | attr_reader :baseurl, :basedir, :title_prefix, :body_element_tag 11 | 12 | # @return [Array<>] a dummy empty Array 13 | attr_accessor :pages 14 | 15 | # @param baseurl [String] URL prefix of every page of the generated site 16 | # @param basedir [String] Path to the generated site's root directory 17 | # @param title_prefix [String] Prefix to strip from page titles 18 | # @param body_element_tag [String] Tag (or tag prefix) identifying the 19 | # main content element within the <body> element of each document. Can 20 | # be a complete tag (ending in '>'), or the prefix of a longer tag. Used 21 | # to strip boilerplate out of the content exported via the API. 22 | def initialize(baseurl, basedir, title_prefix, body_element_tag) 23 | @baseurl = baseurl 24 | @basedir = basedir 25 | @title_prefix = title_prefix 26 | @body_element_tag = body_element_tag 27 | @pages = [] 28 | end 29 | 30 | # Generator yielding each HTML page (as a {GeneratedPage}) that should be 31 | # exported via the API. 32 | def each_site_file 33 | Dir.glob(File.join(self.basedir, '**', '*')) do |f| 34 | next unless f.end_with? '.html' 35 | begin 36 | page = GeneratedPage.new(f, self.basedir, self.title_prefix, 37 | self.body_element_tag, File.read(f)) 38 | yield page unless page.data['title'].nil? 39 | rescue 40 | $stderr.puts "Error while processing #{f}:" 41 | raise 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/generator.rb: -------------------------------------------------------------------------------- 1 | require_relative 'page' 2 | require_relative 'page_without_a_file' 3 | 4 | require 'json' 5 | 6 | module JekyllPagesApi 7 | class Generator 8 | attr_reader :site 9 | 10 | def initialize(site) 11 | @site = site 12 | end 13 | 14 | def pages 15 | result = [] 16 | self.site.each_site_file do |site_file| 17 | page = Page.new(site_file, @site) 18 | result << page if page.html? 19 | end 20 | result 21 | end 22 | 23 | def pages_data 24 | self.pages.map(&:to_json) 25 | end 26 | 27 | def data 28 | { 29 | entries: pages_data 30 | } 31 | end 32 | 33 | def dest_dir 34 | File.join('api', 'v1') 35 | end 36 | 37 | def page 38 | # based on https://github.com/jekyll/jekyll-sitemap/blob/v0.7.0/lib/jekyll-sitemap.rb#L51-L54 39 | page = PageWithoutAFile.new(self.site, File.dirname(__FILE__), self.dest_dir, 'pages.json') 40 | page.output = self.data.to_json 41 | page 42 | end 43 | 44 | def generate 45 | self.site.pages << self.page 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/page.rb: -------------------------------------------------------------------------------- 1 | require_relative 'filters' 2 | 3 | module JekyllPagesApi 4 | # wrapper for a Jekyll::Page 5 | class Page 6 | HTML_EXTS = %w(.html .md .markdown .textile).to_set 7 | attr_reader :page, :site 8 | 9 | # Jekyll::StaticFile doesn't expose a `site` accessor, so we require an 10 | # explicit `site` argument here. 11 | def initialize(page, site) 12 | @page = page 13 | @site = site 14 | end 15 | 16 | def html? 17 | path = self.page.path 18 | path.end_with?('/') || HTML_EXTS.include?(File.extname(path)) 19 | end 20 | 21 | def filterer 22 | @filterer ||= Filters.new 23 | end 24 | 25 | def title 26 | title = self.page.data['title'] if self.page.respond_to?(:data) 27 | title ||= self.page.title if self.page.respond_to?(:title) 28 | self.filterer.decode_html(title || '') 29 | end 30 | 31 | def base_url 32 | self.site.baseurl 33 | end 34 | 35 | def rel_path 36 | path = self.page.url if self.page.respond_to?(:url) 37 | path ||= self.page.relative_path if self.page.respond_to?(:relative_path) 38 | path 39 | end 40 | 41 | def url 42 | [self.base_url, rel_path].join 43 | end 44 | 45 | def body_text 46 | output = self.page.content if self.page.respond_to?(:content) 47 | output ||= File.read(self.page.path) 48 | self.filterer.text_only(output) 49 | end 50 | 51 | def tags 52 | (self.page.data['tags'] if self.page.respond_to?(:data)) || [] 53 | end 54 | 55 | def skip_index? 56 | (self.page.data['skip_index'] if self.page.respond_to?(:data)) || false 57 | end 58 | 59 | def to_json 60 | optional = {} 61 | optional['skip_index'] = true if self.skip_index? 62 | 63 | optional.merge({ 64 | title: self.title, 65 | url: self.url, 66 | tags: self.tags, 67 | body: self.body_text, 68 | meta: self.page.data 69 | }) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/page_without_a_file.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll' 2 | 3 | # https://github.com/jekyll/jekyll-sitemap/blob/v0.7.0/lib/jekyll-sitemap.rb#L3-L8 4 | module JekyllPagesApi 5 | class PageWithoutAFile < Jekyll::Page 6 | def read_yaml(*) 7 | @data ||= {} 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/jekyll_pages_api/version.rb: -------------------------------------------------------------------------------- 1 | module JekyllPagesApi 2 | VERSION = '0.1.6' 3 | end 4 | -------------------------------------------------------------------------------- /spec/filters_spec.rb: -------------------------------------------------------------------------------- 1 | describe JekyllPagesApi::Filters do 2 | describe '#condense' do 3 | it "removes line breaks" do 4 | expect(subject.condense("foo\n bar")).to eq('foo bar') 5 | end 6 | end 7 | describe '#text_only' do 8 | it "removes HTML" do 9 | expect(subject.text_only(test_html)).to eq('some text') 10 | end 11 | end 12 | 13 | def test_html 14 | '<p>some text</p>' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/generated_page_parser_spec.rb: -------------------------------------------------------------------------------- 1 | module JekyllPagesApi 2 | describe GeneratedPageParser do 3 | describe '#parse_basic_tag' do 4 | it "returns nil if content is empty" do 5 | expect(GeneratedPageParser.parse_basic_tag('', '')).to eq(nil) 6 | end 7 | 8 | it "returns nil if the tag is not present" do 9 | expect(GeneratedPageParser.parse_basic_tag( 10 | 'title', 'foobar')).to eq(nil) 11 | end 12 | 13 | it "returns nil if the tag is not closed" do 14 | expect(GeneratedPageParser.parse_basic_tag( 15 | 'title', '<title>foobar')).to eq(nil) 16 | end 17 | 18 | it "returns the content of the tag" do 19 | expect(GeneratedPageParser.parse_basic_tag( 20 | 'title', '<title>foobar')).to eq('foobar') 21 | end 22 | end 23 | 24 | describe '#parse_meta_tags' do 25 | it "returns an empty hash if the head_element is empty" do 26 | expect(GeneratedPageParser.parse_meta_tags('')).to eq({}) 27 | end 28 | 29 | it "returns an empty hash if a meta tag isn't closed properly" do 30 | expect(GeneratedPageParser.parse_meta_tags( 31 | '')).to eq({}) 37 | end 38 | 39 | it "returns a valid hash for a well-formed meta tag" do 40 | expect(GeneratedPageParser.parse_meta_tags( 41 | '')).to eq("foo" => "bar") 42 | end 43 | 44 | it "returns a valid hash for a self-closing meta tag" do 45 | expect(GeneratedPageParser.parse_meta_tags( 46 | '')).to eq("foo" => "bar") 47 | end 48 | 49 | it "returns a valid hash for a meta tag with single-quote delimiters" do 50 | expect(GeneratedPageParser.parse_meta_tags( 51 | "")).to eq("foo" => "bar") 52 | end 53 | 54 | it "returns a valid hash for a meta tag with mixed-quote delimiters" do 55 | expect(GeneratedPageParser.parse_meta_tags( 56 | "")).to eq("foo" => "bar") 57 | end 58 | 59 | it "returns a valid hash regardless of attribute order" do 60 | expect(GeneratedPageParser.parse_meta_tags( 61 | '')).to eq("foo" => "bar") 62 | end 63 | 64 | it "returns a valid hash for multiple meta tags" do 65 | expect(GeneratedPageParser.parse_meta_tags( 66 | ''+ 67 | "" + 68 | '') 69 | ).to eq("foo" => "bar", "baz" => "quux", "xyzzy" => "plugh") 70 | end 71 | end 72 | 73 | describe '#parse_content_from_body' do 74 | it "returns the empty string if passed all empty strings" do 75 | expect(GeneratedPageParser.parse_content_from_body("", "")).to eq("") 76 | end 77 | 78 | it "returns the original content if there are no body tags" do 79 | expect(GeneratedPageParser.parse_content_from_body( 80 | "foobar", "")).to eq("foobar") 81 | end 82 | 83 | it "returns the original content if the body tag isn't closed" do 84 | expect(GeneratedPageParser.parse_content_from_body( 85 | "foobar", "")).to eq("foobar") 86 | end 87 | 88 | it "returns the full body content if the body_element_tag is empty" do 89 | expect(GeneratedPageParser.parse_content_from_body( 90 | "header
foobar
footer", "") 91 | ).to eq("header
foobar
footer") 92 | end 93 | 94 | it "returns only the body content within the body_element_tag" do 95 | expect(GeneratedPageParser.parse_content_from_body( 96 | "
header
"+ 97 | "
foobar
"+ 98 | "
footer
", 99 | "
")).to eq("foobar") 100 | end 101 | 102 | it "returns only the body content when body_element_tag is a prefix" do 103 | expect(GeneratedPageParser.parse_content_from_body( 104 | "
header
"+ 105 | "
foobar
"+ 106 | "
footer
", 107 | "
header
"+ 113 | "
"+ 114 | "blah blah"+ 115 | "
plus
some
nested
divs
"+ 116 | "woof woof"+ 117 | "
"+ 118 | "
footer
", 119 | "
plus
some
nested
divs
"+ 123 | "woof woof" 124 | ) 125 | end 126 | end 127 | 128 | describe '#parse_generated_page' do 129 | it "returns empty values when passed all empty strings" do 130 | data, content = GeneratedPageParser.parse_generated_page "", "", "" 131 | expect(data).to eq({}) 132 | expect(content).to eq("") 133 | end 134 | 135 | it "returns empty values when the head element isn't present" do 136 | data, content = GeneratedPageParser.parse_generated_page( 137 | "foobar", "", "") 138 | expect(data).to eq({}) 139 | expect(content).to eq("") 140 | end 141 | 142 | it "returns a nil title and all body content" do 143 | data, content = GeneratedPageParser.parse_generated_page( 144 | ""+ 145 | "
header
"+ 146 | "
foobar
"+ 147 | "
footer
", "", "") 148 | expect(data).to eq({"title" => nil}) 149 | expect(content).to eq( 150 | "
header
foobar
footer
") 151 | end 152 | 153 | it "returns the title and only body content within body_element_tag" do 154 | data, content = GeneratedPageParser.parse_generated_page( 155 | "Blah Blah Woof Woof"+ 156 | "
header
"+ 157 | "
foobar
"+ 158 | "
footer
", "", "
"Blah Blah Woof Woof"}) 160 | expect(content).to eq("foobar") 161 | end 162 | 163 | it "returns the stripped title body content and metadata" do 164 | data, content = GeneratedPageParser.parse_generated_page( 165 | "18F — Blah Blah Woof Woof"+ 166 | ""+ 167 | ""+ 168 | ""+ 169 | "
header
"+ 170 | "
foobar
"+ 171 | "
footer
", "18F — ", "
"Blah Blah Woof Woof", 174 | "skip-index" => "true", 175 | "tags" => "baz,quux,xyzzy,plugh", 176 | ) 177 | expect(content).to eq("foobar") 178 | end 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /spec/generated_page_spec.rb: -------------------------------------------------------------------------------- 1 | module JekyllPagesApi 2 | describe GeneratedPage do 3 | describe '#initialize' do 4 | it "handles all empty strings correctly" do 5 | page = GeneratedPage.new "", "", "", "", "" 6 | expect(page.path).to eq("") 7 | expect(page.relative_path).to eq("") 8 | expect(page.data).to eq({}) 9 | expect(page.content).to eq("") 10 | end 11 | 12 | it "parses the relative path when basedir ends in SEPARATOR" do 13 | path = File.join "foo", "bar", "baz.html" 14 | basedir = File.join "foo", "" 15 | page = GeneratedPage.new path, basedir, "", "", "" 16 | expect(page.path).to eq("foo/bar/baz.html") 17 | expect(page.relative_path).to eq("/bar/baz.html") 18 | expect(page.data).to eq({}) 19 | expect(page.content).to eq("") 20 | end 21 | 22 | it "parses the relative path when basedir doesn't end in SEPARATOR" do 23 | path = File.join "foo", "bar", "baz.html" 24 | basedir = "foo" 25 | page = GeneratedPage.new path, basedir, "", "", "" 26 | expect(page.path).to eq("foo/bar/baz.html") 27 | expect(page.relative_path).to eq("/bar/baz.html") 28 | expect(page.data).to eq({}) 29 | expect(page.content).to eq("") 30 | end 31 | 32 | it "raises RuntimeError when path does not begin with basedir" do 33 | path = File.join "foo", "bar", "baz.html" 34 | basedir = File.join "quux", "" 35 | expect{GeneratedPage.new path, basedir, "", "", "" 36 | }.to raise_error( 37 | RuntimeError, "#{path} does not start with #{basedir}") 38 | end 39 | 40 | it "parses out index.html suffix and leaves trailing slash" do 41 | path = File.join "foo", "bar", "index.html" 42 | basedir = File.join "foo", "" 43 | page = GeneratedPage.new path, basedir, "", "", "" 44 | expect(page.path).to eq("foo/bar/index.html") 45 | expect(page.relative_path).to eq("/bar/") 46 | expect(page.data).to eq({}) 47 | expect(page.content).to eq("") 48 | end 49 | 50 | it "parses out index.html suffix and leaves trailing slash for root" do 51 | path = File.join "foo", "index.html" 52 | basedir = File.join "foo", "" 53 | page = GeneratedPage.new path, basedir, "", "", "" 54 | expect(page.path).to eq("foo/index.html") 55 | expect(page.relative_path).to eq("/") 56 | expect(page.data).to eq({}) 57 | expect(page.content).to eq("") 58 | end 59 | 60 | it "parses content correctly" do 61 | path = File.join "foo", "bar", "index.html" 62 | basedir = File.join "foo", "" 63 | title_prefix = "18F — " 64 | body_element_tag = "
"+ 67 | ""+ 68 | ""+ 69 | "
header
"+ 70 | "
foobar
"+ 71 | "
footer
" 72 | 73 | page = GeneratedPage.new(path, basedir, title_prefix, 74 | body_element_tag, content) 75 | expect(page.path).to eq("foo/bar/index.html") 76 | expect(page.relative_path).to eq("/bar/") 77 | expect(page.data).to eq( 78 | "title" => "Blah Blah Woof Woof", 79 | "skip-index" => "true", 80 | "tags" => "baz,quux,xyzzy,plugh", 81 | ) 82 | expect(page.content).to eq("foobar") 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/generated_site_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'tmpdir' 3 | 4 | module JekyllPagesApi 5 | describe GeneratedPage do 6 | describe '#each_site_file' do 7 | before :each do 8 | @test_tmpdir = Dir.mktmpdir 9 | end 10 | 11 | after :each do 12 | FileUtils.remove_entry @test_tmpdir 13 | end 14 | 15 | it "should only select .html files containing a title" do 16 | basedir = File.join @test_tmpdir, "foo" 17 | FileUtils.mkdir_p basedir 18 | 19 | content_dir = File.join(basedir, "bar") 20 | FileUtils.mkdir_p content_dir 21 | 22 | File.open(File.join(content_dir, "baz.txt"), "w") do |f| 23 | f << "Text file that should be excluded." 24 | end 25 | 26 | File.open(File.join(content_dir, "quux.html"), "w") do |f| 27 | f << "18F — Include me!" 28 | f << "
header
" 29 | f << "
This page should be included.
" 30 | f << "
footer
" 31 | end 32 | 33 | File.open(File.join(content_dir, "xyzzy.html"), "w") do |f| 34 | f << "" 35 | f << "This page shouldn't be included because it lacks a title." 36 | f << "" 37 | end 38 | 39 | paths = ['baz.txt', 'quux.html', 'xyzzy.html'].sort.map do |f| 40 | File.join content_dir, f 41 | end 42 | expect(Dir.glob(File.join(content_dir, '**', '*'))).to match_array(paths) 43 | 44 | site = GeneratedSite.new("https://unused/", basedir, 45 | "18F — ", "
"Include me!") 54 | expect(page.content).to eq("This page should be included.") 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'json' 4 | require_relative 'support/shell' 5 | 6 | describe "integration" do 7 | BUILD_DIR = File.join(Dir.pwd, 'spec', 'site') 8 | JSON_PATH = File.join(BUILD_DIR, '_site', 'api', 'v1', 'pages.json') 9 | 10 | def read_json(path) 11 | contents = File.read(path) 12 | JSON.parse(contents) 13 | end 14 | 15 | def pages_data 16 | @json ||= read_json(JSON_PATH) 17 | end 18 | 19 | def entries_data 20 | pages_data['entries'] 21 | end 22 | 23 | def page_data(url) 24 | entries_data.find{|page| page['url'] == url } 25 | end 26 | 27 | def homepage_data 28 | page_data('/') 29 | end 30 | 31 | before(:context) do 32 | # http://bundler.io/man/bundle-exec.1.html#Shelling-out 33 | Bundler.with_clean_env do 34 | Dir.chdir(BUILD_DIR) do 35 | run_cmd("JEKYLL_VERSION=#{Jekyll::VERSION} bundle update") 36 | run_cmd("JEKYLL_VERSION=#{Jekyll::VERSION} bundle exec jekyll build") 37 | end 38 | end 39 | end 40 | 41 | it "generates the JSON file" do 42 | expect(File.exist?(JSON_PATH)).to be_truthy 43 | end 44 | 45 | it "includes an entry for every page" do 46 | urls = entries_data.map{|page| page['url'] } 47 | 48 | # not sure why this discrepancy exists... 49 | if Jekyll::VERSION.start_with?('3.') 50 | expect(urls).to eq(%w( 51 | /about/ 52 | / 53 | /unicode.html 54 | /jekyll/update/2015/01/26/welcome-to-jekyll.html 55 | /jekyll/update/2015/05/25/do-not-render-result.html 56 | )) 57 | else 58 | expect(urls).to eq(%w( 59 | /jekyll/update/2015/01/26/welcome-to-jekyll.html 60 | /jekyll/update/2015/05/25/do-not-render-result.html 61 | /about/ 62 | /index.html 63 | /unicode.html 64 | )) 65 | end 66 | end 67 | 68 | it "does not render the pages corpus using Liquid" do 69 | # The content of each page in the pages corpus should be Liquid-rendered, 70 | # but rendering the pages.json corpus may cause pages that contain code 71 | # examples of Liquid tags may produce invalid JSON. 72 | page = page_data '/jekyll/update/2015/05/25/do-not-render-result.html' 73 | expect(page['body']).to_not include('{% raw %}') 74 | expect(page['body']).to include('{% author chrisc %}') 75 | end 76 | 77 | it "removes HTML tags" do 78 | entries_data.each do |page| 79 | expect(page['body']).to_not include('<') 80 | end 81 | end 82 | 83 | it "condenses the content" do 84 | entries_data.each do |page| 85 | expect(page['body']).to_not match(/\s{2,}/m) 86 | end 87 | end 88 | 89 | it "handles unicode" do 90 | page = page_data('/unicode.html') 91 | expect(page['body']).to eq("”Handle the curly quotes!” they said.") 92 | end 93 | 94 | it "includes front matter tags" do 95 | page = page_data('/about/') 96 | expect(page['tags']).to eq(["Jekyll", "test page", "convenient"]) 97 | expect(page['meta']['layout']).to eq("page") 98 | expect(page['meta']['permalink']).to eq("/about/") 99 | expect(page['meta']['skip_index']).to eq(true) 100 | expect(page['meta']['tags']).to eq(["Jekyll", "test page", "convenient"]) 101 | expect(page['meta']['title']).to eq("About") 102 | end 103 | 104 | it "sets skip_index only if it is true" do 105 | page = page_data('/about/') 106 | expect(page['skip_index']).to eq(true) 107 | page = page_data('/unicode.html') 108 | expect(page['skip_index']).to eq(nil) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/page_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'support/create_page' 2 | 3 | BASEURL = Jekyll::Configuration::DEFAULTS['baseurl'] 4 | 5 | describe JekyllPagesApi::Page do 6 | include_context "create page" 7 | 8 | describe '#url' do 9 | it "returns the path" do 10 | expect(create_page(BASEURL, '/foo/').url).to eq('/foo/') 11 | end 12 | 13 | it "prepends the baseurl" do 14 | expect(create_page('/base', '/foo/').url).to eq('/base/foo/') 15 | end 16 | 17 | it "uses the relative path for StaticFiles" do 18 | page = create_static_file('/base', '/foo.html') 19 | expect(page.url).to eq('/base/foo.html') 20 | end 21 | 22 | it "uses the relative path for Documents" do 23 | expect(create_document('/base', '/foo.html').url).to eq('/base/foo.html') 24 | end 25 | end 26 | 27 | describe '#html?' do 28 | it "returns true for paths ending in slash" do 29 | expect(create_page(BASEURL, '/foo/').html?).to eq(true) 30 | end 31 | 32 | it "returns true for paths ending in .html" do 33 | expect(create_page(BASEURL, '/foo/index.html').html?).to eq(true) 34 | end 35 | 36 | it "returns true for paths ending in .md" do 37 | expect(create_page(BASEURL, '/foo/index.html').html?).to eq(true) 38 | end 39 | 40 | it "returns false otherwise" do 41 | expect(create_page(BASEURL, '/foo/index.json').html?).to eq(false) 42 | end 43 | end 44 | 45 | describe '#title' do 46 | it "returns the title field from the page's front matter if present" do 47 | page = create_page(BASEURL, '/foo/', data:{'title' => 'Foo'}) 48 | expect(page.title).to eq('Foo') 49 | end 50 | 51 | it "returns the title field from the post's front matter if present" do 52 | page = create_post(BASEURL, '/foo/', data:{'title' => 'Foo'}) 53 | expect(page.title).to eq('Foo') 54 | end 55 | 56 | it "returns the title method from post method if not in front matter" do 57 | page = create_post(BASEURL, '/foo/', title: 'Foo') 58 | expect(page.title).to eq('Foo') 59 | end 60 | 61 | it "returns the empty string for StaticFiles" do 62 | expect(create_static_file(BASEURL, '/foo/').title).to eq('') 63 | end 64 | 65 | it "returns the title field from the document's front matter if present" do 66 | page = create_document(BASEURL, '/foo/', data:{'title' => 'Foo'}) 67 | expect(page.title).to eq('Foo') 68 | end 69 | 70 | it "returns the empty string for Documents if not in front matter" do 71 | expect(create_document(BASEURL, '/foo/').title).to eq('') 72 | end 73 | end 74 | 75 | describe "#tags" do 76 | it "returns tags if present in the front matter" do 77 | page = create_page(BASEURL, '/foo/', 78 | data:{'tags' => ['foo', 'bar', 'baz']}) 79 | expect(page.tags).to eq(['foo', 'bar', 'baz']) 80 | end 81 | 82 | it "returns the empty list if not present in the front matter" do 83 | expect(create_page(BASEURL, '/foo/').tags).to eq([]) 84 | end 85 | 86 | it "returns the empty list if the page does not contain front matter" do 87 | expect(create_static_file(BASEURL, '/foo/').tags).to eq([]) 88 | end 89 | end 90 | 91 | describe "#body_text" do 92 | it "returns the content if present" do 93 | page = create_page(BASEURL, '/foo/', content: "foo bar baz") 94 | expect(page.body_text).to eq("foo bar baz") 95 | end 96 | 97 | it "returns the file content of StaticFiles" do 98 | page = create_static_file(BASEURL, '/foo.html', content: "foo bar baz") 99 | expect(page.body_text).to eq("foo bar baz") 100 | end 101 | end 102 | 103 | describe "#skip_index?" do 104 | it "defaults to false if the 'data' member isn't present" do 105 | expect(create_static_file(BASEURL, '/foo/').skip_index?).to eq(false) 106 | end 107 | 108 | it "defaults to false if the 'skip_index' field isn't present" do 109 | expect(create_page(BASEURL, '/foo/').skip_index?).to eq(false) 110 | end 111 | 112 | it "returns true if data['skip_index'] is true" do 113 | page = create_page(BASEURL, '/foo/', data:{'skip_index' => true}) 114 | expect(page.skip_index?).to eq(true) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/site/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | Gemfile.lock 4 | .jekyll-metadata 5 | -------------------------------------------------------------------------------- /spec/site/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'jekyll', ENV['JEKYLL_VERSION'] 4 | 5 | group :jekyll_plugins do 6 | gem 'jekyll_pages_api', path: '../..' 7 | end 8 | -------------------------------------------------------------------------------- /spec/site/_config.yml: -------------------------------------------------------------------------------- 1 | # Site settings 2 | title: Your awesome title 3 | email: your-email@domain.com 4 | description: > # this means to ignore newlines until "baseurl:" 5 | Write an awesome description for your new site here. You can edit this 6 | line in _config.yml. It will appear in your document head meta (for 7 | Google search results) and in your feed.xml site description. 8 | baseurl: "" # the subpath of your site, e.g. /blog/ 9 | url: "http://yourdomain.com" # the base hostname & protocol for your site 10 | twitter_username: jekyllrb 11 | github_username: jekyll 12 | 13 | # Build settings 14 | markdown: kramdown 15 | -------------------------------------------------------------------------------- /spec/site/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 56 | -------------------------------------------------------------------------------- /spec/site/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /spec/site/_includes/header.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /spec/site/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include header.html %} 9 | 10 |
11 |
12 | {{ content }} 13 |
14 |
15 | 16 | {% include footer.html %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/site/_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 | 6 |
7 |

{{ page.title }}

8 |
9 | 10 |
11 | {{ content }} 12 |
13 | 14 |
15 | -------------------------------------------------------------------------------- /spec/site/_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 | 6 |
7 |

{{ page.title }}

8 | 9 |
10 | 11 |
12 | {{ content }} 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /spec/site/_posts/2015-01-26-welcome-to-jekyll.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Welcome to Jekyll!" 4 | categories: jekyll update 5 | --- 6 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. 7 | 8 | To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. 9 | 10 | Jekyll also offers powerful support for code snippets: 11 | 12 | {% highlight ruby %} 13 | def print_hi(name) 14 | puts "Hi, #{name}" 15 | end 16 | print_hi('Tom') 17 | #=> prints 'Hi, Tom' to STDOUT. 18 | {% endhighlight %} 19 | 20 | Check out the [Jekyll docs][jekyll] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll’s dedicated Help repository][jekyll-help]. 21 | 22 | [jekyll]: http://jekyllrb.com 23 | [jekyll-gh]: https://github.com/jekyll/jekyll 24 | [jekyll-help]: https://github.com/jekyll/jekyll-help 25 | -------------------------------------------------------------------------------- /spec/site/_posts/2015-05-25-do-not-render-result.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Do Not Render the Result" 4 | categories: jekyll update 5 | --- 6 | Sometimes posts will contain examples of Liquid code, like this from the [18F blog post about Jekyll and webhooks](https://18f.gsa.gov/2014/11/17/taking-control-of-our-website-with-jekyll-and-webhooks/): 7 | 8 | ```html 9 |

10 | by {% raw %}{% author chrisc %}{% endraw %}, {%raw %}{% author mhz %}{% endraw %}, and {% raw %}{% author nick %}{% endraw %} 11 |

12 | ``` 13 | 14 | If we render the `pages.json` corpus, the Liquid tags will generate text that is not properly JSON-escaped, rendering the corpus as invalid JSON. 15 | -------------------------------------------------------------------------------- /spec/site/_sass/_base.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Reset some basic elements 3 | */ 4 | body, h1, h2, h3, h4, h5, h6, 5 | p, blockquote, pre, hr, 6 | dl, dd, ol, ul, figure { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | 12 | 13 | /** 14 | * Basic styling 15 | */ 16 | body { 17 | font-family: $base-font-family; 18 | font-size: $base-font-size; 19 | line-height: $base-line-height; 20 | font-weight: 300; 21 | color: $text-color; 22 | background-color: $background-color; 23 | -webkit-text-size-adjust: 100%; 24 | } 25 | 26 | 27 | 28 | /** 29 | * Set `margin-bottom` to maintain vertical rhythm 30 | */ 31 | h1, h2, h3, h4, h5, h6, 32 | p, blockquote, pre, 33 | ul, ol, dl, figure, 34 | %vertical-rhythm { 35 | margin-bottom: $spacing-unit / 2; 36 | } 37 | 38 | 39 | 40 | /** 41 | * Images 42 | */ 43 | img { 44 | max-width: 100%; 45 | vertical-align: middle; 46 | } 47 | 48 | 49 | 50 | /** 51 | * Figures 52 | */ 53 | figure > img { 54 | display: block; 55 | } 56 | 57 | figcaption { 58 | font-size: $small-font-size; 59 | } 60 | 61 | 62 | 63 | /** 64 | * Lists 65 | */ 66 | ul, ol { 67 | margin-left: $spacing-unit; 68 | } 69 | 70 | li { 71 | > ul, 72 | > ol { 73 | margin-bottom: 0; 74 | } 75 | } 76 | 77 | 78 | 79 | /** 80 | * Headings 81 | */ 82 | h1, h2, h3, h4, h5, h6 { 83 | font-weight: 300; 84 | } 85 | 86 | 87 | 88 | /** 89 | * Links 90 | */ 91 | a { 92 | color: $brand-color; 93 | text-decoration: none; 94 | 95 | &:visited { 96 | color: darken($brand-color, 15%); 97 | } 98 | 99 | &:hover { 100 | color: $text-color; 101 | text-decoration: underline; 102 | } 103 | } 104 | 105 | 106 | 107 | /** 108 | * Blockquotes 109 | */ 110 | blockquote { 111 | color: $grey-color; 112 | border-left: 4px solid $grey-color-light; 113 | padding-left: $spacing-unit / 2; 114 | font-size: 18px; 115 | letter-spacing: -1px; 116 | font-style: italic; 117 | 118 | > :last-child { 119 | margin-bottom: 0; 120 | } 121 | } 122 | 123 | 124 | 125 | /** 126 | * Code formatting 127 | */ 128 | pre, 129 | code { 130 | font-size: 15px; 131 | border: 1px solid $grey-color-light; 132 | border-radius: 3px; 133 | background-color: #eef; 134 | } 135 | 136 | code { 137 | padding: 1px 5px; 138 | } 139 | 140 | pre { 141 | padding: 8px 12px; 142 | overflow-x: scroll; 143 | 144 | > code { 145 | border: 0; 146 | padding-right: 0; 147 | padding-left: 0; 148 | } 149 | } 150 | 151 | 152 | 153 | /** 154 | * Wrapper 155 | */ 156 | .wrapper { 157 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2)); 158 | max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); 159 | margin-right: auto; 160 | margin-left: auto; 161 | padding-right: $spacing-unit; 162 | padding-left: $spacing-unit; 163 | @extend %clearfix; 164 | 165 | @include media-query($on-laptop) { 166 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit})); 167 | max-width: calc(#{$content-width} - (#{$spacing-unit})); 168 | padding-right: $spacing-unit / 2; 169 | padding-left: $spacing-unit / 2; 170 | } 171 | } 172 | 173 | 174 | 175 | /** 176 | * Clearfix 177 | */ 178 | %clearfix { 179 | 180 | &:after { 181 | content: ""; 182 | display: table; 183 | clear: both; 184 | } 185 | } 186 | 187 | 188 | 189 | /** 190 | * Icons 191 | */ 192 | .icon { 193 | 194 | > svg { 195 | display: inline-block; 196 | width: 16px; 197 | height: 16px; 198 | vertical-align: middle; 199 | 200 | path { 201 | fill: $grey-color; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /spec/site/_sass/_layout.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Site header 3 | */ 4 | .site-header { 5 | border-top: 5px solid $grey-color-dark; 6 | border-bottom: 1px solid $grey-color-light; 7 | min-height: 56px; 8 | 9 | // Positioning context for the mobile navigation icon 10 | position: relative; 11 | } 12 | 13 | .site-title { 14 | font-size: 26px; 15 | line-height: 56px; 16 | letter-spacing: -1px; 17 | margin-bottom: 0; 18 | float: left; 19 | 20 | &, 21 | &:visited { 22 | color: $grey-color-dark; 23 | } 24 | } 25 | 26 | .site-nav { 27 | float: right; 28 | line-height: 56px; 29 | 30 | .menu-icon { 31 | display: none; 32 | } 33 | 34 | .page-link { 35 | color: $text-color; 36 | line-height: $base-line-height; 37 | 38 | // Gaps between nav items, but not on the first one 39 | &:not(:first-child) { 40 | margin-left: 20px; 41 | } 42 | } 43 | 44 | @include media-query($on-palm) { 45 | position: absolute; 46 | top: 9px; 47 | right: 30px; 48 | background-color: $background-color; 49 | border: 1px solid $grey-color-light; 50 | border-radius: 5px; 51 | text-align: right; 52 | 53 | .menu-icon { 54 | display: block; 55 | float: right; 56 | width: 36px; 57 | height: 26px; 58 | line-height: 0; 59 | padding-top: 10px; 60 | text-align: center; 61 | 62 | > svg { 63 | width: 18px; 64 | height: 15px; 65 | 66 | path { 67 | fill: $grey-color-dark; 68 | } 69 | } 70 | } 71 | 72 | .trigger { 73 | clear: both; 74 | display: none; 75 | } 76 | 77 | &:hover .trigger { 78 | display: block; 79 | padding-bottom: 5px; 80 | } 81 | 82 | .page-link { 83 | display: block; 84 | padding: 5px 10px; 85 | } 86 | } 87 | } 88 | 89 | 90 | 91 | /** 92 | * Site footer 93 | */ 94 | .site-footer { 95 | border-top: 1px solid $grey-color-light; 96 | padding: $spacing-unit 0; 97 | } 98 | 99 | .footer-heading { 100 | font-size: 18px; 101 | margin-bottom: $spacing-unit / 2; 102 | } 103 | 104 | .contact-list, 105 | .social-media-list { 106 | list-style: none; 107 | margin-left: 0; 108 | } 109 | 110 | .footer-col-wrapper { 111 | font-size: 15px; 112 | color: $grey-color; 113 | margin-left: -$spacing-unit / 2; 114 | @extend %clearfix; 115 | } 116 | 117 | .footer-col { 118 | float: left; 119 | margin-bottom: $spacing-unit / 2; 120 | padding-left: $spacing-unit / 2; 121 | } 122 | 123 | .footer-col-1 { 124 | width: -webkit-calc(35% - (#{$spacing-unit} / 2)); 125 | width: calc(35% - (#{$spacing-unit} / 2)); 126 | } 127 | 128 | .footer-col-2 { 129 | width: -webkit-calc(20% - (#{$spacing-unit} / 2)); 130 | width: calc(20% - (#{$spacing-unit} / 2)); 131 | } 132 | 133 | .footer-col-3 { 134 | width: -webkit-calc(45% - (#{$spacing-unit} / 2)); 135 | width: calc(45% - (#{$spacing-unit} / 2)); 136 | } 137 | 138 | @include media-query($on-laptop) { 139 | .footer-col-1, 140 | .footer-col-2 { 141 | width: -webkit-calc(50% - (#{$spacing-unit} / 2)); 142 | width: calc(50% - (#{$spacing-unit} / 2)); 143 | } 144 | 145 | .footer-col-3 { 146 | width: -webkit-calc(100% - (#{$spacing-unit} / 2)); 147 | width: calc(100% - (#{$spacing-unit} / 2)); 148 | } 149 | } 150 | 151 | @include media-query($on-palm) { 152 | .footer-col { 153 | float: none; 154 | width: -webkit-calc(100% - (#{$spacing-unit} / 2)); 155 | width: calc(100% - (#{$spacing-unit} / 2)); 156 | } 157 | } 158 | 159 | 160 | 161 | /** 162 | * Page content 163 | */ 164 | .page-content { 165 | padding: $spacing-unit 0; 166 | } 167 | 168 | .page-heading { 169 | font-size: 20px; 170 | } 171 | 172 | .post-list { 173 | margin-left: 0; 174 | list-style: none; 175 | 176 | > li { 177 | margin-bottom: $spacing-unit; 178 | } 179 | } 180 | 181 | .post-meta { 182 | font-size: $small-font-size; 183 | color: $grey-color; 184 | } 185 | 186 | .post-link { 187 | display: block; 188 | font-size: 24px; 189 | } 190 | 191 | 192 | 193 | /** 194 | * Posts 195 | */ 196 | .post-header { 197 | margin-bottom: $spacing-unit; 198 | } 199 | 200 | .post-title { 201 | font-size: 42px; 202 | letter-spacing: -1px; 203 | line-height: 1; 204 | 205 | @include media-query($on-laptop) { 206 | font-size: 36px; 207 | } 208 | } 209 | 210 | .post-content { 211 | margin-bottom: $spacing-unit; 212 | 213 | h2 { 214 | font-size: 32px; 215 | 216 | @include media-query($on-laptop) { 217 | font-size: 28px; 218 | } 219 | } 220 | 221 | h3 { 222 | font-size: 26px; 223 | 224 | @include media-query($on-laptop) { 225 | font-size: 22px; 226 | } 227 | } 228 | 229 | h4 { 230 | font-size: 20px; 231 | 232 | @include media-query($on-laptop) { 233 | font-size: 18px; 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /spec/site/_sass/_syntax-highlighting.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Syntax highlighting styles 3 | */ 4 | .highlight { 5 | background: #fff; 6 | @extend %vertical-rhythm; 7 | 8 | .c { color: #998; font-style: italic } // Comment 9 | .err { color: #a61717; background-color: #e3d2d2 } // Error 10 | .k { font-weight: bold } // Keyword 11 | .o { font-weight: bold } // Operator 12 | .cm { color: #998; font-style: italic } // Comment.Multiline 13 | .cp { color: #999; font-weight: bold } // Comment.Preproc 14 | .c1 { color: #998; font-style: italic } // Comment.Single 15 | .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special 16 | .gd { color: #000; background-color: #fdd } // Generic.Deleted 17 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific 18 | .ge { font-style: italic } // Generic.Emph 19 | .gr { color: #a00 } // Generic.Error 20 | .gh { color: #999 } // Generic.Heading 21 | .gi { color: #000; background-color: #dfd } // Generic.Inserted 22 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific 23 | .go { color: #888 } // Generic.Output 24 | .gp { color: #555 } // Generic.Prompt 25 | .gs { font-weight: bold } // Generic.Strong 26 | .gu { color: #aaa } // Generic.Subheading 27 | .gt { color: #a00 } // Generic.Traceback 28 | .kc { font-weight: bold } // Keyword.Constant 29 | .kd { font-weight: bold } // Keyword.Declaration 30 | .kp { font-weight: bold } // Keyword.Pseudo 31 | .kr { font-weight: bold } // Keyword.Reserved 32 | .kt { color: #458; font-weight: bold } // Keyword.Type 33 | .m { color: #099 } // Literal.Number 34 | .s { color: #d14 } // Literal.String 35 | .na { color: #008080 } // Name.Attribute 36 | .nb { color: #0086B3 } // Name.Builtin 37 | .nc { color: #458; font-weight: bold } // Name.Class 38 | .no { color: #008080 } // Name.Constant 39 | .ni { color: #800080 } // Name.Entity 40 | .ne { color: #900; font-weight: bold } // Name.Exception 41 | .nf { color: #900; font-weight: bold } // Name.Function 42 | .nn { color: #555 } // Name.Namespace 43 | .nt { color: #000080 } // Name.Tag 44 | .nv { color: #008080 } // Name.Variable 45 | .ow { font-weight: bold } // Operator.Word 46 | .w { color: #bbb } // Text.Whitespace 47 | .mf { color: #099 } // Literal.Number.Float 48 | .mh { color: #099 } // Literal.Number.Hex 49 | .mi { color: #099 } // Literal.Number.Integer 50 | .mo { color: #099 } // Literal.Number.Oct 51 | .sb { color: #d14 } // Literal.String.Backtick 52 | .sc { color: #d14 } // Literal.String.Char 53 | .sd { color: #d14 } // Literal.String.Doc 54 | .s2 { color: #d14 } // Literal.String.Double 55 | .se { color: #d14 } // Literal.String.Escape 56 | .sh { color: #d14 } // Literal.String.Heredoc 57 | .si { color: #d14 } // Literal.String.Interpol 58 | .sx { color: #d14 } // Literal.String.Other 59 | .sr { color: #009926 } // Literal.String.Regex 60 | .s1 { color: #d14 } // Literal.String.Single 61 | .ss { color: #990073 } // Literal.String.Symbol 62 | .bp { color: #999 } // Name.Builtin.Pseudo 63 | .vc { color: #008080 } // Name.Variable.Class 64 | .vg { color: #008080 } // Name.Variable.Global 65 | .vi { color: #008080 } // Name.Variable.Instance 66 | .il { color: #099 } // Literal.Number.Integer.Long 67 | } 68 | -------------------------------------------------------------------------------- /spec/site/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: About 4 | permalink: /about/ 5 | tags: 6 | - Jekyll 7 | - test page 8 | - convenient 9 | skip_index: true 10 | --- 11 | 12 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](http://jekyllrb.com/) 13 | 14 | You can find the source code for the Jekyll new theme at: [github.com/jglovier/jekyll-new](https://github.com/jglovier/jekyll-new) 15 | 16 | You can find the source code for Jekyll at [github.com/jekyll/jekyll](https://github.com/jekyll/jekyll) 17 | -------------------------------------------------------------------------------- /spec/site/css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # Only the main Sass file needs front matter (the dashes are enough) 3 | --- 4 | @charset "utf-8"; 5 | 6 | 7 | 8 | // Our variables 9 | $base-font-family: Helvetica, Arial, sans-serif; 10 | $base-font-size: 16px; 11 | $small-font-size: $base-font-size * 0.875; 12 | $base-line-height: 1.5; 13 | 14 | $spacing-unit: 30px; 15 | 16 | $text-color: #111; 17 | $background-color: #fdfdfd; 18 | $brand-color: #2a7ae2; 19 | 20 | $grey-color: #828282; 21 | $grey-color-light: lighten($grey-color, 40%); 22 | $grey-color-dark: darken($grey-color, 25%); 23 | 24 | // Width of the content area 25 | $content-width: 800px; 26 | 27 | $on-palm: 600px; 28 | $on-laptop: 800px; 29 | 30 | 31 | 32 | // Using media queries with like this: 33 | // @include media-query($on-palm) { 34 | // .wrapper { 35 | // padding-right: $spacing-unit / 2; 36 | // padding-left: $spacing-unit / 2; 37 | // } 38 | // } 39 | @mixin media-query($device) { 40 | @media screen and (max-width: $device) { 41 | @content; 42 | } 43 | } 44 | 45 | 46 | 47 | // Import partials from `sass_dir` (defaults to `_sass`) 48 | @import 49 | "base", 50 | "layout", 51 | "syntax-highlighting" 52 | ; 53 | -------------------------------------------------------------------------------- /spec/site/feed.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | --- 4 | 5 | 6 | 7 | {{ site.title | xml_escape }} 8 | {{ site.description | xml_escape }} 9 | {{ site.url }}{{ site.baseurl }}/ 10 | 11 | {{ site.time | date_to_rfc822 }} 12 | {{ site.time | date_to_rfc822 }} 13 | Jekyll v{{ jekyll.version }} 14 | {% for post in site.posts limit:10 %} 15 | 16 | {{ post.title | xml_escape }} 17 | {{ post.content | xml_escape }} 18 | {{ post.date | date_to_rfc822 }} 19 | {{ post.url | prepend: site.baseurl | prepend: site.url }} 20 | {{ post.url | prepend: site.baseurl | prepend: site.url }} 21 | {% for tag in post.tags %} 22 | {{ tag | xml_escape }} 23 | {% endfor %} 24 | {% for cat in post.categories %} 25 | {{ cat | xml_escape }} 26 | {% endfor %} 27 | 28 | {% endfor %} 29 | 30 | 31 | -------------------------------------------------------------------------------- /spec/site/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 | 7 |

Posts

8 | 9 |
    10 | {% for post in site.posts %} 11 |
  • 12 | 13 | 14 |

    15 | {{ post.title }} 16 |

    17 |
  • 18 | {% endfor %} 19 |
20 | 21 |

subscribe via RSS

22 | 23 |
24 | -------------------------------------------------------------------------------- /spec/site/unicode.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | ”Handle the curly quotes!” they said. 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 4 | # file to always be loaded, without a need to explicitly require it in any files. 5 | # 6 | # Given that it is always loaded, you are encouraged to keep this file as 7 | # light-weight as possible. Requiring heavyweight dependencies from this file 8 | # will add to the boot time of your test suite on EVERY test run, even for an 9 | # individual file that may not need all of that loaded. Instead, consider making 10 | # a separate helper file that requires the additional dependencies and performs 11 | # the additional setup, and require it from the spec files that actually need it. 12 | # 13 | # The `.rspec` file also contains a few flags that are not defaults but that 14 | # users commonly want. 15 | # 16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 17 | 18 | require_relative '../lib/jekyll_pages_api' 19 | 20 | puts "Using Jekyll v#{Jekyll::VERSION}" 21 | 22 | RSpec.configure do |config| 23 | # rspec-expectations config goes here. You can use an alternate 24 | # assertion/expectation library such as wrong or the stdlib/minitest 25 | # assertions if you prefer. 26 | config.expect_with :rspec do |expectations| 27 | # This option will default to `true` in RSpec 4. It makes the `description` 28 | # and `failure_message` of custom matchers include text for helper methods 29 | # defined using `chain`, e.g.: 30 | # be_bigger_than(2).and_smaller_than(4).description 31 | # # => "be bigger than 2 and smaller than 4" 32 | # ...rather than: 33 | # # => "be bigger than 2" 34 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 35 | end 36 | 37 | # rspec-mocks config goes here. You can use an alternate test double 38 | # library (such as bogus or mocha) by changing the `mock_with` option here. 39 | config.mock_with :rspec do |mocks| 40 | # Prevents you from mocking or stubbing a method that does not exist on 41 | # a real object. This is generally recommended, and will default to 42 | # `true` in RSpec 4. 43 | mocks.verify_partial_doubles = true 44 | end 45 | 46 | # The settings below are suggested to provide a good initial experience 47 | # with RSpec, but feel free to customize to your heart's content. 48 | =begin 49 | # These two settings work together to allow you to limit a spec run 50 | # to individual examples or groups you care about by tagging them with 51 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 52 | # get run. 53 | config.filter_run :focus 54 | config.run_all_when_everything_filtered = true 55 | 56 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 57 | # For more details, see: 58 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 59 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 60 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 61 | config.disable_monkey_patching! 62 | 63 | # This setting enables warnings. It's recommended, but in some cases may 64 | # be too noisy due to issues in dependencies. 65 | config.warnings = true 66 | 67 | # Many RSpec users commonly either run the entire suite or an individual 68 | # file, and it's useful to allow more verbose output when running an 69 | # individual spec file. 70 | if config.files_to_run.one? 71 | # Use the documentation formatter for detailed output, 72 | # unless a formatter has already been configured 73 | # (e.g. via a command-line flag). 74 | config.default_formatter = 'doc' 75 | end 76 | 77 | # Print the 10 slowest examples and example groups at the 78 | # end of the spec run, to help surface which specs are running 79 | # particularly slow. 80 | config.profile_examples = 10 81 | 82 | # Run specs in random order to surface order dependencies. If you find an 83 | # order dependency and want to debug it, you can fix the order by providing 84 | # the seed, which is printed after each run. 85 | # --seed 1234 86 | config.order = :random 87 | 88 | # Seed global randomization in this process using the `--seed` CLI option. 89 | # Setting this allows you to use `--seed` to deterministically reproduce 90 | # test failures related to randomization by passing the same `--seed` value 91 | # as the one that triggered the failure. 92 | Kernel.srand config.seed 93 | =end 94 | end 95 | -------------------------------------------------------------------------------- /spec/support/create_page.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | 3 | RSpec.shared_context "create page" do 4 | before { @tmp_dir = nil } 5 | after { FileUtils.remove_entry(@tmp_dir) if @tmp_dir } 6 | 7 | def create_page(baseurl, page_url, data: nil, content: nil) 8 | site = instance_double(Jekyll::Site, baseurl: baseurl) 9 | jekyll_page = instance_double(Jekyll::Page, site: site, url: page_url, 10 | path: page_url) 11 | allow(jekyll_page).to receive(:data).and_return(data) unless data.nil? 12 | unless content.nil? 13 | allow(jekyll_page).to receive(:content).and_return(content) 14 | end 15 | JekyllPagesApi::Page.new(jekyll_page, site) 16 | end 17 | 18 | def create_static_file(baseurl, relative_path, content: nil) 19 | site = instance_double(Jekyll::Site, baseurl: baseurl) 20 | if content.nil? 21 | jekyll_static_file = instance_double(Jekyll::StaticFile, 22 | relative_path: relative_path) 23 | else content.nil? 24 | @tmp_root = Dir.mktmpdir 25 | @static_file_path = File.join(@tmp_root, relative_path) 26 | FileUtils.mkdir_p(File.dirname(@static_file_path)) 27 | File.open(@static_file_path, 'w') {|f| f << content} 28 | 29 | jekyll_static_file = Jekyll::StaticFile.new(site, @tmp_root, 30 | File.dirname(relative_path), File.basename(relative_path)) 31 | end 32 | JekyllPagesApi::Page.new(jekyll_static_file, site) 33 | end 34 | 35 | def create_post(baseurl, page_url, data: nil, title: nil) 36 | site = instance_double(Jekyll::Site, baseurl: baseurl) 37 | jekyll_post = double(:post) 38 | allow(jekyll_post).to receive(:data).and_return(data) unless data.nil? 39 | allow(jekyll_post).to receive(:title).and_return(title) unless title.nil? 40 | JekyllPagesApi::Page.new(jekyll_post, site) 41 | end 42 | 43 | def create_document(baseurl, relative_path, data: nil) 44 | site = instance_double(Jekyll::Site, baseurl: baseurl) 45 | jekyll_doc = instance_double(Jekyll::Document, relative_path: relative_path) 46 | allow(jekyll_doc).to receive(:data).and_return(data) unless data.nil? 47 | JekyllPagesApi::Page.new(jekyll_doc, site) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/support/shell.rb: -------------------------------------------------------------------------------- 1 | def run_cmd(cmd) 2 | # http://stackoverflow.com/a/2225391/358804 3 | unless system(cmd) 4 | raise "Non-zero exit status for `#{cmd}`" 5 | end 6 | end 7 | --------------------------------------------------------------------------------