├── Gemfile ├── script ├── bootstrap └── cibuild ├── .gitignore ├── test ├── fixtures │ ├── index.md │ ├── test.json │ ├── _docs │ │ └── file.md │ ├── non-mentioned.md │ ├── parkr.txt │ ├── mentioned-markdown.md │ └── leave-liquid-alone.md ├── helper.rb ├── test_jekyll_issue_mentions.rb └── test_issue_mention_filter.rb ├── History.markdown ├── Rakefile ├── .travis.yml ├── jekyll-issue-mentions.gemspec ├── LICENSE ├── README.md └── lib ├── jekyll-issue-mentions.rb └── issue_mention_filter.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | bundle install -j8 4 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | bundle exec rake test 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/gems 2 | .bundle 3 | bin 4 | /*.gem 5 | Gemfile.lock 6 | .*.swp 7 | -------------------------------------------------------------------------------- /test/fixtures/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: I'm a page 3 | --- 4 | 5 | 1234 #1234 1234 6 | -------------------------------------------------------------------------------- /test/fixtures/test.json: -------------------------------------------------------------------------------- 1 | --- 2 | title: I'm a page 3 | --- 4 | 5 | 1234 #1234 1234 6 | -------------------------------------------------------------------------------- /test/fixtures/_docs/file.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: A document 3 | --- 4 | 5 | 1234 #1234 1234 6 | -------------------------------------------------------------------------------- /test/fixtures/non-mentioned.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: don't mention me bro 3 | --- 4 | 5 | 1234 1234 1234 6 | > 1234 7 | -------------------------------------------------------------------------------- /test/fixtures/parkr.txt: -------------------------------------------------------------------------------- 1 | --- 2 | title: Parker Moore 3 | type: author-info 4 | --- 5 | 6 | Parker '#1234' Moore 7 | -------------------------------------------------------------------------------- /test/fixtures/mentioned-markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: mention me but don't eff my markdown 3 | --- 4 | 5 | 1234 #1234 1234 6 | > 1234 7 | -------------------------------------------------------------------------------- /test/fixtures/leave-liquid-alone.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: don't mangle that liquid plz 3 | --- 4 | 5 | 1234 #1234 12341234 6 | -------------------------------------------------------------------------------- /History.markdown: -------------------------------------------------------------------------------- 1 | ## 0.1.5 / 2015-03-28 2 | 3 | * Added tests for issue mention filter 4 | * Restricted the nokogiri version to handle https://github.com/jch/html-pipeline/pull/170 5 | 6 | ## 0.1.3 / 2015-03-27 7 | 8 | * Added support for issue id pattern 9 | 10 | ## 0.1.0 / 2015-03-23 11 | 12 | * First release 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development, :test) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'bundler/gem_tasks' 11 | require 'rake' 12 | require 'rake/testtask' 13 | 14 | Rake::TestTask.new(:test) do |test| 15 | test.libs << 'lib' << 'test' 16 | test.pattern = 'test/**/test_*.rb' 17 | test.verbose = true 18 | end 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2 4 | - 2.1 5 | - 2.0 6 | - 1.9.3 7 | script: script/cibuild 8 | sudo: false 9 | cache: bundler 10 | env: 11 | global: 12 | - NOKOGIRI_USE_SYSTEM_LIBRARIES=true 13 | #notifications: 14 | #irc: 15 | #on_success: change 16 | #on_failure: change 17 | #channels: 18 | #- irc.freenode.org#jekyll 19 | #template: 20 | #- '%{repository}#%{build_number} (%{branch}) %{message} %{build_url}' 21 | #email: 22 | #on_success: never 23 | #on_failure: never 24 | -------------------------------------------------------------------------------- /jekyll-issue-mentions.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "jekyll-issue-mentions" 3 | s.summary = "#issueid support for your Jekyll site" 4 | s.version = "0.1.6" 5 | s.authors = ["Harish Shetty"] 6 | s.email = "support@workato.com" 7 | 8 | s.homepage = "https://github.com/workato/jekyll-issue-mentions" 9 | s.licenses = ["MIT"] 10 | s.files = ["lib/jekyll-issue-mentions.rb", "lib/issue_mention_filter.rb" ] 11 | 12 | s.add_dependency "jekyll", '~> 2.0' 13 | s.add_dependency "html-pipeline", '~> 1.9.0' 14 | s.add_dependency "nokogiri", [">= 1.4", "<= 1.6.5"] 15 | s.add_dependency "github-markdown" 16 | 17 | s.add_development_dependency 'rake' 18 | s.add_development_dependency 'rdoc' 19 | s.add_development_dependency 'shoulda' 20 | s.add_development_dependency 'minitest' 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 GitHub, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'minitest/autorun' 3 | require 'shoulda' 4 | 5 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 6 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 7 | require 'jekyll-issue-mentions' 8 | 9 | TEST_DIR = File.expand_path("../", __FILE__) 10 | FIXTURES_DIR = File.expand_path("fixtures", TEST_DIR) 11 | DEST_DIR = File.expand_path("destination", TEST_DIR) 12 | 13 | module IssueMentionsTestHelpers 14 | def fixture_site 15 | Jekyll::Site.new( 16 | Jekyll::Utils.deep_merge_hashes( 17 | Jekyll::Configuration::DEFAULTS, 18 | { 19 | "source" => FIXTURES_DIR, 20 | "destination" => DEST_DIR, 21 | "collections" => { 22 | "docs" => {} 23 | }, 24 | "jekyll-issue-mentions" => { 25 | "base_url" => "https://github.com/usr1/repo1/issues" 26 | } 27 | } 28 | ) 29 | ) 30 | end 31 | 32 | def page_with_name(site, name) 33 | site.pages.find { |p| p.name == name } 34 | end 35 | 36 | def document(doc_filename) 37 | @site.collections["docs"].docs.find { |d| d.relative_path.match(doc_filename) } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Issue Mentions 2 | 3 | Github #issueid mention support for your Jekyll site 4 | 5 | [![Gem Version](https://badge.fury.io/rb/jekyll-issue-mentions.png)](http://badge.fury.io/rb/jekyll-issue-mentions) 6 | [![Build Status](https://travis-ci.org/workato/jekyll-issue-mentions.svg?branch=master)](https://travis-ci.org/workato/jekyll-issue-mentions) 7 | 8 | ## Usage 9 | 10 | Add the following to your site's `Gemfile` 11 | 12 | ``` 13 | gem 'jekyll-issue-mentions' 14 | ``` 15 | 16 | And add the following to your site's `_config.yml` 17 | 18 | ```yml 19 | gems: 20 | - jekyll-issue-mentions 21 | ``` 22 | 23 | In any page or post, use #issueid as you would normally, e.g. 24 | 25 | ```markdown 26 | Can you look at issue #1 today? 27 | ``` 28 | 29 | Will be converted to 30 | 31 | > Can you look at issue [#1](https://github.com/workato/jekyll-issue-mentions/issues/1) today? 32 | 33 | ## Configuration 34 | 35 | Set the Github repo issue url: 36 | 37 | ```yaml 38 | jekyll-issue-mentions: 39 | base_url: https://github.com/workato/jekyll-issue-mentions/issues 40 | ``` 41 | 42 | Or, you can use this shorthand: 43 | 44 | ```yaml 45 | jekyll-issue-mentions: https://github.com/workato/jekyll-issue-mentions/issues 46 | ``` 47 | 48 | Set the issue id pattern: 49 | 50 | ```yaml 51 | jekyll-issue-mentions: 52 | base_url: https://github.com/workato/jekyll-issue-mentions/issues 53 | issueid_pattern: '[0-9]{2,}' 54 | ``` 55 | -------------------------------------------------------------------------------- /lib/jekyll-issue-mentions.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll' 2 | require 'html/pipeline' 3 | require 'issue_mention_filter' 4 | 5 | module Jekyll 6 | class IssueMentions < Jekyll::Generator 7 | safe true 8 | attr_reader :base_url, :issueid_pattern 9 | 10 | def initialize(config = Hash.new) 11 | validate_config!(config) 12 | end 13 | 14 | def generate(site) 15 | site.pages.each { |page| mentionify page if html_page?(page) } 16 | site.posts.each { |post| mentionify post } 17 | site.docs_to_write.each { |doc| mentionify doc } 18 | end 19 | 20 | def mentionify(page) 21 | return unless page.content.include?('#') 22 | filter = HTML::Pipeline::IssueMentionFilter.new(page.content, base_url: base_url, issueid_pattern: issueid_pattern) 23 | page.content = filter.call.to_s. 24 | gsub(">", ">"). 25 | gsub("<", "<"). 26 | gsub("%7B", "{"). 27 | gsub("%20", " "). 28 | gsub("%7D", "}") 29 | end 30 | 31 | def html_page?(page) 32 | page.html? || page.url.end_with?('/') 33 | end 34 | 35 | private 36 | def validate_config!(configs) 37 | configs = configs['jekyll-issue-mentions'] 38 | base_url = issueid_pattern = nil 39 | case configs 40 | when String 41 | base_url = configs 42 | when Hash 43 | base_url, issueid_pattern = configs['base_url'], configs['issueid_pattern'] 44 | issueid_pattern = /#{issueid_pattern}/ if issueid_pattern.is_a?(String) 45 | end 46 | error_prefix = "jekyll-issue-mentions" 47 | raise ArgumentError.new("#{error_prefix}.base_url is missing/empty") if (base_url.nil? || base_url.empty?) 48 | raise ArgumentError.new("#{error_prefix}.issueid_pattern is invalid") if (!issueid_pattern.nil? && !issueid_pattern.is_a?(Regexp)) 49 | 50 | @base_url, @issueid_pattern = base_url, issueid_pattern 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_jekyll_issue_mentions.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestJekyllIssueMentions < Minitest::Test 4 | include IssueMentionsTestHelpers 5 | 6 | def setup 7 | @site = fixture_site 8 | @site.read 9 | @site.config['jekyll-issue-mentions'] = {"base_url" => "https://github.com/usr1/repo1/issues"} 10 | @mentions = Jekyll::IssueMentions.new(@site.config) 11 | @mention = "1234 #1234 1234" 12 | end 13 | 14 | def content page 15 | # counter UTF-8 encoding https://groups.google.com/forum/#!msg/nokogiri-talk/Q2Nh1cLeQzk/dNyAwQ3vgQsJ 16 | page.content.gsub(/\n\z/, '') 17 | end 18 | 19 | def base_url(configs) 20 | Jekyll::IssueMentions.new("jekyll-issue-mentions" => configs).base_url 21 | end 22 | 23 | def issueid_pattern(pattern) 24 | Jekyll::IssueMentions.new("jekyll-issue-mentions" => { "base_url" => "/", "issueid_pattern" => pattern}).issueid_pattern 25 | end 26 | 27 | should "replace #mention with link" do 28 | page = page_with_name(@site, "index.md") 29 | 30 | @mentions.mentionify page 31 | assert_equal @mention, content(page) 32 | end 33 | 34 | should "replace @mention with link in collections" do 35 | page = document("file.md") 36 | 37 | @mentions.mentionify page 38 | assert_equal @mention, content(page) 39 | end 40 | 41 | should "replace page content on generate" do 42 | @mentions.generate(@site) 43 | assert_equal @mention, content(@site.pages.first) 44 | end 45 | 46 | should "not mangle liquid templating" do 47 | page = page_with_name(@site, "leave-liquid-alone.md") 48 | 49 | @mentions.mentionify page 50 | assert_equal "#{@mention}1234", content(page) 51 | end 52 | 53 | should "not mangle markdown" do 54 | page = page_with_name(@site, "mentioned-markdown.md") 55 | 56 | @mentions.mentionify page 57 | assert_equal "#{@mention}\n> 1234", content(page) 58 | end 59 | 60 | should "not mangle non-mentioned content" do 61 | page = page_with_name(@site, "non-mentioned.md") 62 | 63 | @mentions.mentionify page 64 | assert_equal "1234 1234 1234\n> 1234", content(page) 65 | end 66 | 67 | should "not touch non-HTML pages" do 68 | @mentions.generate(@site) 69 | assert_equal "1234 #1234 1234", content(page_with_name(@site, "test.json")) 70 | end 71 | 72 | should "also convert pages with permalinks ending in /" do 73 | page = page_with_name(@site, "parkr.txt") 74 | 75 | @mentions.mentionify page 76 | assert_equal "Parker '#1234' Moore", content(page) 77 | end 78 | 79 | 80 | context "config" do 81 | context "bad config" do 82 | should "should raise exception for invalid values" do 83 | assert_raises(ArgumentError) { base_url(nil) } 84 | assert_raises(ArgumentError) { base_url({}) } 85 | assert_raises(ArgumentError) { base_url(123) } 86 | end 87 | end 88 | 89 | context "base_url" do 90 | should "handle a raw string" do 91 | assert_equal "https://twitter.com", base_url("https://twitter.com") 92 | end 93 | 94 | should "handle a hash config" do 95 | assert_equal "https://twitter.com", base_url({"base_url" => "https://twitter.com"}) 96 | end 97 | end 98 | 99 | context "issueid_pattern" do 100 | should "should be a string" do 101 | assert_equal(/abc/, issueid_pattern("abc")) 102 | end 103 | 104 | should "should raise exception for invalid config" do 105 | assert_raises(ArgumentError) { issueid_pattern(123) } 106 | assert_raises(ArgumentError) { issueid_pattern({}) } 107 | end 108 | end 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /lib/issue_mention_filter.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module HTML 4 | class Pipeline 5 | # HTML filter that replaces #mention mentions with links to Github issue. Mentions within
,
  6 |     # , "
 42 |     assert_equal body, filter(body).to_html
 43 |   end
 44 | 
 45 |   def test_not_replacing_mentions_in_links
 46 |     body = "

