diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebaac44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +Gemfile.lock +_site +.byebug_history diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6a8e36f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +sudo: false +language: ruby +rvm: + - 2.5.0 +before_install: gem install bundler -v 1.16.1 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fa75df1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..b9c0953 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,23 @@ +(c) 2018 Rob Watson + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5331c4d --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +## jekyll-stealthy-share + +[![Build Status](https://travis-ci.org/rfwatson/jekyll-stealthy-share.svg?branch=master)](https://travis-ci.org/rfwatson/jekyll-stealthy-share) + +This is a [Jekyll](https://jekyllrb.com/) plugin that adds a Liquid tag to inject share buttons into your blog. + +The share buttons are HTML-only and trigger no JavaScript, so they won't track your blog's on behalf of Facebook, Twitter, Reddit or whoever else. + +The injected HTML and CSS is simple and easy to customize or extend. + +See it in action on https://netflux.io. + +## Installation + +Add `jekyll-stealthy-share` to your blog's Gemfile: + +```ruby +group :jekyll_plugins do + gem 'jekyll-stealthy-share', git: 'https://github.com/rfwatson/jekyll-stealthy-share.git' +end +``` + +And add it to your `_config.yml`: + +```yaml +plugins: + - jekyll-stealthy-share +``` + +## Usage + +Somewhere in your layout (for example `_includes/head.html`), include the share button CSS: + +```html +{% stealthy_share_assets %} +``` + +To inject the share buttons into your post, use this tag: + +```html +{% stealthy_share_buttons %} +``` + +## Customizing/adding/removing buttons + +To re-order or remove buttons, you can pass arguments to the liquid tag. For example: + +```html +{% stealthy_share_buttons: facebook, twitter, reddit %} +``` + +It's also possible to add new templates of your own. If a directory `_includes/share_buttons` exists in your site's root folder, `jekyll-stealthy-share` will read templates from this location instead. + +See the [`_includes` directory](https://github.com/rfwatson/jekyll-stealthy-share/tree/master/_includes) for an idea of the expected layout of each template. Additionally, you could choose to not include `{% stealthy_share_assets %}` and write your own custom CSS. + +## TODO + +* Add more share button options +* Make customization of buttons easier (YAML file format to define?) +* Improve default styling +* Write unit tests + +## Contributions + +Welcome. + +## Credits + +The share button SVG templates, colours and some styling are all from http://sharingbuttons.io/. + +## License + +MIT + +## Contact + +rfwatson via GitHub diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..5d1930e --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require "bundler/gem_tasks" +require 'rspec/core/rake_task' + +task default: :spec + +RSpec::Core::RakeTask.new diff --git a/_includes/_container.html b/_includes/_container.html new file mode 100644 index 0000000..808a4fb --- /dev/null +++ b/_includes/_container.html @@ -0,0 +1,7 @@ +
+

Share this post

+

If you enjoyed reading this post, please consider sharing it with your network.