#1234 okay

" 47 | assert_equal body, filter(body).to_html 48 | end 49 | 50 | def test_html_injection 51 | body = "

#1234 <script>alert(0)</script>

" 52 | link = "#1234" 53 | assert_equal "

#{link} <script>alert(0)</script>

", 54 | filter(body, '/').to_html 55 | end 56 | 57 | def test_base_url_slash 58 | body = "

Hi, #561!

" 59 | link = "#561" 60 | assert_equal "

Hi, #{link}!

", 61 | filter(body, '/').to_html 62 | end 63 | 64 | def test_base_url_under_custom_route 65 | body = "

Hi, #561!

" 66 | link = "#561" 67 | assert_equal "

Hi, #{link}!

", 68 | filter(body, '/issues').to_html 69 | end 70 | 71 | def test_base_url_slash_with_tilde 72 | body = "

Hi, #561!

" 73 | link = "#561" 74 | assert_equal "

Hi, #{link}!

", 75 | filter(body, '/~').to_html 76 | end 77 | 78 | MarkdownPipeline = 79 | HTML::Pipeline.new [ 80 | HTML::Pipeline::MarkdownFilter, 81 | HTML::Pipeline::IssueMentionFilter 82 | ] 83 | 84 | def mentioned_issueids 85 | result = {} 86 | MarkdownPipeline.call(@body, {}, result) 87 | result[:mentioned_issueids] 88 | end 89 | 90 | def test_matches_issueids_in_body 91 | @body = "#8756 how are you?" 92 | assert_equal %w[8756], mentioned_issueids 93 | end 94 | 95 | def test_matches_issueids_followed_by_a_single_dot 96 | @body = "okay #9567." 97 | assert_equal %w[9567], mentioned_issueids 98 | end 99 | 100 | def test_matches_issueids_followed_by_multiple_dots 101 | @body = "okay #9567..." 102 | assert_equal %w[9567], mentioned_issueids 103 | end 104 | 105 | 106 | def test_matches_colon_suffixed_names 107 | @body = "#1234: what do you think?" 108 | assert_equal %w[1234], mentioned_issueids 109 | end 110 | 111 | def test_matches_list_of_names 112 | @body = "#5634 #8856 #1234" 113 | assert_equal %w[5634 8856 1234], mentioned_issueids 114 | end 115 | 116 | def test_matches_list_of_names_with_commas 117 | @body = "#5634, #8856, #1234" 118 | assert_equal %w[5634 8856 1234], mentioned_issueids 119 | end 120 | 121 | def test_matches_inside_brackets 122 | @body = "(#5634) and [#8856]" 123 | assert_equal %w[5634 8856], mentioned_issueids 124 | end 125 | 126 | def test_doesnt_ignore_invalid_issues 127 | @body = "#5634 #8856 #1234444444" 128 | assert_equal %w[5634 8856 1234444444], mentioned_issueids 129 | end 130 | 131 | def test_returns_distinct_set 132 | @body = "#5634 #8856 #1234 #5634 #8856 #1234" 133 | assert_equal %w[5634 8856 1234], mentioned_issueids 134 | end 135 | 136 | def test_does_not_match_inline_code_block_with_multiple_code_blocks 137 | @body = "something\n\n`#4456 #3267 #1234`" 138 | assert_equal %w[], mentioned_issueids 139 | end 140 | 141 | def test_mention_at_end_of_parenthetical_sentence 142 | @body = "(We're talking 'bout #2568.)" 143 | assert_equal %w[2568], mentioned_issueids 144 | end 145 | 146 | def test_issueid_pattern_can_be_customized 147 | body = "

#_987: issue.

" 148 | doc = Nokogiri::HTML::DocumentFragment.parse(body) 149 | 150 | res = filter(doc, '/', /(_[0-9]{3})/) 151 | 152 | link = "#_987" 153 | assert_equal "

#{link}: issue.

", 154 | res.to_html 155 | end 156 | 157 | def test_filter_does_not_create_a_new_object_for_default_issueid_pattern 158 | body = "
#8756
" 159 | doc = Nokogiri::HTML::DocumentFragment.parse(body) 160 | 161 | filter(doc.clone, '/', nil) 162 | pattern_count = HTML::Pipeline::IssueMentionFilter::MentionPatterns.length 163 | filter(doc.clone, '/', nil) 164 | 165 | assert_equal pattern_count, HTML::Pipeline::IssueMentionFilter::MentionPatterns.length 166 | filter(doc.clone, '/', /8756/) 167 | assert_equal pattern_count + 1, HTML::Pipeline::IssueMentionFilter::MentionPatterns.length 168 | end 169 | end 170 | --------------------------------------------------------------------------------