+ +
diff --git a/_includes/facebook.html b/_includes/facebook.html new file mode 100644 index 0000000..66c7dc3 --- /dev/null +++ b/_includes/facebook.html @@ -0,0 +1,10 @@ +
  • + +
    + + Facebook +
    +
    +
  • diff --git a/_includes/hacker_news.html b/_includes/hacker_news.html new file mode 100644 index 0000000..5acf1a5 --- /dev/null +++ b/_includes/hacker_news.html @@ -0,0 +1,11 @@ +
  • + +
    + + Hacker News +
    +
    +
  • + diff --git a/_includes/reddit.html b/_includes/reddit.html new file mode 100644 index 0000000..de8efca --- /dev/null +++ b/_includes/reddit.html @@ -0,0 +1,10 @@ +
  • + +
    + + Reddit +
    +
    +
  • diff --git a/_includes/twitter.html b/_includes/twitter.html new file mode 100644 index 0000000..14fe878 --- /dev/null +++ b/_includes/twitter.html @@ -0,0 +1,10 @@ +
  • + +
    + + Twitter +
    +
    +
  • diff --git a/assets/share.css b/assets/share.css new file mode 100644 index 0000000..3871085 --- /dev/null +++ b/assets/share.css @@ -0,0 +1,93 @@ +.share_buttons { + margin: 30px auto; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + padding: 15px 0px; +} + +.share_buttons > ul { + margin-left: 0px; + margin-bottom: 0px; +} + +.share_buttons > ul > li { + list-style-type: none; + display: inline-block; +} + +.share_buttons > ul > li > a, .icon { + display: inline-block; +} + +.share_buttons > ul > li > a { + text-decoration: none; + color: #fff; + margin: 0.5em 0px; + line-height: 1.5em; +} + +.share_buttons > ul > li > a > .button { + border-radius: 5px; + transition: 25ms ease-out; + padding: 0.5em 0.75em; + font-family: Helvetica Neue,Helvetica,Arial,sans-serif; + text-align: center; + min-width: 120px; +} + +.share_buttons > ul > li > a > .button > .icon { + fill: #fff; + stroke: none; + position: relative; + top: -1px; + left: 1px; +} + +.share_buttons > ul > li > a > .button > .icon > svg { + width: 1em; + height: 1em; + margin-right: 0.4em; + line-height: 1em; +} + +.share_buttons > ul > li > a > .button > .icon > svg > path { + fill: #fff; +} + +.share_buttons > ul > li[data-service="facebook"] > a > .button { + background-color: #3b5998; + border-color: #3b5998; +} + +.share_buttons > ul > li[data-service="facebook"] > a:active > .button, + .share_buttons > ul > li[data-service="facebook"] > a:hover > .button { + background-color: #2d4373; + border-color: #2d4373; +} + +.share_buttons > ul > li[data-service="twitter"] > a > .button { + background-color: #55acee; +} + +.share_buttons > ul > li[data-service="twitter"] > a:active > .button, + .share_buttons > ul > li[data-service="twitter"] > a:hover > .button { + background-color: #2795e9; +} + +.share_buttons > ul > li[data-service="reddit"] > a > .button { + background-color: #5f99cf; +} + +.share_buttons > ul > li[data-service="reddit"] > a:active > .button, + .share_buttons > ul > li[data-service="reddit"] > a:hover > .button { + background-color: #3a80c1; +} + +.share_buttons > ul > li[data-service="hacker_news"] > a > .button { + background-color: #ff6600; +} + +.share_buttons > ul > li[data-service="hacker_news"] > a:active > .button, + .share_buttons > ul > li[data-service="hacker_news"] > a:hover > .button { + background-color: #fb6200; +} diff --git a/jekyll-stealthy-share.gemspec b/jekyll-stealthy-share.gemspec new file mode 100644 index 0000000..e0d3f67 --- /dev/null +++ b/jekyll-stealthy-share.gemspec @@ -0,0 +1,27 @@ +# encoding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'jekyll/stealthy_share/version' + +Gem::Specification.new do |spec| + spec.name = "jekyll-stealthy-share" + spec.version = Jekyll::StealthyShare::VERSION + spec.authors = ["Rob Watson"] + spec.email = ["hello@netflux.io"] + spec.description = %q{Privacy-conscious share buttons for Jekyll} + spec.summary = spec.description + spec.homepage = "https://github.com/rfwatson" + spec.license = "MIT" + + spec.files = `git ls-files`.split($/) + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.16" + spec.add_development_dependency "rake" + spec.add_development_dependency "jekyll" + spec.add_development_dependency "rspec" + spec.add_development_dependency 'capybara' + spec.add_development_dependency 'byebug' +end diff --git a/lib/jekyll-stealthy-share.rb b/lib/jekyll-stealthy-share.rb new file mode 100644 index 0000000..1769ba1 --- /dev/null +++ b/lib/jekyll-stealthy-share.rb @@ -0,0 +1,5 @@ +require 'jekyll' +require 'jekyll/stealthy_share' + +Liquid::Template.register_tag('stealthy_share_buttons', Jekyll::StealthyShare::Tag) +Liquid::Template.register_tag('stealthy_share_assets', Jekyll::StealthyShare::AssetTag) diff --git a/lib/jekyll/stealthy_share.rb b/lib/jekyll/stealthy_share.rb new file mode 100644 index 0000000..61e7904 --- /dev/null +++ b/lib/jekyll/stealthy_share.rb @@ -0,0 +1,33 @@ +require 'jekyll/stealthy_share/version' +require 'jekyll/stealthy_share/template' +require 'jekyll/stealthy_share/tag' +require 'jekyll/stealthy_share/asset_tag' +require 'jekyll/stealthy_share/generator' + +module Jekyll + module StealthyShare + class << self + attr_accessor :site + + def base_path + File.join(File.dirname(__FILE__), '../..') + end + + def templates_path + return source_templates_path if File.directory?(source_templates_path) + + default_templates_path + end + + private + + def default_templates_path + File.join(base_path, '_includes') + end + + def source_templates_path + File.join(site.source, '_includes', 'share_buttons') + end + end + end +end diff --git a/lib/jekyll/stealthy_share/asset_tag.rb b/lib/jekyll/stealthy_share/asset_tag.rb new file mode 100644 index 0000000..1b25b3f --- /dev/null +++ b/lib/jekyll/stealthy_share/asset_tag.rb @@ -0,0 +1,9 @@ +module Jekyll + module StealthyShare + class AssetTag < Liquid::Tag + def render(context) + %Q{} + end + end + end +end diff --git a/lib/jekyll/stealthy_share/generator.rb b/lib/jekyll/stealthy_share/generator.rb new file mode 100644 index 0000000..3c4541c --- /dev/null +++ b/lib/jekyll/stealthy_share/generator.rb @@ -0,0 +1,18 @@ +require 'jekyll/generator' + +module Jekyll + module StealthyShare + class Generator < Jekyll::Generator + def generate(site) + StealthyShare.site = site + + site.static_files << StaticFile.new( + site, + StealthyShare.base_path, + 'assets', + 'share.css' + ) + end + end + end +end diff --git a/lib/jekyll/stealthy_share/tag.rb b/lib/jekyll/stealthy_share/tag.rb new file mode 100644 index 0000000..06c6786 --- /dev/null +++ b/lib/jekyll/stealthy_share/tag.rb @@ -0,0 +1,61 @@ +require 'liquid' + +module Jekyll + module StealthyShare + class Tag < Liquid::Tag + HTML = '.html'.freeze + + def initialize(tag_name, text, tokens) + @text = text + end + + def render(context) + permalink = page(context)['url'] + title = page(context)['title'] + url = URI.join(base_url(context), permalink) + + buttons = templates.map do |template| + liquid_template = Liquid::Template.parse(template) + liquid_template.render( + 'url' => url.to_s, + 'title' => title.to_s + ) + end + + container = Template.read('_container.html').first + render_template(container, 'content' => buttons.join) + end + + private + + def render_template(template, data) + t = Liquid::Template.parse(template) + t.render(data) + end + + def templates + basenames = @text.scan(/\w+/) + basenames = Template.basenames if basenames.empty? + + basenames.map! do |basename| + basename.end_with?(HTML) ? basename : basename + HTML + end + + missing = basenames - Template.basenames + if missing.any? + raise "Unknown share button templates: #{missing.inspect}." + end + + Template.read(*basenames) + end + + def page(context) + context.environments.first.page + end + + def base_url(context) + context.registers[:site].config['url'] + end + end + end +end diff --git a/lib/jekyll/stealthy_share/template.rb b/lib/jekyll/stealthy_share/template.rb new file mode 100644 index 0000000..98f213e --- /dev/null +++ b/lib/jekyll/stealthy_share/template.rb @@ -0,0 +1,26 @@ +module Jekyll + module StealthyShare + class Template + class << self + extend Forwardable + def_delegator StealthyShare, :templates_path + + def basenames + all.map(&File.public_method(:basename)).sort + end + + def read(*basenames) + basenames.map do |basename| + File.read(File.join(templates_path, basename)) + end + end + + private + + def all + Dir.glob(File.join(templates_path, '[!_]*.html')) + end + end + end + end +end diff --git a/lib/jekyll/stealthy_share/version.rb b/lib/jekyll/stealthy_share/version.rb new file mode 100644 index 0000000..956a5ab --- /dev/null +++ b/lib/jekyll/stealthy_share/version.rb @@ -0,0 +1,5 @@ +module Jekyll + module StealthyShare + VERSION = '0.1.0' + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..9e9acfe --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,109 @@ +ENV['JEKYLL_LOG_LEVEL'] = 'warn' + +require 'jekyll' +require 'byebug' +require 'capybara/rspec' +require_relative 'support/helpers' + +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end + + config.include Helpers +end diff --git a/spec/stealthy_share_spec.rb b/spec/stealthy_share_spec.rb new file mode 100644 index 0000000..8f78098 --- /dev/null +++ b/spec/stealthy_share_spec.rb @@ -0,0 +1,60 @@ +require 'jekyll/stealthy_share' + +RSpec.describe Jekyll::StealthyShare, type: :feature do + subject { page } + + context 'on a basic site' do + include_context 'basic site' + + context 'a page with share buttons injected' do + before do + visit '/2018/01/01/buttons.html' + end + + it { is_expected.to have_css('.share_buttons') } + it { is_expected.to have_content('Share this post') } + + it 'injects all buttons' do + count = page.all('.share_buttons li').size + expect(count).to eq 4 + end + end + + context 'a page with no share buttons injected' do + before do + visit '/2018/01/02/no-buttons.html' + end + + it { is_expected.not_to have_css('.share_buttons') } + it { is_expected.not_to have_content('Share this:') } + end + end + + context 'a site with overrides' do + include_context 'a site with overrides' + + context 'a page with share buttons injected' do + before do + visit '/2018/01/01/buttons.html' + end + + it { is_expected.to have_css('.share_buttons') } + it { is_expected.to have_content('Share this:') } + it { is_expected.to have_content('Test button') } + + it 'injects all buttons' do + count = page.all('.share_buttons li').size + expect(count).to eq 1 + end + end + + context 'a page with no share buttons injected' do + before do + visit '/2018/01/02/no-buttons.html' + end + + it { is_expected.not_to have_css('.share_buttons') } + it { is_expected.not_to have_content('Share this:') } + end + end +end diff --git a/spec/support/fixtures/basic/_config.yml b/spec/support/fixtures/basic/_config.yml new file mode 100644 index 0000000..e69de29 diff --git a/spec/support/fixtures/basic/_posts/2018-01-01-buttons.md b/spec/support/fixtures/basic/_posts/2018-01-01-buttons.md new file mode 100644 index 0000000..ffe3618 --- /dev/null +++ b/spec/support/fixtures/basic/_posts/2018-01-01-buttons.md @@ -0,0 +1,7 @@ +--- +title: hello +--- + +Hello world + +{% stealthy_share_buttons %} diff --git a/spec/support/fixtures/basic/_posts/2018-01-02-no-buttons.md b/spec/support/fixtures/basic/_posts/2018-01-02-no-buttons.md new file mode 100644 index 0000000..b5b3f8f --- /dev/null +++ b/spec/support/fixtures/basic/_posts/2018-01-02-no-buttons.md @@ -0,0 +1,5 @@ +--- +title: hello +--- + +Hello world diff --git a/spec/support/fixtures/overrides/_config.yml b/spec/support/fixtures/overrides/_config.yml new file mode 100644 index 0000000..e69de29 diff --git a/spec/support/fixtures/overrides/_includes/share_buttons/_container.html b/spec/support/fixtures/overrides/_includes/share_buttons/_container.html new file mode 100644 index 0000000..cc89cd1 --- /dev/null +++ b/spec/support/fixtures/overrides/_includes/share_buttons/_container.html @@ -0,0 +1,6 @@ +
    +

    Share this:

    + +
    diff --git a/spec/support/fixtures/overrides/_includes/share_buttons/button.html b/spec/support/fixtures/overrides/_includes/share_buttons/button.html new file mode 100644 index 0000000..c224dbb --- /dev/null +++ b/spec/support/fixtures/overrides/_includes/share_buttons/button.html @@ -0,0 +1 @@ +
  • Test button
  • diff --git a/spec/support/fixtures/overrides/_posts/2018-01-01-buttons.md b/spec/support/fixtures/overrides/_posts/2018-01-01-buttons.md new file mode 100644 index 0000000..ffe3618 --- /dev/null +++ b/spec/support/fixtures/overrides/_posts/2018-01-01-buttons.md @@ -0,0 +1,7 @@ +--- +title: hello +--- + +Hello world + +{% stealthy_share_buttons %} diff --git a/spec/support/fixtures/overrides/_posts/2018-01-02-no-buttons.md b/spec/support/fixtures/overrides/_posts/2018-01-02-no-buttons.md new file mode 100644 index 0000000..b5b3f8f --- /dev/null +++ b/spec/support/fixtures/overrides/_posts/2018-01-02-no-buttons.md @@ -0,0 +1,5 @@ +--- +title: hello +--- + +Hello world diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb new file mode 100644 index 0000000..8d9c93b --- /dev/null +++ b/spec/support/helpers.rb @@ -0,0 +1,34 @@ +module Helpers + shared_context 'shared' do + let(:config) do + Jekyll.configuration( + 'source' => source, + 'destination' => File.expand_path('_site'), + 'url' => 'http://www.example.com', + 'name' => 'Test site', + 'plugins' => ['jekyll-stealthy-share'] + ) + end + + let(:site) do + Jekyll::Site.new(config) + end + + before do + site.process + Capybara.app = Rack::File.new(site.dest) + end + end + + shared_context 'basic site' do + include_context 'shared' + + let(:source) { File.expand_path('spec/support/fixtures/basic') } + end + + shared_context 'a site with overrides' do + include_context 'shared' + + let(:source) { File.expand_path('spec/support/fixtures/overrides') } + end +end